feat: device detail drill-down modal + smart domain/app display

- Add DeviceDetailModal with 8 tabs (Info, Flows, Apps, Encryption,
  Servers, Password Leaks, IP Reputation, VPN & Events)
- Make device cards in AgentDetailModal clickable (nested modal drill-down)
- Expand fetchDeviceDetails backend to query all 10 correlated tables
  (devices, flows, intel_device_discovery, intel_encryption_audit,
   intel_server_discovery, intel_unencrypted_passwords, intel_ip_reputation,
   intel_vpn_detection, events, mac_bandwidth)
- Smart combined app/domain display: prioritize actual domain names over
  port-only labels (Port 443 -> scontent.fcgk42-1.fna.fbcdn.net)
- Update Flows tab: domain shown in teal monospace, protocols in purple,
  port-only entries muted
- Add 10 new TypeScript interfaces for device detail data types
- Add MAC address fallback queries (by MAC when IP yields no results)
- Fix fmtBytes for very large values
This commit is contained in:
vanne committed 2026-06-30 23:58:04 +07:00
1 parent bba75c7dc6
commit 26179a0270
8 files changed
+2171 -400

No files matched your search

+394 -87
View File
@@ -1214,16 +1214,16 @@ async function fetchAgentDetails(agentUuid) {
const db = require('./database');
const d = db.getDB();
const label = AGENT_LABELS[agentUuid] || agentUuid;
const macs = AGENT_MAC_MAP[agentUuid];
const label = AGENT_LABELS[agentUuid] || agentUuid;
const macs = AGENT_MAC_MAP[agentUuid];
if (!macs || macs.length === 0) {
return { agent_uuid: agentUuid, agent_label: label, summary: null, devices: [], flows: [], top_apps: [] };
}
const emptyResult = { agent_uuid: agentUuid, agent_label: label, summary: null, devices: [], flows: [], top_apps: [], security: { encryption_audit: [], insecure_protocols: [], unencrypted_passwords: [], ip_reputation: [], tor_detections: [], vpn_detections: [] }, events: [], mac_bandwidth: [], server_discovery: [] };
if (!macs || macs.length === 0) return emptyResult;
const ph = inClause(macs);
// 1. Top apps by this agent (aggregate from flows)
// ── 1. Top apps by this agent (from flows) ─────────────────────────────────
const appRows = d.prepare(`
SELECT app_label,
SUM(bytes_download) AS download,
@@ -1246,7 +1246,7 @@ async function fetchAgentDetails(agentUuid) {
upload : r.upload ?? 0,
}));
// 2. Distinct devices (src_ip + device info + risk badge) for this agent
// ── 2. Distinct devices for this agent ─────────────────────────────────────
const devRows = d.prepare(`
SELECT f.src_ip AS ip_address,
f.src_mac AS mac_address,
@@ -1266,51 +1266,48 @@ async function fetchAgentDetails(agentUuid) {
WHERE f.src_mac IN (${ph})
GROUP BY f.src_ip
ORDER BY dl DESC
LIMIT 50
LIMIT 100
`).all(...macs);
// Risk badge per device from intel_encryption_audit (latest snapshot)
const agentIPs = [...new Set(devRows.map(r => r.ip_address).filter(Boolean))];
const phIPs = agentIPs.length > 0 ? agentIPs.map(() => '?').join(',') : null;
const latestEncAudit = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t;
const riskMap = {};
if (latestEncAudit) {
const riskRows = d.prepare(`
SELECT ip_address, encrypted_pct, risk_level
FROM intel_encryption_audit
WHERE fetched_at = ?
`).all(latestEncAudit);
const riskRows = d.prepare(`SELECT ip_address, encrypted_pct, risk_level FROM intel_encryption_audit WHERE fetched_at = ?`).all(latestEncAudit);
for (const r of riskRows) {
if (r.ip_address) riskMap[r.ip_address] = { encrypted_pct: r.encrypted_pct, risk_level: r.risk_level };
}
}
// Insecure protocol flags per IP
const insecureIPs = new Set(
d.prepare(`SELECT DISTINCT ip_address FROM intel_insecure_protocols WHERE ip_address IS NOT NULL`).all().map(r => r.ip_address)
phIPs ? d.prepare(`SELECT DISTINCT ip_address FROM intel_insecure_protocols WHERE ip_address IN (${phIPs})`).all(...agentIPs).map(r => r.ip_address) : []
);
const devices = devRows.map(r => ({
ip_address : r.ip_address,
mac_address : r.mac_address,
device_label : r.device_label || r.ip_address || 'Unknown',
device_type : r.device_type || null,
os_label : r.os_label || null,
manufacturer : r.manufacturer || null,
last_seen : r.last_seen || null,
download : r.dl ?? 0,
upload : r.ul ?? 0,
encrypted_pct : riskMap[r.ip_address]?.encrypted_pct ?? null,
risk_level : riskMap[r.ip_address]?.risk_level ?? null,
has_insecure : insecureIPs.has(r.ip_address),
ip_address : r.ip_address,
mac_address : r.mac_address,
device_label : r.device_label || r.ip_address || 'Unknown',
device_type : r.device_type || null,
os_label : r.os_label || null,
manufacturer : r.manufacturer || null,
last_seen : r.last_seen || null,
download : r.dl ?? 0,
upload : r.ul ?? 0,
encrypted_pct: riskMap[r.ip_address]?.encrypted_pct ?? null,
risk_level : riskMap[r.ip_address]?.risk_level ?? null,
has_insecure : insecureIPs.has(r.ip_address),
}));
// 3. Recent flows for this agent
// ── 3. Recent flows for this agent ─────────────────────────────────────────
const flowRows = d.prepare(`
SELECT src_ip, dst_ip, dst_port, protocol, app_label, domain,
bytes_download AS download, bytes_upload AS upload, last_seen
FROM flows
WHERE src_mac IN (${ph})
ORDER BY last_seen DESC
LIMIT 50
LIMIT 100
`).all(...macs);
const flows = flowRows.map(r => ({
@@ -1325,7 +1322,7 @@ async function fetchAgentDetails(agentUuid) {
last_seen : r.last_seen,
}));
// 4. Summary stats
// ── 4. Summary stats ────────────────────────────────────────────────────────
const sumRow = d.prepare(`
SELECT COUNT(DISTINCT src_ip) AS device_count,
COUNT(*) AS flow_count,
@@ -1336,22 +1333,128 @@ async function fetchAgentDetails(agentUuid) {
`).get(...macs);
const summary = sumRow ? {
total_devices : sumRow.device_count,
active_flows : sumRow.flow_count,
bandwidth_down : sumRow.total_download,
bandwidth_up : sumRow.total_upload,
total_devices : sumRow.device_count ?? 0,
active_flows : sumRow.flow_count ?? 0,
bandwidth_down : sumRow.total_download ?? 0,
bandwidth_up : sumRow.total_upload ?? 0,
} : null;
return { agent_uuid: agentUuid, agent_label: label, summary, devices, flows, top_apps };
// ── 5. Security Intel filtered by agent IPs & MACs ─────────────────────────
const encryptionRows = (latestEncAudit && phIPs)
? d.prepare(`SELECT ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level, detected_at FROM intel_encryption_audit WHERE fetched_at = ? AND ip_address IN (${phIPs}) ORDER BY CASE risk_level WHEN 'Rawan' THEN 1 WHEN 'Sedang' THEN 2 ELSE 3 END`).all(latestEncAudit, ...agentIPs)
: [];
const insecureProtoRows = phIPs
? d.prepare(`SELECT ip_address, mac_address, protocol, risk, app_label, dst_ip, dst_port, download, upload, detected_at FROM intel_insecure_protocols WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs)
: [];
let unencPwdRows = d.prepare(`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 50`).all(...macs);
if (unencPwdRows.length === 0 && phIPs) {
unencPwdRows = d.prepare(`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs);
}
const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t;
const ipReputRows = (latestRepSnap && phIPs)
? d.prepare(`SELECT ip_address, local_ip, mac_address, reputation, score, country, app_label, blacklisted, download, upload, detected_at FROM intel_ip_reputation WHERE fetched_at = ? AND (local_ip IN (${phIPs}) OR ip_address IN (${phIPs})) ORDER BY score DESC LIMIT 50`).all(latestRepSnap, ...agentIPs, ...agentIPs)
: [];
let torRows = d.prepare(`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 20`).all(...macs);
if (torRows.length === 0 && phIPs) {
torRows = d.prepare(`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 20`).all(...agentIPs);
}
let vpnRows = d.prepare(`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 20`).all(...macs);
if (vpnRows.length === 0 && phIPs) {
vpnRows = d.prepare(`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 20`).all(...agentIPs);
}
const serverDiscRows = phIPs
? d.prepare(`SELECT ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload, detected_at FROM intel_server_discovery WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs)
: [];
const security = {
encryption_audit : encryptionRows,
insecure_protocols : insecureProtoRows,
unencrypted_passwords: unencPwdRows,
ip_reputation : ipReputRows,
tor_detections : torRows,
vpn_detections : vpnRows,
};
// ── 6. Events filtered by agent IPs & MACs ─────────────────────────────────
const eventsByIP = phIPs ? d.prepare(`SELECT event_id, event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE ip_address IN (${phIPs}) ORDER BY event_at DESC LIMIT 100`).all(...agentIPs) : [];
const eventsByMAC = d.prepare(`SELECT event_id, event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE mac_address IN (${ph}) ORDER BY event_at DESC LIMIT 100`).all(...macs);
const seenEvt = new Set();
const allEvents = [];
for (const r of [...eventsByIP, ...eventsByMAC]) {
const key = r.event_id || `${r.ip_address}:${r.event_at}`;
if (!seenEvt.has(key)) {
seenEvt.add(key);
allEvents.push({ event_id: r.event_id, event_type: r.event_type, severity: r.severity, ip_address: r.ip_address, mac_address: r.mac_address, description: r.description, event_at: r.event_at });
}
}
allEvents.sort((a, b) => (b.event_at || '').localeCompare(a.event_at || ''));
const events = allEvents.slice(0, 100);
// ── 7. MAC bandwidth for this agent's MACs ──────────────────────────────────
const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t;
const mac_bandwidth = latestMacSnap
? d.prepare(`SELECT mac_address, manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${ph}) ORDER BY download DESC`).all(latestMacSnap, ...macs)
: [];
return {
agent_uuid : agentUuid,
agent_label : label,
summary,
devices,
flows,
top_apps,
security,
events,
mac_bandwidth,
server_discovery: serverDiscRows,
};
}
// Fetch data for a specific device IP — from local DB flows + intel tables
// Fetch data for a specific device IP — from local DB (all 10 correlated tables)
async function fetchDeviceDetails(ip) {
const db = require('./database');
const d = db.getDB();
// Top apps used by this IP — aggregated from flows
const appRows = d.prepare(`
// ── 0. Resolve MAC from flows (most recent) ──────────────────────────────
const macRow = d.prepare(`SELECT src_mac FROM flows WHERE src_ip = ? AND src_mac IS NOT NULL ORDER BY last_seen DESC LIMIT 1`).get(ip);
const mac = macRow?.src_mac || null;
// ── 1. Device info from devices table ────────────────────────────────────
const deviceRow = d.prepare(`
SELECT device_label, device_type, os_label, manufacturer, download, upload, last_seen
FROM devices
WHERE ip_address = ?
ORDER BY fetched_at DESC
LIMIT 1
`).get(ip);
// ── 2. Discovery info (may differ from devices table) ────────────────────
const discRow = d.prepare(`
SELECT device_type, os_label, manufacturer, device_label, is_new
FROM intel_device_discovery
WHERE ip_address = ?
ORDER BY fetched_at DESC
LIMIT 1
`).get(ip);
const device_info = {
device_label : deviceRow?.device_label || discRow?.device_label || null,
device_type : deviceRow?.device_type || discRow?.device_type || null,
os_label : deviceRow?.os_label || discRow?.os_label || null,
manufacturer : deviceRow?.manufacturer || discRow?.manufacturer || null,
mac_address : mac,
is_new : discRow?.is_new ?? null,
};
// ── 3. Named apps (exclude "Port XXX" port-only entries) ─────────────────
const namedAppRows = d.prepare(`
SELECT app_label,
SUM(bytes_download) AS download,
SUM(bytes_upload) AS upload,
@@ -1359,28 +1462,94 @@ async function fetchDeviceDetails(ip) {
FROM flows
WHERE src_ip = ?
AND app_label IS NOT NULL
AND app_label NOT LIKE 'Port %'
GROUP BY app_label
ORDER BY download DESC
LIMIT 20
`).all(ip);
const top_apps = appRows.map(r => ({
app_id : null,
app_label : r.app_label,
category : null,
favicon : null,
download : r.download ?? 0,
upload : r.upload ?? 0,
// ── 4. Top domains accessed by this device ─────────────────────────────
const domainRows = d.prepare(`
SELECT domain,
-- use app_label that appeared most with this domain
(SELECT app_label FROM flows
WHERE src_ip = f.src_ip AND domain = f.domain
AND app_label IS NOT NULL
ORDER BY bytes_download DESC LIMIT 1) AS app_label,
-- extract root domain for display
domain AS display_name,
SUM(bytes_download) AS download,
SUM(bytes_upload) AS upload,
COUNT(*) AS flow_count
FROM flows f
WHERE src_ip = ?
AND domain IS NOT NULL
GROUP BY domain
ORDER BY download DESC
LIMIT 30
`).all(ip);
// ── 5. Smart combined: flows with BOTH domain and app_label, or just one ─
// Build combined display list:
// Priority 1 = rows with actual domain (show domain as label)
// Priority 2 = rows with named app (not port-only)
// Merge & de-duplicate by display name
const combinedMap = new Map();
// Add domains first (higher priority)
for (const r of domainRows) {
combinedMap.set('domain:' + r.domain, {
label : r.domain, // the actual website/domain
sub_label : r.app_label || null, // protocol (HTTPS/TLS etc)
type : 'domain',
download : r.download ?? 0,
upload : r.upload ?? 0,
flow_count : r.flow_count,
});
}
// Add named apps that don't duplicate a domain entry
for (const r of namedAppRows) {
const key = 'app:' + r.app_label;
if (!combinedMap.has(key)) {
combinedMap.set(key, {
label : r.app_label, // protocol name (DNS, HTTPS/TLS, etc)
sub_label : null,
type : 'protocol',
download : r.download ?? 0,
upload : r.upload ?? 0,
flow_count : r.flow_count,
});
}
}
// If neither domain nor named app found, fall back to ALL app_labels incl Port XXX
const top_apps = combinedMap.size > 0
? [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25)
: d.prepare(`
SELECT app_label AS label, NULL AS sub_label, 'port' AS type,
SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count
FROM flows WHERE src_ip = ? AND app_label IS NOT NULL
GROUP BY app_label ORDER BY download DESC LIMIT 25
`).all(ip).map(r => ({ label: r.label, sub_label: null, type: 'port', download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count }));
// top_domains: keep simple list for Info tab
const top_domains = domainRows.map(r => ({
domain : r.domain,
app_label : r.app_label,
download : r.download ?? 0,
upload : r.upload ?? 0,
flow_count : r.flow_count,
}));
// Recent flows from this IP
// ── 5. Recent flows ────────────────────────────────────────────────────
const flowRows = d.prepare(`
SELECT dst_ip, dst_port, protocol, app_label, domain,
bytes_download AS download, bytes_upload AS upload, last_seen
FROM flows
WHERE src_ip = ?
ORDER BY last_seen DESC
LIMIT 50
LIMIT 100
`).all(ip);
const flows = flowRows.map(r => ({
@@ -1394,7 +1563,7 @@ async function fetchDeviceDetails(ip) {
last_seen : r.last_seen,
}));
// Totals for this IP
// ── 6. Totals ─────────────────────────────────────────────────────────
const sumRow = d.prepare(`
SELECT SUM(bytes_download) AS total_download,
SUM(bytes_upload) AS total_upload,
@@ -1403,70 +1572,208 @@ async function fetchDeviceDetails(ip) {
WHERE src_ip = ?
`).get(ip);
// ── Security Info ──────────────────────────────────────────────
// Encryption audit (latest snapshot for this IP)
// ── 7. Encryption audit (latest snapshot) ─────────────────────────────
const latestAudit = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t;
const encRow = latestAudit
? d.prepare(`
SELECT encrypted_pct, encrypted, unencrypted, total, risk_level
SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, mac_address
FROM intel_encryption_audit
WHERE fetched_at = ? AND ip_address = ?
LIMIT 1
`).get(latestAudit, ip)
: null;
// Insecure protocols used by this IP
const insecureRows = d.prepare(`
SELECT DISTINCT protocol, risk
FROM intel_insecure_protocols
// Also try fallback by MAC if not found by IP
const encRowMac = (!encRow && mac && latestAudit)
? d.prepare(`
SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, ip_address
FROM intel_encryption_audit
WHERE fetched_at = ? AND mac_address = ?
ORDER BY detected_at DESC
LIMIT 1
`).get(latestAudit, mac)
: null;
const encFinal = encRow || encRowMac;
const encryption = encFinal ? {
encrypted_pct : encFinal.encrypted_pct ?? null,
encrypted_bytes : encFinal.encrypted ?? null,
unencrypted_bytes: encFinal.unencrypted ?? null,
total_bytes : encFinal.total ?? null,
risk_level : encFinal.risk_level ?? null,
} : null;
// ── 8. Server discovery (servers this device accessed) ─────────────────
const serverRows = d.prepare(`
SELECT DISTINCT server_type, hostname, port, protocol, os_label,
MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at
FROM intel_server_discovery
WHERE ip_address = ?
GROUP BY server_type, port, protocol
ORDER BY download DESC
LIMIT 50
`).all(ip);
// Device discovery info (OS, type, manufacturer)
const discRow = d.prepare(`
SELECT device_type, os_label, manufacturer, device_label
FROM intel_device_discovery
// fallback by MAC if no rows by IP
const serverRowsMac = (serverRows.length === 0 && mac)
? d.prepare(`
SELECT DISTINCT server_type, hostname, port, protocol, os_label,
MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at
FROM intel_server_discovery
WHERE mac_address = ?
GROUP BY server_type, port, protocol
ORDER BY download DESC
LIMIT 50
`).all(mac)
: [];
const server_discovery = (serverRows.length > 0 ? serverRows : serverRowsMac).map(r => ({
server_type : r.server_type,
hostname : r.hostname || null,
port : r.port,
protocol : r.protocol,
os_label : r.os_label || null,
download : r.download ?? 0,
upload : r.upload ?? 0,
detected_at : r.detected_at,
}));
// ── 9. Unencrypted passwords ───────────────────────────────────────────
let pwdRows = d.prepare(`
SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at
FROM intel_unencrypted_passwords
WHERE ip_address = ?
ORDER BY fetched_at DESC
LIMIT 1
`).get(ip);
ORDER BY detected_at DESC
LIMIT 50
`).all(ip);
// IP reputation (is this device flagged?)
const reputRow = d.prepare(`
SELECT reputation, score, blacklisted
FROM intel_ip_reputation
WHERE local_ip = ? OR ip_address = ?
ORDER BY fetched_at DESC
LIMIT 1
`).get(ip, ip);
if (pwdRows.length === 0 && mac) {
pwdRows = d.prepare(`
SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at
FROM intel_unencrypted_passwords
WHERE mac_address = ?
ORDER BY detected_at DESC
LIMIT 50
`).all(mac);
}
const security_info = {
encrypted_pct : encRow?.encrypted_pct ?? null,
encrypted_bytes : encRow?.encrypted ?? null,
unencrypted_bytes: encRow?.unencrypted ?? null,
risk_level : encRow?.risk_level ?? null,
insecure_protocols: insecureRows.map(r => ({ protocol: r.protocol, risk: r.risk })),
device_type : discRow?.device_type ?? null,
os_label : discRow?.os_label ?? null,
manufacturer : discRow?.manufacturer ?? null,
device_label : discRow?.device_label ?? null,
reputation : reputRow?.reputation ?? null,
rep_score : reputRow?.score ?? null,
blacklisted : reputRow?.blacklisted ?? false,
};
const unencrypted_passwords = pwdRows.map(r => ({
dst_ip : r.dst_ip,
dst_port : r.dst_port,
protocol : r.protocol,
username : r.username,
severity : r.severity,
download : r.download ?? 0,
upload : r.upload ?? 0,
detected_at : r.detected_at,
}));
// ── 10. IP Reputation (latest snapshot, this device's local_ip) ─────────
const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t;
const repRows = latestRepSnap
? d.prepare(`
SELECT ip_address, local_ip, reputation, score, country, app_label, blacklisted, download, upload
FROM intel_ip_reputation
WHERE fetched_at = ? AND (local_ip = ? OR ip_address = ?)
ORDER BY score DESC NULLS LAST
LIMIT 30
`).all(latestRepSnap, ip, ip)
: [];
const ip_reputation = repRows.map(r => ({
remote_ip : r.ip_address,
local_ip : r.local_ip,
reputation : r.reputation,
score : r.score,
country : r.country,
app_label : r.app_label,
blacklisted : !!r.blacklisted,
download : r.download ?? 0,
upload : r.upload ?? 0,
}));
// ── 11. VPN detection ─────────────────────────────────────────────────
let vpnRows = d.prepare(`
SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at
FROM intel_vpn_detection
WHERE ip_address = ?
ORDER BY detected_at DESC
LIMIT 20
`).all(ip);
if (vpnRows.length === 0 && mac) {
vpnRows = d.prepare(`
SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at
FROM intel_vpn_detection
WHERE mac_address = ?
ORDER BY detected_at DESC
LIMIT 20
`).all(mac);
}
const vpn_detections = vpnRows.map(r => ({
vpn_type : r.vpn_type,
remote_ip : r.remote_ip,
protocol : r.protocol,
country : r.country,
confidence : r.confidence,
download : r.download ?? 0,
upload : r.upload ?? 0,
detected_at : r.detected_at,
}));
// ── 12. Events ────────────────────────────────────────────────────────
const evtByIP = d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE ip_address = ? ORDER BY event_at DESC LIMIT 50`).all(ip);
const evtByMAC = mac ? d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE mac_address = ? ORDER BY event_at DESC LIMIT 50`).all(mac) : [];
const seenEvt = new Set();
const evtMerged = [];
for (const r of [...evtByIP, ...evtByMAC]) {
const key = `${r.event_type}:${r.event_at}`;
if (!seenEvt.has(key)) {
seenEvt.add(key);
evtMerged.push({ event_type: r.event_type, severity: r.severity, ip_address: r.ip_address, mac_address: r.mac_address, description: r.description, event_at: r.event_at });
}
}
evtMerged.sort((a, b) => (b.event_at || '').localeCompare(a.event_at || ''));
const events = evtMerged.slice(0, 100);
// ── 13. MAC bandwidth (latest snapshot) ───────────────────────────────
const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t;
const macBwRow = (latestMacSnap && mac)
? d.prepare(`SELECT manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address = ? LIMIT 1`).get(latestMacSnap, mac)
: null;
const mac_bandwidth = macBwRow ? {
mac_address : mac,
manufacturer : macBwRow.manufacturer,
download : macBwRow.download ?? 0,
upload : macBwRow.upload ?? 0,
total : macBwRow.total ?? 0,
} : null;
return {
ip,
mac_address : mac,
total_download : sumRow?.total_download ?? 0,
total_upload : sumRow?.total_upload ?? 0,
flow_count : sumRow?.flow_count ?? 0,
device_info,
top_apps,
top_domains : domainRows.map(r => ({ domain: r.domain, download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count })),
flows,
security_info,
encryption,
server_discovery,
unencrypted_passwords,
ip_reputation,
vpn_detections,
events,
mac_bandwidth,
};
}
// Fetch data for a specific application — from local DB
// Fetch data for a specific application// Fetch data for a specific application — from local DB
async function fetchAppDetails(appLabel) {
const db = require('./database');
const d = db.getDB();