From 26179a02704c3d4cb62e377a3a310710c5aa8cac Mon Sep 17 00:00:00 2001 From: vanne Date: Tue, 30 Jun 2026 23:58:04 +0700 Subject: [PATCH] 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 --- backend/netify.js | 481 +++++++++++++--- scratch/check_domain_apps.js | 38 ++ scratch/patch_device_details.js | 325 +++++++++++ scratch/patch_netify.js | 196 +++++++ src/components/ui/AgentDetailModal.tsx | 568 ++++++++++++++----- src/components/ui/DeviceDetailModal.tsx | 713 ++++++++++++++++++------ src/lib/api.ts | 241 +++++++- src/lib/utils.ts | 9 +- 8 files changed, 2171 insertions(+), 400 deletions(-) create mode 100644 scratch/check_domain_apps.js create mode 100644 scratch/patch_device_details.js create mode 100644 scratch/patch_netify.js diff --git a/backend/netify.js b/backend/netify.js index e5e578f..077e86e 100644 --- a/backend/netify.js +++ b/backend/netify.js @@ -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(); diff --git a/scratch/check_domain_apps.js b/scratch/check_domain_apps.js new file mode 100644 index 0000000..dc8bb3b --- /dev/null +++ b/scratch/check_domain_apps.js @@ -0,0 +1,38 @@ +const db = require('../backend/database').getDB(); + +// Check all flows - how many have domain vs just port +const stats = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN domain IS NOT NULL THEN 1 ELSE 0 END) as with_domain, + SUM(CASE WHEN app_label IS NOT NULL AND app_label NOT LIKE 'Port %' THEN 1 ELSE 0 END) as named_app, + SUM(CASE WHEN app_label LIKE 'Port %' THEN 1 ELSE 0 END) as port_only, + SUM(CASE WHEN app_label IS NULL AND domain IS NULL THEN 1 ELSE 0 END) as both_null + FROM flows +`).get(); +console.log('Flow stats:', stats); + +// What are the unique domain values? +const uniqueDomains = db.prepare(`SELECT DISTINCT domain FROM flows WHERE domain IS NOT NULL LIMIT 20`).all(); +console.log('\nUnique domains in DB:', uniqueDomains.length); +uniqueDomains.forEach(r => console.log(' ', r.domain)); + +// What devices have domain data, and how many? +console.log('\n\nDevices with domain+app data:'); +const devDomains = db.prepare(` + SELECT src_ip, src_mac, + COUNT(*) total_flows, + SUM(CASE WHEN domain IS NOT NULL THEN 1 ELSE 0 END) domain_flows, + SUM(CASE WHEN app_label IS NOT NULL AND app_label NOT LIKE 'Port %' THEN 1 ELSE 0 END) named_flows, + GROUP_CONCAT(DISTINCT domain) sample_domains + FROM flows + WHERE domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %') + GROUP BY src_ip + ORDER BY domain_flows DESC + LIMIT 10 +`).all(); +devDomains.forEach(r => { + const {sample_domains, ...rest} = r; + console.log(JSON.stringify(rest)); + if (sample_domains) console.log(' domains:', sample_domains.split(',').slice(0,3).join(', ')); +}); diff --git a/scratch/patch_device_details.js b/scratch/patch_device_details.js new file mode 100644 index 0000000..b68cb70 --- /dev/null +++ b/scratch/patch_device_details.js @@ -0,0 +1,325 @@ +// patch_device_details.js — replaces fetchDeviceDetails in netify.js +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, '..', 'backend', 'netify.js'); +let content = fs.readFileSync(filePath, 'utf8'); + +const startMarker = '// Fetch data for a specific device IP — from local DB flows + intel tables\r\nasync function fetchDeviceDetails(ip) {'; +const endMarker = '}\r\n\r\n// Fetch data for a specific application'; + +const startIdx = content.indexOf(startMarker); +const endIdx = content.indexOf('// Fetch data for a specific application'); + +if (startIdx === -1) { console.error('START not found'); process.exit(1); } +if (endIdx === -1) { console.error('END not found'); process.exit(1); } + +console.log(`Found fetchDeviceDetails: char ${startIdx} → ${endIdx}`); + +const replacement = `// 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(); + + // ── 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. Top apps (flows GROUP BY app_label) ───────────────────────────── + const appRows = d.prepare(\` + SELECT app_label, + 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 20 + \`).all(ip); + + const top_apps = appRows.map(r => ({ + app_label : r.app_label, + download : r.download ?? 0, + upload : r.upload ?? 0, + flow_count : r.flow_count, + })); + + // ── 4. Top domains (flows GROUP BY domain) ───────────────────────────── + const domainRows = d.prepare(\` + SELECT domain, + SUM(bytes_download) AS download, + SUM(bytes_upload) AS upload, + COUNT(*) AS flow_count + FROM flows + WHERE src_ip = ? + AND domain IS NOT NULL + GROUP BY domain + ORDER BY download DESC + LIMIT 20 + \`).all(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 100 + \`).all(ip); + + const flows = flowRows.map(r => ({ + dst_ip : r.dst_ip, + dst_port : r.dst_port, + protocol : r.protocol, + app_label : r.app_label, + domain : r.domain, + download : r.download ?? 0, + upload : r.upload ?? 0, + last_seen : r.last_seen, + })); + + // ── 6. Totals ───────────────────────────────────────────────────────── + const sumRow = d.prepare(\` + SELECT SUM(bytes_download) AS total_download, + SUM(bytes_upload) AS total_upload, + COUNT(*) AS flow_count + FROM flows + WHERE src_ip = ? + \`).get(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, mac_address + FROM intel_encryption_audit + WHERE fetched_at = ? AND ip_address = ? + LIMIT 1 + \`).get(latestAudit, ip) + : null; + + // 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); + + // 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 detected_at DESC + LIMIT 50 + \`).all(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 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, + encryption, + server_discovery, + unencrypted_passwords, + ip_reputation, + vpn_detections, + events, + mac_bandwidth, + }; +} + +// Fetch data for a specific application`; + +const newContent = content.slice(0, startIdx) + replacement + content.slice(endIdx); +fs.writeFileSync(filePath, newContent, 'utf8'); +console.log('SUCCESS: Patched fetchDeviceDetails, new file length:', newContent.length); diff --git a/scratch/patch_netify.js b/scratch/patch_netify.js new file mode 100644 index 0000000..bf327c4 --- /dev/null +++ b/scratch/patch_netify.js @@ -0,0 +1,196 @@ +// patch_netify.js — patches the fetchAgentDetails function in netify.js +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, '..', 'backend', 'netify.js'); +let content = fs.readFileSync(filePath, 'utf8'); + +// Find the start marker (after the top_apps mapping block) +const startMarker = ' // ── 2. Distinct devices for this agent ─────────────────────────────────────\n const devRows = d.prepare(`\n WHERE src_mac IN (${ph})\n ORDER BY last_seen DESC\n LIMIT 50\n `).all(...macs);'; + +const endMarker = 'return { agent_uuid: agentUuid, agent_label: label, summary, devices, flows, top_apps };\r\n}'; + +const startIdx = content.indexOf(' // ── 2. Distinct devices for this agent ─────────────────────────────────────'); +const endIdx = content.indexOf('return { agent_uuid: agentUuid, agent_label: label, summary, devices, flows, top_apps };\r\n}'); + +if (startIdx === -1) { console.error('START MARKER NOT FOUND'); process.exit(1); } +if (endIdx === -1) { console.error('END MARKER NOT FOUND'); process.exit(1); } + +console.log(`Found start at char ${startIdx}, end at char ${endIdx}`); + +const endOffset = endIdx + endMarker.length; + +const replacement = ` // ── 2. Distinct devices for this agent ───────────────────────────────────── + const devRows = d.prepare(\` + SELECT f.src_ip AS ip_address, + f.src_mac AS mac_address, + d.device_label, + d.device_type, + d.os_label, + d.manufacturer, + SUM(f.bytes_download) AS dl, + SUM(f.bytes_upload) AS ul, + MAX(f.last_seen) AS last_seen + FROM flows f + LEFT JOIN ( + SELECT ip_address, device_label, device_type, os_label, manufacturer + FROM devices + GROUP BY ip_address + ) d ON d.ip_address = f.src_ip + WHERE f.src_mac IN (\${ph}) + GROUP BY f.src_ip + ORDER BY dl DESC + LIMIT 100 + \`).all(...macs); + + 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); + for (const r of riskRows) { + if (r.ip_address) riskMap[r.ip_address] = { encrypted_pct: r.encrypted_pct, risk_level: r.risk_level }; + } + } + + const insecureIPs = new Set( + 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), + })); + + // ── 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 100 + \`).all(...macs); + + const flows = flowRows.map(r => ({ + src_ip : r.src_ip, + dst_ip : r.dst_ip, + dst_port : r.dst_port, + protocol : r.protocol, + app_label : r.app_label, + domain : r.domain, + download : r.download ?? 0, + upload : r.upload ?? 0, + last_seen : r.last_seen, + })); + + // ── 4. Summary stats ──────────────────────────────────────────────────────── + const sumRow = d.prepare(\` + SELECT COUNT(DISTINCT src_ip) AS device_count, + COUNT(*) AS flow_count, + SUM(bytes_download) AS total_download, + SUM(bytes_upload) AS total_upload + FROM flows + WHERE src_mac IN (\${ph}) + \`).get(...macs); + + const summary = sumRow ? { + 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; + + // ── 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, + }; +}`; + +const newContent = content.slice(0, startIdx) + replacement + content.slice(endOffset); +fs.writeFileSync(filePath, newContent, 'utf8'); +console.log('SUCCESS: Patched netify.js, new length:', newContent.length); diff --git a/src/components/ui/AgentDetailModal.tsx b/src/components/ui/AgentDetailModal.tsx index ffcc50a..330b3e2 100644 --- a/src/components/ui/AgentDetailModal.tsx +++ b/src/components/ui/AgentDetailModal.tsx @@ -3,7 +3,12 @@ import { useState, useEffect } from "react"; import { fetchAgentDetails, AgentDetails } from "@/lib/api"; import { fmtBytes } from "@/lib/utils"; -import { Loader2, X, Server, Monitor, Activity, Globe, ChevronRight } from "lucide-react"; +import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal"; +import { + Loader2, X, Server, Monitor, Activity, Globe, Shield, + Bell, Wifi, AlertTriangle, Lock, Unlock, ChevronRight, + Eye, Router +} from "lucide-react"; interface Props { agentUuid: string; @@ -11,13 +16,60 @@ interface Props { onClose: () => void; } -type Tab = "devices" | "flows" | "apps"; +type Tab = "devices" | "flows" | "apps" | "security" | "events" | "mac"; +// ── Severity colour helper ───────────────────────────────────────────────── +function severityStyle(sev: string | null) { + switch ((sev || "").toLowerCase()) { + case "critical": return { badge: "bg-red-500/15 text-red-400 ring-red-500/30", dot: "bg-red-400" }; + case "high": return { badge: "bg-orange-500/15 text-orange-400 ring-orange-500/30", dot: "bg-orange-400" }; + case "warning": return { badge: "bg-yellow-500/15 text-yellow-400 ring-yellow-500/30", dot: "bg-yellow-400" }; + case "info": return { badge: "bg-blue-500/15 text-blue-400 ring-blue-500/30", dot: "bg-blue-400" }; + default: return { badge: "bg-slate-500/15 text-slate-400 ring-slate-500/30", dot: "bg-slate-400" }; + } +} + +function riskStyle(level: string | null) { + switch ((level || "").toLowerCase()) { + case "rawan": + case "critical": return { badge: "bg-red-500/15 text-red-400 ring-red-500/30", dot: "bg-red-400" }; + case "sedang": + case "medium": return { badge: "bg-yellow-500/15 text-yellow-400 ring-yellow-500/30", dot: "bg-yellow-400" }; + case "aman": + case "low": return { badge: "bg-green-500/15 text-green-400 ring-green-500/30", dot: "bg-green-400" }; + default: return { badge: "bg-slate-500/15 text-slate-400 ring-slate-500/30", dot: "bg-slate-400" }; + } +} + +// ── Shared table wrapper ──────────────────────────────────────────────────── +function TableWrap({ children, head }: { children: React.ReactNode; head: string[] }) { + return ( +
+ + + + {head.map((h) => ( + + ))} + + + {children} +
{h}
+
+ ); +} + +function EmptyState({ msg }: { msg: string }) { + return

{msg}

; +} + +// ── Main component ────────────────────────────────────────────────────────── export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) { - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("devices"); + const [selectedDevice, setSelectedDevice] = useState<{ ip: string; label: string | null; mac: string | null } | null>(null); useEffect(() => { setIsLoading(true); @@ -28,21 +80,33 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) { .finally(() => setIsLoading(false)); }, [agentUuid]); + // Count security alerts + const secCount = data + ? (data.security?.insecure_protocols?.length ?? 0) + + (data.security?.unencrypted_passwords?.length ?? 0) + + (data.security?.tor_detections?.length ?? 0) + + (data.security?.vpn_detections?.length ?? 0) + : 0; + const tabs: { key: Tab; label: string; icon: React.ReactNode; count?: number }[] = [ - { key: "devices", label: "Devices", icon: , count: data?.devices.length }, - { key: "flows", label: "Active Flows", icon: , count: data?.flows.length }, - { key: "apps", label: "Top Apps", icon: , count: data?.top_apps.length }, + { key: "devices", label: "Devices", icon: , count: data?.devices.length }, + { key: "flows", label: "Flows", icon: , count: data?.flows.length }, + { key: "apps", label: "Top Apps", icon: , count: data?.top_apps.length }, + { key: "security", label: "Security", icon: , count: secCount || undefined }, + { key: "events", label: "Events", icon: , count: data?.events.length }, + { key: "mac", label: "MAC Bandwidth", icon: , count: data?.mac_bandwidth.length }, ]; return ( + <>
e.stopPropagation()} > - {/* Header */} -
+ {/* ── Header ── */} +
@@ -52,52 +116,52 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) {

{agentUuid}

-
- {/* Body */} -
+ {/* ── Body ── */} +
{isLoading ? (
-

Loading agent data...

+

Loading agent data…

) : error ? (
{error}
) : data ? ( <> - {/* KPI Summary row */} -
-
-

Devices

-

{data.devices.length}

-
-
-

Active Flows

-

{data.summary?.active_flows ?? data.flows.length}

-
-
-

Total Download

-

{fmtBytes(data.summary?.bandwidth_down ?? 0)}

-
-
-

Total Upload

-

{fmtBytes(data.summary?.bandwidth_up ?? 0)}

-
+ {/* ── KPI row ── */} +
+ {[ + { label: "Devices", value: data.devices.length, color: "text-white" }, + { label: "Flows", value: data.summary?.active_flows ?? data.flows.length, color: "text-white" }, + { label: "Download", value: fmtBytes(data.summary?.bandwidth_down ?? 0), color: "text-cyan-400" }, + { label: "Upload", value: fmtBytes(data.summary?.bandwidth_up ?? 0), color: "text-green-400" }, + ].map((kpi) => ( +
+

{kpi.label}

+

{kpi.value}

+
+ ))}
- {/* Tabs */} -
+ {/* ── Security alert strip ── */} + {secCount > 0 && ( +
+ + {secCount} security alerts detected on this agent — check the Security tab +
+ )} + + {/* ── Tabs ── */} +
{tabs.map((tab) => (
- {/* Tab content */} -
+ {/* ── Tab content ── */} +
+ + {/* DEVICES */} {activeTab === "devices" && ( -
- {data.devices.length === 0 ? ( -

No devices found for this agent.

- ) : ( - data.devices.map((d, i) => { - const riskCfg = d.risk_level === "Rawan" || d.risk_level === "Critical" - ? { color: "text-red-400", bg: "bg-red-500/10 ring-red-500/30", dot: "bg-red-400", label: d.risk_level } - : d.risk_level === "Sedang" || d.risk_level === "Medium" - ? { color: "text-yellow-400", bg: "bg-yellow-500/10 ring-yellow-500/30", dot: "bg-yellow-400", label: d.risk_level } - : d.risk_level === "Aman" || d.risk_level === "Low" - ? { color: "text-green-400", bg: "bg-green-500/10 ring-green-500/30", dot: "bg-green-400", label: d.risk_level } - : null; + data.devices.length === 0 ? : ( +
+ {data.devices.map((d, i) => { + const rc = riskStyle(d.risk_level); return ( -
+ ); - }) - )} -
+ })} +
+ ) )} + {/* FLOWS */} {activeTab === "flows" && ( -
- {data.flows.length === 0 ? ( -

No flows found for this agent.

- ) : ( - - - - - - - - - - - - - {data.flows.map((f, i) => ( - - - - - - - - - ))} - -
Source IPDestinationApp / DomainProtocolDownloadUpload
{f.src_ip || "-"} - {f.dst_ip || "-"} - {f.dst_port ? :{f.dst_port} : ""} - - {f.app_label || f.domain || "-"} - {f.protocol || "-"}{fmtBytes(f.download)}{fmtBytes(f.upload)}
- )} -
+ data.flows.length === 0 ? : ( + + {data.flows.map((f, i) => ( + + {f.src_ip || "—"} + + {f.dst_ip || "—"}{f.dst_port ? :{f.dst_port} : ""} + + {f.app_label || f.domain || "—"} + {f.protocol || "—"} + {fmtBytes(f.download)} + {fmtBytes(f.upload)} + {f.last_seen?.slice(0, 16) || "—"} + + ))} + + ) )} + {/* TOP APPS */} {activeTab === "apps" && ( -
- {data.top_apps.length === 0 ? ( -

No application data for this agent.

- ) : ( - data.top_apps.map((a, i) => { - const total = a.download + a.upload; - const maxTotal = Math.max(...data.top_apps.map(x => x.download + x.upload), 1); - const pct = (total / maxTotal) * 100; + data.top_apps.length === 0 ? : ( +
+ {data.top_apps.map((a, i) => { + const total = (a.download ?? 0) + (a.upload ?? 0); + const maxTotal = Math.max(...data.top_apps.map(x => (x.download ?? 0) + (x.upload ?? 0)), 1); + const pct = (total / maxTotal) * 100; return ( -
+
- {a.favicon ? ( - {a.app_label} - ) : ( -
- )} + {a.favicon + ? {a.app_label} + :
+ } {a.app_label} {a.category && {a.category}} {fmtBytes(total)}
-
- ↓ {fmtBytes(a.download)} - ↑ {fmtBytes(a.upload)} +
+ ↓ {fmtBytes(a.download ?? 0)} + ↑ {fmtBytes(a.upload ?? 0)}
-
-
+
+
); - }) + })} +
+ ) + )} + + {/* SECURITY */} + {activeTab === "security" && ( +
+ {/* Encryption Audit */} +
+

+ Encryption Audit + {data.security?.encryption_audit.length ?? 0} devices +

+ {(data.security?.encryption_audit.length ?? 0) === 0 ? : ( + + {data.security!.encryption_audit.map((r, i) => { + const rc = riskStyle(r.risk_level); + return ( + + {r.ip_address || "—"} + {r.device_label || "—"} + +
+
+
+
+ {r.encrypted_pct ?? 0}% +
+ + + + + {r.risk_level || "—"} + + + {fmtBytes(r.encrypted ?? 0)} + {fmtBytes(r.unencrypted ?? 0)} + + ); + })} + + )} +
+ + {/* Insecure Protocols */} +
+

+ Insecure Protocols + {data.security?.insecure_protocols.length ?? 0} records +

+ {(data.security?.insecure_protocols.length ?? 0) === 0 ? : ( + + {data.security!.insecure_protocols.map((r, i) => { + const sc = severityStyle(r.risk); + return ( + + {r.ip_address || "—"} + {r.protocol || "—"} + + + + {r.risk || "—"} + + + {r.app_label || "—"} + {r.dst_ip ? `${r.dst_ip}:${r.dst_port ?? "?"}` : "—"} + {fmtBytes(r.download)} + {fmtBytes(r.upload)} + + ); + })} + + )} +
+ + {/* Unencrypted Passwords */} + {(data.security?.unencrypted_passwords.length ?? 0) > 0 && ( +
+

+ Unencrypted Passwords Detected! + {data.security!.unencrypted_passwords.length} records +

+ + {data.security!.unencrypted_passwords.map((r, i) => { + const sc = severityStyle(r.severity); + return ( + + {r.ip_address || "—"} + {r.dst_ip ? `${r.dst_ip}:${r.dst_port ?? "?"}` : "—"} + {r.protocol || "—"} + {r.username || "—"} + + + + {r.severity || "—"} + + + + ); + })} + +
+ )} + + {/* IP Reputation */} + {(data.security?.ip_reputation.length ?? 0) > 0 && ( +
+

+ IP Reputation + {data.security!.ip_reputation.length} records +

+ + {data.security!.ip_reputation.map((r, i) => ( + + {r.ip_address || "—"} + {r.local_ip || "—"} + {r.reputation || "—"} + {r.score ?? "—"} + {r.country || "—"} + + {r.blacklisted + ? ⛔ Yes + : ✓ No + } + + + ))} + +
+ )} + + {/* Tor & VPN side-by-side */} +
+ {/* Tor */} +
+

+ Tor Detections + {data.security?.tor_detections.length ?? 0} +

+ {(data.security?.tor_detections.length ?? 0) === 0 + ? + :
{data.security!.tor_detections.map((r, i) => ( +
+
+ {r.ip_address || "—"} + {r.country || "—"} +
+
Exit: {r.exit_node || "—"}
+
+ ))}
+ } +
+ + {/* VPN */} +
+

+ VPN Detections + {data.security?.vpn_detections.length ?? 0} +

+ {(data.security?.vpn_detections.length ?? 0) === 0 + ? + :
{data.security!.vpn_detections.map((r, i) => ( +
+
+ {r.ip_address || "—"} + {r.vpn_type || "—"} +
+
Remote: {r.remote_ip || "—"} • {r.country || "—"}
+ {r.confidence &&
Confidence: {(r.confidence * 100).toFixed(0)}%
} +
+ ))}
+ } +
+
+ + {/* Server Discovery */} + {(data.server_discovery?.length ?? 0) > 0 && ( +
+

+ Servers Discovered + {data.server_discovery!.length} records +

+ + {data.server_discovery!.map((r, i) => ( + + {r.ip_address || "—"} + {r.hostname || "—"} + {r.server_type || "—"} + {r.port ?? "—"} + {r.protocol || "—"} + {r.os_label || "—"} + + ))} + +
)}
)} + + {/* EVENTS */} + {activeTab === "events" && ( + data.events.length === 0 ? : ( +
+ {data.events.map((ev, i) => { + const sc = severityStyle(ev.severity); + return ( +
+
+ + + {ev.severity || "—"} + +
+
+ {ev.event_type || "Unknown"} + {ev.ip_address || "—"} + {ev.mac_address && {ev.mac_address}} +
+ {ev.description &&

{ev.description}

} +
+ {ev.event_at?.slice(0, 16) || "—"} +
+
+ ); + })} +
+ ) + )} + + {/* MAC BANDWIDTH */} + {activeTab === "mac" && ( + data.mac_bandwidth.length === 0 ? : ( +
+ {data.mac_bandwidth.map((m, i) => { + const maxTotal = Math.max(...data.mac_bandwidth.map(x => x.total ?? 0), 1); + const pct = ((m.total ?? 0) / maxTotal) * 100; + return ( +
+
+ +
+

{m.mac_address}

+

{m.manufacturer || "Unknown Manufacturer"}

+
+
+

{fmtBytes(m.total ?? 0)}

+

Total

+
+
+
+ ↓ {fmtBytes(m.download ?? 0)} + ↑ {fmtBytes(m.upload ?? 0)} +
+
+
+
+
+ ); + })} +
+ ) + )} +
) : null}
+ {selectedDevice && selectedDevice.ip && ( + setSelectedDevice(null)} + /> + )} + ); } diff --git a/src/components/ui/DeviceDetailModal.tsx b/src/components/ui/DeviceDetailModal.tsx index f015c38..bdb7273 100644 --- a/src/components/ui/DeviceDetailModal.tsx +++ b/src/components/ui/DeviceDetailModal.tsx @@ -1,24 +1,73 @@ "use client"; import { useState, useEffect } from "react"; -import { fetchDeviceDetails, DeviceDetails } from "@/lib/api"; +import { + fetchDeviceDetails, DeviceDetails, +} from "@/lib/api"; import { fmtBytes } from "@/lib/utils"; -import { Loader2, X, Monitor, Globe, Activity } from "lucide-react"; +import { + Loader2, X, Monitor, Globe, Activity, Lock, Server, + AlertTriangle, Eye, Shield, Bell, Wifi, Unlock, Router, + Link +} from "lucide-react"; interface Props { - ip: string; + ip : string; deviceLabel?: string | null; - macAddress?: string | null; - onClose: () => void; + macAddress? : string | null; + onClose : () => void; } -type Tab = "apps" | "flows"; +type Tab = "info" | "flows" | "apps" | "encryption" | "servers" | "passwords" | "reputation" | "vpn_events"; +// ── helpers ─────────────────────────────────────────────────────────────────── +function sevStyle(sev: string | null) { + switch ((sev || "").toLowerCase()) { + case "critical": return "bg-red-500/15 text-red-400 ring-1 ring-red-500/30"; + case "high": return "bg-orange-500/15 text-orange-400 ring-1 ring-orange-500/30"; + case "warning": return "bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30"; + case "info": return "bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/30"; + default: return "bg-slate-700/40 text-slate-400"; + } +} + +function riskStyle(lvl: string | null) { + switch ((lvl || "").toLowerCase()) { + case "critical": + case "rawan": return { bar: "bg-red-500", badge: "bg-red-500/15 text-red-400 ring-1 ring-red-500/30" }; + case "medium": + case "sedang": return { bar: "bg-yellow-500", badge: "bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30" }; + case "low": + case "aman": return { bar: "bg-green-500", badge: "bg-green-500/15 text-green-400 ring-1 ring-green-500/30" }; + default: return { bar: "bg-slate-500", badge: "bg-slate-700/40 text-slate-400" }; + } +} + +function TableWrap({ head, children }: { head: string[]; children: React.ReactNode }) { + return ( +
+ + + + {head.map((h) => )} + + + {children} +
{h}
+
+ ); +} + +function Empty({ msg }: { msg: string }) { + return

{msg}

; +} + +// ── Main component ──────────────────────────────────────────────────────────── export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Props) { - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState("apps"); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState("info"); useEffect(() => { setIsLoading(true); @@ -29,141 +78,113 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop .finally(() => setIsLoading(false)); }, [ip]); - const tabs: { key: Tab; label: string; icon: React.ReactNode; count?: number }[] = [ - { key: "apps", label: "Applications", icon: , count: data?.top_apps.length }, - { key: "flows", label: "Active Flows", icon: , count: data?.flows.length }, + const di = data?.device_info; + + // Alert counts + const alertCount = + (data?.unencrypted_passwords.length ?? 0) + + (data?.vpn_detections.length ?? 0) + + (data?.events.filter(e => ["critical","high"].includes((e.severity||"").toLowerCase())).length ?? 0); + + const tabs: { key: Tab; label: string; icon: React.ReactNode; count?: number; alert?: boolean }[] = [ + { key: "info", label: "Info", icon: }, + { key: "flows", label: "Flows", icon: , count: data?.flows.length }, + { key: "apps", label: "Apps", icon: , count: data?.top_apps.length }, + { key: "encryption", label: "Enkripsi", icon: }, + { key: "servers", label: "Servers", icon: , count: data?.server_discovery.length }, + { key: "passwords", label: "Pwd Leaks", icon: , count: data?.unencrypted_passwords.length, alert: (data?.unencrypted_passwords.length ?? 0) > 0 }, + { key: "reputation", label: "Reputasi IP", icon: , count: data?.ip_reputation.length }, + { key: "vpn_events", label: "VPN & Events", icon: , count: (data?.vpn_detections.length ?? 0) + (data?.events.length ?? 0), alert: alertCount > 0 }, ]; return ( -
-
+
+
e.stopPropagation()} > - {/* Header */} -
+ {/* ── Header ── */} +
-

{deviceLabel || ip}

-
-

{ip}

- {macAddress &&

{macAddress}

} +

+ {di?.device_label || deviceLabel || ip} +

+
+ {ip} + {(di?.mac_address || macAddress) && ( + {di?.mac_address || macAddress} + )} + {di?.os_label && ( + {di.os_label} + )} + {di?.device_type && ( + {di.device_type} + )}
-
- {/* Body */} -
+ {/* ── Body ── */} +
{isLoading ? (
-

Loading device data...

+

Loading device data…

) : error ? (
{error}
) : data ? ( <> - {/* KPI Summary */} -
-
-

Total Download

-

{fmtBytes(data.total_download ?? 0)}

-
-
-

Total Upload

-

{fmtBytes(data.total_upload ?? 0)}

-
-
-

Total Flows

-

{data.flow_count ?? data.flows.length}

-
+ {/* ── KPI bar ── */} +
+ {[ + { label: "Download", value: fmtBytes(data.total_download), color: "text-cyan-400" }, + { label: "Upload", value: fmtBytes(data.total_upload), color: "text-green-400" }, + { label: "Total Flows",value: data.flow_count, color: "text-white" }, + ].map(k => ( +
+

{k.label}

+

{k.value}

+
+ ))}
- {/* Security Info Panel */} - {data.security_info && (data.security_info.risk_level || data.security_info.os_label || data.security_info.device_type || data.security_info.insecure_protocols.length > 0 || data.security_info.blacklisted) && (() => { - const si = data.security_info!; - const riskColor = si.risk_level === "Rawan" || si.risk_level === "Critical" ? "text-red-400 bg-red-500/10 ring-red-500/30" - : si.risk_level === "Sedang" || si.risk_level === "Medium" ? "text-yellow-400 bg-yellow-500/10 ring-yellow-500/30" - : "text-green-400 bg-green-500/10 ring-green-500/30"; - const encPct = si.encrypted_pct ?? 0; - return ( -
-

Security Info

+ {/* ── Alert strip ── */} + {alertCount > 0 && ( +
+ + {alertCount} security alerts — cek tab Pwd Leaks atau VPN & Events +
+ )} - {/* Row 1: Device Identity */} - {(si.device_type || si.os_label || si.manufacturer) && ( -
- {si.device_type && {si.device_type}} - {si.os_label && {si.os_label}} - {si.manufacturer && {si.manufacturer}} -
- )} + {/* ── MAC bandwidth strip ── */} + {data.mac_bandwidth && ( +
+ + {data.mac_bandwidth.mac_address} + {data.mac_bandwidth.manufacturer || "Unknown"} + ↓ {fmtBytes(data.mac_bandwidth.download)} + ↑ {fmtBytes(data.mac_bandwidth.upload)} + Total: {fmtBytes(data.mac_bandwidth.total)} +
+ )} - {/* Row 2: Encryption bar */} - {si.encrypted_pct !== null && ( -
-
- Enkripsi Traffic -
- - {si.risk_level} - - {encPct}% -
-
-
-
= 80 ? "bg-green-500" : encPct >= 50 ? "bg-yellow-500" : "bg-red-500"}`} - style={{ width: `${encPct}%` }} - /> -
-
- Terenkripsi: {fmtBytes(si.encrypted_bytes ?? 0)} - Tidak: {fmtBytes(si.unencrypted_bytes ?? 0)} -
-
- )} - - {/* Row 3: Warnings */} -
- {si.insecure_protocols.length > 0 && si.insecure_protocols.map((p, i) => ( - - ⚠ {p.protocol} ({p.risk}) - - ))} - {si.blacklisted && ( - - 🚫 Blacklisted - - )} - {si.reputation && si.reputation !== "Unknown" && !si.blacklisted && ( - - Reputasi: {si.reputation} - - )} -
-
- ); - })()} - - {/* Tabs */} -
+ {/* ── Tabs ── */} +
{tabs.map((tab) => ( ))}
-
- {activeTab === "apps" && ( -
- {data.top_apps.length === 0 ? ( -

No application data for this device. It may not have active flows yet.

- ) : ( - data.top_apps.map((a, i) => { - const total = a.download + a.upload; - const maxTotal = Math.max(...data.top_apps.map(x => x.download + x.upload), 1); - const pct = (total / maxTotal) * 100; - return ( -
-
- {a.favicon ? ( - {a.app_label} - ) : ( -
- )} - {a.app_label} - {a.category && ( - {a.category} - )} - {fmtBytes(total)} -
-
- ↓ {fmtBytes(a.download)} - ↑ {fmtBytes(a.upload)} -
-
-
-
+ {/* ── Tab content ── */} +
+ + {/* ══ INFO ══ */} + {activeTab === "info" && ( +
+ {/* Device identity card */} +
+

Informasi Device

+
+ {[ + { label: "Label", value: di?.device_label }, + { label: "Tipe", value: di?.device_type }, + { label: "OS", value: di?.os_label }, + { label: "Manufacturer", value: di?.manufacturer }, + { label: "IP Address", value: ip }, + { label: "MAC Address", value: di?.mac_address || macAddress }, + { label: "Is New", value: di?.is_new === 1 ? "Perangkat Baru ✨" : di?.is_new === 0 ? "Dikenal" : null }, + ].filter(f => f.value).map(f => ( +
+ {f.label} + {String(f.value)}
- ); - }) + ))} +
+
+ + {/* Top domains */} + {data.top_domains.length > 0 && ( +
+

Top Domains Diakses

+
+ {data.top_domains.map((d, i) => { + const maxDl = Math.max(...data.top_domains.map(x => x.download), 1); + const pct = (d.download / maxDl) * 100; + return ( +
+
+

{d.domain}

+
+
+
+
+
+

{fmtBytes(d.download)}

+

{d.flow_count} flows

+
+
+ ); + })} +
+
)}
)} + {/* ══ FLOWS ══ */} {activeTab === "flows" && ( -
- {data.flows.length === 0 ? ( -

No active flows for this device.

- ) : ( - - - - - - - - - - - - - {data.flows.map((f, i) => ( + data.flows.length === 0 ? : ( + + {data.flows.map((f, i) => ( + + + + + + + + + + ))} + + ) + )} + + {/* ══ APPS ══ */} + {activeTab === "apps" && ( + data.top_apps.length === 0 + ? + : ( +
+ {/* Legend */} +
+ + Domain / Website + + + Protokol + + + Port + +
+ + {data.top_apps.map((a, i) => { + const total = a.download + a.upload; + const maxTotal = Math.max(...data.top_apps.map(x => x.download + x.upload), 1); + const pct = (total / maxTotal) * 100; + + const isDomain = a.type === 'domain'; + const isProtocol = a.type === 'protocol'; + + // Color scheme by type + const barColor = isDomain ? 'from-teal-500 to-cyan-500' + : isProtocol ? 'from-blue-500 to-indigo-500' + : 'from-slate-600 to-slate-500'; + const iconBg = isDomain ? 'bg-teal-500/15' : isProtocol ? 'bg-blue-500/15' : 'bg-slate-700/40'; + const iconColor = isDomain ? 'text-teal-400' : isProtocol ? 'text-blue-400' : 'text-slate-500'; + + return ( +
+
+ {/* Icon */} +
+ {isDomain + ? + : isProtocol + ? + : } +
+ + {/* Label block */} +
+
+ {/* Main label: domain name or protocol */} + + {a.label} + + {/* Sub-label: protocol badge if domain */} + {a.sub_label && ( + + {a.sub_label} + + )} + {/* Type badge */} + {isDomain && ( + + website + + )} +
+
+ + {/* Stats */} +
+

{fmtBytes(total)}

+

{a.flow_count} flows

+
+
+ + {/* Bandwidth breakdown */} +
+ ↓ {fmtBytes(a.download)} + ↑ {fmtBytes(a.upload)} +
+ + {/* Progress bar */} +
+
+
+
+ ); + })} +
+ ) + )} + + {/* ══ ENCRYPTION ══ */} + {activeTab === "encryption" && (() => { + const enc = data.encryption; + if (!enc) return ; + const pct = enc.encrypted_pct ?? 0; + const { bar, badge } = riskStyle(enc.risk_level); + return ( +
+
+
+

Status Enkripsi Traffic

+ + {enc.risk_level || "Unknown"} + +
+ + {/* Big donut-style display */} +
+
+ + + = 80 ? "#22c55e" : pct >= 50 ? "#eab308" : "#ef4444"} + strokeWidth="3" strokeDasharray={`${pct} 100`} strokeLinecap="round" + /> + +
+ {pct}% +
+
+
+
+
+ Terenkripsi + {fmtBytes(enc.encrypted_bytes ?? 0)} +
+
+
+
+
+
+
+ Tidak Terenkripsi + {fmtBytes(enc.unencrypted_bytes ?? 0)} +
+
+
+
+
+
+
+ +
+ Total: {fmtBytes(enc.total_bytes ?? 0)} +
+
+ + {/* Risk interpretation */} +
+ {(enc.risk_level || "").toLowerCase().includes("critical") || (enc.risk_level || "").toLowerCase().includes("rawan") + ? "🚨 Device ini memiliki risiko tinggi — sebagian besar traffic tidak terenkripsi!" + : (enc.risk_level || "").toLowerCase().includes("medium") || (enc.risk_level || "").toLowerCase().includes("sedang") + ? "⚠️ Traffic device ini sebagian tidak terenkripsi. Pertimbangkan upgrade ke HTTPS/TLS." + : "✅ Device ini memiliki enkripsi yang baik."} +
+
+ ); + })()} + + {/* ══ SERVERS ══ */} + {activeTab === "servers" && ( + data.server_discovery.length === 0 ? : ( + + {data.server_discovery.map((s, i) => ( +
+ + + + + + + + + ))} + + ) + )} + + {/* ══ PASSWORDS ══ */} + {activeTab === "passwords" && ( + data.unencrypted_passwords.length === 0 + ?
+

✅

+

Tidak ada password cleartext yang terdeteksi.

+
+ : ( +
+
+ + {data.unencrypted_passwords.length} koneksi dengan password tidak terenkripsi terdeteksi! +
+ + {data.unencrypted_passwords.map((p, i) => (
- - - + + + + - - - + ))} - -
Destination IPPortApp / DomainProtocolDownloadUpload
{f.dst_ip || "—"}{f.dst_port ?? "—"} + {f.domain ? ( + /* Domain name takes priority — shown in teal monospace */ + + {f.domain} + + ) : f.app_label && !f.app_label.startsWith('Port ') ? ( + /* Named protocol — purple */ + {f.app_label} + ) : f.app_label ? ( + /* Port-only entry — muted */ + {f.app_label} + ) : ( + — + )} + {/* Show app_label as secondary if domain is set */} + {f.domain && f.app_label && ( + {f.app_label} + )} + {f.protocol || "—"}{fmtBytes(f.download)}{fmtBytes(f.upload)}{f.last_seen?.slice(0, 16) || "—"}
+ + {s.server_type || "—"} + + {s.hostname || "—"}{s.port ?? "—"}{s.protocol || "—"}{s.os_label || "—"}{fmtBytes(s.download)}{fmtBytes(s.upload)}
{f.dst_ip || "-"}{f.dst_port ?? "-"} - {(f as any).app_label || (f as any).domain || "-"} + {p.dst_ip || "—"}{p.dst_port ?? "—"}{p.protocol || "—"}{p.username || "—"} + + {p.severity || "—"} + {f.protocol || "-"}{fmtBytes(f.download)}{fmtBytes(f.upload)}{fmtBytes(p.download)}
- )} + +
+ ) + )} + + {/* ══ REPUTATION ══ */} + {activeTab === "reputation" && ( + data.ip_reputation.length === 0 ? : ( + + {data.ip_reputation.map((r, i) => ( + + {r.remote_ip || "—"} + {r.reputation || "—"} + {r.score ?? "—"} + {r.country || "—"} + {r.app_label || "—"} + + {r.blacklisted + ? ⛔ Yes + : ✓ No} + + {fmtBytes(r.download)} + + ))} + + ) + )} + + {/* ══ VPN & EVENTS ══ */} + {activeTab === "vpn_events" && ( +
+ {/* VPN */} +
+

+ Deteksi VPN + {data.vpn_detections.length} records +

+ {data.vpn_detections.length === 0 ? ( +

Tidak ada penggunaan VPN yang terdeteksi.

+ ) : ( +
+ {data.vpn_detections.map((v, i) => ( +
+
+ {v.vpn_type || "Unknown VPN"} + {v.country || "—"} +
+ {v.remote_ip &&

→ {v.remote_ip}

} +
+ {v.protocol || "—"} + {v.confidence !== null && v.confidence !== undefined && ( + + Conf: {((v.confidence) * 100).toFixed(0)}% + + )} +
+
+ ))} +
+ )} +
+ + {/* Events */} +
+

+ Security Events + {data.events.length} records +

+ {data.events.length === 0 ? ( +

Tidak ada security events untuk device ini.

+ ) : ( +
+ {data.events.map((ev, i) => ( +
+
+ + {ev.severity || "Info"} + +
+

{ev.event_type || "Unknown"}

+ {ev.description &&

{ev.description}

} +
+ {ev.event_at?.slice(0, 16) || "—"} +
+
+ ))} +
+ )} +
)} +
) : null} diff --git a/src/lib/api.ts b/src/lib/api.ts index a310d52..c31f254 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -505,6 +505,122 @@ export interface AgentSummary { bandwidth_up: number; } +export interface AgentEncryptionItem { + ip_address: string | null; + mac_address: string | null; + device_label: string | null; + encrypted_pct: number | null; + unencrypted: number | null; + encrypted: number | null; + total: number | null; + risk_level: string | null; + detected_at: string | null; +} + +export interface AgentInsecureProtocolItem { + ip_address: string | null; + mac_address: string | null; + protocol: string | null; + risk: string | null; + app_label: string | null; + dst_ip: string | null; + dst_port: number | null; + download: number; + upload: number; + detected_at: string | null; +} + +export interface AgentUnencryptedPwdItem { + ip_address: string | null; + mac_address: string | null; + dst_ip: string | null; + dst_port: number | null; + protocol: string | null; + username: string | null; + severity: string | null; + download: number; + upload: number; + detected_at: string | null; +} + +export interface AgentIpReputationItem { + ip_address: string | null; + local_ip: string | null; + mac_address: string | null; + reputation: string | null; + score: number | null; + country: string | null; + app_label: string | null; + blacklisted: number | boolean; + download: number; + upload: number; + detected_at: string | null; +} + +export interface AgentTorItem { + ip_address: string | null; + mac_address: string | null; + exit_node: string | null; + circuit_id: string | null; + country: string | null; + download: number; + upload: number; + detected_at: string | null; +} + +export interface AgentVpnItem { + ip_address: string | null; + mac_address: string | null; + vpn_type: string | null; + remote_ip: string | null; + protocol: string | null; + country: string | null; + confidence: number | null; + download: number; + upload: number; + detected_at: string | null; +} + +export interface AgentSecurity { + encryption_audit: AgentEncryptionItem[]; + insecure_protocols: AgentInsecureProtocolItem[]; + unencrypted_passwords: AgentUnencryptedPwdItem[]; + ip_reputation: AgentIpReputationItem[]; + tor_detections: AgentTorItem[]; + vpn_detections: AgentVpnItem[]; +} + +export interface AgentEventItem { + event_id: string | null; + event_type: string | null; + severity: string | null; + ip_address: string | null; + mac_address: string | null; + description: string | null; + event_at: string | null; +} + +export interface AgentMacBandwidthItem { + mac_address: string; + manufacturer: string | null; + download: number; + upload: number; + total: number; +} + +export interface AgentServerDiscoveryItem { + ip_address: string | null; + mac_address: string | null; + server_type: string | null; + hostname: string | null; + port: number | null; + protocol: string | null; + os_label: string | null; + download: number; + upload: number; + detected_at: string | null; +} + export interface AgentDetails { agent_uuid: string; agent_label: string; @@ -512,6 +628,10 @@ export interface AgentDetails { devices: AgentDeviceItem[]; flows: AgentFlowItem[]; top_apps: AgentAppItem[]; + security: AgentSecurity | null; + events: AgentEventItem[]; + mac_bandwidth: AgentMacBandwidthItem[]; + server_discovery: AgentServerDiscoveryItem[]; } export interface DeviceFlowItem { @@ -540,16 +660,121 @@ export interface SecurityInfo { blacklisted: boolean; } -export interface DeviceDetails { - ip: string; - total_download: number; - total_upload: number; - flow_count: number; - top_apps: AgentAppItem[]; - flows: DeviceFlowItem[]; - security_info: SecurityInfo | null; +export interface DeviceInfo { + device_label : string | null; + device_type : string | null; + os_label : string | null; + manufacturer : string | null; + mac_address : string | null; + is_new : number | null; } +export interface DeviceAppItem { + label : string; // domain name OR protocol name + sub_label : string | null; // protocol when label is domain; null otherwise + type : 'domain' | 'protocol' | 'port'; + download : number; + upload : number; + flow_count : number; +} + +export interface DeviceDomainItem { + domain : string; + download : number; + upload : number; + flow_count : number; +} + +export interface DeviceEncryption { + encrypted_pct : number | null; + encrypted_bytes : number | null; + unencrypted_bytes: number | null; + total_bytes : number | null; + risk_level : string | null; +} + +export interface DeviceServerItem { + server_type : string | null; + hostname : string | null; + port : number | null; + protocol : string | null; + os_label : string | null; + download : number; + upload : number; + detected_at : string | null; +} + +export interface DevicePwdItem { + dst_ip : string | null; + dst_port : number | null; + protocol : string | null; + username : string | null; + severity : string | null; + download : number; + upload : number; + detected_at : string | null; +} + +export interface DeviceReputationItem { + remote_ip : string | null; + local_ip : string | null; + reputation : string | null; + score : number | null; + country : string | null; + app_label : string | null; + blacklisted : boolean; + download : number; + upload : number; +} + +export interface DeviceVpnItem { + vpn_type : string | null; + remote_ip : string | null; + protocol : string | null; + country : string | null; + confidence : number | null; + download : number; + upload : number; + detected_at : string | null; +} + +export interface DeviceEventItem { + event_type : string | null; + severity : string | null; + ip_address : string | null; + mac_address : string | null; + description : string | null; + event_at : string | null; +} + +export interface DeviceMacBandwidth { + mac_address : string; + manufacturer : string | null; + download : number; + upload : number; + total : number; +} + +export interface DeviceDetails { + ip : string; + mac_address : string | null; + total_download : number; + total_upload : number; + flow_count : number; + device_info : DeviceInfo; + top_apps : DeviceAppItem[]; + top_domains : DeviceDomainItem[]; + flows : DeviceFlowItem[]; + encryption : DeviceEncryption | null; + server_discovery : DeviceServerItem[]; + unencrypted_passwords: DevicePwdItem[]; + ip_reputation : DeviceReputationItem[]; + vpn_detections : DeviceVpnItem[]; + events : DeviceEventItem[]; + mac_bandwidth : DeviceMacBandwidth | null; +} + + export interface AppAgentItem { agent_uuid: string | null; agent_label: string; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8b6cf74..736d3b7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,9 +5,10 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function fmtBytes(bytes?: number) { - if (!bytes || bytes === 0) return '0 B'; - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); +export function fmtBytes(bytes?: number | null) { + if (bytes == null || bytes === 0 || isNaN(bytes)) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; + const i = Math.min(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), units.length - 1); + if (i < 0) return '0 B'; return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; }