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 &&