Files
Deep-Package-Inspection/backend/routes/deviceDetailsHandler.js
T

253 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// backend/routes/deviceDetailsHandler.js
// ─────────────────────────────────────────────────────────────────────────────
// Device Detail Handler — reads 100% from MongoDB (no live DPI API calls)
//
// Architecture:
// 1. Total download/upload → DeviceStat (latest, DPI API cumulative per-IP)
// 2. Apps tab → DeviceAppStat (DPI API per-IP per-app, collected
// by proxy every 5min for top 30 devices)
// 3. Protocols + Domains → Flow collection (sampled, enriched with domain map)
// 4. Network Flows tab → Flow collection
// 5. Threats tab → Threat collection
// ─────────────────────────────────────────────────────────────────────────────
const { DeviceStat, DeviceAppStat, Flow, Threat, CustomDeviceLabel } = require('../models/Schemas');
const User = require('../models/User');
// Domain → App label for protocol/domain tab enrichment only (NOT for apps tab)
const DOMAIN_APP_MAP = {
'youtube.com': 'YouTube', 'googlevideo.com': 'YouTube', 'yt.be': 'YouTube',
'facebook.com': 'Facebook', 'fbcdn.net': 'Facebook', 'instagram.com': 'Instagram',
'whatsapp.com': 'WhatsApp', 'wa.me': 'WhatsApp',
'tiktok.com': 'TikTok', 'tiktokv.com': 'TikTok',
'cloudflare.com': 'Cloudflare', 'cloudflare-dns.com': 'Cloudflare',
'google.com': 'Google', 'googleapis.com': 'Google', 'gstatic.com': 'Google',
'microsoft.com': 'Microsoft', 'microsoftonline.com': 'Microsoft', 'windows.com': 'Microsoft',
'office.com': 'Microsoft', 'live.com': 'Microsoft', 'azure.com': 'Microsoft',
'netflix.com': 'Netflix', 'nflximg.net': 'Netflix',
'twitter.com': 'X (Twitter)', 'twimg.com': 'X (Twitter)',
'telegram.org': 'Telegram', 'telegram.me': 'Telegram',
'zoom.us': 'Zoom', 'zoomgov.com': 'Zoom',
'amazon.com': 'Amazon', 'amazonaws.com': 'Amazon AWS',
'apple.com': 'Apple', 'icloud.com': 'iCloud',
'spotify.com': 'Spotify', 'wazuh.com': 'Wazuh (Security)',
'adobe.com': 'Adobe', 'dropbox.com': 'Dropbox',
};
function inferAppFromDomain(domain) {
if (!domain) return null;
const lower = domain.toLowerCase().replace(/^www\./, '');
if (DOMAIN_APP_MAP[lower]) return DOMAIN_APP_MAP[lower];
for (const [key, app] of Object.entries(DOMAIN_APP_MAP)) {
if (lower.endsWith('.' + key) || lower === key) return app;
}
return null;
}
module.exports = async function deviceDetailsHandler(req, res, helpers) {
const t0 = Date.now();
try {
const {
getTimeFilter, getBaseFilter, generateMacFromIp,
resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel
} = helpers;
const baseFilter = getBaseFilter(req);
let ip = String(req.query.ip ?? '');
const mac = String(req.query.mac ?? '');
if (!ip && mac) {
const dev = await DeviceStat.findOne({ mac_address: mac, ...baseFilter }).sort({ timestamp: -1 }).lean();
if (dev) {
ip = dev.ip_address;
} else {
const flow = await Flow.findOne({ src_mac: mac, ...baseFilter }).sort({ timestamp: -1 }).lean();
if (flow) ip = flow.src_ip;
}
}
if (!ip && !mac) return res.status(400).json({ ok: false, message: 'ip or mac required' });
// Find device stats by ip if set, else by mac
const deviceQuery = ip ? { ip_address: ip } : { mac_address: mac };
const device = await DeviceStat.findOne({ ...deviceQuery, ...baseFilter }).sort({ timestamp: -1 }).lean();
if (!ip && device?.ip_address) {
ip = device.ip_address;
}
const agentUuid = baseFilter.agent_uuid || device?.agent_uuid || null;
// ── PRIMARY bandwidth source ─────────────────────────────────────────────
const totalDownload = device?.download || 0;
const totalUpload = device?.upload || 0;
// ── Flow filter ──────────────────────────────────────────────────────────
const flowFilter = {};
if (agentUuid) flowFilter.agent_uuid = agentUuid;
if (req.user?.site_uuid) flowFilter.site_uuid = req.user.site_uuid;
const rawTimeRange = String(req.query.timeRange ?? 'all');
if (rawTimeRange !== 'all') {
const tf = getTimeFilter(req);
if (tf) flowFilter.timestamp = tf;
}
// ── Parallel queries ─────────────────────────────────────────────────────
const flowQueryConditions = [];
if (ip) {
flowQueryConditions.push({ src_ip: ip }, { dst_ip: ip });
}
if (mac) {
flowQueryConditions.push({ src_mac: mac }, { dst_mac: mac });
}
const threatQuery = {
...(agentUuid ? { agent_uuid: agentUuid } : {})
};
if (ip && mac) {
threatQuery.$or = [{ ip_address: ip }, { mac_address: mac }, { src_mac: mac }];
} else if (ip) {
threatQuery.ip_address = ip;
} else if (mac) {
threatQuery.$or = [{ mac_address: mac }, { src_mac: mac }];
}
const appFilter = agentUuid ? { agent_uuid: agentUuid, ip_address: ip } : { ip_address: ip };
if (req.user?.site_uuid) appFilter.site_uuid = req.user.site_uuid;
const [deviceAppStats, flowsQuery, rawThreats] = await Promise.all([
// Only query DeviceAppStat if we have an IP
ip ? DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean() : [],
flowQueryConditions.length > 0
? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(5000).lean()
: [],
Threat.find(threatQuery).sort({ detected_at: -1 }).lean(),
]);
// Aggregate by app_label and sum download and upload
const appLatest = {};
for (const a of deviceAppStats) {
const key = a.app_label;
if (!appLatest[key]) {
appLatest[key] = {
app_label: a.app_label,
download: 0,
upload: 0,
flows: 0,
first_seen: a.created_at || a.timestamp,
last_seen: a.updated_at || a.timestamp,
};
}
appLatest[key].download += a.download || 0;
appLatest[key].upload += a.upload || 0;
appLatest[key].flows += a.flows || 0;
if (new Date(a.timestamp) > new Date(appLatest[key].last_seen)) {
appLatest[key].last_seen = a.timestamp;
}
}
const apps = Object.values(appLatest)
.filter(a => (a.download || 0) + (a.upload || 0) > 0)
.sort((a, b) => (b.download || 0) - (a.download || 0));
// ── Protocol / Domain tabs — from Flow collection ────────────────────────
const protocolsMap = {}, domainsMap = {}, destinationsMap = {};
const bump = (map, key, down, up, ls) => {
if (!map[key]) map[key] = { app_label: key, download: 0, upload: 0, last_seen: ls };
else if (new Date(ls) > new Date(map[key].last_seen)) map[key].last_seen = ls;
map[key].download += down;
map[key].upload += up;
};
for (const f of flowsQuery) {
const isOutbound = ip ? (f.src_ip === ip) : (mac ? (f.src_mac === mac) : false);
if (!isOutbound) continue; // outbound only
const down = f.download || 0;
const up = f.upload || 0;
const ls = f.last_seen || (f.timestamp ? new Date(f.timestamp).toISOString() : new Date().toISOString());
if (f.app_label) bump(protocolsMap, f.app_label, down, up, ls);
else if (f.protocol) bump(protocolsMap, f.protocol, down, up, ls);
const domainVal = f.sni_hostname || f.domain;
if (domainVal) bump(domainsMap, domainVal, down, up, ls);
if (f.dst_ip) bump(destinationsMap, f.dst_ip, down, up, ls);
}
// ── Device metadata ───────────────────────────────────────────────────────
const targetMac = device?.mac_address || mac || generateMacFromIp(ip);
const type = (device?.device_type && !['−', 'Unknown', '-'].includes(device.device_type)) ? device.device_type : resolveDeviceTypeFromIp(ip);
const man = (device?.manufacturer && !['−', 'Unknown', '-'].includes(device.manufacturer)) ? device.manufacturer : resolveVendorFromIp(ip);
const os = (device?.os_label && !['−', 'Unknown', '-'].includes(device.os_label)) ? device.os_label : resolveOSFromIp(ip);
const lastSeen = device?.last_seen || device?.timestamp || new Date().toISOString();
const customLabelDoc = await CustomDeviceLabel.findOne({ mac_address: targetMac }).lean();
const baseLabel = customLabelDoc?.device_label || device?.device_label;
const finalLabel = baseLabel && !['−', 'Unknown', 'Generic Client', '-'].includes(baseLabel)
? baseLabel : generateAutoLabel(ip, targetMac, man, type);
let agent_label = agentUuid;
if (agentUuid) {
const agentUser = await User.findOne({ agent_uuid: agentUuid, role: 'AGENT_VIEWER' }).lean();
if (agentUser?.account_name) agent_label = agentUser.account_name;
}
const threats = rawThreats.map(t => ({
id: t._id?.toString(),
threat_type: t.threat_type,
severity: t.severity,
ip_address: t.ip_address || t.src_ip,
dst_ip: t.dst_ip,
mac_address: t.mac_address || t.src_mac || null,
app_label: t.app_label || null,
domain: t.domain || null,
detected_at: t.detected_at || t.timestamp,
description: t.description || `Suspicious activity detected from ${t.ip_address || t.src_ip}`,
agent_uuid: t.agent_uuid,
}));
const flows = flowsQuery
.filter(f => ip ? (f.src_ip === ip) : (mac ? (f.src_mac === mac) : false))
.map(f => ({
flow_id: f.flow_id || f._id.toString(),
src_ip: f.src_ip,
dst_ip: f.dst_ip,
dst_port: f.dst_port,
protocol: f.protocol,
app_label: inferAppFromDomain(f.sni_hostname || f.domain) || f.app_label || 'Other',
domain: f.sni_hostname || f.domain || null,
download: f.download || 0,
upload: f.upload || 0,
last_seen: f.last_seen || (f.timestamp ? new Date(f.timestamp).toISOString() : null),
}));
const elapsed = Date.now() - t0;
console.log(`[DeviceDetails] ip=${ip} agent=${agentUuid} appsFromDB=${apps.length} flows=${flowsQuery.length} dl=${(totalDownload/1e9).toFixed(2)}GB time=${elapsed}ms`);
return res.json({
ok: true,
data: {
ip_address: ip,
mac_address: targetMac,
device_label: finalLabel,
device_type: type,
os_label: os,
manufacturer: man,
last_seen: lastSeen,
total_download: totalDownload,
total_upload: totalUpload,
agent_uuid: agentUuid,
agent_label,
flows,
apps,
protocols: Object.values(protocolsMap).sort((a, b) => b.download - a.download),
domains: Object.values(domainsMap).sort((a, b) => b.download - a.download),
destinations: Object.values(destinationsMap).sort((a, b) => b.download - a.download),
threats,
},
});
} catch (err) {
console.error('[DeviceDetailsHandler] Error:', err);
return res.status(500).json({ ok: false, message: err.message });
}
};