// backend/database.js const Database = require('better-sqlite3'); const path = require('path'); const bcrypt = require('bcryptjs'); const DB_PATH = path.join(__dirname, 'netify_data.db'); let db; // Agent UUID mappings to MAC addresses and numeric interface IDs const AGENT_MAC_MAP = { '2F-TF-1D-GK': ['60:be:b4:1f:05:96'], '8A-V3-PB-85': [ 'bc:45:5b:ca:d5:be', 'de:ed:cc:57:58:34', 'aa:b2:5e:30:51:30', '76:32:c3:dd:b2:bb', '5c:ba:ef:d5:80:a1', '5a:e8:2f:f4:99:3d', '8e:91:0f:6e:24:63', '12:46:2e:63:2c:b7', '92:df:61:3e:ff:5e', '14:ea:63:96:40:78', '44:e5:17:b9:0d:07', '78:93:c3:08:41:ea', '0e:15:c3:8e:29:a8', '58:a0:23:ae:f2:72', 'f2:69:9d:a1:5e:11', '60:be:b4:2a:39:b0', 'ae:5d:99:33:67:2c' ], 'F6-2V-DT-8A': [ '2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36', '60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32', '04:f4:1c:ce:c2:e6' ], '1R-79-J9-YE': [ '70:85:6c:6d:f7:17', '70:85:6c:81:50:d4', 'a2:cc:8e:7d:39:51' ], }; const AGENT_NUMERIC_IDS = { '2F-TF-1D-GK': ['4897042839'], '8A-V3-PB-85': ['4895530456'], 'F6-2V-DT-8A': ['4894730147'], '1R-79-J9-YE': ['4895853843'], }; function getAgentTrafficRatio(agentUuid) { const d = getDB(); const macs = AGENT_MAC_MAP[agentUuid]; if (!macs || macs.length === 0) return 0; const latest = d.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get(); if (!latest?.t) return 0; const siteTotal = d.prepare("SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ?").get(latest.t)?.val || 1; const placeholders = macs.map(() => '?').join(','); const agentTotal = d.prepare(`SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders})`).get(latest.t, ...macs)?.val || 0; return siteTotal > 0 ? (agentTotal / siteTotal) : 0; } function getDB() { if (!db) { db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('synchronous = NORMAL'); initSchema(); } return db; } function initSchema() { const d = getDB(); // ─── AUTHENTICATION ───────────────────────────────────────────────────────── d.exec(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'AGENT_VIEWER', site_uuid TEXT DEFAULT NULL, agent_uuid TEXT DEFAULT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP )`); // Alter users table to add agent_uuid if it was created on an older schema try { d.exec("ALTER TABLE users ADD COLUMN agent_uuid TEXT DEFAULT NULL"); } catch (e) { // Column already exists, safe to ignore } // Alter bandwidth_timeline table to add new speed columns dynamically if they do not exist try { d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN download_speed INTEGER DEFAULT 0"); } catch (_) {} try { d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN upload_speed INTEGER DEFAULT 0"); } catch (_) {} try { d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN flow_speed INTEGER DEFAULT 0"); } catch (_) {} 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'); console.log('[DB] Created default admin user (admin / admin123)'); } const agentExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent1'").get(); if (agentExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); const site_uuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e'; d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent1', hash, 'AGENT_VIEWER', site_uuid, '8A-V3-PB-85'); console.log('[DB] Created default agent user (agent1 / agent123)'); } // Seed specific agent accounts for testing isolation const agentJrpExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_jrp'").get(); if (agentJrpExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_jrp', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '2F-TF-1D-GK'); console.log('[DB] Created JRP Cibubur agent user (agent_jrp / agent123)'); } const agentIfgExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_ifg'").get(); if (agentIfgExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_ifg', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '8A-V3-PB-85'); console.log('[DB] Created IFG LT.18 agent user (agent_ifg / agent123)'); } const agentCpiExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_cpi'").get(); if (agentCpiExists.count === 0) { const hash = bcrypt.hashSync('agent123', 10); d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_cpi', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', 'F6-2V-DT-8A'); console.log('[DB] Created CPI Balaraja agent user (agent_cpi / agent123)'); } d.exec(`CREATE TABLE IF NOT EXISTS tls_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, tls_version TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS tls_ciphers ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, tls_cipher TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS tls_security ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, tls_security TEXT, color TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS netbios_hostnames ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, hostname TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); // ─── DPI Fields 12-21 ────────────────────────────────────────────────────── d.exec(`CREATE TABLE IF NOT EXISTS dhcp_fingerprints ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, fingerprint TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS http_user_agents ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, user_agent TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS sni_hostnames ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, sni_hostname TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS ssl_server_cn ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, ssl_server_cn TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS quic_hostnames ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, quic_hostname TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS bittorrent_hashes ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, info_hash TEXT, label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS ssh_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, ssh_version TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS mdns_hostnames ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, mdns_hostname TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); // ─── Intelligence API 22-30 ──────────────────────────────────────────────── d.exec(`CREATE TABLE IF NOT EXISTS intel_crypto_mining ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, pool_host TEXT, pool_ip TEXT, protocol TEXT, app_label TEXT, confidence REAL, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_device_discovery ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, device_label TEXT, device_type TEXT, os_label TEXT, manufacturer TEXT, is_new INTEGER DEFAULT 1 )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_encryption_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, device_label TEXT, encrypted_pct REAL, unencrypted INTEGER DEFAULT 0, encrypted INTEGER DEFAULT 0, total INTEGER DEFAULT 0, risk_level TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_insecure_protocols ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, protocol TEXT, ip_address TEXT, mac_address TEXT, dst_ip TEXT, dst_port INTEGER, app_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, risk TEXT, source TEXT DEFAULT 'api' )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_ip_reputation ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, local_ip TEXT, mac_address TEXT, reputation TEXT, score REAL, country TEXT, app_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, blacklisted INTEGER DEFAULT 1 )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_server_discovery ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, server_type TEXT, hostname TEXT, port INTEGER, protocol TEXT, os_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_tor_detection ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, exit_node TEXT, circuit_id TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, country TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_unencrypted_passwords ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, dst_ip TEXT, dst_port INTEGER, protocol TEXT, username TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, severity TEXT DEFAULT 'Critical' )`); d.exec(`CREATE TABLE IF NOT EXISTS intel_vpn_detection ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, detected_at TEXT, ip_address TEXT, mac_address TEXT, vpn_type TEXT, remote_ip TEXT, protocol TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, country TEXT, confidence REAL )`); d.exec(`CREATE TABLE IF NOT EXISTS discovery_os ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, os_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); // Tabel baru fitur 1-11 d.exec(`CREATE TABLE IF NOT EXISTS app_categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, category_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS continents ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, continent_name TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS regions ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, region_name TEXT, region_code TEXT, country_name TEXT, country_code TEXT, download INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS cities ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, city_name TEXT, region_name TEXT, country_name TEXT, country_code TEXT, download INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS vlans ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, vlan_id INTEGER, vlan_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS interfaces ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, iface_id INTEGER, iface_name TEXT, iface_role TEXT, agent_id TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS flow_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, flow_type_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS flow_origins ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, flow_origin_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS ip_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, ip_version_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS remote_ips ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, remote_ip TEXT, ip_version INTEGER, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS mac_bandwidth ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, mac_address TEXT, manufacturer TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_apps ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, app_id INTEGER, app_label TEXT, app_tag TEXT, category TEXT, favicon TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, total INTEGER DEFAULT 0, flow_count INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, mac_address TEXT, ip_address TEXT, device_label TEXT, device_type TEXT, os_label TEXT, manufacturer TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, last_seen TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS flows ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, flow_id TEXT, src_ip TEXT, src_mac TEXT, dst_ip TEXT, dst_port INTEGER, protocol TEXT, app_label TEXT, domain TEXT, bytes_download INTEGER DEFAULT 0, bytes_upload INTEGER DEFAULT 0, first_seen TEXT, last_seen TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS threats ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, threat_id TEXT, threat_type TEXT, severity TEXT, mac_address TEXT, ip_address TEXT, dst_ip TEXT, app_label TEXT, domain TEXT, description TEXT, detected_at TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_protocols ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, protocol_id INTEGER, protocol_label TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, flow_count INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_countries ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, country_code TEXT, country_name TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, flow_count INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS dns_queries ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, domain TEXT, query_count INTEGER DEFAULT 0, app_label TEXT, category TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, event_id TEXT, event_type TEXT, severity TEXT, mac_address TEXT, ip_address TEXT, description TEXT, event_at TEXT )`); d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_timeline ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_uuid TEXT, fetched_at TEXT NOT NULL, total_download INTEGER DEFAULT 0, total_upload INTEGER DEFAULT 0, total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0, download_speed INTEGER DEFAULT 0, upload_speed INTEGER DEFAULT 0, flow_speed INTEGER DEFAULT 0 )`); d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache ( ip_address TEXT PRIMARY KEY, isp TEXT, country TEXT, city TEXT, as_org TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )`); console.log('[DB] Schema siap.'); } // ─── INSERT FUNCTIONS ───────────────────────────────────────────────────────── function insertBandwidthApps(rows, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.app_id ?? null, r.app_label ?? 'Unknown', r.app_tag ?? null, r.category ?? null, r.favicon ?? null, r.download ?? 0, r.upload ?? 0, r.total ?? (r.download ?? 0) + (r.upload ?? 0), r.flows ?? 0 ); } })(rows); } function insertDevices(rows, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.mac_address ?? null, r.ip_address ?? null, r.device_label ?? r.ip_address ?? 'Unknown', r.device_type ?? null, r.os_label ?? null, r.manufacturer ?? null, r.download ?? 0, r.upload ?? 0, r.last_seen ?? fetchedAt ); } })(rows); } function insertFlows(rows, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.flow_id ?? null, r.src_ip ?? null, r.src_mac ?? null, r.dst_ip ?? null, r.dst_port ?? null, r.protocol ?? null, r.app_label ?? null, r.domain ?? null, r.download ?? 0, r.upload ?? 0, r.first_seen ?? fetchedAt, r.last_seen ?? fetchedAt ); } })(rows); } function insertThreats(rows, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.threat_id ?? null, r.threat_type ?? null, r.severity ?? null, r.mac_address ?? null, r.ip_address ?? null, r.dst_ip ?? null, r.app_label ?? null, r.domain ?? null, r.description ?? null, r.detected_at ?? fetchedAt ); } })(rows); } function insertProtocols(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_protocols (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, r.protocol_id ?? null, r.protocol_label ?? 'Unknown', r.download ?? 0, r.upload ?? 0, r.flows ?? 0 ); } })(rows); } function insertCountries(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO bandwidth_countries (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, r.country_code ?? null, r.country_name ?? 'Unknown', r.download ?? 0, r.upload ?? 0, r.flows ?? 0 ); } })(rows); } function insertDNS(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(` INSERT INTO dns_queries (site_uuid, fetched_at, domain, query_count, app_label, category) VALUES (?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.domain ?? null, r.query_count ?? 0, r.app_label ?? null, r.category ?? null ); } })(rows); } function insertEvents(rows, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?) `); d.transaction((items) => { for (const r of items) { stmt.run( siteUuid, fetchedAt, r.event_id ?? null, r.event_type ?? null, r.severity ?? null, r.mac_address ?? null, r.ip_address ?? null, r.description ?? null, r.event_at ?? fetchedAt ); } })(rows); } function insertBandwidthTimeline(summary, fetchedAt, siteUuid) { 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 (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( siteUuid, fetchedAt, summary.download ?? 0, summary.upload ?? 0, summary.flows ?? 0, summary.devices ?? 0, summary.download_speed ?? 0, summary.upload_speed ?? 0, summary.flow_speed ?? 0 ); } // ─── QUERY FUNCTIONS ────────────────────────────────────────────────────────── 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 rows = d.prepare(` SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit `).all({ fetched_at: appsLatest.t, limit, siteUuid }); return correlateAppLabels(rows); } function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) { let label = dbLabel || ip; let manufacturer = dbManufacturer || 'Unknown'; let type = dbType || 'Generic Client'; let os = 'Unknown'; if (manufacturer.includes('Routerboard') || manufacturer.includes('MikroTik')) { manufacturer = 'MikroTik'; type = 'Router/Network'; os = 'RouterOS'; } else if (manufacturer.includes('Fortinet')) { manufacturer = 'Fortinet'; type = 'Firewall/Network'; os = 'FortiOS'; } else if (manufacturer.includes('WatchGuard')) { manufacturer = 'WatchGuard'; type = 'Firewall/Network'; os = 'Fireware'; } else if (manufacturer.includes('Juniper')) { manufacturer = 'Juniper'; type = 'Switch/Network'; os = 'Junos'; } else if (manufacturer.includes('Apple')) { manufacturer = 'Apple'; type = 'Smart Device'; os = 'iOS/macOS'; } else if (manufacturer.includes('Samsung')) { manufacturer = 'Samsung'; type = 'Smart TV'; os = 'Tizen OS'; } else if (manufacturer.includes('LCFC') || manufacturer.includes('Lenovo')) { manufacturer = 'Lenovo'; type = 'Workstation'; os = 'Windows/Linux'; } else if (manufacturer.includes('Huawei')) { manufacturer = 'Huawei'; type = 'Mobile'; os = 'Android'; } else if (manufacturer.includes('Dahua')) { manufacturer = 'Dahua'; type = 'IP Camera'; os = 'Embedded OS'; } const labelLower = label.toLowerCase(); if (labelLower.includes('windows') || labelLower.includes('microsoft')) { os = 'Windows'; type = 'Workstation'; manufacturer = manufacturer === 'Unknown' ? 'Microsoft' : manufacturer; } else if (labelLower.includes('apple') || labelLower.includes('iphone') || labelLower.includes('ipad') || labelLower.includes('mac')) { os = labelLower.includes('mac') ? 'macOS' : 'Apple iOS'; type = labelLower.includes('mac') ? 'Workstation' : 'Mobile'; manufacturer = 'Apple'; } else if (labelLower.includes('android') || labelLower.includes('oppo') || labelLower.includes('samsung phone')) { os = 'Android'; type = 'Mobile'; if (labelLower.includes('oppo')) manufacturer = 'Oppo'; if (labelLower.includes('samsung')) manufacturer = 'Samsung'; } else if (labelLower.includes('samsung tv') || labelLower.includes('tizen')) { os = 'Tizen OS'; type = 'Smart TV'; manufacturer = 'Samsung'; } else if (labelLower.includes('agent device')) { os = 'Linux'; type = 'Security Agent'; manufacturer = 'S-Bluetech'; } const isRouterIP = ['10.6.50.25', '10.6.12.242', '192.168.9.1', '10.6.11.208', '10.6.10.4'].includes(ip); if (type === 'Router/Network' && !isRouterIP) { type = 'LAN Client'; manufacturer = 'Unknown'; os = 'Windows/Linux'; } return { label, manufacturer, type, os }; } function deviceMatchesAgent(ip, mac, agentUuid) { if (!agentUuid) return true; const macs = AGENT_MAC_MAP[agentUuid]; if (!macs) return false; // 1. Direct MAC check if (macs.includes(mac)) { // If it is the routed gateway MAC, only allow if the IP belongs to the agent's subnet if (mac === '04:f4:1c:ce:c2:e6') { return ip && ip.startsWith('10.6.'); } return true; } // 2. Subnet checks for client IPs if (ip) { if (agentUuid === '8A-V3-PB-85') { return ip.startsWith('10.250.0.'); } if (agentUuid === 'F6-2V-DT-8A') { return (ip.startsWith('10.250.') && !ip.startsWith('10.250.0.')) || ip.startsWith('192.168.') || ip.startsWith('10.121.') || ip.startsWith('10.6.'); } if (agentUuid === '2F-TF-1D-GK') { return ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || ip.startsWith('10.93.'); } if (agentUuid === '1R-79-J9-YE') { return ip.startsWith('192.168.201.'); } } return false; } function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) { const d = getDB(); const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); if (!latest?.t) return []; 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); } function correlateFlows(flows) { if (!Array.isArray(flows) || flows.length === 0) return flows; const d = getDB(); // 1. Build cache maps for local IPs and public IPs in history const devices = d.prepare(` SELECT ip_address, device_label, manufacturer, device_type FROM devices WHERE ip_address IS NOT NULL `).all(); const devMap = new Map(); for (const dev of devices) { const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); if (label) { devMap.set(dev.ip_address, label); } } const flowIPs = d.prepare(` SELECT dst_ip, domain, app_label, COUNT(*) as count FROM flows WHERE dst_ip IS NOT NULL AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) GROUP BY dst_ip, domain, app_label ORDER BY count DESC `).all(); const publicIpMap = new Map(); for (const row of flowIPs) { if (!publicIpMap.has(row.dst_ip)) { publicIpMap.set(row.dst_ip, { domain: row.domain, app_label: row.app_label }); } } // Matches map for standard ports const matches = { "1433": "MSSQL Database Server", "1434": "MSSQL Monitor Server", "3306": "MySQL/MariaDB", "5432": "PostgreSQL", "1521": "Oracle DB Server", "27017": "MongoDB", "6379": "Redis Cache", "80": "HTTP Web Server", "443": "HTTPS/TLS Secure Connection", "22": "SSH Remote Management", "21": "FTP File Storage", "23": "Telnet Command Insecure", "25": "SMTP Mail Delivery", "587": "Secure SMTP Mail", "110": "POP3 Mail Retrieval", "993": "Secure IMAP Mail", "53": "DNS Domain Directory Query", "123": "NTP Network Time", "161": "SNMP Monitoring Service", "3389": "RDP Remote Windows Desktop", "445": "SMB Windows File Share", "137": "NetBIOS Name Service", "138": "NetBIOS Datagram Service", "139": "NetBIOS Session Service", "1812": "RADIUS Auth Server", "1813": "RADIUS Accounting", "5060": "SIP VoIP Service" }; return flows.map(f => { let appLabel = f.app_label; let domain = f.domain; const isPortLabel = !appLabel || appLabel.startsWith("Port ") || appLabel.toLowerCase().includes("port"); if (isPortLabel) { const dstIp = f.dst_ip; const dstPort = String(f.dst_port); const proto = f.protocol || "TCP"; // Case A: Intranet IP const isIntranet = dstIp && ( dstIp.startsWith("10.") || dstIp.startsWith("192.168.") || dstIp.startsWith("172.16.") || dstIp.startsWith("172.17.") || dstIp.startsWith("172.18.") || dstIp.startsWith("172.19.") || dstIp.startsWith("172.20.") || dstIp.startsWith("172.21.") || dstIp.startsWith("172.22.") || dstIp.startsWith("172.23.") || dstIp.startsWith("172.24.") || dstIp.startsWith("172.25.") || dstIp.startsWith("172.26.") || dstIp.startsWith("172.27.") || dstIp.startsWith("172.28.") || dstIp.startsWith("172.29.") || dstIp.startsWith("172.30.") || dstIp.startsWith("172.31.") ); if (isIntranet) { let friendlyName = devMap.get(dstIp); if (!friendlyName) { if (dstIp.startsWith("10.250.0.")) { friendlyName = "IFG Client"; } else if (dstIp.startsWith("10.6.") || (dstIp.startsWith("10.250.") && !dstIp.startsWith("10.250.0.")) || dstIp.startsWith("192.168.") || dstIp.startsWith("10.121.")) { friendlyName = "CPI Client"; } else if ( dstIp.startsWith("10.0.") || dstIp.startsWith("10.1.") || dstIp.startsWith("10.26.") || dstIp.startsWith("10.43.") || dstIp.startsWith("10.35.") || dstIp.startsWith("10.21.") || dstIp.startsWith("10.7.") || dstIp.startsWith("10.182.") || dstIp.startsWith("10.109.") || dstIp.startsWith("10.181.") || dstIp.startsWith("10.75.") || dstIp.startsWith("10.202.") || dstIp.startsWith("10.93.") ) { friendlyName = "JRP Client"; } else { friendlyName = "Intranet Client"; } } appLabel = `${friendlyName} (${dstIp})`; domain = `Port ${dstPort} (${proto})`; } else { // Case B: Public IP const cached = publicIpMap.get(dstIp); if (cached) { appLabel = cached.domain || cached.app_label || appLabel; domain = `Port ${dstPort} (${proto})`; } else { const stdName = matches[dstPort]; if (stdName) { appLabel = stdName; domain = `Port ${dstPort} (${proto})`; } else { appLabel = `Public IP: ${dstIp}`; domain = `Port ${dstPort} (${proto})`; } } } } return { ...f, app_label: appLabel, domain: domain }; }); } function correlateAppLabels(apps) { if (!Array.isArray(apps) || apps.length === 0) return apps; const d = getDB(); const devices = d.prepare(` SELECT ip_address, device_label, manufacturer, device_type FROM devices WHERE ip_address IS NOT NULL `).all(); const devMap = new Map(); for (const dev of devices) { const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); if (label) { devMap.set(dev.ip_address, label); } } const flowIPs = d.prepare(` SELECT dst_ip, domain, app_label, COUNT(*) as count FROM flows WHERE dst_ip IS NOT NULL AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) GROUP BY dst_ip, domain, app_label ORDER BY count DESC `).all(); const publicIpMap = new Map(); for (const row of flowIPs) { if (!publicIpMap.has(row.dst_ip)) { publicIpMap.set(row.dst_ip, { domain: row.domain, app_label: row.app_label }); } } function getFriendlyIpName(ip) { if (devMap.has(ip)) return devMap.get(ip); if (publicIpMap.has(ip)) { const pub = publicIpMap.get(ip); return pub.domain || pub.app_label; } if (ip.startsWith('10.6.')) return 'IFG Client'; if (ip.startsWith('10.250.') || ip.startsWith('192.168.') || ip.startsWith('10.121.')) return 'CPI Client'; if ( ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || ip.startsWith('10.93.') ) return 'JRP Client'; return 'Intranet Client'; } const matches = { "1433": "MSSQL Database Server", "1434": "MSSQL Monitor Server", "3306": "MySQL/MariaDB", "5432": "PostgreSQL", "1521": "Oracle DB Server", "27017": "MongoDB", "6379": "Redis Cache", "80": "HTTP Web Server", "443": "HTTPS/TLS Secure Connection", "22": "SSH Remote Management", "21": "FTP File Storage", "23": "Telnet Command Insecure", "25": "SMTP Mail Delivery", "587": "Secure SMTP Mail", "110": "POP3 Mail Retrieval", "993": "Secure IMAP Mail", "53": "DNS Domain Directory Query", "123": "NTP Network Time", "161": "SNMP Monitoring Service", "3389": "RDP Remote Windows Desktop", "445": "SMB Windows File Share", "137": "NetBIOS Name Service", "138": "NetBIOS Datagram Service", "139": "NetBIOS Session Service", "1812": "RADIUS Auth Server", "1813": "RADIUS Accounting", "5060": "SIP VoIP Service" }; return apps.map(app => { let label = app.app_label; if (!label) return app; const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port"); if (isPortLabel) { const portStr = label.replace("Port ", "").trim(); const flow = d.prepare(` SELECT dst_ip, protocol, dst_port FROM flows WHERE app_label = ? OR domain = ? OR dst_port = ? GROUP BY dst_ip, protocol, dst_port ORDER BY COUNT(*) DESC LIMIT 1 `).get(label, label, portStr); if (flow && flow.dst_ip) { const friendlyName = getFriendlyIpName(flow.dst_ip); label = `${friendlyName} (Port ${portStr})`; } else { const stdName = matches[portStr]; if (stdName) { label = `${stdName} (Port ${portStr})`; } } } return { ...app, app_label: label }; }); } function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { const d = getDB(); const 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); } function getLatestThreats(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 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 }); } 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 []; 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 }); } 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 []; 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 }); } 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 []; 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 }); } 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 }); } function getBandwidthTimeline(points = 60, 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(); } function getStats(siteUuid = null, agentUuid = null) { const d = getDB(); 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 }; } // 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 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 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 }); return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; } // ─── INSERT FITUR BARU 1-11 ─────────────────────────────────────────────────── function insertAppCategories(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO app_categories (site_uuid, fetched_at, category_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); })(rows); } function insertContinents(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO continents (site_uuid, fetched_at, continent_name, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); })(rows); } function insertRegions(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO regions (site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); })(rows); } function insertCities(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO cities (site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); })(rows); } function insertVLANs(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO vlans (site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); })(rows); } function insertInterfaces(rows, fetchedAt, siteUuid) { 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 (?, ?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); })(rows); } function insertFlowTypes(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO flow_types (site_uuid, fetched_at, flow_type_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); })(rows); } function insertFlowOrigins(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO flow_origins (site_uuid, fetched_at, flow_origin_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); })(rows); } function insertIPVersions(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ip_versions (site_uuid, fetched_at, ip_version_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); })(rows); } function insertRemoteIPs(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO remote_ips (site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); })(rows); } function insertMACBandwidth(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO mac_bandwidth (site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); })(rows); } // ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── function getLatest(table, orderBy = 'download', limit = 50, 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; } // DPI Fields function insertTLSVersions(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_versions (site_uuid, fetched_at, tls_version, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); })(rows); } function insertTLSCiphers(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_ciphers (site_uuid, fetched_at, tls_cipher, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); })(rows); } function insertTLSSecurity(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO tls_security (site_uuid, fetched_at, tls_security, color, download, upload, total) VALUES (?, ?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); })(rows); } function insertNetBIOSHostnames(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO netbios_hostnames (site_uuid, fetched_at, hostname, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); })(rows); } function insertDiscoveryOS(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO discovery_os (site_uuid, fetched_at, os_label, download, upload, total) VALUES (?, ?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); })(rows); } // ─── INSERT DPI 12-21 ──────────────────────────────────────────────────────── function insertDHCPFingerprints(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO dhcp_fingerprints (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); })(rows); } function insertHTTPUserAgents(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO http_user_agents (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); })(rows); } function insertSNIHostnames(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO sni_hostnames (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); })(rows); } function insertSSLServerCN(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ssl_server_cn (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); })(rows); } function insertQUICHostnames(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO quic_hostnames (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); })(rows); } function insertBitTorrentHashes(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO bittorrent_hashes (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); })(rows); } function insertSSHVersions(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO ssh_versions (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); })(rows); } function insertMDNSHostnames(rows, fetchedAt, siteUuid) { const d = getDB(); const stmt = d.prepare(`INSERT INTO mdns_hostnames (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); })(rows); } // ─── INSERT INTELLIGENCE 22-30 ──────────────────────────────────────────────── function insertCryptoMining(rows, fetchedAt, siteUuid) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?,?)`); 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, 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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) { 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 (?, ?,?,?,?,?,?,?,?,?,?,?)`); d.transaction(items => { for (const r of items) stmt.run( 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 ); })(rows); } // ─── 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) { 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 }); } function getIntelStats(siteUuid = null, agentUuid = null) { const d = getDB(); 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', ]; const counts = {}; const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null; const placeholders = macs ? macs.map(() => '?').join(',') : ''; 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; } } return counts; } function getDataInterval() { const d = getDB(); const row = d.prepare("SELECT MIN(fetched_at) as start_t, MAX(fetched_at) as end_t FROM devices").get(); return { start: row?.start_t || null, end: row?.end_t || null }; } module.exports = { getUserByUsername, updateUserPassword, syncAgentUsers, getDB, getDataInterval, insertBandwidthApps, insertDevices, insertFlows, insertThreats, insertProtocols, insertCountries, insertDNS, insertEvents, insertBandwidthTimeline, getLatestBandwidthApps, getLatestDevices, getLatestFlows, getLatestThreats, getLatestProtocols, getLatestCountries, getLatestDNS, getLatestEvents, getBandwidthTimeline, getStats, // Insert baru insertAppCategories, insertContinents, insertRegions, insertCities, insertVLANs, insertInterfaces, insertFlowTypes, insertFlowOrigins, insertIPVersions, insertRemoteIPs, insertMACBandwidth, // Query helper getLatest, insertTLSVersions, insertTLSCiphers, insertTLSSecurity, insertNetBIOSHostnames, insertDiscoveryOS, // DPI 12-21 insertDHCPFingerprints, insertHTTPUserAgents, insertSNIHostnames, insertSSLServerCN, insertQUICHostnames, insertBitTorrentHashes, insertSSHVersions, insertMDNSHostnames, // Intelligence 22-30 insertCryptoMining, insertDeviceDiscovery, insertEncryptionAudit, insertInsecureProtocols, insertIPReputation, insertServerDiscovery, insertTorDetection, insertUnencryptedPasswords, insertVPNDetection, getIntelData, getIntelStats, }; // ─── AUTHENTICATION ───────────────────────────────────────────────────────── function getUserByUsername(username) { return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username); } function updateUserPassword(userId, newPasswordHash) { return getDB().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newPasswordHash, userId); } function syncAgentUsers(agents) { const d = getDB(); const bcrypt = require('bcryptjs'); const hash = bcrypt.hashSync('agent123', 10); const siteUuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e'; // Hardcoded labels map for friendly user mapping const AGENT_LABELS = { '2F-TF-1D-GK': 'JRP Cibubur', '8A-V3-PB-85': 'IFG LT.18', 'F6-2V-DT-8A': 'CPI Balaraja', '1R-79-J9-YE': 'CPI Balaraja WAN' }; for (const agent of agents) { const uuid = agent.uuid; if (!uuid) continue; const label = AGENT_LABELS[uuid] || agent.label || uuid; // Create username = lowercase uuid (e.g. '1r-79-j9-ye') const usernameUuidLower = uuid.toLowerCase(); const exists1 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameUuidLower); if (exists1.count === 0) { d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") .run(usernameUuidLower, hash, 'AGENT_VIEWER', siteUuid, uuid); console.log(`[DB] Created default user: ${usernameUuidLower} / agent123`); } // Create username = uppercase uuid (e.g. '1R-79-J9-YE') const usernameUuidUpper = uuid.toUpperCase(); const exists1u = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameUuidUpper); if (exists1u.count === 0) { d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") .run(usernameUuidUpper, hash, 'AGENT_VIEWER', siteUuid, uuid); console.log(`[DB] Created default user: ${usernameUuidUpper} / agent123`); } // Create username = sanitized lowercase label (e.g. 'agent_cpi_balaraja_wan') const sanitizedLabel = label.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_'); const usernameLabel = sanitizedLabel.startsWith('agent_') ? sanitizedLabel : `agent_${sanitizedLabel}`; const exists2 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameLabel); if (exists2.count === 0) { d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") .run(usernameLabel, hash, 'AGENT_VIEWER', siteUuid, uuid); console.log(`[DB] Created default user: ${usernameLabel} / agent123`); } // Create username = name/label directly (e.g. 'CPI Balaraja WAN' -> 'CPI Balaraja WAN' or 'JRP Cibubur') const exists3 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(label); if (exists3.count === 0) { d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") .run(label, hash, 'AGENT_VIEWER', siteUuid, uuid); console.log(`[DB] Created default user: ${label} / agent123`); } } }