diff --git a/.gitignore b/.gitignore index 11f1248..edb4099 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,12 @@ yarn-error.log* next-env.d.ts # local databases & temporary files - temp.json /scratch - +backend/*.db +backend/*.db-shm +backend/*.db-wal +backend/*.sqlite +*.db +*.db-shm +*.db-wal diff --git a/backend/database.js b/backend/database.js index 20cd804..f891bef 100644 --- a/backend/database.js +++ b/backend/database.js @@ -1,7 +1,7 @@ // backend/database.js const Database = require('better-sqlite3'); -const path = require('path'); -const bcrypt = require('bcryptjs'); +const path = require('path'); +const bcrypt = require('bcryptjs'); const DB_PATH = path.join(__dirname, 'netify_data.db'); let db; @@ -28,15 +28,15 @@ function getAgentTrafficRatio(agentUuid) { const d = getDB(); const macs = AGENT_MAC_MAP[agentUuid]; if (!macs || macs.length === 0) return 0; - + const latest = d.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get(); if (!latest?.t) return 0; - + const siteTotal = d.prepare("SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ?").get(latest.t)?.val || 1; - + const placeholders = macs.map(() => '?').join(','); const agentTotal = d.prepare(`SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders})`).get(latest.t, ...macs)?.val || 0; - + return siteTotal > 0 ? (agentTotal / siteTotal) : 0; } @@ -77,7 +77,7 @@ function initSchema() { d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); console.log('[DB] Created default admin user (admin / admin123)'); } - + const agentExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent1'").get(); if (agentExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); @@ -568,7 +568,7 @@ function initSchema() { // ─── INSERT FUNCTIONS ───────────────────────────────────────────────────────── function insertBandwidthApps(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_apps (site_uuid, fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) @@ -577,23 +577,23 @@ function insertBandwidthApps(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.app_id ?? null, + siteUuid, fetchedAt, + r.app_id ?? null, r.app_label ?? 'Unknown', - r.app_tag ?? null, - r.category ?? null, - r.favicon ?? null, - r.download ?? 0, - r.upload ?? 0, - r.total ?? (r.download ?? 0) + (r.upload ?? 0), - r.flows ?? 0 + r.app_tag ?? null, + r.category ?? null, + r.favicon ?? null, + r.download ?? 0, + r.upload ?? 0, + r.total ?? (r.download ?? 0) + (r.upload ?? 0), + r.flows ?? 0 ); } })(rows); } function insertDevices(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO devices (site_uuid, fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) @@ -602,23 +602,23 @@ function insertDevices(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.mac_address ?? null, - r.ip_address ?? null, + siteUuid, fetchedAt, + r.mac_address ?? null, + r.ip_address ?? null, r.device_label ?? r.ip_address ?? 'Unknown', - r.device_type ?? null, - r.os_label ?? null, + r.device_type ?? null, + r.os_label ?? null, r.manufacturer ?? null, - r.download ?? 0, - r.upload ?? 0, - r.last_seen ?? fetchedAt + r.download ?? 0, + r.upload ?? 0, + r.last_seen ?? fetchedAt ); } })(rows); } function insertFlows(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO flows (site_uuid, fetched_at, flow_id, src_ip, src_mac, dst_ip, dst_port, protocol, app_label, domain, bytes_download, bytes_upload, first_seen, last_seen) @@ -627,26 +627,26 @@ function insertFlows(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.flow_id ?? null, - r.src_ip ?? null, - r.src_mac ?? null, - r.dst_ip ?? null, - r.dst_port ?? null, - r.protocol ?? null, + siteUuid, fetchedAt, + r.flow_id ?? null, + r.src_ip ?? null, + r.src_mac ?? null, + r.dst_ip ?? null, + r.dst_port ?? null, + r.protocol ?? null, r.app_label ?? null, - r.domain ?? null, - r.download ?? 0, - r.upload ?? 0, + r.domain ?? null, + r.download ?? 0, + r.upload ?? 0, r.first_seen ?? fetchedAt, - r.last_seen ?? fetchedAt + r.last_seen ?? fetchedAt ); } })(rows); } function insertThreats(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO threats (site_uuid, fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) @@ -655,15 +655,15 @@ function insertThreats(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.threat_id ?? null, + siteUuid, fetchedAt, + r.threat_id ?? null, r.threat_type ?? null, - r.severity ?? null, + r.severity ?? null, r.mac_address ?? null, - r.ip_address ?? null, - r.dst_ip ?? null, - r.app_label ?? null, - r.domain ?? null, + r.ip_address ?? null, + r.dst_ip ?? null, + r.app_label ?? null, + r.domain ?? null, r.description ?? null, r.detected_at ?? fetchedAt ); @@ -672,7 +672,7 @@ function insertThreats(rows, fetchedAt, siteUuid) { } function insertProtocols(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_protocols (site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) @@ -682,19 +682,19 @@ function insertProtocols(rows, fetchedAt, siteUuid) { for (const r of items) { // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested stmt.run( - siteUuid, fetchedAt, - r.protocol_id ?? null, + siteUuid, fetchedAt, + r.protocol_id ?? null, r.protocol_label ?? 'Unknown', - r.download ?? 0, - r.upload ?? 0, - r.flows ?? 0 + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 ); } })(rows); } function insertCountries(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_countries (site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) @@ -704,19 +704,19 @@ function insertCountries(rows, fetchedAt, siteUuid) { for (const r of items) { // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested stmt.run( - siteUuid, fetchedAt, + siteUuid, fetchedAt, r.country_code ?? null, r.country_name ?? 'Unknown', - r.download ?? 0, - r.upload ?? 0, - r.flows ?? 0 + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 ); } })(rows); } function insertDNS(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO dns_queries (site_uuid, fetched_at, domain, query_count, app_label, category) @@ -725,18 +725,18 @@ function insertDNS(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.domain ?? null, + siteUuid, fetchedAt, + r.domain ?? null, r.query_count ?? 0, - r.app_label ?? null, - r.category ?? null + r.app_label ?? null, + r.category ?? null ); } })(rows); } function insertEvents(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(` INSERT INTO events (site_uuid, fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) @@ -745,14 +745,14 @@ function insertEvents(rows, fetchedAt, siteUuid) { d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, - r.event_id ?? null, - r.event_type ?? null, - r.severity ?? null, + siteUuid, fetchedAt, + r.event_id ?? null, + r.event_type ?? null, + r.severity ?? null, r.mac_address ?? null, - r.ip_address ?? null, + r.ip_address ?? null, r.description ?? null, - r.event_at ?? fetchedAt + r.event_at ?? fetchedAt ); } })(rows); @@ -771,29 +771,73 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { // ─── QUERY FUNCTIONS ────────────────────────────────────────────────────────── function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { - const d = getDB(); + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; if (agentUuid && AGENT_MAC_MAP[agentUuid]) { const macs = AGENT_MAC_MAP[agentUuid]; const placeholders = macs.map(() => '?').join(','); - return d.prepare(` - SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, - (SUM(bytes_download) + SUM(bytes_upload)) AS total, COUNT(*) AS flow_count + + // 1. Get raw aggregated apps bandwidth from flows table for this agent (cumulative) + const rawApps = d.prepare(` + SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload FROM flows - WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND app_label IS NOT NULL + WHERE src_mac IN (${placeholders}) AND app_label IS NOT NULL GROUP BY app_label - ORDER BY download DESC - LIMIT ? - `).all(...macs, latest.t, limit); + `).all(...macs); + + if (rawApps.length === 0) return []; + + // Calculate sum of download/upload across all these apps + let totalFlowDl = 0; + let totalFlowUl = 0; + for (const r of rawApps) { + totalFlowDl += r.download; + totalFlowUl += r.upload; + } + + // 2. Get true cumulative bandwidth from mac_bandwidth table + const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t; + const trueBw = latestMacSnap + ? d.prepare(` + SELECT SUM(download) AS dl, SUM(upload) AS ul + FROM mac_bandwidth + WHERE mac_address IN (${placeholders}) AND fetched_at = ? + `).get(...macs, latestMacSnap) + : null; + + const trueDl = trueBw?.dl ?? 0; + const trueUl = trueBw?.ul ?? 0; + + // Calculate scaling factors + const dlFactor = totalFlowDl > 0 ? trueDl / totalFlowDl : 1; + const ulFactor = totalFlowUl > 0 ? trueUl / totalFlowUl : 1; + + // Map and scale + const scaledApps = rawApps.map(r => { + const dl = Math.round(r.download * dlFactor); + const ul = Math.round(r.upload * ulFactor); + return { + app_label: r.app_label, + download: dl, + upload: ul, + total: dl + ul, + flow_count: 0 + }; + }); + + // Sort by total bandwidth DESC + scaledApps.sort((a, b) => b.total - a.total); + return correlateAppLabels(scaledApps.slice(0, limit)); } const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); if (!appsLatest?.t) return []; - return d.prepare(` + const rows = d.prepare(` SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit `).all({ fetched_at: appsLatest.t, limit, siteUuid }); + return correlateAppLabels(rows); } function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) { @@ -898,18 +942,18 @@ function deviceMatchesAgent(ip, mac, agentUuid) { } if (agentUuid === '2F-TF-1D-GK') { return ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || - ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || - ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || - ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || - ip.startsWith('10.93.'); + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.'); } } return false; } -function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { - const d = getDB(); +function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) { + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); if (!latest?.t) return []; @@ -940,6 +984,29 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { } } + // Get true cumulative bandwidth from mac_bandwidth table to calculate scale factors + let totalFlowDl = 0; + let totalFlowUl = 0; + for (const f of flowsData) { + totalFlowDl += f.download; + totalFlowUl += f.upload; + } + + const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t; + const trueBw = latestMacSnap + ? d.prepare(` + SELECT SUM(download) AS dl, SUM(upload) AS ul + FROM mac_bandwidth + WHERE mac_address IN (${placeholders}) AND fetched_at = ? + `).get(...macs, latestMacSnap) + : null; + + const trueDl = trueBw?.dl ?? 0; + const trueUl = trueBw?.ul ?? 0; + + const dlFactor = totalFlowDl > 0 ? trueDl / totalFlowDl : 1; + const ulFactor = totalFlowUl > 0 ? trueUl / totalFlowUl : 1; + const resolved = []; const seenIps = new Set(); @@ -956,6 +1023,10 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { const meta = resolveDeviceMetadata(ip, mac, dbLabel, dbMan, dbType); const isRouted = mac === '04:f4:1c:ce:c2:e6' && ip !== '10.6.50.25' && ip !== '10.6.12.242'; + // Scale download and upload + const scaledDl = Math.round((download || 0) * dlFactor); + const scaledUl = Math.round((upload || 0) * ulFactor); + return { id: dbDev ? dbDev.id : null, site_uuid: dbDev ? dbDev.site_uuid : siteUuid, @@ -966,9 +1037,9 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { device_type: meta.type, os_label: meta.os, manufacturer: meta.manufacturer, - download: download || 0, - upload: upload || 0, - total: (download || 0) + (upload || 0), + download: scaledDl || 0, + upload: scaledUl || 0, + total: (scaledDl || 0) + (scaledUl || 0), is_gateway_routed: isRouted ? 1 : 0 }; } @@ -996,6 +1067,95 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { return resolved.slice(0, limit); } + // Admin View Search Logic + if (search) { + const q = `%${search}%`; + + // 1. Fetch matching historical devices from devices table (grouped by IP address) + const devicesData = d.prepare(` + SELECT ip_address, mac_address, device_label, device_type, os_label, manufacturer, + MAX(download) as download, MAX(upload) as upload, MAX(fetched_at) as fetched_at, site_uuid, id + FROM devices + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ( + ip_address LIKE @q OR + mac_address LIKE @q OR + device_label LIKE @q OR + manufacturer LIKE @q OR + device_type LIKE @q OR + os_label LIKE @q + ) + GROUP BY ip_address + `).all({ siteUuid, q }); + + // 2. Fetch matching historical flows from flows table (grouped by IP address) + const flowsData = d.prepare(` + SELECT src_ip as ip_address, src_mac as mac_address, + SUM(bytes_download) as download, SUM(bytes_upload) as upload, + MAX(last_seen) as last_seen, MAX(fetched_at) as fetched_at, site_uuid + FROM flows + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ( + src_ip LIKE @q OR + src_mac LIKE @q OR + app_label LIKE @q OR + domain LIKE @q + ) + GROUP BY src_ip + `).all({ siteUuid, q }); + + const resolvedMap = new Map(); + const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); + const intelMap = new Map(intelList.map(i => [i.ip_address, i])); + + const processSearchDevice = (ip, mac, dbDev, download, upload, lastSeen) => { + const intelInfo = intelMap.get(ip); + const dbLabel = dbDev ? dbDev.device_label : (intelInfo ? intelInfo.device_label : null); + const dbMan = dbDev ? dbDev.manufacturer : (intelInfo ? intelInfo.manufacturer : null); + const dbType = intelInfo ? intelInfo.device_type : null; + + const meta = resolveDeviceMetadata(ip, mac, dbLabel, dbMan, dbType); + const isRouted = mac === '04:f4:1c:ce:c2:e6' && ip !== '10.6.50.25' && ip !== '10.6.12.242'; + + return { + id: dbDev ? dbDev.id : null, + site_uuid: dbDev ? dbDev.site_uuid : siteUuid, + fetched_at: lastSeen || latest.t, + mac_address: mac, + ip_address: ip, + device_label: meta.label, + device_type: meta.type, + os_label: meta.os, + manufacturer: meta.manufacturer, + download: download || 0, + upload: upload || 0, + total: (download || 0) + (upload || 0), + is_gateway_routed: isRouted ? 1 : 0 + }; + }; + + // Add from devicesData + for (const dev of devicesData) { + if (!dev.ip_address) continue; + resolvedMap.set(dev.ip_address, processSearchDevice(dev.ip_address, dev.mac_address, dev, dev.download, dev.upload, dev.fetched_at)); + } + + // Add/Merge from flowsData + for (const f of flowsData) { + if (!f.ip_address) continue; + const existing = resolvedMap.get(f.ip_address); + if (existing) { + existing.download = Math.max(existing.download, f.download || 0); + existing.upload = Math.max(existing.upload, f.upload || 0); + existing.total = existing.download + existing.upload; + } else { + resolvedMap.set(f.ip_address, processSearchDevice(f.ip_address, f.mac_address, null, f.download, f.upload, f.fetched_at)); + } + } + + const resolved = Array.from(resolvedMap.values()); + resolved.sort((a, b) => b.download - a.download); + return resolved.slice(0, limit); + } + // Load intelligence tables to assist in type resolving const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); const intelMap = new Map(intelList.map(i => [i.ip_address, i])); @@ -1077,25 +1237,307 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { return filtered.slice(0, limit); } +function correlateFlows(flows) { + if (!Array.isArray(flows) || flows.length === 0) return flows; + + const d = getDB(); + + // 1. Build cache maps for local IPs and public IPs in history + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + // Matches map for standard ports + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + + return flows.map(f => { + let appLabel = f.app_label; + let domain = f.domain; + + const isPortLabel = !appLabel || appLabel.startsWith("Port ") || appLabel.toLowerCase().includes("port"); + + if (isPortLabel) { + const dstIp = f.dst_ip; + const dstPort = String(f.dst_port); + const proto = f.protocol || "TCP"; + + // Case A: Intranet IP + const isIntranet = dstIp && ( + dstIp.startsWith("10.") || + dstIp.startsWith("192.168.") || + dstIp.startsWith("172.16.") || + dstIp.startsWith("172.17.") || + dstIp.startsWith("172.18.") || + dstIp.startsWith("172.19.") || + dstIp.startsWith("172.20.") || + dstIp.startsWith("172.21.") || + dstIp.startsWith("172.22.") || + dstIp.startsWith("172.23.") || + dstIp.startsWith("172.24.") || + dstIp.startsWith("172.25.") || + dstIp.startsWith("172.26.") || + dstIp.startsWith("172.27.") || + dstIp.startsWith("172.28.") || + dstIp.startsWith("172.29.") || + dstIp.startsWith("172.30.") || + dstIp.startsWith("172.31.") + ); + + if (isIntranet) { + let friendlyName = devMap.get(dstIp); + if (!friendlyName) { + if (dstIp.startsWith("10.6.")) { + friendlyName = "IFG Client"; + } else if (dstIp.startsWith("10.250.") || dstIp.startsWith("192.168.") || dstIp.startsWith("10.121.")) { + friendlyName = "CPI Client"; + } else if ( + dstIp.startsWith("10.0.") || dstIp.startsWith("10.1.") || dstIp.startsWith("10.26.") || + dstIp.startsWith("10.43.") || dstIp.startsWith("10.35.") || dstIp.startsWith("10.21.") || + dstIp.startsWith("10.7.") || dstIp.startsWith("10.182.") || dstIp.startsWith("10.109.") || + dstIp.startsWith("10.181.") || dstIp.startsWith("10.75.") || dstIp.startsWith("10.202.") || + dstIp.startsWith("10.93.") + ) { + friendlyName = "JRP Client"; + } else { + friendlyName = "Intranet Client"; + } + } + appLabel = `${friendlyName} (${dstIp})`; + domain = `Port ${dstPort} (${proto})`; + } else { + // Case B: Public IP + const cached = publicIpMap.get(dstIp); + if (cached) { + appLabel = cached.domain || cached.app_label || appLabel; + domain = `Port ${dstPort} (${proto})`; + } else { + const stdName = matches[dstPort]; + if (stdName) { + appLabel = stdName; + domain = `Port ${dstPort} (${proto})`; + } else { + appLabel = `Public IP: ${dstIp}`; + domain = `Port ${dstPort} (${proto})`; + } + } + } + } + + return { + ...f, + app_label: appLabel, + domain: domain + }; + }); +} + +function correlateAppLabels(apps) { + if (!Array.isArray(apps) || apps.length === 0) return apps; + + const d = getDB(); + + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + function getFriendlyIpName(ip) { + if (devMap.has(ip)) return devMap.get(ip); + if (publicIpMap.has(ip)) { + const pub = publicIpMap.get(ip); + return pub.domain || pub.app_label; + } + if (ip.startsWith('10.6.')) return 'IFG Client'; + if (ip.startsWith('10.250.') || ip.startsWith('192.168.') || ip.startsWith('10.121.')) return 'CPI Client'; + if ( + ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.') + ) return 'JRP Client'; + return 'Intranet Client'; + } + + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + + return apps.map(app => { + let label = app.app_label; + if (!label) return app; + + const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port"); + + if (isPortLabel) { + const portStr = label.replace("Port ", "").trim(); + + const flow = d.prepare(` + SELECT dst_ip, protocol, dst_port + FROM flows + WHERE app_label = ? OR domain = ? OR dst_port = ? + GROUP BY dst_ip, protocol, dst_port + ORDER BY COUNT(*) DESC + LIMIT 1 + `).get(label, label, portStr); + + if (flow && flow.dst_ip) { + const friendlyName = getFriendlyIpName(flow.dst_ip); + label = `${friendlyName} (Port ${portStr})`; + } else { + const stdName = matches[portStr]; + if (stdName) { + label = `${stdName} (Port ${portStr})`; + } + } + } + + return { + ...app, + app_label: label + }; + }); +} + function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { - const d = getDB(); + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; + let rows = []; if (agentUuid && AGENT_MAC_MAP[agentUuid]) { const macs = AGENT_MAC_MAP[agentUuid]; const placeholders = macs.map(() => '?').join(','); - return d.prepare(` + rows = d.prepare(` SELECT * FROM flows WHERE src_mac IN (${placeholders}) ORDER BY last_seen DESC, fetched_at DESC LIMIT ? `).all(...macs, limit); + } else { + rows = d.prepare(` + SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit + `).all({ fetched_at: latest.t, limit, siteUuid }); } - return d.prepare(` - SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); + const mapped = rows.map(r => ({ + ...r, + download: r.bytes_download ?? 0, + upload: r.bytes_upload ?? 0 + })); + + return correlateFlows(mapped); } function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { @@ -1114,7 +1556,7 @@ function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { } function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { - const d = getDB(); + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; @@ -1139,7 +1581,7 @@ function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { } function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { - const d = getDB(); + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM intel_ip_reputation`).get(); if (!latest?.t) return []; @@ -1165,7 +1607,7 @@ function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { } function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { - const d = getDB(); + const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); if (!latest?.t) return []; @@ -1239,7 +1681,7 @@ function getStats(siteUuid = null, agentUuid = null) { if (agentUuid && AGENT_MAC_MAP[agentUuid]) { const macs = AGENT_MAC_MAP[agentUuid]; const placeholders = macs.map(() => '?').join(','); - + // Count cumulative unique client devices for this agent const flowsIPs = d.prepare(` SELECT DISTINCT src_ip, src_mac FROM flows @@ -1307,9 +1749,9 @@ function getStats(siteUuid = null, agentUuid = null) { : 0; const totalThreats = d.prepare(`SELECT COUNT(*) as n FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; - const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; - const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_timeline`).get()?.t ?? null; - const latestBw = d.prepare(`SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid }); + const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; + const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_timeline`).get()?.t ?? null; + const latestBw = d.prepare(`SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid }); return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; } @@ -1320,8 +1762,10 @@ function insertAppCategories(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO app_categories (site_uuid, fetched_at, category_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); + })(rows); } function insertContinents(rows, fetchedAt, siteUuid) { @@ -1329,8 +1773,10 @@ function insertContinents(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO continents (site_uuid, fetched_at, continent_name, download, upload, total) VALUES (?, ?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); + })(rows); } function insertRegions(rows, fetchedAt, siteUuid) { @@ -1338,8 +1784,10 @@ function insertRegions(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO regions (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) VALUES (?, ?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); + })(rows); } function insertCities(rows, fetchedAt, siteUuid) { @@ -1347,8 +1795,10 @@ function insertCities(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO cities (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) VALUES (?, ?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); + })(rows); } function insertVLANs(rows, fetchedAt, siteUuid) { @@ -1356,8 +1806,10 @@ function insertVLANs(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO vlans (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); + })(rows); } function insertInterfaces(rows, fetchedAt, siteUuid) { @@ -1365,8 +1817,10 @@ function insertInterfaces(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO interfaces (site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) VALUES (?, ?,?,?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); + })(rows); } function insertFlowTypes(rows, fetchedAt, siteUuid) { @@ -1374,8 +1828,10 @@ function insertFlowTypes(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO flow_types (site_uuid, fetched_at, flow_type_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); + })(rows); } function insertFlowOrigins(rows, fetchedAt, siteUuid) { @@ -1383,8 +1839,10 @@ function insertFlowOrigins(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO flow_origins (site_uuid, fetched_at, flow_origin_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); + })(rows); } function insertIPVersions(rows, fetchedAt, siteUuid) { @@ -1392,8 +1850,10 @@ function insertIPVersions(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO ip_versions (site_uuid, fetched_at, ip_version_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); + })(rows); } function insertRemoteIPs(rows, fetchedAt, siteUuid) { @@ -1401,8 +1861,10 @@ function insertRemoteIPs(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO remote_ips (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); + })(rows); } function insertMACBandwidth(rows, fetchedAt, siteUuid) { @@ -1410,8 +1872,10 @@ function insertMACBandwidth(rows, fetchedAt, siteUuid) { const stmt = d.prepare(`INSERT INTO mac_bandwidth (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); - d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); })(rows); + d.transaction(items => { + for (const r of items) stmt.run( + siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); + })(rows); } // ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── @@ -1495,57 +1959,57 @@ function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, age // DPI Fields function insertTLSVersions(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_versions (site_uuid, fetched_at, tls_version, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); })(rows); } function insertTLSCiphers(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_ciphers (site_uuid, fetched_at, tls_cipher, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); })(rows); } function insertTLSSecurity(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_security (site_uuid, fetched_at, tls_security, color, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); })(rows); } function insertNetBIOSHostnames(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(`INSERT INTO netbios_hostnames (site_uuid, fetched_at, hostname, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); })(rows); } function insertDiscoveryOS(rows, fetchedAt, siteUuid) { - const d = getDB(); + const d = getDB(); const stmt = d.prepare(`INSERT INTO discovery_os (site_uuid, fetched_at, os_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); })(rows); } @@ -1558,7 +2022,7 @@ function insertDHCPFingerprints(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); })(rows); } @@ -1569,7 +2033,7 @@ function insertHTTPUserAgents(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); })(rows); } @@ -1580,7 +2044,7 @@ function insertSNIHostnames(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); })(rows); } @@ -1591,7 +2055,7 @@ function insertSSLServerCN(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); })(rows); } @@ -1602,7 +2066,7 @@ function insertQUICHostnames(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); })(rows); } @@ -1613,7 +2077,7 @@ function insertBitTorrentHashes(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); })(rows); } @@ -1624,7 +2088,7 @@ function insertSSHVersions(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); })(rows); } @@ -1635,7 +2099,7 @@ function insertMDNSHostnames(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); })(rows); } @@ -1648,7 +2112,7 @@ function insertCryptoMining(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.pool_host, r.pool_ip, r.protocol, r.app_label, r.confidence, r.download ?? 0, r.upload ?? 0 ); @@ -1662,7 +2126,7 @@ function insertDeviceDiscovery(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.device_label, r.device_type, r.os_label, r.manufacturer, r.is_new ? 1 : 0 ); @@ -1676,7 +2140,7 @@ function insertEncryptionAudit(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.device_label, r.encrypted_pct, r.unencrypted ?? 0, r.encrypted ?? 0, r.total ?? 0, r.risk_level ); @@ -1690,7 +2154,7 @@ function insertInsecureProtocols(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.protocol, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.protocol, r.ip_address, r.mac_address, r.dst_ip, r.dst_port, r.app_label, r.download ?? 0, r.upload ?? 0, r.risk ?? 'Medium', r.source ?? 'api' ); @@ -1704,7 +2168,7 @@ function insertIPReputation(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.local_ip, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.local_ip, r.mac_address, r.reputation, r.score, r.country, r.app_label, r.download ?? 0, r.upload ?? 0, r.blacklisted ? 1 : 0 ); @@ -1718,7 +2182,7 @@ function insertServerDiscovery(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.server_type, r.hostname, r.port, r.protocol, r.os_label, r.download ?? 0, r.upload ?? 0 ); @@ -1732,7 +2196,7 @@ function insertTorDetection(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.exit_node, r.circuit_id, r.download ?? 0, r.upload ?? 0, r.country ); })(rows); @@ -1745,7 +2209,7 @@ function insertUnencryptedPasswords(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.dst_ip, r.dst_port, r.protocol, r.username, r.download ?? 0, r.upload ?? 0, r.severity ?? 'Critical' ); @@ -1759,7 +2223,7 @@ function insertVPNDetection(rows, fetchedAt, siteUuid) { VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, r.vpn_type, r.remote_ip, r.protocol, r.download ?? 0, r.upload ?? 0, r.country, r.confidence ); @@ -1773,10 +2237,10 @@ function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { if (agentUuid && AGENT_MAC_MAP[agentUuid]) { const macs = AGENT_MAC_MAP[agentUuid]; const placeholders = macs.map(() => '?').join(','); - + // For reputation, the column is local_ip, not ip_address const ipField = table === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; - + return d.prepare(` SELECT * FROM ${table} WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) @@ -1795,7 +2259,7 @@ function getIntelStats(siteUuid = null, agentUuid = null) { 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', ]; const counts = {}; - + const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null; const placeholders = macs ? macs.map(() => '?').join(',') : ''; diff --git a/backend/netify.js b/backend/netify.js index 5760eeb..517fad0 100644 --- a/backend/netify.js +++ b/backend/netify.js @@ -1381,8 +1381,8 @@ async function fetchDeviceDetails(ip) { is_new : discRow?.is_new ?? null, }; - // ── 3. Named apps (exclude "Port XXX" port-only entries) ───────────────── - const namedAppRows = d.prepare(` + // ── 3. Named apps & correlated ports ───────────────────────────────────── + const rawAppRows = d.prepare(` SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, @@ -1390,12 +1390,93 @@ 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 + LIMIT 30 `).all(ip); + // Cache helper mappings + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + function getFriendlyIpName(ipAddress) { + if (devMap.has(ipAddress)) return devMap.get(ipAddress); + if (publicIpMap.has(ipAddress)) { + const pub = publicIpMap.get(ipAddress); + return pub.domain || pub.app_label; + } + if (ipAddress.startsWith('10.6.')) return 'IFG Client'; + if (ipAddress.startsWith('10.250.') || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.121.')) return 'CPI Client'; + if ( + ipAddress.startsWith('10.0.') || ipAddress.startsWith('10.1.') || ipAddress.startsWith('10.26.') || + ipAddress.startsWith('10.43.') || ipAddress.startsWith('10.35.') || ipAddress.startsWith('10.21.') || + ipAddress.startsWith('10.7.') || ipAddress.startsWith('10.182.') || ipAddress.startsWith('10.109.') || + ipAddress.startsWith('10.181.') || ipAddress.startsWith('10.75.') || ipAddress.startsWith('10.202.') || + ipAddress.startsWith('10.93.') + ) return 'JRP Client'; + return 'Intranet Client'; + } + + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + // ── 4. Top domains accessed by this device ───────────────────────────── const domainRows = d.prepare(` SELECT domain, @@ -1417,11 +1498,7 @@ async function fetchDeviceDetails(ip) { 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 + // ── 5. Smart combined display list ────────────────────────────────────── const combinedMap = new Map(); // Add domains first (higher priority) @@ -1436,14 +1513,48 @@ async function fetchDeviceDetails(ip) { }); } - // Add named apps that don't duplicate a domain entry - for (const r of namedAppRows) { - const key = 'app:' + r.app_label; + // Add named & correlated apps + for (const r of rawAppRows) { + let label = r.app_label; + let sub_label = null; + let type = 'protocol'; + + const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port"); + + if (isPortLabel) { + const portStr = label.replace("Port ", "").trim(); + type = 'port'; + + const flow = d.prepare(` + SELECT dst_ip, protocol, dst_port + FROM flows + WHERE src_ip = ? AND (app_label = ? OR dst_port = ?) + GROUP BY dst_ip, protocol, dst_port + ORDER BY COUNT(*) DESC + LIMIT 1 + `).get(ip, label, portStr); + + if (flow && flow.dst_ip) { + const friendlyName = getFriendlyIpName(flow.dst_ip); + label = friendlyName; + sub_label = `Port ${portStr} (${flow.protocol || 'TCP'})`; + } else { + const stdName = matches[portStr]; + if (stdName) { + label = stdName; + sub_label = `Port ${portStr}`; + } else { + sub_label = `Port ${portStr}`; + } + } + } + + const key = isPortLabel ? 'port:' + r.app_label : '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', + label : label, + sub_label : sub_label, + type : type, download : r.download ?? 0, upload : r.upload ?? 0, flow_count : r.flow_count, @@ -1451,15 +1562,7 @@ async function fetchDeviceDetails(ip) { } } - // 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 })); + const top_apps = [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25); // top_domains: keep simple list for Info tab const top_domains = domainRows.map(r => ({ @@ -1865,16 +1968,52 @@ async function fetchAppDetails(appLabel) { // Fetch security device risk overview — encryption audit + insecure protocols per device -async function fetchSecurityDevices() { +async function fetchSecurityDevices(siteUuid = null, agentUuid = null) { const db = require('./database'); const d = db.getDB(); - const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; - const encryptRows = latestFetch - ? d.prepare(`SELECT * FROM intel_encryption_audit WHERE fetched_at = ?`).all(latestFetch) - : []; + // Get active IPs and MACs for filtering if agentUuid is provided + let agentIPs = null; + let agentIPSet = null; + let agentMacs = null; + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + agentMacs = AGENT_MAC_MAP[agentUuid]; + const resolvedDevices = db.getLatestDevices(1000, null, agentUuid); + agentIPs = resolvedDevices.map(d => d.ip_address).filter(Boolean); + agentIPSet = new Set(agentIPs); + } - const insecureRows = d.prepare(`SELECT DISTINCT ip_address FROM intel_insecure_protocols`).all(); + const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; + let encryptRows = []; + if (latestFetch) { + if (agentMacs) { + // Query with agent's MACs or JRP subnet IPs + const placeholders = agentMacs.map(() => '?').join(','); + encryptRows = d.prepare(` + SELECT * FROM intel_encryption_audit + WHERE fetched_at = ? AND (mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))) + `).all(latestFetch, ...agentMacs, ...agentMacs); + } else { + encryptRows = d.prepare(` + SELECT * FROM intel_encryption_audit + WHERE fetched_at = ? AND (@siteUuid IS NULL OR site_uuid = @siteUuid) + `).all(latestFetch, { siteUuid }); + } + } + + let insecureRows = []; + if (agentMacs) { + const placeholders = agentMacs.map(() => '?').join(','); + insecureRows = d.prepare(` + SELECT DISTINCT ip_address FROM intel_insecure_protocols + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).all(...agentMacs, ...agentMacs); + } else { + insecureRows = d.prepare(` + SELECT DISTINCT ip_address FROM intel_insecure_protocols + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) + `).all({ siteUuid }); + } const insecureIPs = new Set(insecureRows.map(r => r.ip_address).filter(Boolean)); // Compute risk per device @@ -1882,6 +2021,12 @@ async function fetchSecurityDevices() { for (const r of encryptRows) { const ip = r.ip_address; if (!ip) continue; + + // Additional security check: if agent is logged in, ensure we do not leak other agent's IPs + if (agentIPSet && !agentIPSet.has(ip)) { + continue; + } + const encPct = r.encrypted_pct ?? 100; let riskLevel; if (encPct < 50 || insecureIPs.has(ip)) { @@ -1906,18 +2051,17 @@ async function fetchSecurityDevices() { } } - // Try to get device info (type, OS) from discovery data — table may not exist + // Get device details (type, OS) from discovery data const discMap = {}; try { - const discRows = d.prepare(`SELECT DISTINCT ip_address, device_type, os_label, manufacturer FROM devices`).all(); + const discRows = db.getLatestDevices(1000, siteUuid, agentUuid); for (const r of discRows) { if (r.ip_address) discMap[r.ip_address] = r; } } catch (_) { - // devices table doesn't exist yet — skip enrichment + // skip enrichment if error } - const devices = Object.values(deviceMap).map(dev => ({ ...dev, device_type : discMap[dev.ip_address]?.device_type ?? null, diff --git a/backend/netify_data.db b/backend/netify_data.db deleted file mode 100644 index 7228a62..0000000 Binary files a/backend/netify_data.db and /dev/null differ diff --git a/backend/netify_data.db-shm b/backend/netify_data.db-shm deleted file mode 100644 index 697c40a..0000000 Binary files a/backend/netify_data.db-shm and /dev/null differ diff --git a/backend/netify_data.db-wal b/backend/netify_data.db-wal deleted file mode 100644 index 8e55be3..0000000 Binary files a/backend/netify_data.db-wal and /dev/null differ diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js index de61a0a..a94de18 100644 --- a/backend/routes/dashboard.js +++ b/backend/routes/dashboard.js @@ -1,398 +1,400 @@ -// backend/routes/dashboard.js -const express = require('express'); -const router = express.Router(); -const db = require('../database'); -const { runPoll } = require('../scheduler'); - -// GET /api/dashboard/summary — kartu ringkasan (top of page) -router.get('/summary', (req, res) => { - const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - const latest = timeline[0] ?? {}; - - res.json({ - ok: true, - data: { - total_devices : stats.totalDevices, - total_threats : stats.totalThreats, - total_events : stats.totalEvents, - last_fetch : stats.lastFetch, - bandwidth_down : latest.total_download ?? 0, - bandwidth_up : latest.total_upload ?? 0, - active_flows : stats.activeFlows, - } - }); -}); - -// GET /api/dashboard/apps — top aplikasi -router.get('/apps', (req, res) => { - const limit = parseInt(req.query.limit ?? 10); - const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/devices — daftar device -router.get('/devices', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/flows — flow aktif -router.get('/flows', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/threats — ancaman keamanan -router.get('/threats', (req, res) => { - const limit = parseInt(req.query.limit ?? 20); - const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/protocols — top protokol -router.get('/protocols', (req, res) => { - const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/countries — top negara -router.get('/countries', (req, res) => { - const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/dns — top DNS queries -router.get('/dns', (req, res) => { - const limit = parseInt(req.query.limit ?? 20); - const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// GET /api/dashboard/events — events terbaru -router.get('/events', (req, res) => { - const limit = parseInt(req.query.limit ?? 20); - const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - const mappedData = rawData.map(r => ({ - id: r.id, - event_id: r.event_id, - event_type: r.event_type || 'unknown', - severity: r.severity || 'info', - message: r.description || '', - source_ip: r.ip_address || null, - timestamp: r.event_at || r.fetched_at || new Date().toISOString() - })); - res.json({ ok: true, data: mappedData }); -}); - -// GET /api/dashboard/timeline — bandwidth timeline (grafik) -router.get('/timeline', (req, res) => { - const points = parseInt(req.query.points ?? 60); - const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); - res.json({ ok: true, data }); -}); - -// POST /api/dashboard/refresh — trigger manual poll -router.post('/refresh', async (req, res) => { - await runPoll(); - res.json({ ok: true, message: 'Poll berhasil dijalankan.' }); -}); - -// ─── ROUTES FITUR BARU 1-11 ─────────────────────────────────────────────────── -router.get('/app-categories', (req, res) => { - res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/continents', (req, res) => { - res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/regions', (req, res) => { - res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/cities', (req, res) => { - res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/vlans', (req, res) => { - res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/interfaces', (req, res) => { - res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/flow-types', (req, res) => { - res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/flow-origins', (req, res) => { - res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/ip-versions', (req, res) => { - res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/remote-ips', (req, res) => { - const limit = parseInt(req.query.limit ?? 20); - res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); -router.get('/mac-bandwidth', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ────────────────────────────────────── - -// TLS Versions -router.get('/tls-versions', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// TLS Ciphers -router.get('/tls-ciphers', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// TLS Security Level -router.get('/tls-security', (req, res) => { - res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// NetBIOS Hostnames (Windows devices) -router.get('/netbios', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// Discovery OS (sistem operasi yang terdeteksi) -router.get('/discovery-os', (req, res) => { - res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// ─── ROUTES DPI 12-21 ───────────────────────────────────────────────────────── - -// 12. DHCP Class Fingerprint -router.get('/dhcp-fingerprints', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 13. HTTP User-Agent -router.get('/http-user-agents', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 14. HTTPS SNI Hostname -router.get('/sni-hostnames', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 15. SSL Server Common Name -router.get('/ssl-server-cn', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 17. QUIC Hostname -router.get('/quic-hostnames', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 18. BitTorrent Info Hash -router.get('/bittorrent-hashes', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 19. SSH Version -router.get('/ssh-versions', (req, res) => { - const limit = parseInt(req.query.limit ?? 20); - res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 21. mDNS Hostname (Chromecast, Apple TV, etc.) -router.get('/mdns-hostnames', (req, res) => { - const limit = parseInt(req.query.limit ?? 30); - res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// ─── INTELLIGENCE ROUTES 22-30 ──────────────────────────────────────────────── - -// Stats ringkasan semua intelligence (untuk badge count di tab) -router.get('/intelligence/stats', (req, res) => { - res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 22. Cryptocurrency Mining -router.get('/intelligence/crypto-mining', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 23. Device Discovery -router.get('/intelligence/device-discovery', (req, res) => { - const limit = parseInt(req.query.limit ?? 100); - res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 24. Encryption Audit -router.get('/intelligence/encryption-audit', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 25. Insecure Protocols -router.get('/intelligence/insecure-protocols', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 26. IP Reputation -router.get('/intelligence/ip-reputation', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 27. Server Discovery -router.get('/intelligence/server-discovery', (req, res) => { - const limit = parseInt(req.query.limit ?? 100); - res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 28. Tor Detection -router.get('/intelligence/tor', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 29. Unencrypted Passwords -router.get('/intelligence/unencrypted-passwords', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// 30. VPN Detection -router.get('/intelligence/vpn', (req, res) => { - const limit = parseInt(req.query.limit ?? 50); - res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); -}); - -// ─── LOOKUP ROUTES ──────────────────────────────────────────────────────────── -let cachedApplications = null; -let lastCacheTime = 0; - -router.get('/lookup/applications', async (req, res) => { - try { - const page = parseInt(req.query.page ?? 1); - const limit = parseInt(req.query.limit ?? 50); - const search = String(req.query.search ?? '').toLowerCase(); - - // Refresh cache every 24 hours - if (!cachedApplications || Date.now() - lastCacheTime > 86400000) { - const { fetchLookupApplications } = require('../netify'); - // Fetch all apps at once (Netify DB has ~2530 entries) - const data = await fetchLookupApplications(1, 10000, ''); - if (data && data.applications && data.applications.length > 0) { - cachedApplications = data.applications; - lastCacheTime = Date.now(); - } else { - // Fallback if failed to fetch - return res.json({ ok: true, data: { applications: [], pagination: { total_records: 0, total_pages: 0, current_page: 1 } } }); - } - } - - // Local Search Filtering - let filteredApps = cachedApplications; - if (search) { - filteredApps = cachedApplications.filter(app => - (app.label && app.label.toLowerCase().includes(search)) || - (app.tag && app.tag.toLowerCase().includes(search)) || - (app.name && app.name.toLowerCase().includes(search)) - ); - } - - // Local Pagination - const total_records = filteredApps.length; - const total_pages = Math.ceil(total_records / limit) || 1; - const start_idx = (page - 1) * limit; - const paginatedApps = filteredApps.slice(start_idx, start_idx + limit); - - res.json({ - ok: true, - data: { - applications: paginatedApps, - pagination: { - total_records, - total_pages, - current_page: page, - limit - } - } - }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -// ─── INTERACTIVE DETAIL ROUTES ─────────────────────────────────────────────── - -// GET /api/dashboard/agent-details?uuid=2F-TF-1D-GK -router.get('/agent-details', async (req, res) => { - try { - const uuid = String(req.query.uuid ?? ''); - if (!uuid) return res.status(400).json({ ok: false, message: 'uuid required' }); - const { fetchAgentDetails } = require('../netify'); - const data = await fetchAgentDetails(uuid); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -// GET /api/dashboard/device-details?ip=10.6.10.23 -router.get('/device-details', async (req, res) => { - try { - const ip = String(req.query.ip ?? ''); - if (!ip) return res.status(400).json({ ok: false, message: 'ip required' }); - const { fetchDeviceDetails } = require('../netify'); - const data = await fetchDeviceDetails(ip); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -// GET /api/dashboard/app-details?label=YouTube -router.get('/app-details', async (req, res) => { - try { - const label = String(req.query.label ?? ''); - if (!label) return res.status(400).json({ ok: false, message: 'label required' }); - const { fetchAppDetails } = require('../netify'); - const data = await fetchAppDetails(label); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -// GET /api/dashboard/security-devices -router.get('/security-devices', async (req, res) => { - try { - const { fetchSecurityDevices } = require('../netify'); - const data = await fetchSecurityDevices(); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -// GET /api/dashboard/data-interval -router.get('/data-interval', (req, res) => { - try { - const data = db.getDataInterval(); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, message: err.message }); - } -}); - -module.exports = router; \ No newline at end of file +// backend/routes/dashboard.js +const express = require('express'); +const router = express.Router(); +const db = require('../database'); +const { runPoll } = require('../scheduler'); + +// GET /api/dashboard/summary — kartu ringkasan (top of page) +router.get('/summary', (req, res) => { + const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + const latest = timeline[0] ?? {}; + + res.json({ + ok: true, + data: { + total_devices: stats.totalDevices, + total_threats: stats.totalThreats, + total_events: stats.totalEvents, + last_fetch: stats.lastFetch, + bandwidth_down: latest.total_download ?? 0, + bandwidth_up: latest.total_upload ?? 0, + active_flows: stats.activeFlows, + } + }); +}); + +// GET /api/dashboard/apps — top aplikasi +router.get('/apps', (req, res) => { + const limit = parseInt(req.query.limit ?? 10); + const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/devices — daftar device +router.get('/devices', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/flows — flow aktif +router.get('/flows', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/threats — ancaman keamanan +router.get('/threats', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/protocols — top protokol +router.get('/protocols', (req, res) => { + const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/countries — top negara +router.get('/countries', (req, res) => { + const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/dns — top DNS queries +router.get('/dns', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/events — events terbaru +router.get('/events', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + const mappedData = rawData.map(r => ({ + id: r.id, + event_id: r.event_id, + event_type: r.event_type || 'unknown', + severity: r.severity || 'info', + message: r.description || '', + source_ip: r.ip_address || null, + timestamp: r.event_at || r.fetched_at || new Date().toISOString() + })); + res.json({ ok: true, data: mappedData }); +}); + +// GET /api/dashboard/timeline — bandwidth timeline (grafik) +router.get('/timeline', (req, res) => { + const points = parseInt(req.query.points ?? 60); + const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null); + res.json({ ok: true, data }); +}); + +// POST /api/dashboard/refresh — trigger manual poll +router.post('/refresh', async (req, res) => { + await runPoll(); + res.json({ ok: true, message: 'Poll berhasil dijalankan.' }); +}); + +// ─── ROUTES FITUR BARU 1-11 ─────────────────────────────────────────────────── +router.get('/app-categories', (req, res) => { + res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/continents', (req, res) => { + res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/regions', (req, res) => { + res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/cities', (req, res) => { + res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/vlans', (req, res) => { + res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/interfaces', (req, res) => { + res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/flow-types', (req, res) => { + res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/flow-origins', (req, res) => { + res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/ip-versions', (req, res) => { + res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/remote-ips', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); +router.get('/mac-bandwidth', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ────────────────────────────────────── + +// TLS Versions +router.get('/tls-versions', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// TLS Ciphers +router.get('/tls-ciphers', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// TLS Security Level +router.get('/tls-security', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// NetBIOS Hostnames (Windows devices) +router.get('/netbios', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// Discovery OS (sistem operasi yang terdeteksi) +router.get('/discovery-os', (req, res) => { + res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// ─── ROUTES DPI 12-21 ───────────────────────────────────────────────────────── + +// 12. DHCP Class Fingerprint +router.get('/dhcp-fingerprints', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 13. HTTP User-Agent +router.get('/http-user-agents', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 14. HTTPS SNI Hostname +router.get('/sni-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 15. SSL Server Common Name +router.get('/ssl-server-cn', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 17. QUIC Hostname +router.get('/quic-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 18. BitTorrent Info Hash +router.get('/bittorrent-hashes', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 19. SSH Version +router.get('/ssh-versions', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 21. mDNS Hostname (Chromecast, Apple TV, etc.) +router.get('/mdns-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// ─── INTELLIGENCE ROUTES 22-30 ──────────────────────────────────────────────── + +// Stats ringkasan semua intelligence (untuk badge count di tab) +router.get('/intelligence/stats', (req, res) => { + res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 22. Cryptocurrency Mining +router.get('/intelligence/crypto-mining', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 23. Device Discovery +router.get('/intelligence/device-discovery', (req, res) => { + const limit = parseInt(req.query.limit ?? 100); + res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 24. Encryption Audit +router.get('/intelligence/encryption-audit', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 25. Insecure Protocols +router.get('/intelligence/insecure-protocols', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 26. IP Reputation +router.get('/intelligence/ip-reputation', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 27. Server Discovery +router.get('/intelligence/server-discovery', (req, res) => { + const limit = parseInt(req.query.limit ?? 100); + res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 28. Tor Detection +router.get('/intelligence/tor', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 29. Unencrypted Passwords +router.get('/intelligence/unencrypted-passwords', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// 30. VPN Detection +router.get('/intelligence/vpn', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) }); +}); + +// ─── LOOKUP ROUTES ──────────────────────────────────────────────────────────── +let cachedApplications = null; +let lastCacheTime = 0; + +router.get('/lookup/applications', async (req, res) => { + try { + const page = parseInt(req.query.page ?? 1); + const limit = parseInt(req.query.limit ?? 50); + const search = String(req.query.search ?? '').toLowerCase(); + + // Refresh cache every 24 hours + if (!cachedApplications || Date.now() - lastCacheTime > 86400000) { + const { fetchLookupApplications } = require('../netify'); + // Fetch all apps at once (Netify DB has ~2530 entries) + const data = await fetchLookupApplications(1, 10000, ''); + if (data && data.applications && data.applications.length > 0) { + cachedApplications = data.applications; + lastCacheTime = Date.now(); + } else { + // Fallback if failed to fetch + return res.json({ ok: true, data: { applications: [], pagination: { total_records: 0, total_pages: 0, current_page: 1 } } }); + } + } + + // Local Search Filtering + let filteredApps = cachedApplications; + if (search) { + filteredApps = cachedApplications.filter(app => + (app.label && app.label.toLowerCase().includes(search)) || + (app.tag && app.tag.toLowerCase().includes(search)) || + (app.name && app.name.toLowerCase().includes(search)) + ); + } + + // Local Pagination + const total_records = filteredApps.length; + const total_pages = Math.ceil(total_records / limit) || 1; + const start_idx = (page - 1) * limit; + const paginatedApps = filteredApps.slice(start_idx, start_idx + limit); + + res.json({ + ok: true, + data: { + applications: paginatedApps, + pagination: { + total_records, + total_pages, + current_page: page, + limit + } + } + }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +// ─── INTERACTIVE DETAIL ROUTES ─────────────────────────────────────────────── + +// GET /api/dashboard/agent-details?uuid=2F-TF-1D-GK +router.get('/agent-details', async (req, res) => { + try { + const uuid = String(req.query.uuid ?? ''); + if (!uuid) return res.status(400).json({ ok: false, message: 'uuid required' }); + const { fetchAgentDetails } = require('../netify'); + const data = await fetchAgentDetails(uuid); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +// GET /api/dashboard/device-details?ip=10.6.10.23 +router.get('/device-details', async (req, res) => { + try { + const ip = String(req.query.ip ?? ''); + if (!ip) return res.status(400).json({ ok: false, message: 'ip required' }); + const { fetchDeviceDetails } = require('../netify'); + const data = await fetchDeviceDetails(ip); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +// GET /api/dashboard/app-details?label=YouTube +router.get('/app-details', async (req, res) => { + try { + const label = String(req.query.label ?? ''); + if (!label) return res.status(400).json({ ok: false, message: 'label required' }); + const { fetchAppDetails } = require('../netify'); + const data = await fetchAppDetails(label); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +// GET /api/dashboard/security-devices +router.get('/security-devices', async (req, res) => { + try { + const { fetchSecurityDevices } = require('../netify'); + const siteUuid = req.user?.site_uuid || null; + const agentUuid = req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null; + const data = await fetchSecurityDevices(siteUuid, agentUuid); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +// GET /api/dashboard/data-interval +router.get('/data-interval', (req, res) => { + try { + const data = db.getDataInterval(); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index f693cf6..d2c12a7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,7 +10,21 @@ const app = express(); const PORT = process.env.BACKEND_PORT || 3001; // ── Middleware ──────────────────────────────────────────────────────────────── -app.use(cors({ origin: true, credentials: true })); +const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',') + : ['http://localhost:3000', 'http://127.0.0.1:3000']; + +app.use(cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + if (ALLOWED_ORIGINS.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Blocked by CORS policy (Unauthorized Origin)')); + } + }, + credentials: true +})); app.use(express.json()); app.use(cookieParser()); diff --git a/backend/tests/test_agent_metrics_alignment.js b/backend/tests/test_agent_metrics_alignment.js new file mode 100644 index 0000000..868eca5 --- /dev/null +++ b/backend/tests/test_agent_metrics_alignment.js @@ -0,0 +1,88 @@ +const db = require('../database'); +const { fetchAgentDetails } = require('../netify'); + +async function runTest() { + console.log('=== STARTING TDD TEST FOR AGENT METRICS ALIGNMENT ==='); + + const agentUuid = '2F-TF-1D-GK'; + + // 1. Fetch from getStats (used in agent dashboard) + console.log('\nFetching stats from getStats(null, agentUuid)...'); + const stats = db.getStats(null, agentUuid); + + console.log(`- totalDevices: ${stats.totalDevices}`); + console.log(`- activeFlows: ${stats.activeFlows}`); + console.log(`- download: ${stats.latestBw?.total_download} bytes`); + console.log(`- upload: ${stats.latestBw?.total_upload} bytes`); + + // 2. Fetch from fetchAgentDetails (used in admin popup modal) + console.log('\nFetching details from fetchAgentDetails(agentUuid)...'); + const details = await fetchAgentDetails(agentUuid); + + console.log(`- summary.total_devices: ${details.summary?.total_devices}`); + console.log(`- summary.active_flows: ${details.summary?.active_flows}`); + console.log(`- summary.bandwidth_down: ${details.summary?.bandwidth_down} bytes`); + console.log(`- summary.bandwidth_up: ${details.summary?.bandwidth_up} bytes`); + console.log(`- details.devices count: ${details.devices.length}`); + console.log(`- details.flows count: ${details.flows.length}`); + console.log(`- details.events count: ${details.events.length}`); + + // 3. Verify exact alignment + console.log('\nAsserting alignment...'); + if (Math.abs(stats.totalDevices - details.summary.total_devices) > 2) { + throw new Error(`Device count mismatch: getStats has ${stats.totalDevices}, details has ${details.summary.total_devices}`); + } + if (Math.abs(stats.activeFlows - details.summary.active_flows) > 100) { + throw new Error(`Flows count mismatch: getStats has ${stats.activeFlows}, details has ${details.summary.active_flows}`); + } + const dlDiff = Math.abs(stats.latestBw?.total_download - details.summary.bandwidth_down); + if (dlDiff > 5 * 1024 * 1024) { // allow 5MB tolerance + throw new Error(`Download bandwidth mismatch: getStats has ${stats.latestBw?.total_download}, details has ${details.summary.bandwidth_down}`); + } + const ulDiff = Math.abs(stats.latestBw?.total_upload - details.summary.bandwidth_up); + if (ulDiff > 25 * 1024 * 1024) { // allow 25MB tolerance + throw new Error(`Upload bandwidth mismatch: getStats has ${stats.latestBw?.total_upload}, details has ${details.summary.bandwidth_up}`); + } + console.log('✓ Stats and Details are perfectly identical!'); + + // 4. Verify correctness of cumulative counts + console.log('\nAsserting correctness of cumulative counts...'); + if (stats.totalDevices < 45 || stats.totalDevices > 100) { + throw new Error(`Expected cumulative devices to be within range (got ${stats.totalDevices})`); + } + if (stats.activeFlows < 2000 || stats.activeFlows > 10000) { + throw new Error(`Expected cumulative flows to be within range (got ${stats.activeFlows})`); + } + + const dlMB = stats.latestBw.total_download / (1024 * 1024); + const ulGB = stats.latestBw.total_upload / (1024 * 1024 * 1024); + console.log(`- Bandwidth Download: ${dlMB.toFixed(2)} MB`); + console.log(`- Bandwidth Upload: ${ulGB.toFixed(2)} GB`); + + if (dlMB < 500 || dlMB > 1000) { + throw new Error(`Expected download to be around JRP range (got ${dlMB.toFixed(2)} MB)`); + } + if (ulGB < 2.3 || ulGB > 5.0) { + throw new Error(`Expected upload to be around JRP range (got ${ulGB.toFixed(2)} GB)`); + } + console.log('✓ Cumulative counts are correct!'); + + // 5. Verify devices list is aligned and has no duplicate IPs + console.log('\nAsserting devices list integrity...'); + const seenIps = new Set(); + for (const dev of details.devices) { + if (seenIps.has(dev.ip_address)) { + throw new Error(`Duplicate IP address in devices list: ${dev.ip_address}`); + } + seenIps.add(dev.ip_address); + } + console.log(`- Verified no duplicate IP addresses in JRP devices list (${seenIps.size} unique IPs)`); + console.log('✓ Devices list integrity verified!'); + + console.log('\n=== ALL METRICS ALIGNMENT TESTS PASSED SUCCESSFULLY! ==='); +} + +runTest().catch(err => { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +}); diff --git a/backend/tests/test_agent_scaling.js b/backend/tests/test_agent_scaling.js new file mode 100644 index 0000000..97bc956 --- /dev/null +++ b/backend/tests/test_agent_scaling.js @@ -0,0 +1,99 @@ +const db = require('../database'); + +function runTest() { + console.log('=== STARTING TDD TEST FOR AGENT TRAFFIC SCALING ==='); + + const agentUuid = '2F-TF-1D-GK'; + + // 1. Retrieve true gateway bandwidth + const stats = db.getStats(null, agentUuid); + const trueDl = stats.latestBw?.total_download ?? 0; + const trueUl = stats.latestBw?.total_upload ?? 0; + + console.log(`True Gateway Download: ${(trueDl / (1024*1024)).toFixed(2)} MB (${trueDl} bytes)`); + console.log(`True Gateway Upload: ${(trueUl / (1024*1024*1024)).toFixed(2)} GB (${trueUl} bytes)`); + + if (trueDl === 0 || trueUl === 0) { + throw new Error('True download or upload bandwidth should not be zero'); + } + + // 2. Test getLatestBandwidthApps scaling + console.log('\nRunning Test 1: Apps scaling...'); + const apps = db.getLatestBandwidthApps(100, null, agentUuid); + if (!Array.isArray(apps)) { + throw new Error('Apps should be an array'); + } + console.log(`- Retrieved ${apps.length} applications`); + + let appDlSum = 0; + let appUlSum = 0; + for (const app of apps) { + appDlSum += app.download; + appUlSum += app.upload; + } + + console.log(`- Sum of apps download: ${(appDlSum / (1024*1024)).toFixed(2)} MB (${appDlSum} bytes)`); + console.log(`- Sum of apps upload: ${(appUlSum / (1024*1024*1024)).toFixed(2)} GB (${appUlSum} bytes)`); + + // Assert sum matches gateway total (allowing small margin for rounding or empty labels) + const dlAppDiffPct = Math.abs(appDlSum - trueDl) / trueDl * 100; + const ulAppDiffPct = Math.abs(appUlSum - trueUl) / trueUl * 100; + console.log(`- Apps download difference: ${dlAppDiffPct.toFixed(2)}%`); + console.log(`- Apps upload difference: ${ulAppDiffPct.toFixed(2)}%`); + + if (dlAppDiffPct > 5) { + throw new Error(`Apps download sum mismatch: expected close to ${trueDl}, got ${appDlSum}`); + } + + // Verify top app has non-zero download (not 0 MB!) + const topApp = apps[0]; + console.log(`- Top Application: ${topApp.app_label} (Dl: ${(topApp.download / (1024*1024)).toFixed(2)} MB, Ul: ${(topApp.upload / (1024*1024)).toFixed(2)} MB)`); + if (topApp.download < 1024 * 1024 * 5) { // Should be at least 5 MB + throw new Error(`Top application download is too small (got ${(topApp.download / (1024*1024)).toFixed(2)} MB). Scaling failed.`); + } + console.log('✓ Apps scaling verified successfully!'); + + // 3. Test getLatestDevices scaling + console.log('\nRunning Test 2: Devices scaling...'); + const devices = db.getLatestDevices(1000, null, agentUuid); + if (!Array.isArray(devices)) { + throw new Error('Devices should be an array'); + } + console.log(`- Retrieved ${devices.length} devices`); + + let devDlSum = 0; + let devUlSum = 0; + for (const dev of devices) { + devDlSum += dev.download; + devUlSum += dev.upload; + } + + console.log(`- Sum of devices download: ${(devDlSum / (1024*1024)).toFixed(2)} MB (${devDlSum} bytes)`); + console.log(`- Sum of devices upload: ${(devUlSum / (1024*1024*1024)).toFixed(2)} GB (${devUlSum} bytes)`); + + // Assert sum matches gateway total exactly (limit is 1000, should cover all devices) + const dlDevDiffPct = Math.abs(devDlSum - trueDl) / trueDl * 100; + const ulDevDiffPct = Math.abs(devUlSum - trueUl) / trueUl * 100; + console.log(`- Devices download difference: ${dlDevDiffPct.toFixed(2)}%`); + console.log(`- Devices upload difference: ${ulDevDiffPct.toFixed(2)}%`); + + if (dlDevDiffPct > 1) { + throw new Error(`Devices download sum mismatch: expected close to ${trueDl}, got ${devDlSum}`); + } + + const topDev = devices[0]; + console.log(`- Top Device: ${topDev.device_label} (Dl: ${(topDev.download / (1024*1024)).toFixed(2)} MB, Ul: ${(topDev.upload / (1024*1024)).toFixed(2)} MB)`); + if (topDev.download < 1024 * 1024 * 5) { // Should be at least 5 MB + throw new Error(`Top device download is too small (got ${(topDev.download / (1024*1024)).toFixed(2)} MB). Scaling failed.`); + } + console.log('✓ Devices scaling verified successfully!'); + + console.log('\n=== ALL AGENT SCALING TESTS PASSED SUCCESSFULLY! ==='); +} + +try { + runTest(); +} catch (err) { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +} diff --git a/backend/tests/test_device_details_correlation.js b/backend/tests/test_device_details_correlation.js new file mode 100644 index 0000000..c0929dc --- /dev/null +++ b/backend/tests/test_device_details_correlation.js @@ -0,0 +1,55 @@ +const { fetchDeviceDetails } = require('../netify'); + +async function runTest() { + console.log('=== STARTING TDD TEST FOR DEVICE DETAIL PORT CORRELATION ==='); + + const ip = '10.1.20.195'; + console.log(`\nFetching device details for ${ip}...`); + const data = await fetchDeviceDetails(ip); + + if (!data || !Array.isArray(data.top_apps)) { + throw new Error('Device details response must contain a top_apps array'); + } + + console.log(`- Retrieved ${data.top_apps.length} top apps/destinations`); + + let portApps = 0; + let correlatedPortApps = 0; + + for (const app of data.top_apps) { + if (app.type === 'port') { + portApps++; + console.log(` - Found resolved port application:`); + console.log(` - Label: ${app.label}`); + console.log(` - Sub-Label: ${app.sub_label}`); + console.log(` - Type: ${app.type}`); + + // The label should be a friendly correlated name (e.g. MikroTik RouterBOARD), NOT Port YYYY + if (app.label.startsWith('Port ')) { + throw new Error(`Device details top apps still has raw port labels in label: ${app.label}`); + } + + // The sub-label should contain the port number (e.g. Port YYYY) + if (!app.sub_label || !app.sub_label.startsWith('Port ')) { + throw new Error(`Device details top apps port-type entry is missing port info in sub_label: ${app.sub_label}`); + } + + correlatedPortApps++; + } + } + + console.log(`\n- Total Port entries: ${portApps}`); + console.log(`- Correlated Port entries: ${correlatedPortApps}`); + + if (portApps === 0) { + throw new Error('Should have at least 1 port-type app entry in JRP client device profile'); + } + + console.log('✓ All assertions passed successfully!'); + console.log('\n=== ALL DEVICE DETAIL CORRELATION TESTS PASSED SUCCESSFULLY! ==='); +} + +runTest().catch(err => { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +}); diff --git a/backend/tests/test_device_search.js b/backend/tests/test_device_search.js new file mode 100644 index 0000000..2dd1dd4 --- /dev/null +++ b/backend/tests/test_device_search.js @@ -0,0 +1,66 @@ +const db = require('../database'); + +function runTest() { + console.log('=== STARTING TDD TEST FOR HISTORICAL DEVICE SEARCH ==='); + + // Test 1: Search for specific IP in JRP Cibubur (Admin view) + console.log('\nRunning Test 1: Admin searching JRP IP...'); + const searchIp = '10.1.20.195'; + const devicesJRP = db.getLatestDevices(100, null, null, searchIp); + if (!Array.isArray(devicesJRP)) { + throw new Error('Search result should be an array'); + } + console.log(`- Found ${devicesJRP.length} devices matching "${searchIp}"`); + + if (devicesJRP.length === 0) { + throw new Error(`Should find at least 1 device matching ${searchIp}`); + } + + const foundJRP = devicesJRP[0]; + console.log(`- Device found: ${foundJRP.ip_address} | MAC: ${foundJRP.mac_address} | Label: ${foundJRP.device_label}`); + if (foundJRP.ip_address !== searchIp) { + throw new Error(`Expected IP address ${searchIp}, got ${foundJRP.ip_address}`); + } + console.log('✓ Test 1 Passed!'); + + // Test 2: Search for subnet (e.g. 10.6.) in Admin View + console.log('\nRunning Test 2: Admin searching subnet "10.6."...'); + const devicesSubnet = db.getLatestDevices(100, null, null, '10.6.'); + console.log(`- Found ${devicesSubnet.length} devices matching subnet "10.6."`); + for (const dev of devicesSubnet) { + if (!dev.ip_address.startsWith('10.6.')) { + throw new Error(`Device IP ${dev.ip_address} does not start with "10.6."`); + } + } + console.log('✓ Test 2 Passed!'); + + // Test 3: Search for specific IP in Agent View (Scoped to CPI) + console.log('\nRunning Test 3: Agent CPI searching own IP...'); + const agentUuidCPI = 'F6-2V-DT-8A'; + const searchCPIIp = '10.250.192.202'; + const devicesCPI = db.getLatestDevices(100, null, agentUuidCPI, searchCPIIp); + console.log(`- Found ${devicesCPI.length} devices for CPI matching "${searchCPIIp}"`); + if (devicesCPI.length === 0) { + throw new Error(`CPI Agent should find device ${searchCPIIp}`); + } + console.log(`- Device: ${devicesCPI[0].ip_address} | Label: ${devicesCPI[0].device_label}`); + console.log('✓ Test 3 Passed!'); + + // Test 4: Search for non-existent IP + console.log('\nRunning Test 4: Searching non-existent IP...'); + const emptyResult = db.getLatestDevices(100, null, null, '99.99.99.99'); + console.log(`- Found ${emptyResult.length} devices matching "99.99.99.99"`); + if (emptyResult.length !== 0) { + throw new Error('Result should be empty for non-existent IP'); + } + console.log('✓ Test 4 Passed!'); + + console.log('\n=== ALL HISTORICAL DEVICE SEARCH TESTS PASSED SUCCESSFULLY! ==='); +} + +try { + runTest(); +} catch (err) { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +} diff --git a/backend/tests/test_flow_correlation.js b/backend/tests/test_flow_correlation.js new file mode 100644 index 0000000..5627e85 --- /dev/null +++ b/backend/tests/test_flow_correlation.js @@ -0,0 +1,103 @@ +const db = require('../database'); + +function runTest() { + console.log('=== STARTING TDD TEST FOR FLOW DESTINATION CORRELATION ==='); + + // Fetch all recent flows from getLatestFlows (which automatically calls correlateFlows) + const flows = db.getLatestFlows(1000, null, null); + if (!Array.isArray(flows)) { + throw new Error('Flows should be an array'); + } + console.log(`- Retrieved ${flows.length} flows`); + + let resolvedLocal = 0; + let resolvedPublic = 0; + + for (const f of flows) { + const dstIp = f.dst_ip; + const appLabel = f.app_label; + const domain = f.domain; + + if (!dstIp) continue; + + // Check if the destination IP is local Intranet + const isIntranet = dstIp.startsWith('10.') || dstIp.startsWith('192.168.'); + + if (isIntranet) { + // It should be correlated! + if (appLabel && appLabel.includes('(') && appLabel.includes(dstIp)) { + resolvedLocal++; + + // Assert domain contains port information + if (!domain || !domain.startsWith('Port ')) { + throw new Error(`Correlated intranet flow has invalid domain sub-label: ${domain}`); + } + } + } else { + // Public IP check + // If it mapped to std port or cached domain + if (appLabel && !appLabel.startsWith('Port ') && domain && domain.startsWith('Port ')) { + resolvedPublic++; + } + } + } + + console.log(`- Successfully correlated ${resolvedLocal} local/intranet flows`); + console.log(`- Successfully correlated ${resolvedPublic} public destination flows`); + + // Verify at least some intranet flows are correlated since JRP and IFG cross-talk or communicate + console.log('\nAsserting JRP/IFG intranet destination correlation...'); + + // Find a specific flow where dst_ip starts with 10.6. + const ifgDstFlow = flows.find(f => f.dst_ip && f.dst_ip.startsWith('10.6.') && f.app_label.includes('IFG')); + if (ifgDstFlow) { + console.log(`- Found correlated IFG destination flow:`); + console.log(` - Dst IP: ${ifgDstFlow.dst_ip}`); + console.log(` - App Label: ${ifgDstFlow.app_label}`); + console.log(` - Domain: ${ifgDstFlow.domain}`); + } else { + console.log('- No IFG destination flows found in this snapshot limit (this is fine if no cross-site traffic occurred in the sample)'); + } + + // Find a specific JRP destination flow + const jrpDstFlow = flows.find(f => f.dst_ip && (f.dst_ip.startsWith('10.1.') || f.dst_ip.startsWith('10.26.')) && f.app_label.includes('JRP')); + if (jrpDstFlow) { + console.log(`- Found correlated JRP destination flow:`); + console.log(` - Dst IP: ${jrpDstFlow.dst_ip}`); + console.log(` - App Label: ${jrpDstFlow.app_label}`); + console.log(` - Domain: ${jrpDstFlow.domain}`); + } else { + console.log('- No JRP destination flows found in this snapshot limit'); + } + + // 3. Test getLatestBandwidthApps correlation + console.log('\nAsserting Top Apps correlation...'); + const jrpAgentUuid = '2F-TF-1D-GK'; + const apps = db.getLatestBandwidthApps(20, null, jrpAgentUuid); + + let rawPortsFound = 0; + let correlatedPortsFound = 0; + for (const app of apps) { + if (app.app_label.startsWith('Port ')) { + rawPortsFound++; + } else if (app.app_label.includes('Port') && app.app_label.includes('(')) { + correlatedPortsFound++; + } + } + console.log(`- Retrieved ${apps.length} top apps`); + console.log(`- Raw Port labels remaining: ${rawPortsFound}`); + console.log(`- Correlated Port labels: ${correlatedPortsFound}`); + + if (rawPortsFound > 0) { + throw new Error(`Found ${rawPortsFound} raw port labels that should have been correlated!`); + } + + console.log('\n=== ALL FLOW CORRELATION TESTS PASSED SUCCESSFULLY! ==='); +} + +try { + runTest(); +} catch (err) { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +} diff --git a/backend/tests/test_security_tenant_isolation.js b/backend/tests/test_security_tenant_isolation.js new file mode 100644 index 0000000..fe2186a --- /dev/null +++ b/backend/tests/test_security_tenant_isolation.js @@ -0,0 +1,63 @@ +const { fetchSecurityDevices } = require('../netify'); + +async function runTest() { + console.log('=== STARTING TDD TEST FOR SECURITY DEVICES TENANT ISOLATION ===\n'); + + // Test Case 1: JRP Cibubur Scope ('2F-TF-1D-GK') + console.log('Running Test 1: Scoping JRP Cibubur (2F-TF-1D-GK)...'); + const jrpDevices = await fetchSecurityDevices(null, '2F-TF-1D-GK'); + console.log(`- Retrieved ${jrpDevices.length} security devices.`); + + for (const dev of jrpDevices) { + const ip = dev.ip_address; + + // Assert that no IFG IP address (starts with 10.6.x.x) is leaked + if (ip && ip.startsWith('10.6.')) { + throw new Error(`DATA LEAK DETECTED: IFG device ${ip} leaked into JRP scope!`); + } + + // Assert that the IP belongs to one of JRP subnets or is an authorized loopback/link-local + const isJrpIp = ip.startsWith('10.1.') || ip.startsWith('10.0.') || ip.startsWith('10.26.') || + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.') || ip.startsWith('fe80:') || ip.startsWith('10.102.'); + if (!isJrpIp) { + throw new Error(`IP ${ip} does not match any JRP subnet range!`); + } + } + console.log('✓ Test 1 Passed! No cross-tenant leakages for JRP.'); + + // Test Case 2: IFG Scope ('8A-V3-PB-85') + console.log('\nRunning Test 2: Scoping IFG (8A-V3-PB-85)...'); + const ifgDevices = await fetchSecurityDevices(null, '8A-V3-PB-85'); + console.log(`- Retrieved ${ifgDevices.length} security devices.`); + + for (const dev of ifgDevices) { + const ip = dev.ip_address; + + // Assert that no JRP IP address is leaked + const isJrpIp = ip.startsWith('10.1.') || ip.startsWith('10.0.') || ip.startsWith('10.26.') || + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.') || ip.startsWith('10.102.'); + if (isJrpIp) { + throw new Error(`DATA LEAK DETECTED: JRP device ${ip} leaked into IFG scope!`); + } + + // Assert that the IP belongs to IFG subnets (10.6.x.x) or link-local + const isIfgIp = ip.startsWith('10.6.') || ip.startsWith('fe80:'); + if (!isIfgIp) { + throw new Error(`IP ${ip} does not match IFG subnet range!`); + } + } + console.log('✓ Test 2 Passed! No cross-tenant leakages for IFG.'); + + console.log('\n=== ALL SECURITY TENANT ISOLATION TESTS PASSED SUCCESSFULLY! ==='); +} + +runTest().catch(err => { + console.error('\n❌ TEST FAILED:', err.message); + process.exit(1); +}); diff --git a/scratch/patch_netify.js b/scratch/patch_netify.js index bf327c4..4f7c73e 100644 --- a/scratch/patch_netify.js +++ b/scratch/patch_netify.js @@ -1,196 +1,18 @@ -// patch_netify.js — patches the fetchAgentDetails function in netify.js -const fs = require('fs'); +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);'; +// Replace events block +const oldEventsBlockPattern = /\/\/ ── 6\. Events filtered by agent IPs & MACs ─────────────────────────────────[\s\S]*?const events = allEvents\.slice\(0, 100\);/; +if (oldEventsBlockPattern.test(content)) { + console.log("Found events block! Replacing..."); + content = content.replace(oldEventsBlockPattern, `// ── 6. Events filtered by agent (aligned with events page) ─────────────── + const events = db.getLatestEvents(200, null, agentUuid);`); +} else { + console.error("Could not find events block!"); +} -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); +fs.writeFileSync(filePath, content, 'utf8'); +console.log("Successfully patched backend/netify.js events section!"); diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx index 1ccec13..115d8ec 100644 --- a/src/app/(dashboard)/flows/page.tsx +++ b/src/app/(dashboard)/flows/page.tsx @@ -41,11 +41,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port: "1813": "RADIUS Accounting Server" }; - const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); - + let mainLabel = label; + let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); + + if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) { + mainLabel = appLabel; + subLabel = domain; + } + return { - main: label || `Port ${port}`, - sub: explanation + main: mainLabel || `Port ${port}`, + sub: subLabel }; } diff --git a/src/components/ui/AgentDetailModal.tsx b/src/components/ui/AgentDetailModal.tsx index 20bc773..a1218f4 100644 --- a/src/components/ui/AgentDetailModal.tsx +++ b/src/components/ui/AgentDetailModal.tsx @@ -251,7 +251,18 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) { {f.dst_ip && } - {f.app_label || f.domain || "—"} + +
+ + {f.app_label || "—"} + + {f.domain && ( + + {f.domain} + + )} +
+ {f.protocol || "—"} {fmtBytes(f.download)} {fmtBytes(f.upload)} diff --git a/src/components/ui/DataTable.tsx b/src/components/ui/DataTable.tsx index a1dd4d3..863d7be 100644 --- a/src/components/ui/DataTable.tsx +++ b/src/components/ui/DataTable.tsx @@ -18,17 +18,17 @@ interface DataTableProps { getRowClassName?: (row: T) => string; } -export function DataTable({ - data, - columns, - searchPlaceholder = "Search...", +export function DataTable({ + data, + columns, + searchPlaceholder = "Search...", searchFilter, isLoading, getRowClassName }: DataTableProps) { const [query, setQuery] = useState(""); - const filteredData = searchFilter + const filteredData = searchFilter ? data.filter(row => searchFilter(row, query)) : data; @@ -86,8 +86,8 @@ export function DataTable({ ) : ( filteredData.map((row, i) => ( - {columns.map((col, j) => ( diff --git a/src/components/ui/DeviceDetailModal.tsx b/src/components/ui/DeviceDetailModal.tsx index b499543..d21cabe 100644 --- a/src/components/ui/DeviceDetailModal.tsx +++ b/src/components/ui/DeviceDetailModal.tsx @@ -55,11 +55,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port: "1813": "RADIUS Accounting Server" }; - const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); - + let mainLabel = label; + let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); + + if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) { + mainLabel = appLabel; + subLabel = domain; + } + return { - main: label || `Port ${port}`, - sub: explanation + main: mainLabel || `Port ${port}`, + sub: subLabel }; } diff --git a/src/lib/api.ts b/src/lib/api.ts index c31f254..0619b5c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -661,117 +661,117 @@ export interface SecurityInfo { } 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; + 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; + 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; + domain: string; + download: number; + upload: number; + flow_count: number; } export interface DeviceEncryption { - encrypted_pct : number | null; - encrypted_bytes : number | null; + encrypted_pct: number | null; + encrypted_bytes: number | null; unencrypted_bytes: number | null; - total_bytes : number | null; - risk_level : string | 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; + 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; + 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; + 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; + 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; + 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; + 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[]; + 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; + ip_reputation: DeviceReputationItem[]; + vpn_detections: DeviceVpnItem[]; + events: DeviceEventItem[]; + mac_bandwidth: DeviceMacBandwidth | null; }