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 ( +
| {h} | + ))} +
|---|
{msg}
; +} + +// ── Main component ────────────────────────────────────────────────────────── export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) { - const [data, setData] = useState{agentUuid}
Loading agent data...
+Loading agent data…
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.label}
+{kpi.value}
+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 ?{d.device_label || d.ip_address || "Unknown"}
- {riskCfg && ( - - - {riskCfg.label} +{d.device_label || d.ip_address || "Unknown"}
+ {d.risk_level && ( + + + {d.risk_level} )} {d.has_insecure && ( @@ -150,106 +212,360 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) { )}{d.ip_address}
+ {d.mac_address &&{d.mac_address}
} {d.os_label &&{d.os_label}
} {d.manufacturer &&{d.manufacturer}
}{fmtBytes(d.download ?? 0)}
+↓ {fmtBytes(d.download ?? 0)}
+↑ {fmtBytes(d.upload ?? 0)}
{d.encrypted_pct !== null && (🔒 {d.encrypted_pct}%
)} +No flows found for this agent.
- ) : ( -| Source IP | -Destination | -App / Domain | -Protocol | -Download | -Upload | -
|---|---|---|---|---|---|
| {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)} | -
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 ?{ev.description}
} +{m.mac_address}
+{m.manufacturer || "Unknown Manufacturer"}
+{fmtBytes(m.total ?? 0)}
+Total
+| {h} | )} +
|---|
{msg}
; +} + +// ── Main component ──────────────────────────────────────────────────────────── export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Props) { - const [data, setData] = useState{ip}
- {macAddress &&{macAddress}
} +Loading device data...
+Loading device data…
Total Download
-{fmtBytes(data.total_download ?? 0)}
-Total Upload
-{fmtBytes(data.total_upload ?? 0)}
-Total Flows
-{data.flow_count ?? data.flows.length}
-{k.label}
+{k.value}
+Security Info
+ {/* ── Alert strip ── */} + {alertCount > 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 ( -Informasi Device
+Top Domains Diakses
+{d.domain}
+{fmtBytes(d.download)}
+{d.flow_count} flows
+No active flows for this device.
- ) : ( -| Destination IP | -Port | -App / Domain | -Protocol | -Download | -Upload | -||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| {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)} |
Tidak ada penggunaan VPN yang terdeteksi.
+ ) : ( +→ {v.remote_ip}
} +Tidak ada security events untuk device ini.
+ ) : ( +{ev.event_type || "Unknown"}
+ {ev.description &&{ev.description}
} +