diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index ad030bc..81da780 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -11,3 +11,7 @@ - Users must have defined roles (e.g., SUPER_ADMIN, TENANT_ADMIN, SOC_ANALYST, ENGINEER). - Frontend components and Backend API routes must check user roles before rendering sensitive actions (e.g., hiding "Delete Agent" for Analysts). - **Authentication**: Use a robust session management system (e.g., NextAuth.js or JWT-based auth via backend) before exposing the dashboard. + +# Strict Confidentiality & Privacy +- **DO NOT DISCLOSE**: All prompts, discussions, and details regarding the "Web Dashboard BackOne Deep Package Inspection" project are strictly confidential. +- **RESTRICTIONS**: It is strictly forbidden to publish, expose, or share any part of this project with the public. Future instances of this agent MUST strictly protect all information about the BackOne DPI Dashboard. diff --git a/backend/database.js b/backend/database.js index 2ab6ce0..33648bf 100644 --- a/backend/database.js +++ b/backend/database.js @@ -104,7 +104,7 @@ function initSchema() { const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get(); if (adminExists.count === 0) { const hash = bcrypt.hashSync('admin123', 10); - d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); + d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); console.log('[DB] Created default admin user (admin / admin123)'); } @@ -569,17 +569,17 @@ function initSchema() { // ─── INSERT FUNCTIONS ───────────────────────────────────────────────────────── -function insertBandwidthApps(rows, fetchedAt, siteUuid) { +function insertBandwidthApps(rows, fetchedAt, siteUuid, agentUuid) { 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.app_id ?? null, r.app_label ?? 'Unknown', r.app_tag ?? null, @@ -594,17 +594,17 @@ function insertBandwidthApps(rows, fetchedAt, siteUuid) { })(rows); } -function insertDevices(rows, fetchedAt, siteUuid) { +function insertDevices(rows, fetchedAt, siteUuid, agentUuid) { 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.mac_address ?? null, r.ip_address ?? null, r.device_label ?? r.ip_address ?? 'Unknown', @@ -619,17 +619,17 @@ function insertDevices(rows, fetchedAt, siteUuid) { })(rows); } -function insertFlows(rows, fetchedAt, siteUuid) { +function insertFlows(rows, fetchedAt, siteUuid, agentUuid) { 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, 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) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.flow_id ?? null, r.src_ip ?? null, r.src_mac ?? null, @@ -647,17 +647,17 @@ function insertFlows(rows, fetchedAt, siteUuid) { })(rows); } -function insertThreats(rows, fetchedAt, siteUuid) { +function insertThreats(rows, fetchedAt, siteUuid, agentUuid) { 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.threat_id ?? null, r.threat_type ?? null, r.severity ?? null, @@ -673,18 +673,18 @@ function insertThreats(rows, fetchedAt, siteUuid) { })(rows); } -function insertProtocols(rows, fetchedAt, siteUuid) { +function insertProtocols(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_protocols - (site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) - VALUES (?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.protocol_id ?? null, r.protocol_label ?? 'Unknown', r.download ?? 0, @@ -695,18 +695,18 @@ function insertProtocols(rows, fetchedAt, siteUuid) { })(rows); } -function insertCountries(rows, fetchedAt, siteUuid) { +function insertCountries(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_countries - (site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) - VALUES (?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.country_code ?? null, r.country_name ?? 'Unknown', r.download ?? 0, @@ -717,17 +717,17 @@ function insertCountries(rows, fetchedAt, siteUuid) { })(rows); } -function insertDNS(rows, fetchedAt, siteUuid) { +function insertDNS(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO dns_queries - (site_uuid, fetched_at, domain, query_count, app_label, category) - VALUES (?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, domain, query_count, app_label, category) + VALUES (?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.domain ?? null, r.query_count ?? 0, r.app_label ?? null, @@ -737,17 +737,17 @@ function insertDNS(rows, fetchedAt, siteUuid) { })(rows); } -function insertEvents(rows, fetchedAt, siteUuid) { +function insertEvents(rows, fetchedAt, siteUuid, agentUuid) { 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( - siteUuid, fetchedAt, + agentUuid, siteUuid, fetchedAt, r.event_id ?? null, r.event_type ?? null, r.severity ?? null, @@ -760,13 +760,14 @@ function insertEvents(rows, fetchedAt, siteUuid) { })(rows); } -function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { +function insertBandwidthTimeline(summary, fetchedAt, siteUuid, agentUuid) { const d = getDB(); d.prepare(` INSERT INTO bandwidth_timeline - (site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + (agent_uuid, site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( + agentUuid, siteUuid, fetchedAt, summary.download ?? 0, @@ -846,8 +847,8 @@ function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); if (!appsLatest?.t) return []; 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 }); + SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: appsLatest.t, limit, siteUuid, agentUuid }); return correlateAppLabels(rows); } @@ -1093,7 +1094,7 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search 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 ( + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND ( ip_address LIKE @q OR mac_address LIKE @q OR device_label LIKE @q OR @@ -1110,7 +1111,7 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search 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 ( + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND ( src_ip LIKE @q OR src_mac LIKE @q OR app_label LIKE @q OR @@ -1544,8 +1545,8 @@ function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { `).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 }); + SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit + `).all({ fetched_at: latest.t, limit, siteUuid, agentUuid }); } const mapped = rows.map(r => ({ @@ -1569,7 +1570,7 @@ function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { LIMIT ? `).all(...macs, ...macs, limit); } - return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); + return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); } function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { @@ -1593,8 +1594,8 @@ function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { const protoLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); if (!protoLatest?.t) return []; return d.prepare(` - SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: protoLatest.t, limit, siteUuid }); + SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: protoLatest.t, limit, siteUuid, agentUuid }); } function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { @@ -1711,8 +1712,8 @@ function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); if (!countryLatest?.t) return []; return d.prepare(` - SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: countryLatest.t, limit, siteUuid }); + SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit + `).all({ fetched_at: countryLatest.t, limit, siteUuid, agentUuid }); } function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { @@ -1736,8 +1737,8 @@ function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { const dnsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); if (!dnsLatest?.t) return []; return d.prepare(` - SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit - `).all({ fetched_at: dnsLatest.t, limit, siteUuid }); + SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit + `).all({ fetched_at: dnsLatest.t, limit, siteUuid, agentUuid }); } function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { @@ -1752,7 +1753,7 @@ function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { LIMIT ? `).all(...macs, ...macs, limit); } - return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); + return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); } function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { @@ -1780,7 +1781,7 @@ function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { `).all(...macs, ...macs, points).reverse(); } return d.prepare(` - SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit + SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit `).all({ limit: points, siteUuid }).reverse(); } @@ -1849,141 +1850,141 @@ function getStats(siteUuid = null, agentUuid = null) { // Fallback to site-wide stats if no agentUuid const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); const totalDevices = latestDevFetch?.t - ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) + ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) : 0; const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); const activeFlows = latestFlowFetch?.t - ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) + ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) : 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 totalThreats = d.prepare(`SELECT COUNT(*) as n FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0; + const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.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 latestBw = d.prepare(`SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid, agentUuid }); return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; } // ─── INSERT FITUR BARU 1-11 ─────────────────────────────────────────────────── -function insertAppCategories(rows, fetchedAt, siteUuid) { +function insertAppCategories(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO app_categories - (site_uuid, fetched_at, category_label, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); })(rows); } -function insertContinents(rows, fetchedAt, siteUuid) { +function insertContinents(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO continents - (site_uuid, fetched_at, continent_name, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); })(rows); } -function insertRegions(rows, fetchedAt, siteUuid) { +function insertRegions(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO regions - (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, 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, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO cities - (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, 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, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO vlans - (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, 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, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO interfaces - (site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) - VALUES (?, ?,?,?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, 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, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO flow_types - (site_uuid, fetched_at, flow_type_label, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); })(rows); } -function insertFlowOrigins(rows, fetchedAt, siteUuid) { +function insertFlowOrigins(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO flow_origins - (site_uuid, fetched_at, flow_origin_label, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); })(rows); } -function insertIPVersions(rows, fetchedAt, siteUuid) { +function insertIPVersions(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ip_versions - (site_uuid, fetched_at, ip_version_label, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); })(rows); } -function insertRemoteIPs(rows, fetchedAt, siteUuid) { +function insertRemoteIPs(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO remote_ips - (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, 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, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO mac_bandwidth - (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); })(rows); } @@ -2048,7 +2049,7 @@ function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, age } // Fallback to standard site-wide select - rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid }); + rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid, agentUuid }); // If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) { @@ -2067,272 +2068,272 @@ function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, age } // DPI Fields -function insertTLSVersions(rows, fetchedAt, siteUuid) { +function insertTLSVersions(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_versions - (site_uuid, fetched_at, tls_version, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); })(rows); } -function insertTLSCiphers(rows, fetchedAt, siteUuid) { +function insertTLSCiphers(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_ciphers - (site_uuid, fetched_at, tls_cipher, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); })(rows); } -function insertTLSSecurity(rows, fetchedAt, siteUuid) { +function insertTLSSecurity(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_security - (site_uuid, fetched_at, tls_security, color, download, upload, total) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); })(rows); } -function insertNetBIOSHostnames(rows, fetchedAt, siteUuid) { +function insertNetBIOSHostnames(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO netbios_hostnames - (site_uuid, fetched_at, hostname, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); })(rows); } -function insertDiscoveryOS(rows, fetchedAt, siteUuid) { +function insertDiscoveryOS(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO discovery_os - (site_uuid, fetched_at, os_label, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, 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); + agentUuid, siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); })(rows); } // ─── INSERT DPI 12-21 ──────────────────────────────────────────────────────── -function insertDHCPFingerprints(rows, fetchedAt, siteUuid) { +function insertDHCPFingerprints(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO dhcp_fingerprints - (site_uuid, fetched_at, fingerprint, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, fingerprint, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); })(rows); } -function insertHTTPUserAgents(rows, fetchedAt, siteUuid) { +function insertHTTPUserAgents(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO http_user_agents - (site_uuid, fetched_at, user_agent, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, user_agent, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); })(rows); } -function insertSNIHostnames(rows, fetchedAt, siteUuid) { +function insertSNIHostnames(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO sni_hostnames - (site_uuid, fetched_at, sni_hostname, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, sni_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); })(rows); } -function insertSSLServerCN(rows, fetchedAt, siteUuid) { +function insertSSLServerCN(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ssl_server_cn - (site_uuid, fetched_at, ssl_server_cn, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, ssl_server_cn, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); })(rows); } -function insertQUICHostnames(rows, fetchedAt, siteUuid) { +function insertQUICHostnames(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO quic_hostnames - (site_uuid, fetched_at, quic_hostname, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, quic_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); })(rows); } -function insertBitTorrentHashes(rows, fetchedAt, siteUuid) { +function insertBitTorrentHashes(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO bittorrent_hashes - (site_uuid, fetched_at, info_hash, label, download, upload, total) - VALUES (?, ?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, info_hash, label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); })(rows); } -function insertSSHVersions(rows, fetchedAt, siteUuid) { +function insertSSHVersions(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ssh_versions - (site_uuid, fetched_at, ssh_version, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, ssh_version, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); })(rows); } -function insertMDNSHostnames(rows, fetchedAt, siteUuid) { +function insertMDNSHostnames(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO mdns_hostnames - (site_uuid, fetched_at, mdns_hostname, download, upload, total) - VALUES (?, ?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, mdns_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + agentUuid, siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); })(rows); } // ─── INSERT INTELLIGENCE 22-30 ──────────────────────────────────────────────── -function insertCryptoMining(rows, fetchedAt, siteUuid) { +function insertCryptoMining(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_crypto_mining - (site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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 ); })(rows); } -function insertDeviceDiscovery(rows, fetchedAt, siteUuid) { +function insertDeviceDiscovery(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_device_discovery - (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) - VALUES (?, ?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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 ); })(rows); } -function insertEncryptionAudit(rows, fetchedAt, siteUuid) { +function insertEncryptionAudit(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_encryption_audit - (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) - VALUES (?, ?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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 ); })(rows); } -function insertInsecureProtocols(rows, fetchedAt, siteUuid) { +function insertInsecureProtocols(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_insecure_protocols - (site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) + 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, + agentUuid, 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' ); })(rows); } -function insertIPReputation(rows, fetchedAt, siteUuid) { +function insertIPReputation(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_ip_reputation - (site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) + 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, + agentUuid, 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 ); })(rows); } -function insertServerDiscovery(rows, fetchedAt, siteUuid) { +function insertServerDiscovery(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_server_discovery - (site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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 ); })(rows); } -function insertTorDetection(rows, fetchedAt, siteUuid) { +function insertTorDetection(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_tor_detection - (site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) - VALUES (?, ?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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); } -function insertUnencryptedPasswords(rows, fetchedAt, siteUuid) { +function insertUnencryptedPasswords(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_unencrypted_passwords - (site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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' ); })(rows); } -function insertVPNDetection(rows, fetchedAt, siteUuid) { +function insertVPNDetection(rows, fetchedAt, siteUuid, agentUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO intel_vpn_detection - (site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) - VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( - siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + agentUuid, 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 ); @@ -2357,7 +2358,7 @@ function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { LIMIT ? `).all(...macs, ...macs, limit); } - return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); + return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); } function getIntelStats(siteUuid = null, agentUuid = null) { @@ -2381,7 +2382,7 @@ function getIntelStats(siteUuid = null, agentUuid = null) { WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) `).get(...macs, ...macs)?.n ?? 0; } else { - counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; + counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0; } } catch { counts[t] = 0; } } diff --git a/backend/netify.js b/backend/netify.js index 6b575ac..0605b44 100644 --- a/backend/netify.js +++ b/backend/netify.js @@ -353,7 +353,8 @@ function getMockDataForPath(path, params = {}) { return []; } -async function netifyFetch(path, params = {}, useApiKey = false) { +async function netifyFetch(path, params = {}, useApiKey = false, agentUuid = null) { + if (agentUuid) params.settings_agent = agentUuid; if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) { return getMockDataForPath(path, params); } @@ -376,10 +377,10 @@ async function netifyFetch(path, params = {}, useApiKey = false) { } // ─── TOP APPS: download + upload digabung ───────────────────────────────────── -async function fetchTopApps(interval = 1440, limit = 20) { +async function fetchTopApps(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; @@ -406,10 +407,10 @@ async function fetchTopApps(interval = 1440, limit = 20) { } // ─── TOP DEVICES: download + upload digabung ────────────────────────────────── -async function fetchTopDevices(interval = 1440, limit = 50) { +async function fetchTopDevices(interval = 1440, limit = 50, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; @@ -461,11 +462,11 @@ const PORT_SERVICE_MAP = { // NOTE: API flows tidak punya field app/domain — enrichment dilakukan via: // 1. PORT_SERVICE_MAP (reliable, berdasarkan dst port) // 2. tls_sni field (satu-satunya SNI field valid, tapi nilai sering kosong) -async function fetchFlows(limit = 500) { +async function fetchFlows(limit = 500, agentUuid = null) { // Hanya fetch flows + tls_sni (satu-satunya SNI field yang valid = bukan 422) const [raw, tslSniRaw] = await Promise.all([ - netifyFetch('/data/flows', { settings_limit: limit }), - netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }), + netifyFetch('/data/flows', { settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), ]); if (!raw || !Array.isArray(raw)) return null; @@ -513,10 +514,10 @@ async function fetchFlows(limit = 500) { // ─── PROTOCOLS ──────────────────────────────────────────────────────────────── -async function fetchTopProtocols(interval = 1440, limit = 20) { +async function fetchTopProtocols(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; @@ -538,10 +539,10 @@ async function fetchTopProtocols(interval = 1440, limit = 20) { } // ─── COUNTRIES ──────────────────────────────────────────────────────────────── -async function fetchTopCountries(interval = 1440, limit = 15) { +async function fetchTopCountries(interval = 1440, limit = 15, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; @@ -563,12 +564,12 @@ async function fetchTopCountries(interval = 1440, limit = 15) { } // ─── DNS/HOSTNAME — gunakan tls_sni karena hostname endpoint timeout ────────── -async function fetchTopDomains(interval = 1440, limit = 50) { +async function fetchTopDomains(interval = 1440, limit = 50, agentUuid = null) { // NOTE: /data/stats/top/hostname/download selalu timeout // Gunakan tls_sni yang confirmed bekerja di API ini const raw = await netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: interval, settings_limit: limit, - }); + }, false, agentUuid); if (!raw) return null; return raw @@ -584,8 +585,8 @@ async function fetchTopDomains(interval = 1440, limit = 50) { // ─── BANDWIDTH SUMMARY untuk timeline ───────────────────────────────────────── -async function fetchBandwidthSummary(interval = 30) { - const rawSummary = await netifyFetch('/data/stats/summary', { filter_interval: interval }); +async function fetchBandwidthSummary(interval = 30, agentUuid = null) { + const rawSummary = await netifyFetch('/data/stats/summary', { filter_interval: interval }, false, agentUuid); if (rawSummary) { return { download: rawSummary.download ?? 0, @@ -600,8 +601,8 @@ async function fetchBandwidthSummary(interval = 30) { // Fallback to old way (aggregate top 100 application stats) const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }), + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), ]); const download = (dlData ?? []).reduce((s, r) => s + (r.download ?? 0), 0); @@ -620,15 +621,15 @@ async function fetchBandwidthSummary(interval = 30) { } // ─── THREATS — gunakan flows dengan filter anomali sebagai fallback ──────────── -async function fetchCyberThreats(limit = 50) { +async function fetchCyberThreats(limit = 50, agentUuid = null) { // Events/threats belum ada di v1 — return array kosong agar tidak error // Akan diisi saat endpoint ditemukan return []; } // ─── EVENTS ─────────────────────────────────────────────────────────────────── -async function fetchEvents(limit = 50) { - const raw = await netifyFetch('/event/events', { settings_limit: limit }); +async function fetchEvents(limit = 50, agentUuid = null) { + const raw = await netifyFetch('/event/events', { settings_limit: limit }, false, agentUuid); if (!raw || !Array.isArray(raw)) return []; return raw.map(r => { @@ -673,13 +674,13 @@ async function fetchEvents(limit = 50) { }); } -async function fetchDiscoveredDevices(interval = 1440, limit = 500) { +async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null) { // Ambil lebih banyak bandwidth data (limit x2) supaya coverage IP lebih luas const [intelRaw, dlData, ulData, flowsRaw] = await Promise.all([ - netifyFetch('/intelligence/discovery/devices', { settings_limit: limit }), - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/intelligence/discovery/devices', { settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), ]); if (!intelRaw || !Array.isArray(intelRaw)) return []; @@ -804,10 +805,10 @@ const OUI_MAP = { // ─── TAMBAHAN FITUR 1-11 ────────────────────────────────────────────────────── // 1. Top Application Category -async function fetchTopAppCategories(interval = 1440, limit = 15) { +async function fetchTopAppCategories(interval = 1440, limit = 15, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -827,10 +828,10 @@ async function fetchTopAppCategories(interval = 1440, limit = 15) { } // 2. Top Continent -async function fetchTopContinents(interval = 1440, limit = 10) { +async function fetchTopContinents(interval = 1440, limit = 10, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/continent/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/continent/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/continent/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/continent/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -850,10 +851,10 @@ async function fetchTopContinents(interval = 1440, limit = 10) { } // 3. Top Region -async function fetchTopRegions(interval = 1440, limit = 20) { +async function fetchTopRegions(interval = 1440, limit = 20, agentUuid = null) { const raw = await netifyFetch('/data/stats/top/region/download', { filter_interval: interval, settings_limit: limit, - }); + }, false, agentUuid); if (!raw) return null; return raw.map(r => ({ region_name : r.region?.region_name?.trim() || '(Unknown Region)', @@ -865,10 +866,10 @@ async function fetchTopRegions(interval = 1440, limit = 20) { } // 4. Top City -async function fetchTopCities(interval = 1440, limit = 20) { +async function fetchTopCities(interval = 1440, limit = 20, agentUuid = null) { const raw = await netifyFetch('/data/stats/top/city/download', { filter_interval: interval, settings_limit: limit, - }); + }, false, agentUuid); if (!raw) return null; return raw.map(r => ({ city_name : r.city?.city?.trim() || '(Unknown City)', @@ -880,10 +881,10 @@ async function fetchTopCities(interval = 1440, limit = 20) { } // 5. Top VLAN -async function fetchTopVLANs(interval = 1440, limit = 20) { +async function fetchTopVLANs(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/vlan/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/vlan/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/vlan/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/vlan/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -901,10 +902,10 @@ async function fetchTopVLANs(interval = 1440, limit = 20) { } // 6. Top Interface -async function fetchTopInterfaces(interval = 1440, limit = 20) { +async function fetchTopInterfaces(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/interface/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/interface/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/interface/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/interface/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -924,10 +925,10 @@ async function fetchTopInterfaces(interval = 1440, limit = 20) { } // 7. Top Flow Type -async function fetchTopFlowTypes(interval = 1440, limit = 10) { +async function fetchTopFlowTypes(interval = 1440, limit = 10, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/flow_type/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/flow_type/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/flow_type/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/flow_type/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -947,10 +948,10 @@ async function fetchTopFlowTypes(interval = 1440, limit = 10) { } // 8. Top Flow Origin -async function fetchTopFlowOrigins(interval = 1440, limit = 10) { +async function fetchTopFlowOrigins(interval = 1440, limit = 10, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/flow_origin/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/flow_origin/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -970,10 +971,10 @@ async function fetchTopFlowOrigins(interval = 1440, limit = 10) { } // 9. Top IP Version -async function fetchTopIPVersions(interval = 1440, limit = 5) { +async function fetchTopIPVersions(interval = 1440, limit = 5, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/ip_version/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/ip_version/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/ip_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/ip_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -993,10 +994,10 @@ async function fetchTopIPVersions(interval = 1440, limit = 5) { } // 10. Top Remote IP -async function fetchTopRemoteIPs(interval = 1440, limit = 20) { +async function fetchTopRemoteIPs(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/remote_ip/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/remote_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1017,11 +1018,11 @@ async function fetchTopRemoteIPs(interval = 1440, limit = 20) { } // 11. Top Local MAC + Discovery OS -async function fetchTopLocalMACs(interval = 1440, limit = 50) { +async function fetchTopLocalMACs(interval = 1440, limit = 50, agentUuid = null) { const [dlData, ulData, osData] = await Promise.all([ - netifyFetch('/data/stats/top/local_mac/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/local_mac/upload', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: 20 }), + netifyFetch('/data/stats/top/local_mac/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_mac/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: 20 }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1048,10 +1049,10 @@ async function fetchTopLocalMACs(interval = 1440, limit = 50) { } // ─── DISCOVERY OS (standalone) ─────────────────────────────────────────────── -async function fetchTopDiscoveryOS(interval = 1440, limit = 20) { +async function fetchTopDiscoveryOS(interval = 1440, limit = 20, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/discovery_os/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/discovery_os/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1073,10 +1074,10 @@ async function fetchTopDiscoveryOS(interval = 1440, limit = 20) { // ─── DPI FIELDS ─────────────────────────────────────────────────────────────── // TLS Version -async function fetchTLSVersions(interval = 1440, limit = 10) { +async function fetchTLSVersions(interval = 1440, limit = 10, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1096,10 +1097,10 @@ async function fetchTLSVersions(interval = 1440, limit = 10) { } // TLS Cipher -async function fetchTLSCiphers(interval = 1440, limit = 15) { +async function fetchTLSCiphers(interval = 1440, limit = 15, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1119,10 +1120,10 @@ async function fetchTLSCiphers(interval = 1440, limit = 15) { } // TLS Security Level -async function fetchTLSSecurity(interval = 1440, limit = 10) { +async function fetchTLSSecurity(interval = 1440, limit = 10, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1149,10 +1150,10 @@ async function fetchTLSSecurity(interval = 1440, limit = 10) { } // NetBIOS Hostname (nama PC Windows) -async function fetchNetBIOSHostnames(interval = 1440, limit = 30) { +async function fetchNetBIOSHostnames(interval = 1440, limit = 30, agentUuid = null) { const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/netbios_hostname/download', { filter_interval: interval, settings_limit: limit }), - netifyFetch('/data/stats/top/netbios_hostname/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/netbios_hostname/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/netbios_hostname/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dlData) return null; const ulMap = {}; @@ -1171,7 +1172,7 @@ async function fetchNetBIOSHostnames(interval = 1440, limit = 30) { }); } -async function fetchLookupApplications(page = 1, limit = 50, search = '') { +async function fetchLookupApplications(page = 1, limit = 50, search = '', agentUuid = null) { if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) { return { applications: [ @@ -1381,7 +1382,7 @@ async function fetchAgentDetails(agentUuid) { } // Fetch data for a specific device IP — from local DB (all 10 correlated tables) -async function fetchDeviceDetails(ip) { +async function fetchDeviceDetails(ip, agentUuid = null) { const db = require('./database'); const d = db.getDB(); @@ -1840,7 +1841,7 @@ async function fetchDeviceDetails(ip) { } // Fetch data for a specific application// Fetch data for a specific application — from local DB -async function fetchAppDetails(appLabel) { +async function fetchAppDetails(appLabel, agentUuid = null) { const db = require('./database'); const d = db.getDB(); @@ -2163,10 +2164,10 @@ module.exports = { // mdns_hostname → mdns_hostname ✓ (sudah benar, mungkin belum aktif di akun) // Helper builder untuk DPI single-field -async function _dpiTopField(fieldName, interval, limit) { +async function _dpiTopField(fieldName, interval, limit, agentUuid = null) { const [dl, ul] = await Promise.all([ - netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }), - netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }), + netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }, false, agentUuid), ]); if (!dl) return null; const ulMap = {}; @@ -2178,8 +2179,8 @@ async function _dpiTopField(fieldName, interval, limit) { } // 12. DHCP Class (field: dhcp_class) -async function fetchDHCPClassFingerprints(interval = 1440, limit = 30) { - const res = await _dpiTopField('dhcp_class', interval, limit); +async function fetchDHCPClassFingerprints(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('dhcp_class', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const label = r.dhcp_class?.name ?? r.dhcp_class?.label ?? String(r.dhcp_class ?? 'Unknown'); @@ -2193,8 +2194,8 @@ async function fetchDHCPClassFingerprints(interval = 1440, limit = 30) { } // 13. HTTP User-Agent (field: http_useragent) -async function fetchHTTPUserAgents(interval = 1440, limit = 30) { - const res = await _dpiTopField('http_useragent', interval, limit); +async function fetchHTTPUserAgents(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('http_useragent', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const label = r.http_useragent?.name ?? r.http_useragent?.label ?? String(r.http_useragent ?? 'Unknown'); @@ -2208,8 +2209,8 @@ async function fetchHTTPUserAgents(interval = 1440, limit = 30) { } // 14. HTTPS SNI Hostname (field: https_sni_hostname) -async function fetchSNIHostnames(interval = 1440, limit = 30) { - const res = await _dpiTopField('https_sni_hostname', interval, limit); +async function fetchSNIHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('https_sni_hostname', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const name = r.https_sni_hostname?.name ?? r.https_sni_hostname?.label ?? String(r.https_sni_hostname ?? 'Unknown'); @@ -2223,8 +2224,8 @@ async function fetchSNIHostnames(interval = 1440, limit = 30) { } // 15. SSL Server Common Name (field: ssl_server_cn) -async function fetchSSLServerCN(interval = 1440, limit = 30) { - const res = await _dpiTopField('ssl_server_cn', interval, limit); +async function fetchSSLServerCN(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('ssl_server_cn', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const name = r.ssl_server_cn?.name ?? r.ssl_server_cn?.label ?? String(r.ssl_server_cn ?? 'Unknown'); @@ -2238,8 +2239,8 @@ async function fetchSSLServerCN(interval = 1440, limit = 30) { } // 17. QUIC Hostname (field: quic_hostname) -async function fetchQUICHostnames(interval = 1440, limit = 30) { - const res = await _dpiTopField('quic_hostname', interval, limit); +async function fetchQUICHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('quic_hostname', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const name = r.quic_hostname?.name ?? r.quic_hostname?.label ?? String(r.quic_hostname ?? 'Unknown'); @@ -2253,8 +2254,8 @@ async function fetchQUICHostnames(interval = 1440, limit = 30) { } // 18. BitTorrent Info Hash (field: bittorrent_info_hash) -async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30) { - const res = await _dpiTopField('bittorrent_info_hash', interval, limit); +async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('bittorrent_info_hash', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const hash = r.bittorrent_info_hash?.hash ?? r.bittorrent_info_hash?.name ?? String(r.bittorrent_info_hash ?? 'Unknown'); @@ -2270,8 +2271,8 @@ async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30) { } // 19a. SSH Client (field: ssh_client) -async function fetchSSHClients(interval = 1440, limit = 20) { - const res = await _dpiTopField('ssh_client', interval, limit); +async function fetchSSHClients(interval = 1440, limit = 20, agentUuid = null) { + const res = await _dpiTopField('ssh_client', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const label = r.ssh_client?.name ?? r.ssh_client?.label ?? String(r.ssh_client ?? 'Unknown'); @@ -2286,8 +2287,8 @@ async function fetchSSHClients(interval = 1440, limit = 20) { } // 19b. SSH Server (field: ssh_server) -async function fetchSSHServers(interval = 1440, limit = 20) { - const res = await _dpiTopField('ssh_server', interval, limit); +async function fetchSSHServers(interval = 1440, limit = 20, agentUuid = null) { + const res = await _dpiTopField('ssh_server', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const label = r.ssh_server?.name ?? r.ssh_server?.label ?? String(r.ssh_server ?? 'Unknown'); @@ -2302,8 +2303,8 @@ async function fetchSSHServers(interval = 1440, limit = 20) { } // 21. mDNS Hostname (field: mdns_hostname) -async function fetchMDNSHostnames(interval = 1440, limit = 30) { - const res = await _dpiTopField('mdns_hostname', interval, limit); +async function fetchMDNSHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('mdns_hostname', interval, limit, agentUuid); if (!res) return null; return res.dl.map(r => { const name = r.mdns_hostname?.name ?? r.mdns_hostname?.label ?? String(r.mdns_hostname ?? 'Unknown'); @@ -2324,13 +2325,13 @@ async function fetchMDNSHostnames(interval = 1440, limit = 30) { // 22. Cryptocurrency Mining — derive dari apps dengan nama mengandung "crypto" / "mining" // + flows ke port mining pool (3333, 4444, 8333, 9999, 14444) -async function fetchCryptoMining(interval = 1440, limit = 50) { +async function fetchCryptoMining(interval = 1440, limit = 50, agentUuid = null) { const MINING_POOLS_PORTS = new Set([3333, 4444, 5555, 7777, 8333, 9332, 9999, 14444, 45560, 45700]); const MINING_APPS = ['bitcoin', 'crypto', 'mining', 'monero', 'ethereum', 'nicehash', 'nanopool', 'f2pool', 'antpool', 'slushpool']; const [appData, flowsRaw] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }), - netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), ]); const results = []; @@ -2384,10 +2385,10 @@ async function fetchCryptoMining(interval = 1440, limit = 50) { } // 23. Device Discovery — derive dari flows (perangkat unik dengan MAC) -async function fetchDeviceDiscovery(limit = 100) { +async function fetchDeviceDiscovery(limit = 100, agentUuid = null) { const [flowsRaw, dlData] = await Promise.all([ - netifyFetch('/data/flows', { settings_limit: 500 }), - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: 1440, settings_limit: 200 }), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: 1440, settings_limit: 200 }, false, agentUuid), ]); const seen = new Map(); @@ -2434,10 +2435,10 @@ async function fetchDeviceDiscovery(limit = 100) { } // 24. Encryption Audit — derive dari TLS security per-IP dari flows -async function fetchEncryptionAudit(limit = 50) { +async function fetchEncryptionAudit(limit = 50, agentUuid = null) { const [tlsSec, flowsRaw] = await Promise.all([ - netifyFetch('/data/stats/top/tls_security/download', { filter_interval: 1440, settings_limit: 10 }), - netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), ]); // Hitung per-IP: encrypted vs unencrypted flows @@ -2485,7 +2486,7 @@ async function fetchEncryptionAudit(limit = 50) { } // 25. Insecure Protocols — derive dari top protocols (confirmed bekerja) -async function fetchInsecureProtocols(interval = 1440, limit = 50) { +async function fetchInsecureProtocols(interval = 1440, limit = 50, agentUuid = null) { const INSECURE = { 'HTTP' : { port: 80, risk: 'High' }, 'FTP' : { port: 21, risk: 'Critical' }, @@ -2504,8 +2505,8 @@ async function fetchInsecureProtocols(interval = 1440, limit = 50) { }; const [protoData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: 100 }), - netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: 100 }), + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), ]); if (!protoData) return []; @@ -2538,7 +2539,7 @@ async function fetchInsecureProtocols(interval = 1440, limit = 50) { } // 26. IP Reputation — derive dari top remote_ip + cross-check negara berisiko tinggi -async function fetchIPReputation(limit = 50) { +async function fetchIPReputation(limit = 50, agentUuid = null) { // Negara dengan risiko tinggi berdasarkan threat intel umum const HIGH_RISK_COUNTRIES = new Set([ 'China', 'Russia', 'Iran', 'North Korea', 'Nigeria', 'Romania', @@ -2546,8 +2547,8 @@ async function fetchIPReputation(limit = 50) { ]); const [ipData, countryData] = await Promise.all([ - netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 100 }), - netifyFetch('/data/stats/top/country/download', { filter_interval: 1440, settings_limit: 50 }), + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/country/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), ]); const results = []; @@ -2602,7 +2603,7 @@ async function fetchIPReputation(limit = 50) { } // 27. Server Discovery — derive dari flows dengan flow_origin=Server + port well-known server -async function fetchServerDiscovery(limit = 100) { +async function fetchServerDiscovery(limit = 100, agentUuid = null) { const SERVER_PORTS = { 80: 'HTTP', 443: 'HTTPS', 22: 'SSH', 21: 'FTP', 25: 'SMTP', 110: 'POP3', 143: 'IMAP', 3306: 'MySQL', 5432: 'PostgreSQL', @@ -2612,8 +2613,8 @@ async function fetchServerDiscovery(limit = 100) { }; const [flowOriginData, flowsRaw] = await Promise.all([ - netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: 1440, settings_limit: 10 }), - netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), ]); const serverMap = {}; @@ -2656,12 +2657,12 @@ async function fetchServerDiscovery(limit = 100) { } // 28. Tor Detection — derive dari top remote_ip + app/hostname matching Tor -async function fetchTorDetection(limit = 50) { +async function fetchTorDetection(limit = 50, agentUuid = null) { const TOR_INDICATORS = ['tor', '.onion', 'torproject', 'torbrowser']; const [appData, domainData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }), - netifyFetch('/data/stats/top/hostname/download', { filter_interval: 1440, settings_limit: 100 }), + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/hostname/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), ]); const results = []; @@ -2711,7 +2712,7 @@ async function fetchTorDetection(limit = 50) { } // 29. Unencrypted Passwords — derive dari flows ke port cleartext auth -async function fetchUnencryptedPasswords(limit = 50) { +async function fetchUnencryptedPasswords(limit = 50, agentUuid = null) { // Port yang dikenal mengirim kredensial cleartext const CLEARTEXT_AUTH_PORTS = { 21 : { protocol: 'FTP', severity: 'Critical' }, @@ -2726,7 +2727,7 @@ async function fetchUnencryptedPasswords(limit = 50) { 514 : { protocol: 'rsh', severity: 'Critical' }, }; - const flowsRaw = await netifyFetch('/data/flows', { settings_limit: 500 }); + const flowsRaw = await netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid); if (!flowsRaw) return []; const seen = new Map(); @@ -2760,7 +2761,7 @@ async function fetchUnencryptedPasswords(limit = 50) { } // 30. VPN Detection — derive dari apps/protocols/hostnames yang mengindikasikan VPN -async function fetchVPNDetection(limit = 50) { +async function fetchVPNDetection(limit = 50, agentUuid = null) { const VPN_APPS = [ 'openvpn', 'wireguard', 'nordvpn', 'expressvpn', 'surfshark', 'tunnelbear', 'mullvad', 'protonvpn', 'ipvanish', 'pia', 'private internet access', @@ -2772,9 +2773,9 @@ async function fetchVPNDetection(limit = 50) { const VPN_PORTS = new Set([1194, 51820, 500, 4500, 1701, 1723, 8388, 443]); const [appData, protoData, flowsRaw] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }), - netifyFetch('/data/stats/top/protocol/download', { filter_interval: 1440, settings_limit: 50 }), - netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), ]); const results = []; @@ -2849,7 +2850,7 @@ async function fetchVPNDetection(limit = 50) { } async function fetchAgents() { - const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }); + const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, false, null); if (!data || !Array.isArray(data)) return []; return data.map(r => ({ id: r.agent?.id, diff --git a/backend/netify_data.db b/backend/netify_data.db index f4b935b..4f951a5 100644 Binary files a/backend/netify_data.db and b/backend/netify_data.db differ diff --git a/backend/scheduler.js b/backend/scheduler.js index 55cd04a..7642de2 100644 --- a/backend/scheduler.js +++ b/backend/scheduler.js @@ -18,7 +18,7 @@ async function runPoll() { try { // 0. Sync agents and seed default user accounts dynamically try { - const apiAgents = await netify.fetchAgents(); + apiAgents = await netify.fetchAgents(); if (apiAgents && apiAgents.length > 0) { db.syncAgentUsers(apiAgents); console.log(`[Scheduler] OK Sync Agents : ${apiAgents.length} agen terdeteksi`); @@ -27,256 +27,270 @@ async function runPoll() { console.error('[Scheduler] Gagal sync agent users:', err.message); } - // 1. Top Aplikasi - const apps = await netify.fetchTopApps(1440, 20); + async function fetchAndStore(fetchedAt, agentUuid) { + const agentLabel = agentUuid ? agentUuid : 'Global'; + console.log(`[Scheduler] Fetching data for ${agentLabel}`); +// 1. Top Aplikasi + const apps = await netify.fetchTopApps(1440, 20, agentUuid); if (apps && Array.isArray(apps)) { - db.insertBandwidthApps(apps, fetchedAt, SITE_UUID); + db.insertBandwidthApps(apps, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Apps : ${apps.length} baris`); } else { console.log(`[Scheduler] -- Apps : tidak ada data`); } // 2. Top Devices — pakai fetchDiscoveredDevices yg sudah dinormalisasi - const devices = await netify.fetchDiscoveredDevices(1440, 200); + const devices = await netify.fetchDiscoveredDevices(1440, 200, agentUuid); if (devices && Array.isArray(devices)) { - db.insertDevices(devices, fetchedAt, SITE_UUID); + db.insertDevices(devices, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Devices : ${devices.length} baris`); } else { console.log(`[Scheduler] -- Devices : tidak ada data`); } // 3. Top Protokol - const protocols = await netify.fetchTopProtocols(1440, 20); + const protocols = await netify.fetchTopProtocols(1440, 20, agentUuid); if (protocols && Array.isArray(protocols)) { - db.insertProtocols(protocols, fetchedAt, SITE_UUID); + db.insertProtocols(protocols, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Protocols : ${protocols.length} baris`); } else { console.log(`[Scheduler] -- Protocols : tidak ada data`); } // 4. Top Negara - const countries = await netify.fetchTopCountries(1440, 15); + const countries = await netify.fetchTopCountries(1440, 15, agentUuid); if (countries && Array.isArray(countries)) { - db.insertCountries(countries, fetchedAt, SITE_UUID); + db.insertCountries(countries, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Countries : ${countries.length} baris`); } else { console.log(`[Scheduler] -- Countries : tidak ada data`); } // 5. Top Domain/DNS - const domains = await netify.fetchTopDomains(1440, 20); + const domains = await netify.fetchTopDomains(1440, 20, agentUuid); if (domains && Array.isArray(domains)) { - db.insertDNS(domains, fetchedAt, SITE_UUID); + db.insertDNS(domains, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DNS : ${domains.length} baris`); } else { console.log(`[Scheduler] -- DNS : tidak ada data`); } // 6. Flows — pakai local_ip sebagai proxy - const flows = await netify.fetchFlows(200); + const flows = await netify.fetchFlows(200, agentUuid); if (flows && Array.isArray(flows)) { - db.insertFlows(flows, fetchedAt, SITE_UUID); + db.insertFlows(flows, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Flows : ${flows.length} baris`); } else { console.log(`[Scheduler] -- Flows : tidak ada data`); } // 7. Threats — dari Events Status - const threats = await netify.fetchCyberThreats(1440, 50); + const threats = await netify.fetchCyberThreats(1440, 50, agentUuid); if (threats && Array.isArray(threats)) { - db.insertThreats(threats, fetchedAt, SITE_UUID); + db.insertThreats(threats, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Threats : ${threats.length} baris`); } else { console.log(`[Scheduler] -- Threats : tidak ada data`); } // 8. Events Log - const events = await netify.fetchEvents(50); + const events = await netify.fetchEvents(50, agentUuid); if (events && Array.isArray(events)) { - db.insertEvents(events, fetchedAt, SITE_UUID); + db.insertEvents(events, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Events : ${events.length} baris`); } else { console.log(`[Scheduler] -- Events : tidak ada data`); } // 10. App Categories - const appCats = await netify.fetchTopAppCategories(1440, 15); - if (appCats?.length) { db.insertAppCategories(appCats, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK AppCats : ${appCats.length} baris`); } + const appCats = await netify.fetchTopAppCategories(1440, 15, agentUuid); + if (appCats?.length) { db.insertAppCategories(appCats, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK AppCats : ${appCats.length} baris`); } else console.log(`[Scheduler] -- AppCats : tidak ada data`); // 11. Continents - const continents = await netify.fetchTopContinents(1440, 10); - if (continents?.length) { db.insertContinents(continents, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK Continents : ${continents.length} baris`); } + const continents = await netify.fetchTopContinents(1440, 10, agentUuid); + if (continents?.length) { db.insertContinents(continents, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Continents : ${continents.length} baris`); } else console.log(`[Scheduler] -- Continents : tidak ada data`); // 12. Regions - const regions = await netify.fetchTopRegions(1440, 20); - if (regions?.length) { db.insertRegions(regions, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK Regions : ${regions.length} baris`); } + const regions = await netify.fetchTopRegions(1440, 20, agentUuid); + if (regions?.length) { db.insertRegions(regions, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Regions : ${regions.length} baris`); } else console.log(`[Scheduler] -- Regions : tidak ada data`); // 13. Cities - const cities = await netify.fetchTopCities(1440, 20); - if (cities?.length) { db.insertCities(cities, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK Cities : ${cities.length} baris`); } + const cities = await netify.fetchTopCities(1440, 20, agentUuid); + if (cities?.length) { db.insertCities(cities, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Cities : ${cities.length} baris`); } else console.log(`[Scheduler] -- Cities : tidak ada data`); // 14. VLANs - const vlans = await netify.fetchTopVLANs(1440, 20); - if (vlans?.length) { db.insertVLANs(vlans, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK VLANs : ${vlans.length} baris`); } + const vlans = await netify.fetchTopVLANs(1440, 20, agentUuid); + if (vlans?.length) { db.insertVLANs(vlans, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK VLANs : ${vlans.length} baris`); } else console.log(`[Scheduler] -- VLANs : tidak ada data`); // 15. Interfaces - const ifaces = await netify.fetchTopInterfaces(1440, 20); - if (ifaces?.length) { db.insertInterfaces(ifaces, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK Interfaces : ${ifaces.length} baris`); } + const ifaces = await netify.fetchTopInterfaces(1440, 20, agentUuid); + if (ifaces?.length) { db.insertInterfaces(ifaces, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Interfaces : ${ifaces.length} baris`); } else console.log(`[Scheduler] -- Interfaces : tidak ada data`); // 16. Flow Types - const flowTypes = await netify.fetchTopFlowTypes(1440, 10); - if (flowTypes?.length) { db.insertFlowTypes(flowTypes, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK FlowTypes : ${flowTypes.length} baris`); } + const flowTypes = await netify.fetchTopFlowTypes(1440, 10, agentUuid); + if (flowTypes?.length) { db.insertFlowTypes(flowTypes, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK FlowTypes : ${flowTypes.length} baris`); } else console.log(`[Scheduler] -- FlowTypes : tidak ada data`); // 17. Flow Origins - const flowOrigins = await netify.fetchTopFlowOrigins(1440, 10); - if (flowOrigins?.length) { db.insertFlowOrigins(flowOrigins, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK FlowOrigin : ${flowOrigins.length} baris`); } + const flowOrigins = await netify.fetchTopFlowOrigins(1440, 10, agentUuid); + if (flowOrigins?.length) { db.insertFlowOrigins(flowOrigins, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK FlowOrigin : ${flowOrigins.length} baris`); } else console.log(`[Scheduler] -- FlowOrigin : tidak ada data`); // 18. IP Versions - const ipVersions = await netify.fetchTopIPVersions(1440, 5); - if (ipVersions?.length) { db.insertIPVersions(ipVersions, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK IPVersions : ${ipVersions.length} baris`); } + const ipVersions = await netify.fetchTopIPVersions(1440, 5, agentUuid); + if (ipVersions?.length) { db.insertIPVersions(ipVersions, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK IPVersions : ${ipVersions.length} baris`); } else console.log(`[Scheduler] -- IPVersions : tidak ada data`); // 19. Remote IPs - const remoteIPs = await netify.fetchTopRemoteIPs(1440, 20); - if (remoteIPs?.length) { db.insertRemoteIPs(remoteIPs, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK RemoteIPs : ${remoteIPs.length} baris`); } + const remoteIPs = await netify.fetchTopRemoteIPs(1440, 20, agentUuid); + if (remoteIPs?.length) { db.insertRemoteIPs(remoteIPs, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK RemoteIPs : ${remoteIPs.length} baris`); } else console.log(`[Scheduler] -- RemoteIPs : tidak ada data`); // 20. MAC Bandwidth - const macBW = await netify.fetchTopLocalMACs(1440, 50); - if (macBW?.length) { db.insertMACBandwidth(macBW, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK MACBandwdh : ${macBW.length} baris`); } + const macBW = await netify.fetchTopLocalMACs(1440, 50, agentUuid); + if (macBW?.length) { db.insertMACBandwidth(macBW, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK MACBandwdh : ${macBW.length} baris`); } else console.log(`[Scheduler] -- MACBandwdh : tidak ada data`); // 9. Bandwidth Timeline - const summary = await netify.fetchBandwidthSummary(1440); + const summary = await netify.fetchBandwidthSummary(1440, agentUuid); const devCount = devices?.length ?? 0; - db.insertBandwidthTimeline({ ...summary, devices: devCount }, fetchedAt, SITE_UUID); + db.insertBandwidthTimeline({ ...summary, devices: devCount }, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Timeline : saved`); // 21. TLS Versions - const tlsVer = await netify.fetchTLSVersions(1440, 10); - if (tlsVer?.length) { db.insertTLSVersions(tlsVer, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK TLS Ver : ${tlsVer.length} baris`); } + const tlsVer = await netify.fetchTLSVersions(1440, 10, agentUuid); + if (tlsVer?.length) { db.insertTLSVersions(tlsVer, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Ver : ${tlsVer.length} baris`); } else console.log(`[Scheduler] -- TLS Ver : tidak ada data`); // 22. TLS Ciphers - const tlsCipher = await netify.fetchTLSCiphers(1440, 15); - if (tlsCipher?.length) { db.insertTLSCiphers(tlsCipher, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK TLS Cipher : ${tlsCipher.length} baris`); } + const tlsCipher = await netify.fetchTLSCiphers(1440, 15, agentUuid); + if (tlsCipher?.length) { db.insertTLSCiphers(tlsCipher, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Cipher : ${tlsCipher.length} baris`); } else console.log(`[Scheduler] -- TLS Cipher : tidak ada data`); // 23. TLS Security - const tlsSec = await netify.fetchTLSSecurity(1440, 10); - if (tlsSec?.length) { db.insertTLSSecurity(tlsSec, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK TLS Sec : ${tlsSec.length} baris`); } + const tlsSec = await netify.fetchTLSSecurity(1440, 10, agentUuid); + if (tlsSec?.length) { db.insertTLSSecurity(tlsSec, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Sec : ${tlsSec.length} baris`); } else console.log(`[Scheduler] -- TLS Sec : tidak ada data`); // 24. NetBIOS Hostnames - const netbios = await netify.fetchNetBIOSHostnames(1440, 30); - if (netbios?.length) { db.insertNetBIOSHostnames(netbios, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK NetBIOS : ${netbios.length} baris`); } + const netbios = await netify.fetchNetBIOSHostnames(1440, 30, agentUuid); + if (netbios?.length) { db.insertNetBIOSHostnames(netbios, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK NetBIOS : ${netbios.length} baris`); } else console.log(`[Scheduler] -- NetBIOS : tidak ada data`); // 25. Discovery OS (standalone — OS yang terdeteksi di jaringan) - const discOs = await netify.fetchTopDiscoveryOS(1440, 20); - if (discOs?.length) { db.insertDiscoveryOS(discOs, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK DiscOS : ${discOs.length} baris`); } + const discOs = await netify.fetchTopDiscoveryOS(1440, 20, agentUuid); + if (discOs?.length) { db.insertDiscoveryOS(discOs, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DiscOS : ${discOs.length} baris`); } else console.log(`[Scheduler] -- DiscOS : tidak ada data`); // 26. DHCP Class Fingerprint - const dhcpFp = await netify.fetchDHCPClassFingerprints(1440, 30); - if (dhcpFp?.length) { db.insertDHCPFingerprints(dhcpFp, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK DHCP FP : ${dhcpFp.length} baris`); } + const dhcpFp = await netify.fetchDHCPClassFingerprints(1440, 30, agentUuid); + if (dhcpFp?.length) { db.insertDHCPFingerprints(dhcpFp, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DHCP FP : ${dhcpFp.length} baris`); } else console.log(`[Scheduler] -- DHCP FP : tidak ada data`); // 27. HTTP User-Agent - const userAgents = await netify.fetchHTTPUserAgents(1440, 30); - if (userAgents?.length) { db.insertHTTPUserAgents(userAgents, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK UserAgent : ${userAgents.length} baris`); } + const userAgents = await netify.fetchHTTPUserAgents(1440, 30, agentUuid); + if (userAgents?.length) { db.insertHTTPUserAgents(userAgents, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK UserAgent : ${userAgents.length} baris`); } else console.log(`[Scheduler] -- UserAgent : tidak ada data`); // 28. HTTPS SNI Hostname - const sniHosts = await netify.fetchSNIHostnames(1440, 30); - if (sniHosts?.length) { db.insertSNIHostnames(sniHosts, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK SNI Host : ${sniHosts.length} baris`); } + const sniHosts = await netify.fetchSNIHostnames(1440, 30, agentUuid); + if (sniHosts?.length) { db.insertSNIHostnames(sniHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SNI Host : ${sniHosts.length} baris`); } else console.log(`[Scheduler] -- SNI Host : tidak ada data`); // 29. SSL Server Common Name - const sslCN = await netify.fetchSSLServerCN(1440, 30); - if (sslCN?.length) { db.insertSSLServerCN(sslCN, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK SSL CN : ${sslCN.length} baris`); } + const sslCN = await netify.fetchSSLServerCN(1440, 30, agentUuid); + if (sslCN?.length) { db.insertSSLServerCN(sslCN, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSL CN : ${sslCN.length} baris`); } else console.log(`[Scheduler] -- SSL CN : tidak ada data`); // 30. QUIC Hostname - const quicHosts = await netify.fetchQUICHostnames(1440, 30); - if (quicHosts?.length) { db.insertQUICHostnames(quicHosts, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK QUIC Host : ${quicHosts.length} baris`); } + const quicHosts = await netify.fetchQUICHostnames(1440, 30, agentUuid); + if (quicHosts?.length) { db.insertQUICHostnames(quicHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK QUIC Host : ${quicHosts.length} baris`); } else console.log(`[Scheduler] -- QUIC Host : tidak ada data`); // 31. BitTorrent Info Hash - const btHashes = await netify.fetchBitTorrentInfoHashes(1440, 30); - if (btHashes?.length) { db.insertBitTorrentHashes(btHashes, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK BT Hash : ${btHashes.length} baris`); } + const btHashes = await netify.fetchBitTorrentInfoHashes(1440, 30, agentUuid); + if (btHashes?.length) { db.insertBitTorrentHashes(btHashes, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK BT Hash : ${btHashes.length} baris`); } else console.log(`[Scheduler] -- BT Hash : tidak ada data`); // 32. SSH Client (field: ssh_client) - const sshClient = await netify.fetchSSHClients(1440, 20); - if (sshClient?.length) { db.insertSSHVersions(sshClient, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK SSH Client : ${sshClient.length} baris`); } + const sshClient = await netify.fetchSSHClients(1440, 20, agentUuid); + if (sshClient?.length) { db.insertSSHVersions(sshClient, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSH Client : ${sshClient.length} baris`); } else console.log(`[Scheduler] -- SSH Client : tidak ada data`); // 32b. SSH Server (field: ssh_server) - const sshServer = await netify.fetchSSHServers(1440, 20); - if (sshServer?.length) { db.insertSSHVersions(sshServer, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK SSH Server : ${sshServer.length} baris`); } + const sshServer = await netify.fetchSSHServers(1440, 20, agentUuid); + if (sshServer?.length) { db.insertSSHVersions(sshServer, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSH Server : ${sshServer.length} baris`); } else console.log(`[Scheduler] -- SSH Server : tidak ada data`); // 33. mDNS Hostname (Chromecast, Apple TV, etc.) - const mdnsHosts = await netify.fetchMDNSHostnames(1440, 30); - if (mdnsHosts?.length) { db.insertMDNSHostnames(mdnsHosts, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK mDNS Host : ${mdnsHosts.length} baris`); } + const mdnsHosts = await netify.fetchMDNSHostnames(1440, 30, agentUuid); + if (mdnsHosts?.length) { db.insertMDNSHostnames(mdnsHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK mDNS Host : ${mdnsHosts.length} baris`); } else console.log(`[Scheduler] -- mDNS Host : tidak ada data`); // ─── INTELLIGENCE 22-30 (derive dari data yang tersedia) ───────────────── // 34. Cryptocurrency Mining (derive dari apps + flows ke port mining) - const cryptoMining = await netify.fetchCryptoMining(50); - if (cryptoMining?.length) { db.insertCryptoMining(cryptoMining, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK CryptoMine : ${cryptoMining.length} baris`); } + const cryptoMining = await netify.fetchCryptoMining(50, agentUuid); + if (cryptoMining?.length) { db.insertCryptoMining(cryptoMining, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK CryptoMine : ${cryptoMining.length} baris`); } else console.log(`[Scheduler] -- CryptoMine : tidak ada data`); // 35. Device Discovery (derive dari flows + bandwidth per-IP) - const devDisc = await netify.fetchDeviceDiscovery(100); - if (devDisc?.length) { db.insertDeviceDiscovery(devDisc, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK DevDisc : ${devDisc.length} baris`); } + const devDisc = await netify.fetchDeviceDiscovery(100, agentUuid); + if (devDisc?.length) { db.insertDeviceDiscovery(devDisc, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DevDisc : ${devDisc.length} baris`); } else console.log(`[Scheduler] -- DevDisc : tidak ada data`); // 36. Encryption Audit (derive dari flows per-IP: port encrypted vs plain) - const encAudit = await netify.fetchEncryptionAudit(50); - if (encAudit?.length) { db.insertEncryptionAudit(encAudit, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK EncAudit : ${encAudit.length} baris`); } + const encAudit = await netify.fetchEncryptionAudit(50, agentUuid); + if (encAudit?.length) { db.insertEncryptionAudit(encAudit, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK EncAudit : ${encAudit.length} baris`); } else console.log(`[Scheduler] -- EncAudit : tidak ada data`); // 37. Insecure Protocols (derive dari top protocols) - const insecProto = await netify.fetchInsecureProtocols(1440, 50); - if (insecProto?.length) { db.insertInsecureProtocols(insecProto, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK InsecProto : ${insecProto.length} baris`); } + const insecProto = await netify.fetchInsecureProtocols(1440, 50, agentUuid); + if (insecProto?.length) { db.insertInsecureProtocols(insecProto, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK InsecProto : ${insecProto.length} baris`); } else console.log(`[Scheduler] -- InsecProto : tidak ada data`); // 38. IP Reputation (derive dari top remote_ip + high-risk countries) - const ipRep = await netify.fetchIPReputation(50); - if (ipRep?.length) { db.insertIPReputation(ipRep, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK IPRepute : ${ipRep.length} baris`); } + const ipRep = await netify.fetchIPReputation(50, agentUuid); + if (ipRep?.length) { db.insertIPReputation(ipRep, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK IPRepute : ${ipRep.length} baris`); } else console.log(`[Scheduler] -- IPRepute : tidak ada data`); // 39. Server Discovery (derive dari flows ke port server well-known) - const srvDisc = await netify.fetchServerDiscovery(100); - if (srvDisc?.length) { db.insertServerDiscovery(srvDisc, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK SrvDisc : ${srvDisc.length} baris`); } + const srvDisc = await netify.fetchServerDiscovery(100, agentUuid); + if (srvDisc?.length) { db.insertServerDiscovery(srvDisc, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SrvDisc : ${srvDisc.length} baris`); } else console.log(`[Scheduler] -- SrvDisc : tidak ada data`); // 40. Tor Detection (derive dari apps/hostnames mengandung "tor") - const torDet = await netify.fetchTorDetection(50); - if (torDet?.length) { db.insertTorDetection(torDet, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK TorDet : ${torDet.length} baris`); } + const torDet = await netify.fetchTorDetection(50, agentUuid); + if (torDet?.length) { db.insertTorDetection(torDet, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TorDet : ${torDet.length} baris`); } else console.log(`[Scheduler] -- TorDet : tidak ada data`); // 41. Unencrypted Password (derive dari flows ke port cleartext auth) - const unencPwd = await netify.fetchUnencryptedPasswords(50); - if (unencPwd?.length) { db.insertUnencryptedPasswords(unencPwd, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK UnencPwd : ${unencPwd.length} baris`); } + const unencPwd = await netify.fetchUnencryptedPasswords(50, agentUuid); + if (unencPwd?.length) { db.insertUnencryptedPasswords(unencPwd, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK UnencPwd : ${unencPwd.length} baris`); } else console.log(`[Scheduler] -- UnencPwd : tidak ada data`); // 42. VPN Detection (derive dari apps/protocols/ports VPN) - const vpnDet = await netify.fetchVPNDetection(50); - if (vpnDet?.length) { db.insertVPNDetection(vpnDet, fetchedAt, SITE_UUID); console.log(`[Scheduler] OK VPNDet : ${vpnDet.length} baris`); } + const vpnDet = await netify.fetchVPNDetection(50, agentUuid); + if (vpnDet?.length) { db.insertVPNDetection(vpnDet, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK VPNDet : ${vpnDet.length} baris`); } else console.log(`[Scheduler] -- VPNDet : tidak ada data`); + } + + // --- Main loop + await fetchAndStore(fetchedAt, null); + if (apiAgents && apiAgents.length > 0) { + for (const agent of apiAgents) { + if (agent && agent.uuid) { + await fetchAndStore(fetchedAt, agent.uuid); + } + } + } } catch (err) { console.error('[Scheduler] ERROR:', err); } finally { diff --git a/diff.txt b/diff.txt new file mode 100644 index 0000000..ead2986 --- /dev/null +++ b/diff.txt @@ -0,0 +1,2067 @@ +diff --git a/backend/database.js b/backend/database.js +index 2ab6ce0..d3aa1c0 100644 +--- a/backend/database.js ++++ b/backend/database.js +@@ -65,6 +65,8 @@ function initSchema() { + + // ΓöÇΓöÇΓöÇ AUTHENTICATION ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + d.exec(`CREATE TABLE IF NOT EXISTS users ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, +@@ -104,11 +106,13 @@ function initSchema() { + const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get(); + if (adminExists.count === 0) { + const hash = bcrypt.hashSync('admin123', 10); +- d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); ++ d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); + console.log('[DB] Created default admin user (admin / admin123)'); + } + + d.exec(`CREATE TABLE IF NOT EXISTS tls_versions ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -119,6 +123,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_ciphers ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -129,6 +135,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_security ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -140,6 +148,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS netbios_hostnames ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -152,6 +162,8 @@ function initSchema() { + // ΓöÇΓöÇΓöÇ DPI Fields 12-21 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + + d.exec(`CREATE TABLE IF NOT EXISTS dhcp_fingerprints ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -162,6 +174,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS http_user_agents ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -172,6 +186,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS sni_hostnames ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -182,6 +198,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssl_server_cn ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -192,6 +210,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS quic_hostnames ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -202,6 +222,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bittorrent_hashes ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -213,6 +235,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssh_versions ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -223,6 +247,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mdns_hostnames ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -235,6 +261,8 @@ function initSchema() { + // ΓöÇΓöÇΓöÇ Intelligence API 22-30 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + + d.exec(`CREATE TABLE IF NOT EXISTS intel_crypto_mining ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -248,6 +276,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_device_discovery ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -259,6 +289,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_encryption_audit ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -273,6 +305,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_insecure_protocols ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -288,6 +322,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_ip_reputation ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -302,6 +338,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_server_discovery ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -315,6 +353,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_tor_detection ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -327,6 +367,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_unencrypted_passwords ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -340,6 +382,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_vpn_detection ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -353,6 +397,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS discovery_os ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -364,6 +410,8 @@ function initSchema() { + + // Tabel baru fitur 1-11 + d.exec(`CREATE TABLE IF NOT EXISTS app_categories ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -374,6 +422,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS continents ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -384,6 +434,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS regions ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -393,6 +445,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS cities ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -402,6 +456,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS vlans ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -412,6 +468,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS interfaces ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -423,6 +481,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_types ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -433,6 +493,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_origins ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -443,6 +505,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ip_versions ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -453,6 +517,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS remote_ips ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -463,6 +529,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mac_bandwidth ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -473,6 +541,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_apps ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -482,6 +552,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS devices ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -491,6 +563,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flows ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -502,6 +576,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS threats ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -511,6 +587,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_protocols ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -520,6 +598,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_countries ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -529,6 +609,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS dns_queries ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -537,6 +619,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS events ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -546,6 +630,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_timeline ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, +@@ -556,6 +642,8 @@ function initSchema() { + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache ( ++ agent_uuid TEXT DEFAULT NULL, ++ agent_uuid TEXT DEFAULT NULL, + ip_address TEXT PRIMARY KEY, + isp TEXT, + country TEXT, +@@ -569,17 +657,17 @@ function initSchema() { + + // ΓöÇΓöÇΓöÇ INSERT FUNCTIONS ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertBandwidthApps(rows, fetchedAt, siteUuid) { ++function insertBandwidthApps(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.app_id ?? null, + r.app_label ?? 'Unknown', + r.app_tag ?? null, +@@ -594,17 +682,17 @@ function insertBandwidthApps(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertDevices(rows, fetchedAt, siteUuid) { ++function insertDevices(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.mac_address ?? null, + r.ip_address ?? null, + r.device_label ?? r.ip_address ?? 'Unknown', +@@ -619,17 +707,17 @@ function insertDevices(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertFlows(rows, fetchedAt, siteUuid) { ++function insertFlows(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, 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) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.flow_id ?? null, + r.src_ip ?? null, + r.src_mac ?? null, +@@ -647,17 +735,17 @@ function insertFlows(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertThreats(rows, fetchedAt, siteUuid) { ++function insertThreats(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.threat_id ?? null, + r.threat_type ?? null, + r.severity ?? null, +@@ -673,18 +761,18 @@ function insertThreats(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertProtocols(rows, fetchedAt, siteUuid) { ++function insertProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_protocols +- (site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) +- VALUES (?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js ΓÇö akses langsung tanpa nested + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.protocol_id ?? null, + r.protocol_label ?? 'Unknown', + r.download ?? 0, +@@ -695,18 +783,18 @@ function insertProtocols(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertCountries(rows, fetchedAt, siteUuid) { ++function insertCountries(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_countries +- (site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) +- VALUES (?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js ΓÇö akses langsung tanpa nested + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.country_code ?? null, + r.country_name ?? 'Unknown', + r.download ?? 0, +@@ -717,17 +805,17 @@ function insertCountries(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertDNS(rows, fetchedAt, siteUuid) { ++function insertDNS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO dns_queries +- (site_uuid, fetched_at, domain, query_count, app_label, category) +- VALUES (?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, domain, query_count, app_label, category) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.domain ?? null, + r.query_count ?? 0, + r.app_label ?? null, +@@ -737,17 +825,17 @@ function insertDNS(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertEvents(rows, fetchedAt, siteUuid) { ++function insertEvents(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.event_id ?? null, + r.event_type ?? null, + r.severity ?? null, +@@ -760,12 +848,12 @@ function insertEvents(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { ++function insertBandwidthTimeline(summary, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + d.prepare(` + INSERT INTO bandwidth_timeline +- (site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + siteUuid, + fetchedAt, +@@ -783,71 +871,15 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { + + function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { + 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(','); +- +- // 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 app_label IS NOT NULL +- GROUP BY app_label +- `).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 []; ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; + 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 }); ++ SELECT * FROM bandwidth_apps ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + return correlateAppLabels(rows); + } + +@@ -969,289 +1001,22 @@ function deviceMatchesAgent(ip, mac, agentUuid) { + return false; + } + +-function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) { ++function getLatestDevices(limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); +- const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); +- if (!latest?.t) return []; +- +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- const macs = AGENT_MAC_MAP[agentUuid]; +- const placeholders = macs.map(() => '?').join(','); +- +- // Fetch all flows aggregated by IP address across all time/snapshots +- const flowsData = d.prepare(` +- SELECT src_ip, src_mac, SUM(bytes_download) as download, SUM(bytes_upload) as upload, MAX(last_seen) as last_seen, MAX(fetched_at) as fetched_at +- FROM flows +- WHERE src_mac IN (${placeholders}) +- GROUP BY src_ip +- `).all(...macs); +- +- // Fetch all devices matching this agent's criteria across all snapshots +- const devicesData = d.prepare(` +- SELECT ip_address, mac_address, device_label, device_type, os_label, manufacturer, download, upload, fetched_at +- FROM devices +- GROUP BY ip_address +- `).all(); +- +- // Map discovered devices to allow lookups by IP +- const devicesMap = new Map(); +- for (const dev of devicesData) { +- if (dev.ip_address && deviceMatchesAgent(dev.ip_address, dev.mac_address, agentUuid)) { +- devicesMap.set(dev.ip_address, dev); +- } +- } +- +- // 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(); +- +- // 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])); +- +- function processAgentDevice(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'; +- +- // 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, +- 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: scaledDl || 0, +- upload: scaledUl || 0, +- total: (scaledDl || 0) + (scaledUl || 0), +- is_gateway_routed: isRouted ? 1 : 0 +- }; +- } +- +- // 1. Process flowsData +- for (const f of flowsData) { +- if (!f.src_ip || seenIps.has(f.src_ip)) continue; +- if (deviceMatchesAgent(f.src_ip, f.src_mac, agentUuid)) { +- seenIps.add(f.src_ip); +- const dbDev = devicesMap.get(f.src_ip); +- resolved.push(processAgentDevice(f.src_ip, f.src_mac, dbDev, f.download, f.upload, f.last_seen || f.fetched_at)); +- } +- } +- +- // 2. Process devicesData that were not in flowsData but match agent +- for (const dev of devicesData) { +- if (!dev.ip_address || seenIps.has(dev.ip_address)) continue; +- if (deviceMatchesAgent(dev.ip_address, dev.mac_address, agentUuid)) { +- seenIps.add(dev.ip_address); +- resolved.push(processAgentDevice(dev.ip_address, dev.mac_address, dev, dev.download, dev.upload, dev.fetched_at)); +- } +- } +- +- resolved.sort((a, b) => b.download - a.download); +- 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])); +- +- // Get all devices in latest snapshot from devices table +- const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t); +- +- // Get active flow bandwidth in latest flows snapshot +- const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); +- let flowsBandwidth = []; +- if (latestFlowFetch?.t) { +- flowsBandwidth = d.prepare(` +- SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload +- FROM flows +- WHERE fetched_at = ? +- GROUP BY src_ip +- `).all(latestFlowFetch.t); +- } +- const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f])); +- +- const resolved = []; +- const seenIps = new Set(); +- +- function processDevice(ip, mac, dbDev, flowInfo) { +- 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'; +- +- const download = flowInfo ? flowInfo.flow_download : (dbDev ? dbDev.download : 0); +- const upload = flowInfo ? flowInfo.flow_upload : (dbDev ? dbDev.upload : 0); +- +- return { +- id: dbDev ? dbDev.id : null, +- site_uuid: dbDev ? dbDev.site_uuid : siteUuid, +- fetched_at: 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 +- }; +- } +- +- // 1. Process all devices in the devices table +- for (const dev of devices) { +- if (!dev.ip_address) continue; +- if (seenIps.has(dev.ip_address)) continue; +- seenIps.add(dev.ip_address); +- const flowInfo = flowMap.get(dev.ip_address); +- resolved.push(processDevice(dev.ip_address, dev.mac_address, dev, flowInfo)); +- } +- +- // 2. Process any active flow IPs that are NOT present in the devices table (e.g. dynamic client IPs) +- for (const flow of flowsBandwidth) { +- if (!flow.src_ip || seenIps.has(flow.src_ip)) continue; +- seenIps.add(flow.src_ip); +- resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow)); +- } +- +- // 3. Filter list based on role (Admin vs Agent) +- let filtered = resolved; +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- filtered = resolved.filter(dev => deviceMatchesAgent(dev.ip_address, dev.mac_address, agentUuid)); +- } else if (siteUuid) { +- filtered = resolved.filter(dev => dev.site_uuid === siteUuid); +- } +- +- // 4. Sort by download DESC +- filtered.sort((a, b) => b.download - a.download); +- return filtered.slice(0, limit); ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ const rows = d.prepare(` ++ SELECT * FROM devices ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); ++ ++ return rows.map(r => { ++ const meta = resolveDeviceMetadata(r.ip_address, r.mac_address, r.device_label, r.manufacturer, r.device_type); ++ return { ...r, ...meta, total: (r.download||0) + (r.upload||0) }; ++ }); + } + + function correlateFlows(flows) { +@@ -1527,812 +1292,518 @@ function correlateAppLabels(apps) { + }); + } + +-function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { ++function getLatestFlows(limit = 50, siteUuid = null, agentUuid = null) { + 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(','); +- 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 }); +- } +- +- const mapped = rows.map(r => ({ +- ...r, +- download: r.bytes_download ?? 0, +- upload: r.bytes_upload ?? 0 +- })); +- +- return correlateFlows(mapped); ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ return d.prepare(` ++ SELECT * FROM flows ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY bytes_download DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + +-function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { ++function getLatestThreats(limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- const macs = AGENT_MAC_MAP[agentUuid]; +- const placeholders = macs.map(() => '?').join(','); +- return d.prepare(` +- SELECT * FROM threats +- WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) +- ORDER BY fetched_at DESC +- LIMIT ? +- `).all(...macs, ...macs, limit); +- } +- return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM threats WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ return d.prepare(` ++ SELECT * FROM threats ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY id DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + + function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { + 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 protocol AS protocol_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count +- FROM flows +- WHERE src_mac IN (${placeholders}) AND fetched_at = ? +- GROUP BY protocol +- ORDER BY download DESC +- LIMIT ? +- `).all(...macs, latest.t, limit); +- } +- +- const protoLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); +- if (!protoLatest?.t) return []; ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; + return d.prepare(` +- SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit +- `).all({ fetched_at: protoLatest.t, limit, siteUuid }); ++ SELECT * FROM bandwidth_protocols ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + + function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { + const d = getDB(); +- +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- const macs = AGENT_MAC_MAP[agentUuid]; +- const placeholders = macs.map(() => '?').join(','); +- +- const rows = d.prepare(` +- SELECT g.country AS country_name, +- SUM(f.bytes_download) AS download, +- SUM(f.bytes_upload) AS upload, +- COUNT(*) AS flow_count +- FROM ( +- SELECT dst_ip, bytes_download, bytes_upload +- FROM flows +- WHERE src_mac IN (${placeholders}) +- ORDER BY last_seen DESC, fetched_at DESC +- LIMIT 500 +- ) f +- JOIN geoip_cache g ON f.dst_ip = g.ip_address +- WHERE g.country IS NOT NULL +- AND g.country != 'Local' +- GROUP BY g.country +- ORDER BY download DESC +- LIMIT ? +- `).all(...macs, limit); +- +- // Build dynamic name-to-code mapping from bandwidth_countries +- const nameToCodeMap = {}; +- try { +- const mappingRows = d.prepare("SELECT DISTINCT country_code, country_name FROM bandwidth_countries WHERE country_code IS NOT NULL").all(); +- for (const r of mappingRows) { +- if (r.country_name) { +- nameToCodeMap[r.country_name.toLowerCase().trim()] = r.country_code; +- } +- } +- } catch (err) { +- console.error('[DB] Failed to build country code mapping:', err); +- } +- +- // Fallback/standard mapping +- const fallbackMap = { +- 'indonesia': 'ID', +- 'united states': 'US', +- 'united kingdom': 'GB', +- 'great britain': 'GB', +- 'singapore': 'SG', +- 'hong kong': 'HK', +- 'japan': 'JP', +- 'canada': 'CA', +- 'australia': 'AU', +- 'germany': 'DE', +- 'france': 'FR', +- 'india': 'IN', +- 'china': 'CN', +- 'south korea': 'KR', +- 'russia': 'RU', +- 'russian federation': 'RU', +- 'brazil': 'BR', +- 'italy': 'IT', +- 'spain': 'ES', +- 'netherlands': 'NL', +- 'the netherlands': 'NL', +- 'switzerland': 'CH', +- 'sweden': 'SE', +- 'norway': 'NO', +- 'finland': 'FI', +- 'denmark': 'DK', +- 'ireland': 'IE', +- 'belgium': 'BE', +- 'austria': 'AT', +- 'malaysia': 'MY', +- 'thailand': 'TH', +- 'vietnam': 'VN', +- 'philippines': 'PH', +- 'taiwan': 'TW', +- 'new zealand': 'NZ', +- 'south africa': 'ZA', +- 'mexico': 'MX', +- 'argentina': 'AR', +- 'chile': 'CL', +- 'colombia': 'CO', +- 'turkey': 'TR', +- 'saudi arabia': 'SA', +- 'united arab emirates': 'AE', +- 'egypt': 'EG', +- 'israel': 'IL', +- 'ukraine': 'UA', +- 'poland': 'PL', +- 'romania': 'RO', +- 'greece': 'GR', +- 'hungary': 'HU', +- 'bangladesh': 'BD', +- 'seychelles': 'SC', +- 'luxembourg': 'LU', +- 'pakistan': 'PK' +- }; +- +- return rows.map(r => { +- const normalizedName = r.country_name.toLowerCase().trim(); +- const code = nameToCodeMap[normalizedName] || fallbackMap[normalizedName] || null; +- return { +- country_code: code, +- country_name: r.country_name, +- download: r.download ?? 0, +- upload: r.upload ?? 0, +- flow_count: r.flow_count ?? 0 +- }; +- }); +- } +- +- const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); +- if (!countryLatest?.t) return []; ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; + return d.prepare(` +- SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit +- `).all({ fetched_at: countryLatest.t, limit, siteUuid }); ++ SELECT * FROM bandwidth_countries ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + + function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { + 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 domain, COUNT(*) AS query_count, app_label, 'Web' AS category +- FROM flows +- WHERE src_mac IN (${placeholders}) AND domain IS NOT NULL AND fetched_at = ? +- GROUP BY domain +- ORDER BY query_count DESC +- LIMIT ? +- `).all(...macs, latest.t, limit); +- } +- +- const dnsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); +- if (!dnsLatest?.t) return []; ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; + return d.prepare(` +- SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit +- `).all({ fetched_at: dnsLatest.t, limit, siteUuid }); ++ SELECT * FROM dns_queries ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY query_count DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + + function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- const macs = AGENT_MAC_MAP[agentUuid]; +- const placeholders = macs.map(() => '?').join(','); +- return d.prepare(` +- SELECT * FROM events +- WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) +- ORDER BY fetched_at DESC +- LIMIT ? +- `).all(...macs, ...macs, limit); +- } +- return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM events WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ return d.prepare(` ++ SELECT * FROM events ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ ORDER BY event_at DESC LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + +-function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { ++function getBandwidthTimeline(limit = 12, siteUuid = null, agentUuid = null) { + const d = getDB(); +- if (agentUuid && AGENT_MAC_MAP[agentUuid]) { +- const macs = AGENT_MAC_MAP[agentUuid]; +- const placeholders = macs.map(() => '?').join(','); +- return d.prepare(` +- SELECT mb.fetched_at, +- SUM(mb.download) AS total_download, +- SUM(mb.upload) AS total_upload, +- COALESCE(fl.flow_count, 0) AS total_flows, +- COALESCE(fl.device_count, 0) AS active_devices +- FROM mac_bandwidth mb +- LEFT JOIN ( +- SELECT fetched_at, COUNT(*) AS flow_count, COUNT(DISTINCT src_ip) AS device_count +- FROM flows +- WHERE src_mac IN (${placeholders}) +- GROUP BY fetched_at +- ) fl ON fl.fetched_at = mb.fetched_at +- WHERE mb.mac_address IN (${placeholders}) +- GROUP BY mb.fetched_at +- ORDER BY mb.fetched_at DESC +- LIMIT ? +- `).all(...macs, ...macs, points).reverse(); +- } + return d.prepare(` +- SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit +- `).all({ limit: points, siteUuid }).reverse(); ++ SELECT * FROM bandwidth_timeline ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ ORDER BY fetched_at DESC LIMIT @limit ++ `).all({ limit, siteUuid, agentUuid }); + } + + function getStats(siteUuid = null, agentUuid = null) { + const d = getDB(); ++ ++ const devRows = d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ const devT = devRows?.t; ++ const totalDevices = devT ? d.prepare(`SELECT count(*) as c FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ${buildAgentWhere(agentUuid)} AND fetched_at = @t`).get({siteUuid, t: devT, agentUuid}).c : 0; + +- 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 +- WHERE src_mac IN (${placeholders}) +- `).all(...macs); +- +- const devicesIPs = d.prepare(` +- SELECT DISTINCT ip_address, mac_address FROM devices +- `).all(); +- +- const uniqueDevs = new Set(); +- const addDev = (ip, mac) => { +- if (!ip) return; +- if (deviceMatchesAgent(ip, mac, agentUuid)) { +- uniqueDevs.add(ip); +- } +- }; +- for (const f of flowsIPs) addDev(f.src_ip, f.src_mac); +- for (const dev of devicesIPs) addDev(dev.ip_address, dev.mac_address); +- const totalDevices = uniqueDevs.size; +- +- // Count cumulative flows for this agent +- const activeFlows = d.prepare(` +- SELECT COUNT(*) as n FROM flows +- WHERE src_mac IN (${placeholders}) +- `).get(...macs)?.n ?? 0; +- +- // Count threats for this agent +- const totalThreats = d.prepare(` +- SELECT COUNT(*) as n FROM threats +- WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) +- `).get(...macs, ...macs)?.n ?? 0; +- +- // Count events for this agent +- const totalEvents = d.prepare(` +- SELECT COUNT(*) as n FROM events +- WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) +- `).get(...macs, ...macs)?.n ?? 0; +- +- const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get()?.t ?? null; +- +- // Construct latest bandwidth timeline summary for this agent +- const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t; +- const latestBw = latestMacSnap +- ? d.prepare(` +- SELECT SUM(download) AS total_download, SUM(upload) AS total_upload, +- ? AS total_flows, ? AS active_devices, ? as fetched_at +- FROM mac_bandwidth +- WHERE mac_address IN (${placeholders}) AND fetched_at = ? +- `).get(activeFlows, totalDevices, latestMacSnap, ...macs, latestMacSnap) +- : {}; +- +- return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; +- } ++ const flowRows = d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ const flowT = flowRows?.t; ++ const activeFlows = flowT ? d.prepare(`SELECT count(*) as c FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ${buildAgentWhere(agentUuid)} AND fetched_at = @t`).get({siteUuid, t: flowT, agentUuid}).c : 0; + +- // Fallback to site-wide stats if no agentUuid +- const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); +- const totalDevices = latestDevFetch?.t +- ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) +- : 0; ++ const threatRows = d.prepare(`SELECT count(*) as c FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ${buildAgentWhere(agentUuid)}`).get({siteUuid, agentUuid}); ++ const totalThreats = threatRows.c; + +- const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); +- const activeFlows = latestFlowFetch?.t +- ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) +- : 0; ++ const eventRows = d.prepare(`SELECT count(*) as c FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND ${buildAgentWhere(agentUuid)}`).get({siteUuid, agentUuid}); ++ const totalEvents = eventRows.c; + +- 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 }); ++ let lastFetch = devT || flowT || null; + +- return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; ++ return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch }; + } + + // ΓöÇΓöÇΓöÇ INSERT FITUR BARU 1-11 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ +-function insertAppCategories(rows, fetchedAt, siteUuid) { ++function insertAppCategories(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO app_categories +- (site_uuid, fetched_at, category_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertContinents(rows, fetchedAt, siteUuid) { ++function insertContinents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO continents +- (site_uuid, fetched_at, continent_name, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); + })(rows); + } + +-function insertRegions(rows, fetchedAt, siteUuid) { ++function insertRegions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO regions +- (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO cities +- (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO vlans +- (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO interfaces +- (site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_types +- (site_uuid, fetched_at, flow_type_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertFlowOrigins(rows, fetchedAt, siteUuid) { ++function insertFlowOrigins(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_origins +- (site_uuid, fetched_at, flow_origin_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertIPVersions(rows, fetchedAt, siteUuid) { ++function insertIPVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ip_versions +- (site_uuid, fetched_at, ip_version_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertRemoteIPs(rows, fetchedAt, siteUuid) { ++function insertRemoteIPs(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO remote_ips +- (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mac_bandwidth +- (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); + })(rows); + } + + // ΓöÇΓöÇΓöÇ QUERY FITUR BARU ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ +-function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) { ++function getLatest(table, limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); +- const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get(); +- if (!latest?.t) return []; +- +- let rows = []; +- +- // If agentUuid is provided, handle direct MAC or interface filtering +- const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null; +- if (agentUuid && macs) { +- if (table === 'mac_bandwidth') { +- const placeholders = macs.map(() => '?').join(','); +- rows = d.prepare(`SELECT * FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...macs, limit); +- return rows; +- } +- if (table === 'interfaces') { +- const numericIds = AGENT_NUMERIC_IDS[agentUuid] || []; +- if (numericIds.length > 0) { +- const placeholders = numericIds.map(() => '?').join(','); +- rows = d.prepare(`SELECT * FROM interfaces WHERE fetched_at = ? AND agent_id IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...numericIds, limit); +- return rows; +- } +- } +- if (table === 'remote_ips') { +- const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get(); +- if (latestFlowsFetch?.t) { +- const placeholders = macs.map(() => '?').join(','); +- rows = d.prepare(` +- SELECT dst_ip AS remote_ip, +- CASE WHEN dst_ip LIKE '%:%' THEN 6 ELSE 4 END AS ip_version, +- SUM(bytes_download) AS download, +- SUM(bytes_upload) AS upload, +- SUM(bytes_download + bytes_upload) AS total +- FROM flows +- WHERE src_mac IN (${placeholders}) AND fetched_at = ? +- GROUP BY dst_ip +- ORDER BY download DESC +- LIMIT ? +- `).all(...macs, latestFlowsFetch.t, limit); +- return rows; +- } +- } +- if (table === 'dns_queries') { +- const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get(); +- if (latestFlowsFetch?.t) { +- const placeholders = macs.map(() => '?').join(','); +- rows = d.prepare(` +- SELECT domain, COUNT(*) AS query_count +- FROM flows +- WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND domain IS NOT NULL +- GROUP BY domain +- ORDER BY query_count DESC +- LIMIT ? +- `).all(...macs, latestFlowsFetch.t, limit); +- return rows; +- } +- } +- } +- +- // Fallback to standard site-wide select +- rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid }); +- +- // If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint +- if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) { +- const ratio = getAgentTrafficRatio(agentUuid); +- return rows.map(r => ({ +- ...r, +- download: Math.round((r.download || 0) * ratio), +- upload: Math.round((r.upload || 0) * ratio), +- total: Math.round((r.total || 0) * ratio), +- query_count: Math.round((r.query_count || 0) * ratio), +- flow_count: Math.round((r.flow_count || 0) * ratio), +- })); +- } +- +- return rows; ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ return d.prepare(` ++ SELECT * FROM ${table} ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + +-// DPI Fields +-function insertTLSVersions(rows, fetchedAt, siteUuid) { ++function insertTLSVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_versions +- (site_uuid, fetched_at, tls_version, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); + })(rows); + } + +-function insertTLSCiphers(rows, fetchedAt, siteUuid) { ++function insertTLSCiphers(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_ciphers +- (site_uuid, fetched_at, tls_cipher, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); + })(rows); + } + +-function insertTLSSecurity(rows, fetchedAt, siteUuid) { ++function insertTLSSecurity(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_security +- (site_uuid, fetched_at, tls_security, color, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); + })(rows); + } + +-function insertNetBIOSHostnames(rows, fetchedAt, siteUuid) { ++function insertNetBIOSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO netbios_hostnames +- (site_uuid, fetched_at, hostname, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertDiscoveryOS(rows, fetchedAt, siteUuid) { ++function insertDiscoveryOS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO discovery_os +- (site_uuid, fetched_at, os_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); + })(rows); + } + + // ΓöÇΓöÇΓöÇ INSERT DPI 12-21 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertDHCPFingerprints(rows, fetchedAt, siteUuid) { ++function insertDHCPFingerprints(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO dhcp_fingerprints +- (site_uuid, fetched_at, fingerprint, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, fingerprint, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); + })(rows); + } + +-function insertHTTPUserAgents(rows, fetchedAt, siteUuid) { ++function insertHTTPUserAgents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO http_user_agents +- (site_uuid, fetched_at, user_agent, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, user_agent, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); + })(rows); + } + +-function insertSNIHostnames(rows, fetchedAt, siteUuid) { ++function insertSNIHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO sni_hostnames +- (site_uuid, fetched_at, sni_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, sni_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertSSLServerCN(rows, fetchedAt, siteUuid) { ++function insertSSLServerCN(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssl_server_cn +- (site_uuid, fetched_at, ssl_server_cn, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, ssl_server_cn, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + })(rows); + } + +-function insertQUICHostnames(rows, fetchedAt, siteUuid) { ++function insertQUICHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO quic_hostnames +- (site_uuid, fetched_at, quic_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, quic_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertBitTorrentHashes(rows, fetchedAt, siteUuid) { ++function insertBitTorrentHashes(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO bittorrent_hashes +- (site_uuid, fetched_at, info_hash, label, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, info_hash, label, download, upload, total) + VALUES (?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + })(rows); + } + +-function insertSSHVersions(rows, fetchedAt, siteUuid) { ++function insertSSHVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssh_versions +- (site_uuid, fetched_at, ssh_version, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, ssh_version, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); + })(rows); + } + +-function insertMDNSHostnames(rows, fetchedAt, siteUuid) { ++function insertMDNSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mdns_hostnames +- (site_uuid, fetched_at, mdns_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, mdns_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + })(rows); + } + + // ΓöÇΓöÇΓöÇ INSERT INTELLIGENCE 22-30 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertCryptoMining(rows, fetchedAt, siteUuid) { ++function insertCryptoMining(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_crypto_mining +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertDeviceDiscovery(rows, fetchedAt, siteUuid) { ++function insertDeviceDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_device_discovery +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) + VALUES (?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertEncryptionAudit(rows, fetchedAt, siteUuid) { ++function insertEncryptionAudit(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_encryption_audit +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) + VALUES (?, ?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertInsecureProtocols(rows, fetchedAt, siteUuid) { ++function insertInsecureProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_insecure_protocols +- (site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) ++ (agent_uuid, site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) + 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, ++ agentUuid, 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' + ); + })(rows); + } + +-function insertIPReputation(rows, fetchedAt, siteUuid) { ++function insertIPReputation(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_ip_reputation +- (site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) + 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, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertServerDiscovery(rows, fetchedAt, siteUuid) { ++function insertServerDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_server_discovery +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertTorDetection(rows, fetchedAt, siteUuid) { ++function insertTorDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_tor_detection +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) + VALUES (?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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); + } + +-function insertUnencryptedPasswords(rows, fetchedAt, siteUuid) { ++function insertUnencryptedPasswords(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_unencrypted_passwords +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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' + ); + })(rows); + } + +-function insertVPNDetection(rows, fetchedAt, siteUuid) { ++function insertVPNDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_vpn_detection +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); +@@ -2341,23 +1812,25 @@ function insertVPNDetection(rows, fetchedAt, siteUuid) { + + // ΓöÇΓöÇΓöÇ QUERY Intelligence ΓÇö ambil semua row tanpa batasan fetched_at ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + // (karena event ini tidak diposting ulang tiap menit, simpan kumulatif) +-function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { ++function getIntelData(table, limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); +- 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})) +- ORDER BY fetched_at DESC +- LIMIT ? +- `).all(...macs, ...macs, limit); +- } +- return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ const allowed = [ ++ 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', ++ 'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery', ++ 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection' ++ ]; ++ if (!allowed.includes(table)) return []; ++ ++ const tRows = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${buildAgentWhere(agentUuid)}`).get({agentUuid}); ++ if (!tRows?.t) return []; ++ ++ return d.prepare(` ++ SELECT * FROM ${table} ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ AND fetched_at = @fetched_at ++ LIMIT @limit ++ `).all({ fetched_at: tRows.t, limit, siteUuid, agentUuid }); + } + + function getIntelStats(siteUuid = null, agentUuid = null) { +@@ -2365,27 +1838,18 @@ function getIntelStats(siteUuid = null, agentUuid = null) { + const tables = [ + 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', + 'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery', +- 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', ++ '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(',') : ''; +- ++ const stats = {}; + for (const t of tables) { +- try { +- if (agentUuid && macs) { +- const ipField = t === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; +- counts[t] = d.prepare(` +- SELECT COUNT(*) as n FROM ${t} +- WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) +- `).get(...macs, ...macs)?.n ?? 0; +- } else { +- counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; +- } +- } catch { counts[t] = 0; } ++ const r = d.prepare(` ++ SELECT count(*) as c FROM ${t} ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ++ AND ${buildAgentWhere(agentUuid)} ++ `).get({siteUuid, agentUuid}); ++ stats[t] = r.c; + } +- return counts; ++ return stats; + } + + function getDataInterval() { +@@ -2468,7 +1932,7 @@ function syncAgentUsers(agents) { + for (const agent of agents) { + const uuid = agent.uuid; + if (!uuid) continue; +- const label = AGENT_LABELS[uuid] || agent.label || uuid; ++ const label = agent.label || uuid; + + // Create username = lowercase uuid (e.g. '1r-79-j9-ye') + const usernameUuidLower = uuid.toLowerCase(); diff --git a/diff2.txt b/diff2.txt new file mode 100644 index 0000000..8cfbb2b --- /dev/null +++ b/diff2.txt @@ -0,0 +1,855 @@ +diff --git a/backend/database.js b/backend/database.js +index 2ab6ce0..d4c2c68 100644 +--- a/backend/database.js ++++ b/backend/database.js +@@ -104,7 +104,7 @@ function initSchema() { + const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get(); + if (adminExists.count === 0) { + const hash = bcrypt.hashSync('admin123', 10); +- d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); ++ d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); + console.log('[DB] Created default admin user (admin / admin123)'); + } + +@@ -569,17 +569,17 @@ function initSchema() { + + // ΓöÇΓöÇΓöÇ INSERT FUNCTIONS ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertBandwidthApps(rows, fetchedAt, siteUuid) { ++function insertBandwidthApps(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.app_id ?? null, + r.app_label ?? 'Unknown', + r.app_tag ?? null, +@@ -594,17 +594,17 @@ function insertBandwidthApps(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertDevices(rows, fetchedAt, siteUuid) { ++function insertDevices(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.mac_address ?? null, + r.ip_address ?? null, + r.device_label ?? r.ip_address ?? 'Unknown', +@@ -619,17 +619,17 @@ function insertDevices(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertFlows(rows, fetchedAt, siteUuid) { ++function insertFlows(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, 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) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.flow_id ?? null, + r.src_ip ?? null, + r.src_mac ?? null, +@@ -647,17 +647,17 @@ function insertFlows(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertThreats(rows, fetchedAt, siteUuid) { ++function insertThreats(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.threat_id ?? null, + r.threat_type ?? null, + r.severity ?? null, +@@ -673,18 +673,18 @@ function insertThreats(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertProtocols(rows, fetchedAt, siteUuid) { ++function insertProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_protocols +- (site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) +- VALUES (?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js ΓÇö akses langsung tanpa nested + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.protocol_id ?? null, + r.protocol_label ?? 'Unknown', + r.download ?? 0, +@@ -695,18 +695,18 @@ function insertProtocols(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertCountries(rows, fetchedAt, siteUuid) { ++function insertCountries(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_countries +- (site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) +- VALUES (?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js ΓÇö akses langsung tanpa nested + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.country_code ?? null, + r.country_name ?? 'Unknown', + r.download ?? 0, +@@ -717,17 +717,17 @@ function insertCountries(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertDNS(rows, fetchedAt, siteUuid) { ++function insertDNS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO dns_queries +- (site_uuid, fetched_at, domain, query_count, app_label, category) +- VALUES (?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, domain, query_count, app_label, category) ++ VALUES (?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.domain ?? null, + r.query_count ?? 0, + r.app_label ?? null, +@@ -737,17 +737,17 @@ function insertDNS(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertEvents(rows, fetchedAt, siteUuid) { ++function insertEvents(rows, fetchedAt, siteUuid, agentUuid) { + 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) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( +- siteUuid, fetchedAt, ++ agentUuid, siteUuid, fetchedAt, + r.event_id ?? null, + r.event_type ?? null, + r.severity ?? null, +@@ -760,13 +760,14 @@ function insertEvents(rows, fetchedAt, siteUuid) { + })(rows); + } + +-function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { ++function insertBandwidthTimeline(summary, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + d.prepare(` + INSERT INTO bandwidth_timeline +- (site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) +- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ++ (agent_uuid, site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) ++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( ++ agentUuid, + siteUuid, + fetchedAt, + summary.download ?? 0, +@@ -846,8 +847,8 @@ function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { + const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); + if (!appsLatest?.t) return []; + 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 }); ++ SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: appsLatest.t, limit, siteUuid, agentUuid }); + return correlateAppLabels(rows); + } + +@@ -1093,7 +1094,7 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search + 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 ( ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND ( + ip_address LIKE @q OR + mac_address LIKE @q OR + device_label LIKE @q OR +@@ -1110,7 +1111,7 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search + 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 ( ++ WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND ( + src_ip LIKE @q OR + src_mac LIKE @q OR + app_label LIKE @q OR +@@ -1544,8 +1545,8 @@ function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { + `).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 }); ++ SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit ++ `).all({ fetched_at: latest.t, limit, siteUuid, agentUuid }); + } + + const mapped = rows.map(r => ({ +@@ -1569,7 +1570,7 @@ function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { + LIMIT ? + `).all(...macs, ...macs, limit); + } +- return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); + } + + function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { +@@ -1593,8 +1594,8 @@ function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { + const protoLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); + if (!protoLatest?.t) return []; + return d.prepare(` +- SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit +- `).all({ fetched_at: protoLatest.t, limit, siteUuid }); ++ SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: protoLatest.t, limit, siteUuid, agentUuid }); + } + + function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { +@@ -1711,8 +1712,8 @@ function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { + const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); + if (!countryLatest?.t) return []; + return d.prepare(` +- SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit +- `).all({ fetched_at: countryLatest.t, limit, siteUuid }); ++ SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit ++ `).all({ fetched_at: countryLatest.t, limit, siteUuid, agentUuid }); + } + + function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { +@@ -1736,8 +1737,8 @@ function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { + const dnsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); + if (!dnsLatest?.t) return []; + return d.prepare(` +- SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit +- `).all({ fetched_at: dnsLatest.t, limit, siteUuid }); ++ SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit ++ `).all({ fetched_at: dnsLatest.t, limit, siteUuid, agentUuid }); + } + + function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { +@@ -1752,7 +1753,7 @@ function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { + LIMIT ? + `).all(...macs, ...macs, limit); + } +- return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); + } + + function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { +@@ -1780,7 +1781,7 @@ function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { + `).all(...macs, ...macs, points).reverse(); + } + return d.prepare(` +- SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit ++ SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit + `).all({ limit: points, siteUuid }).reverse(); + } + +@@ -1849,141 +1850,141 @@ function getStats(siteUuid = null, agentUuid = null) { + // Fallback to site-wide stats if no agentUuid + const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); + const totalDevices = latestDevFetch?.t +- ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) ++ ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0) + : 0; + + const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + const activeFlows = latestFlowFetch?.t +- ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) ++ ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0) + : 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 totalThreats = d.prepare(`SELECT COUNT(*) as n FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0; ++ const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.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 latestBw = d.prepare(`SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid, agentUuid }); + + return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; + } + + // ΓöÇΓöÇΓöÇ INSERT FITUR BARU 1-11 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ +-function insertAppCategories(rows, fetchedAt, siteUuid) { ++function insertAppCategories(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO app_categories +- (site_uuid, fetched_at, category_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertContinents(rows, fetchedAt, siteUuid) { ++function insertContinents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO continents +- (site_uuid, fetched_at, continent_name, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); + })(rows); + } + +-function insertRegions(rows, fetchedAt, siteUuid) { ++function insertRegions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO regions +- (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO cities +- (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO vlans +- (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO interfaces +- (site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_types +- (site_uuid, fetched_at, flow_type_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertFlowOrigins(rows, fetchedAt, siteUuid) { ++function insertFlowOrigins(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_origins +- (site_uuid, fetched_at, flow_origin_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertIPVersions(rows, fetchedAt, siteUuid) { ++function insertIPVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ip_versions +- (site_uuid, fetched_at, ip_version_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); + })(rows); + } + +-function insertRemoteIPs(rows, fetchedAt, siteUuid) { ++function insertRemoteIPs(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO remote_ips +- (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, 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, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mac_bandwidth +- (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); + })(rows); + } + +@@ -2048,7 +2049,7 @@ function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, age + } + + // Fallback to standard site-wide select +- rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid }); ++ rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid, agentUuid }); + + // If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint + if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) { +@@ -2067,272 +2068,272 @@ function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, age + } + + // DPI Fields +-function insertTLSVersions(rows, fetchedAt, siteUuid) { ++function insertTLSVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_versions +- (site_uuid, fetched_at, tls_version, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); + })(rows); + } + +-function insertTLSCiphers(rows, fetchedAt, siteUuid) { ++function insertTLSCiphers(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_ciphers +- (site_uuid, fetched_at, tls_cipher, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); + })(rows); + } + +-function insertTLSSecurity(rows, fetchedAt, siteUuid) { ++function insertTLSSecurity(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_security +- (site_uuid, fetched_at, tls_security, color, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); + })(rows); + } + +-function insertNetBIOSHostnames(rows, fetchedAt, siteUuid) { ++function insertNetBIOSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO netbios_hostnames +- (site_uuid, fetched_at, hostname, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertDiscoveryOS(rows, fetchedAt, siteUuid) { ++function insertDiscoveryOS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO discovery_os +- (site_uuid, fetched_at, os_label, download, upload, total) ++ (agent_uuid, 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); ++ agentUuid, siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); + })(rows); + } + + // ΓöÇΓöÇΓöÇ INSERT DPI 12-21 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertDHCPFingerprints(rows, fetchedAt, siteUuid) { ++function insertDHCPFingerprints(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO dhcp_fingerprints +- (site_uuid, fetched_at, fingerprint, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, fingerprint, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); + })(rows); + } + +-function insertHTTPUserAgents(rows, fetchedAt, siteUuid) { ++function insertHTTPUserAgents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO http_user_agents +- (site_uuid, fetched_at, user_agent, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, user_agent, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); + })(rows); + } + +-function insertSNIHostnames(rows, fetchedAt, siteUuid) { ++function insertSNIHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO sni_hostnames +- (site_uuid, fetched_at, sni_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, sni_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertSSLServerCN(rows, fetchedAt, siteUuid) { ++function insertSSLServerCN(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssl_server_cn +- (site_uuid, fetched_at, ssl_server_cn, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, ssl_server_cn, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + })(rows); + } + +-function insertQUICHostnames(rows, fetchedAt, siteUuid) { ++function insertQUICHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO quic_hostnames +- (site_uuid, fetched_at, quic_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, quic_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + })(rows); + } + +-function insertBitTorrentHashes(rows, fetchedAt, siteUuid) { ++function insertBitTorrentHashes(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO bittorrent_hashes +- (site_uuid, fetched_at, info_hash, label, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, info_hash, label, download, upload, total) + VALUES (?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + })(rows); + } + +-function insertSSHVersions(rows, fetchedAt, siteUuid) { ++function insertSSHVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssh_versions +- (site_uuid, fetched_at, ssh_version, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, ssh_version, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); + })(rows); + } + +-function insertMDNSHostnames(rows, fetchedAt, siteUuid) { ++function insertMDNSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mdns_hostnames +- (site_uuid, fetched_at, mdns_hostname, download, upload, total) ++ (agent_uuid, site_uuid, fetched_at, mdns_hostname, download, upload, total) + VALUES (?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); ++ agentUuid, siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + })(rows); + } + + // ΓöÇΓöÇΓöÇ INSERT INTELLIGENCE 22-30 ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +-function insertCryptoMining(rows, fetchedAt, siteUuid) { ++function insertCryptoMining(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_crypto_mining +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertDeviceDiscovery(rows, fetchedAt, siteUuid) { ++function insertDeviceDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_device_discovery +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) + VALUES (?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertEncryptionAudit(rows, fetchedAt, siteUuid) { ++function insertEncryptionAudit(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_encryption_audit +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) + VALUES (?, ?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertInsecureProtocols(rows, fetchedAt, siteUuid) { ++function insertInsecureProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_insecure_protocols +- (site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) ++ (agent_uuid, site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) + 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, ++ agentUuid, 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' + ); + })(rows); + } + +-function insertIPReputation(rows, fetchedAt, siteUuid) { ++function insertIPReputation(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_ip_reputation +- (site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) + 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, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertServerDiscovery(rows, fetchedAt, siteUuid) { ++function insertServerDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_server_discovery +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); + })(rows); + } + +-function insertTorDetection(rows, fetchedAt, siteUuid) { ++function insertTorDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_tor_detection +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) + VALUES (?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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); + } + +-function insertUnencryptedPasswords(rows, fetchedAt, siteUuid) { ++function insertUnencryptedPasswords(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_unencrypted_passwords +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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' + ); + })(rows); + } + +-function insertVPNDetection(rows, fetchedAt, siteUuid) { ++function insertVPNDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_vpn_detection +- (site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) ++ (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) + VALUES (?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( +- siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, ++ agentUuid, 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 + ); +@@ -2357,7 +2358,7 @@ function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { + LIMIT ? + `).all(...macs, ...macs, limit); + } +- return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); ++ return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid }); + } + + function getIntelStats(siteUuid = null, agentUuid = null) { +@@ -2381,7 +2382,7 @@ function getIntelStats(siteUuid = null, agentUuid = null) { + WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).get(...macs, ...macs)?.n ?? 0; + } else { +- counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0; ++ counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0; + } + } catch { counts[t] = 0; } + } diff --git a/migrate.js b/migrate.js new file mode 100644 index 0000000..84a9607 --- /dev/null +++ b/migrate.js @@ -0,0 +1,29 @@ +const Database = require('better-sqlite3'); +const path = require('path'); +const DB_PATH = path.join('d:/FILE/Magang/Deep Package Inspection/backend/netify_data.db'); + +const db = new Database(DB_PATH); +const tables = [ + 'bandwidth_apps', 'bandwidth_protocols', 'bandwidth_countries', 'dns_queries', 'events', + 'bandwidth_timeline', 'threats', 'devices', 'flows', 'tls_versions', 'tls_ciphers', + 'tls_security', 'netbios_hostnames', 'dhcp_fingerprints', 'http_user_agents', 'sni_hostnames', + 'ssl_server_cn', 'quic_hostnames', 'bittorrent_hashes', 'ssh_versions', 'mdns_hostnames', + 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', 'intel_insecure_protocols', + 'intel_ip_reputation', 'intel_server_discovery', 'intel_tor_detection', 'intel_unencrypted_passwords', + 'intel_vpn_detection', 'app_categories', 'continents', 'regions', 'cities', 'vlans', + 'interfaces', 'flow_types', 'flow_origins', 'ip_versions', 'remote_ips', 'mac_bandwidth', 'discovery_os' +]; + +let success = 0; +for (const t of tables) { + try { + db.exec(`ALTER TABLE ${t} ADD COLUMN agent_uuid TEXT DEFAULT NULL`); + success++; + } catch (e) { + if (!e.message.includes('duplicate column name')) { + console.log(`Failed to alter ${t}:`, e.message); + } + } +} +console.log(`Migration complete! Successfully altered ${success} tables.`); +db.close();