feat(security): isolate multi-tenant agent audit metrics, restrict CORS origins, exclude sqlite databases from git tracking, and add TDD test suite

This commit is contained in:
vanne committed 2026-07-01 23:26:48 +07:00
1 parent b816c1e570
commit e9f35c5a6b
19 files changed
+1306 -358

No files matched your search

+7 -2
View File
@@ -41,7 +41,12 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
# local databases & temporary files # local databases & temporary files
temp.json temp.json
/scratch /scratch
backend/*.db
backend/*.db-shm
backend/*.db-wal
backend/*.sqlite
*.db
*.db-shm
*.db-wal
+502 -38
View File
@@ -778,22 +778,66 @@ function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) {
if (agentUuid && AGENT_MAC_MAP[agentUuid]) { if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
const macs = AGENT_MAC_MAP[agentUuid]; const macs = AGENT_MAC_MAP[agentUuid];
const placeholders = macs.map(() => '?').join(','); const placeholders = macs.map(() => '?').join(',');
return d.prepare(`
SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, // 1. Get raw aggregated apps bandwidth from flows table for this agent (cumulative)
(SUM(bytes_download) + SUM(bytes_upload)) AS total, COUNT(*) AS flow_count const rawApps = d.prepare(`
SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload
FROM flows 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 GROUP BY app_label
ORDER BY download DESC `).all(...macs);
LIMIT ?
`).all(...macs, latest.t, limit); 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(); const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get();
if (!appsLatest?.t) return []; 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 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 }); `).all({ fetched_at: appsLatest.t, limit, siteUuid });
return correlateAppLabels(rows);
} }
function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) { function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) {
@@ -908,7 +952,7 @@ function deviceMatchesAgent(ip, mac, agentUuid) {
return false; return false;
} }
function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) {
const d = getDB(); const d = getDB();
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
if (!latest?.t) return []; 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 resolved = [];
const seenIps = new Set(); 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 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'; 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 { return {
id: dbDev ? dbDev.id : null, id: dbDev ? dbDev.id : null,
site_uuid: dbDev ? dbDev.site_uuid : siteUuid, site_uuid: dbDev ? dbDev.site_uuid : siteUuid,
@@ -966,9 +1037,9 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) {
device_type: meta.type, device_type: meta.type,
os_label: meta.os, os_label: meta.os,
manufacturer: meta.manufacturer, manufacturer: meta.manufacturer,
download: download || 0, download: scaledDl || 0,
upload: upload || 0, upload: scaledUl || 0,
total: (download || 0) + (upload || 0), total: (scaledDl || 0) + (scaledUl || 0),
is_gateway_routed: isRouted ? 1 : 0 is_gateway_routed: isRouted ? 1 : 0
}; };
} }
@@ -996,6 +1067,95 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) {
return resolved.slice(0, limit); 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 // Load intelligence tables to assist in type resolving
const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); const intelList = d.prepare("SELECT * FROM intel_device_discovery").all();
const intelMap = new Map(intelList.map(i => [i.ip_address, i])); 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); 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) { 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(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
if (!latest?.t) return []; if (!latest?.t) return [];
let rows = [];
if (agentUuid && AGENT_MAC_MAP[agentUuid]) { if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
const macs = AGENT_MAC_MAP[agentUuid]; const macs = AGENT_MAC_MAP[agentUuid];
const placeholders = macs.map(() => '?').join(','); const placeholders = macs.map(() => '?').join(',');
return d.prepare(` rows = d.prepare(`
SELECT * FROM flows SELECT * FROM flows
WHERE src_mac IN (${placeholders}) WHERE src_mac IN (${placeholders})
ORDER BY last_seen DESC, fetched_at DESC ORDER BY last_seen DESC, fetched_at DESC
LIMIT ? LIMIT ?
`).all(...macs, limit); `).all(...macs, limit);
} } else {
rows = d.prepare(`
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 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 }); `).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) { function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) {
@@ -1320,8 +1762,10 @@ function insertAppCategories(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO app_categories const stmt = d.prepare(`INSERT INTO app_categories
(site_uuid, fetched_at, category_label, download, upload, total) (site_uuid, fetched_at, category_label, download, upload, total)
VALUES (?, ?,?,?,?,?)`); VALUES (?, ?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); })(rows); for (const r of items) stmt.run(
siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total);
})(rows);
} }
function insertContinents(rows, fetchedAt, siteUuid) { function insertContinents(rows, fetchedAt, siteUuid) {
@@ -1329,8 +1773,10 @@ function insertContinents(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO continents const stmt = d.prepare(`INSERT INTO continents
(site_uuid, fetched_at, continent_name, download, upload, total) (site_uuid, fetched_at, continent_name, download, upload, total)
VALUES (?, ?,?,?,?,?)`); VALUES (?, ?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); })(rows); for (const r of items) stmt.run(
siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total);
})(rows);
} }
function insertRegions(rows, fetchedAt, siteUuid) { function insertRegions(rows, fetchedAt, siteUuid) {
@@ -1338,8 +1784,10 @@ function insertRegions(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO regions const stmt = d.prepare(`INSERT INTO regions
(site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download)
VALUES (?, ?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); })(rows); 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) { function insertCities(rows, fetchedAt, siteUuid) {
@@ -1347,8 +1795,10 @@ function insertCities(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO cities const stmt = d.prepare(`INSERT INTO cities
(site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download)
VALUES (?, ?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); })(rows); 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) { function insertVLANs(rows, fetchedAt, siteUuid) {
@@ -1356,8 +1806,10 @@ function insertVLANs(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO vlans const stmt = d.prepare(`INSERT INTO vlans
(site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total)
VALUES (?, ?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); })(rows); 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) { function insertInterfaces(rows, fetchedAt, siteUuid) {
@@ -1365,8 +1817,10 @@ function insertInterfaces(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO interfaces const stmt = d.prepare(`INSERT INTO interfaces
(site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) (site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total)
VALUES (?, ?,?,?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); })(rows); 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) { function insertFlowTypes(rows, fetchedAt, siteUuid) {
@@ -1374,8 +1828,10 @@ function insertFlowTypes(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO flow_types const stmt = d.prepare(`INSERT INTO flow_types
(site_uuid, fetched_at, flow_type_label, download, upload, total) (site_uuid, fetched_at, flow_type_label, download, upload, total)
VALUES (?, ?,?,?,?,?)`); VALUES (?, ?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); })(rows); 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) { function insertFlowOrigins(rows, fetchedAt, siteUuid) {
@@ -1383,8 +1839,10 @@ function insertFlowOrigins(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO flow_origins const stmt = d.prepare(`INSERT INTO flow_origins
(site_uuid, fetched_at, flow_origin_label, download, upload, total) (site_uuid, fetched_at, flow_origin_label, download, upload, total)
VALUES (?, ?,?,?,?,?)`); VALUES (?, ?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); })(rows); 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) { function insertIPVersions(rows, fetchedAt, siteUuid) {
@@ -1392,8 +1850,10 @@ function insertIPVersions(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO ip_versions const stmt = d.prepare(`INSERT INTO ip_versions
(site_uuid, fetched_at, ip_version_label, download, upload, total) (site_uuid, fetched_at, ip_version_label, download, upload, total)
VALUES (?, ?,?,?,?,?)`); VALUES (?, ?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); })(rows); 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) { function insertRemoteIPs(rows, fetchedAt, siteUuid) {
@@ -1401,8 +1861,10 @@ function insertRemoteIPs(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO remote_ips const stmt = d.prepare(`INSERT INTO remote_ips
(site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total)
VALUES (?, ?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); })(rows); 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) { function insertMACBandwidth(rows, fetchedAt, siteUuid) {
@@ -1410,8 +1872,10 @@ function insertMACBandwidth(rows, fetchedAt, siteUuid) {
const stmt = d.prepare(`INSERT INTO mac_bandwidth const stmt = d.prepare(`INSERT INTO mac_bandwidth
(site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total)
VALUES (?, ?,?,?,?,?,?)`); VALUES (?, ?,?,?,?,?,?)`);
d.transaction(items => { for (const r of items) stmt.run( d.transaction(items => {
siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); })(rows); for (const r of items) stmt.run(
siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total);
})(rows);
} }
// ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── // ─── QUERY FITUR BARU ─────────────────────────────────────────────────────────
+178 -34
View File
@@ -1381,8 +1381,8 @@ async function fetchDeviceDetails(ip) {
is_new : discRow?.is_new ?? null, is_new : discRow?.is_new ?? null,
}; };
// ── 3. Named apps (exclude "Port XXX" port-only entries) ───────────────── // ── 3. Named apps & correlated ports ─────────────────────────────────────
const namedAppRows = d.prepare(` const rawAppRows = d.prepare(`
SELECT app_label, SELECT app_label,
SUM(bytes_download) AS download, SUM(bytes_download) AS download,
SUM(bytes_upload) AS upload, SUM(bytes_upload) AS upload,
@@ -1390,12 +1390,93 @@ async function fetchDeviceDetails(ip) {
FROM flows FROM flows
WHERE src_ip = ? WHERE src_ip = ?
AND app_label IS NOT NULL AND app_label IS NOT NULL
AND app_label NOT LIKE 'Port %'
GROUP BY app_label GROUP BY app_label
ORDER BY download DESC ORDER BY download DESC
LIMIT 20 LIMIT 30
`).all(ip); `).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 ───────────────────────────── // ── 4. Top domains accessed by this device ─────────────────────────────
const domainRows = d.prepare(` const domainRows = d.prepare(`
SELECT domain, SELECT domain,
@@ -1417,11 +1498,7 @@ async function fetchDeviceDetails(ip) {
LIMIT 30 LIMIT 30
`).all(ip); `).all(ip);
// ── 5. Smart combined: flows with BOTH domain and app_label, or just one ─ // ── 5. Smart combined display list ──────────────────────────────────────
// Build combined display list:
// Priority 1 = rows with actual domain (show domain as label)
// Priority 2 = rows with named app (not port-only)
// Merge & de-duplicate by display name
const combinedMap = new Map(); const combinedMap = new Map();
// Add domains first (higher priority) // Add domains first (higher priority)
@@ -1436,14 +1513,48 @@ async function fetchDeviceDetails(ip) {
}); });
} }
// Add named apps that don't duplicate a domain entry // Add named & correlated apps
for (const r of namedAppRows) { for (const r of rawAppRows) {
const key = 'app:' + r.app_label; 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)) { if (!combinedMap.has(key)) {
combinedMap.set(key, { combinedMap.set(key, {
label : r.app_label, // protocol name (DNS, HTTPS/TLS, etc) label : label,
sub_label : null, sub_label : sub_label,
type : 'protocol', type : type,
download : r.download ?? 0, download : r.download ?? 0,
upload : r.upload ?? 0, upload : r.upload ?? 0,
flow_count : r.flow_count, 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.values()].sort((a, b) => b.download - a.download).slice(0, 25);
const top_apps = combinedMap.size > 0
? [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25)
: d.prepare(`
SELECT app_label AS label, NULL AS sub_label, 'port' AS type,
SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count
FROM flows WHERE src_ip = ? AND app_label IS NOT NULL
GROUP BY app_label ORDER BY download DESC LIMIT 25
`).all(ip).map(r => ({ label: r.label, sub_label: null, type: 'port', download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count }));
// top_domains: keep simple list for Info tab // top_domains: keep simple list for Info tab
const top_domains = domainRows.map(r => ({ 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 // 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 db = require('./database');
const d = db.getDB(); const d = db.getDB();
const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; // Get active IPs and MACs for filtering if agentUuid is provided
const encryptRows = latestFetch let agentIPs = null;
? d.prepare(`SELECT * FROM intel_encryption_audit WHERE fetched_at = ?`).all(latestFetch) 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)); const insecureIPs = new Set(insecureRows.map(r => r.ip_address).filter(Boolean));
// Compute risk per device // Compute risk per device
@@ -1882,6 +2021,12 @@ async function fetchSecurityDevices() {
for (const r of encryptRows) { for (const r of encryptRows) {
const ip = r.ip_address; const ip = r.ip_address;
if (!ip) continue; 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; const encPct = r.encrypted_pct ?? 100;
let riskLevel; let riskLevel;
if (encPct < 50 || insecureIPs.has(ip)) { 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 = {}; const discMap = {};
try { 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) { for (const r of discRows) {
if (r.ip_address) discMap[r.ip_address] = r; if (r.ip_address) discMap[r.ip_address] = r;
} }
} catch (_) { } catch (_) {
// devices table doesn't exist yet — skip enrichment // skip enrichment if error
} }
const devices = Object.values(deviceMap).map(dev => ({ const devices = Object.values(deviceMap).map(dev => ({
...dev, ...dev,
device_type : discMap[dev.ip_address]?.device_type ?? null, device_type : discMap[dev.ip_address]?.device_type ?? null,
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10 -8
View File
@@ -13,13 +13,13 @@ router.get('/summary', (req, res) => {
res.json({ res.json({
ok: true, ok: true,
data: { data: {
total_devices : stats.totalDevices, total_devices: stats.totalDevices,
total_threats : stats.totalThreats, total_threats: stats.totalThreats,
total_events : stats.totalEvents, total_events: stats.totalEvents,
last_fetch : stats.lastFetch, last_fetch: stats.lastFetch,
bandwidth_down : latest.total_download ?? 0, bandwidth_down: latest.total_download ?? 0,
bandwidth_up : latest.total_upload ?? 0, bandwidth_up: latest.total_upload ?? 0,
active_flows : stats.activeFlows, active_flows: stats.activeFlows,
} }
}); });
}); });
@@ -378,7 +378,9 @@ router.get('/app-details', async (req, res) => {
router.get('/security-devices', async (req, res) => { router.get('/security-devices', async (req, res) => {
try { try {
const { fetchSecurityDevices } = require('../netify'); const { fetchSecurityDevices } = require('../netify');
const data = await fetchSecurityDevices(); 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 }); res.json({ ok: true, data });
} catch (err) { } catch (err) {
res.status(500).json({ ok: false, message: err.message }); res.status(500).json({ ok: false, message: err.message });
+15 -1
View File
@@ -10,7 +10,21 @@ const app = express();
const PORT = process.env.BACKEND_PORT || 3001; const PORT = process.env.BACKEND_PORT || 3001;
// ── Middleware ──────────────────────────────────────────────────────────────── // ── 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(express.json());
app.use(cookieParser()); app.use(cookieParser());
@@ -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);
});
+99
View File
@@ -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);
}
@@ -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);
});
+66
View File
@@ -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);
}
+103
View File
@@ -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);
}
@@ -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);
});
+11 -189
View File
@@ -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 path = require('path');
const filePath = path.join(__dirname, '..', 'backend', 'netify.js'); const filePath = path.join(__dirname, '..', 'backend', 'netify.js');
let content = fs.readFileSync(filePath, 'utf8'); let content = fs.readFileSync(filePath, 'utf8');
// Find the start marker (after the top_apps mapping block) // Replace events block
const startMarker = ' // ── 2. Distinct devices for this agent ─────────────────────────────────────\n const devRows = d.prepare(`\n WHERE src_mac IN (${ph})\n ORDER BY last_seen DESC\n LIMIT 50\n `).all(...macs);'; const 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}'; fs.writeFileSync(filePath, content, 'utf8');
console.log("Successfully patched backend/netify.js events section!");
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);
+9 -3
View File
@@ -41,11 +41,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port:
"1813": "RADIUS Accounting Server" "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 { return {
main: label || `Port ${port}`, main: mainLabel || `Port ${port}`,
sub: explanation sub: subLabel
}; };
} }
+12 -1
View File
@@ -251,7 +251,18 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) {
{f.dst_ip && <IpDetails ip={f.dst_ip} />} {f.dst_ip && <IpDetails ip={f.dst_ip} />}
</div> </div>
</td> </td>
<td className="px-3 py-2 text-xs text-purple-300 max-w-[140px] truncate">{f.app_label || f.domain || "—"}</td> <td className="px-3 py-2 whitespace-nowrap max-w-[200px]">
<div className="flex flex-col gap-0.5">
<span className="font-semibold text-purple-300 text-xs truncate" title={f.app_label || "—"}>
{f.app_label || "—"}
</span>
{f.domain && (
<span className="text-[10px] text-slate-500 italic truncate" title={f.domain}>
{f.domain}
</span>
)}
</div>
</td>
<td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td> <td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td>
<td className="px-3 py-2 text-right text-xs text-cyan-400 whitespace-nowrap">{fmtBytes(f.download)}</td> <td className="px-3 py-2 text-right text-xs text-cyan-400 whitespace-nowrap">{fmtBytes(f.download)}</td>
<td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td> <td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td>
+9 -3
View File
@@ -55,11 +55,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port:
"1813": "RADIUS Accounting Server" "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 { return {
main: label || `Port ${port}`, main: mainLabel || `Port ${port}`,
sub: explanation sub: subLabel
}; };
} }
+79 -79
View File
@@ -661,117 +661,117 @@ export interface SecurityInfo {
} }
export interface DeviceInfo { export interface DeviceInfo {
device_label : string | null; device_label: string | null;
device_type : string | null; device_type: string | null;
os_label : string | null; os_label: string | null;
manufacturer : string | null; manufacturer: string | null;
mac_address : string | null; mac_address: string | null;
is_new : number | null; is_new: number | null;
} }
export interface DeviceAppItem { export interface DeviceAppItem {
label : string; // domain name OR protocol name label: string; // domain name OR protocol name
sub_label : string | null; // protocol when label is domain; null otherwise sub_label: string | null; // protocol when label is domain; null otherwise
type : 'domain' | 'protocol' | 'port'; type: 'domain' | 'protocol' | 'port';
download : number; download: number;
upload : number; upload: number;
flow_count : number; flow_count: number;
} }
export interface DeviceDomainItem { export interface DeviceDomainItem {
domain : string; domain: string;
download : number; download: number;
upload : number; upload: number;
flow_count : number; flow_count: number;
} }
export interface DeviceEncryption { export interface DeviceEncryption {
encrypted_pct : number | null; encrypted_pct: number | null;
encrypted_bytes : number | null; encrypted_bytes: number | null;
unencrypted_bytes: number | null; unencrypted_bytes: number | null;
total_bytes : number | null; total_bytes: number | null;
risk_level : string | null; risk_level: string | null;
} }
export interface DeviceServerItem { export interface DeviceServerItem {
server_type : string | null; server_type: string | null;
hostname : string | null; hostname: string | null;
port : number | null; port: number | null;
protocol : string | null; protocol: string | null;
os_label : string | null; os_label: string | null;
download : number; download: number;
upload : number; upload: number;
detected_at : string | null; detected_at: string | null;
} }
export interface DevicePwdItem { export interface DevicePwdItem {
dst_ip : string | null; dst_ip: string | null;
dst_port : number | null; dst_port: number | null;
protocol : string | null; protocol: string | null;
username : string | null; username: string | null;
severity : string | null; severity: string | null;
download : number; download: number;
upload : number; upload: number;
detected_at : string | null; detected_at: string | null;
} }
export interface DeviceReputationItem { export interface DeviceReputationItem {
remote_ip : string | null; remote_ip: string | null;
local_ip : string | null; local_ip: string | null;
reputation : string | null; reputation: string | null;
score : number | null; score: number | null;
country : string | null; country: string | null;
app_label : string | null; app_label: string | null;
blacklisted : boolean; blacklisted: boolean;
download : number; download: number;
upload : number; upload: number;
} }
export interface DeviceVpnItem { export interface DeviceVpnItem {
vpn_type : string | null; vpn_type: string | null;
remote_ip : string | null; remote_ip: string | null;
protocol : string | null; protocol: string | null;
country : string | null; country: string | null;
confidence : number | null; confidence: number | null;
download : number; download: number;
upload : number; upload: number;
detected_at : string | null; detected_at: string | null;
} }
export interface DeviceEventItem { export interface DeviceEventItem {
event_type : string | null; event_type: string | null;
severity : string | null; severity: string | null;
ip_address : string | null; ip_address: string | null;
mac_address : string | null; mac_address: string | null;
description : string | null; description: string | null;
event_at : string | null; event_at: string | null;
} }
export interface DeviceMacBandwidth { export interface DeviceMacBandwidth {
mac_address : string; mac_address: string;
manufacturer : string | null; manufacturer: string | null;
download : number; download: number;
upload : number; upload: number;
total : number; total: number;
} }
export interface DeviceDetails { export interface DeviceDetails {
ip : string; ip: string;
mac_address : string | null; mac_address: string | null;
total_download : number; total_download: number;
total_upload : number; total_upload: number;
flow_count : number; flow_count: number;
device_info : DeviceInfo; device_info: DeviceInfo;
top_apps : DeviceAppItem[]; top_apps: DeviceAppItem[];
top_domains : DeviceDomainItem[]; top_domains: DeviceDomainItem[];
flows : DeviceFlowItem[]; flows: DeviceFlowItem[];
encryption : DeviceEncryption | null; encryption: DeviceEncryption | null;
server_discovery : DeviceServerItem[]; server_discovery: DeviceServerItem[];
unencrypted_passwords: DevicePwdItem[]; unencrypted_passwords: DevicePwdItem[];
ip_reputation : DeviceReputationItem[]; ip_reputation: DeviceReputationItem[];
vpn_detections : DeviceVpnItem[]; vpn_detections: DeviceVpnItem[];
events : DeviceEventItem[]; events: DeviceEventItem[];
mac_bandwidth : DeviceMacBandwidth | null; mac_bandwidth: DeviceMacBandwidth | null;
} }