From 53008983b3d09d70be3f905692817824bad95432 Mon Sep 17 00:00:00 2001 From: vanne Date: Thu, 2 Jul 2026 00:42:17 +0700 Subject: [PATCH] feat: implement dynamic agent detection, automatic default account seeding, and strict data isolation filtering --- backend/database.js | 64 +++++++++++++++++++- backend/netify.js | 13 ++++ backend/scheduler.js | 11 ++++ backend/tests/test_dynamic_agents_users.js | 69 ++++++++++++++++++++++ src/app/(dashboard)/agents/page.tsx | 1 + src/components/layout/Header.tsx | 1 + src/lib/actions/agents.ts | 66 ++++++++++++++++----- 7 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 backend/tests/test_dynamic_agents_users.js diff --git a/backend/database.js b/backend/database.js index c7c4ec4..4d9085c 100644 --- a/backend/database.js +++ b/backend/database.js @@ -16,12 +16,14 @@ const AGENT_MAC_MAP = { '70:85:6c:81:50:d4', '70:85:6c:6d:f7:17', '60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32' ], + '1R-79-J9-YE': ['a2:cc:8e:7d:39:51', 'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'], }; const AGENT_NUMERIC_IDS = { - '2F-TF-1D-GK': ['4894730147'], + '2F-TF-1D-GK': ['4897042839'], '8A-V3-PB-85': ['4895530456'], - 'F6-2V-DT-8A': ['4895853843', '4897042839'], + 'F6-2V-DT-8A': ['4894730147'], + '1R-79-J9-YE': ['4895853843'], }; function getAgentTrafficRatio(agentUuid) { @@ -2383,6 +2385,7 @@ function getDataInterval() { module.exports = { getUserByUsername, updateUserPassword, + syncAgentUsers, getDB, getDataInterval, insertBandwidthApps, insertDevices, insertFlows, insertThreats, @@ -2417,3 +2420,60 @@ function getUserByUsername(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`); + } + } +} diff --git a/backend/netify.js b/backend/netify.js index 517fad0..a695f86 100644 --- a/backend/netify.js +++ b/backend/netify.js @@ -1194,6 +1194,7 @@ 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', }; // Maps each agent UUID to its gateway/interface MAC address(es) in flows @@ -1202,6 +1203,7 @@ const AGENT_MAC_MAP = { '8A-V3-PB-85': ['04:f4:1c:ce:c2:e6'], 'F6-2V-DT-8A': ['2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be', 'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36'], + '1R-79-J9-YE': ['a2:cc:8e:7d:39:51', 'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'], }; // Build SQL IN clause placeholders @@ -2078,6 +2080,7 @@ async function fetchSecurityDevices(siteUuid = null, agentUuid = null) { module.exports = { fetchLookupApplications, + fetchAgents, fetchTopApps, fetchTopDevices, fetchTopProtocols, fetchTopCountries, fetchTopDomains, fetchBandwidthSummary, fetchDiscoveredDevices, fetchCyberThreats, fetchFlows, fetchEvents, @@ -2810,4 +2813,14 @@ async function fetchVPNDetection(limit = 50) { } return results.slice(0, limit); +} + +async function fetchAgents() { + const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }); + if (!data || !Array.isArray(data)) return []; + return data.map(r => ({ + id: r.agent?.id, + uuid: r.agent?.uuid, + label: r.agent?.label, + })); } \ No newline at end of file diff --git a/backend/scheduler.js b/backend/scheduler.js index bb559d2..55cd04a 100644 --- a/backend/scheduler.js +++ b/backend/scheduler.js @@ -16,6 +16,17 @@ async function runPoll() { console.log(`[Scheduler] Mulai polling... (${fetchedAt})`); try { + // 0. Sync agents and seed default user accounts dynamically + try { + const apiAgents = await netify.fetchAgents(); + if (apiAgents && apiAgents.length > 0) { + db.syncAgentUsers(apiAgents); + console.log(`[Scheduler] OK Sync Agents : ${apiAgents.length} agen terdeteksi`); + } + } catch (err) { + console.error('[Scheduler] Gagal sync agent users:', err.message); + } + // 1. Top Aplikasi const apps = await netify.fetchTopApps(1440, 20); if (apps && Array.isArray(apps)) { diff --git a/backend/tests/test_dynamic_agents_users.js b/backend/tests/test_dynamic_agents_users.js new file mode 100644 index 0000000..f96628b --- /dev/null +++ b/backend/tests/test_dynamic_agents_users.js @@ -0,0 +1,69 @@ +const Database = require('better-sqlite3'); +const path = require('path'); +const bcrypt = require('bcryptjs'); + +const dbPath = path.join(__dirname, '../netify_data.db'); +const db = new Database(dbPath); + +console.log("=== STARTING TDD TEST FOR DYNAMIC AGENTS & USERS ==="); + +// 1. Mock agents list returned from API +const mockAgents = [ + { id: 4897042839, uuid: "2F-TF-1D-GK", label: "2F-TF-1D-GK" }, + { id: 4895530456, uuid: "8A-V3-PB-85", label: "8A-V3-PB-85" }, + { id: 4894730147, uuid: "F6-2V-DT-8A", label: "F6-2V-DT-8A" }, + { id: 4895853843, uuid: "1R-79-J9-YE", label: "1R-79-J9-YE" } // New agent! +]; + +// 2. Call syncAgentUsers to dynamically seed users +const dbModule = require('../database'); +console.log("- Running syncAgentUsers..."); +dbModule.syncAgentUsers(mockAgents); + +// 3. Verify users are created +const expectedUsernames = [ + '1r-79-j9-ye', + '1R-79-J9-YE', + 'agent_cpi_balaraja_wan', + '2f-tf-1d-gk', + '2F-TF-1D-GK', + 'agent_jrp_cibubur' +]; + +for (const u of expectedUsernames) { + const user = dbModule.getUserByUsername(u); + if (!user) { + throw new Error(`❌ TEST FAILED: User '${u}' was not created!`); + } + console.log(`✅ User verified: '${u}' (Role: ${user.role}, Agent UUID: ${user.agent_uuid})`); + + // Verify password matches agent123 + const pwOk = bcrypt.compareSync('agent123', user.password_hash); + if (!pwOk) { + throw new Error(`❌ TEST FAILED: Password for user '${u}' is incorrect!`); + } +} + +// 4. Verify data scoping/isolation for 1R-79-J9-YE +console.log("- Verifying data scoping/isolation for 1R-79-J9-YE..."); +const agentUuid = '1R-79-J9-YE'; + +// A. Check flows count +const flows = dbModule.getLatestFlows(100, null, agentUuid); +console.log(` Scoped flows count: ${flows.length}`); +for (const f of flows) { + if (!['a2:cc:8e:7d:39:51', 'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'].includes(f.src_mac)) { + throw new Error(`❌ TEST FAILED: Flow src_mac '${f.src_mac}' leaked into 1R-79-J9-YE scope!`); + } +} + +// B. Check devices count +const devices = dbModule.getLatestDevices(100, null, agentUuid); +console.log(` Scoped devices count: ${devices.length}`); + +// C. Check countries stats +const countries = dbModule.getLatestCountries(10, null, agentUuid); +console.log(` Scoped countries count: ${countries.length}`); + +console.log("=== ALL DYNAMIC AGENT AND USER TESTS PASSED! ==="); +process.exit(0); diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx index 5a9987d..6fd8e32 100644 --- a/src/app/(dashboard)/agents/page.tsx +++ b/src/app/(dashboard)/agents/page.tsx @@ -30,6 +30,7 @@ export default function AgentsPage() { '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', }; // Form state diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 420dfd5..73e4247 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -9,6 +9,7 @@ const AGENT_LABELS: Record = { '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', }; interface AppNotification { diff --git a/src/lib/actions/agents.ts b/src/lib/actions/agents.ts index 261f1f9..ba1cb88 100644 --- a/src/lib/actions/agents.ts +++ b/src/lib/actions/agents.ts @@ -59,6 +59,36 @@ export async function getAgents(): Promise { const dbPath = path.join(process.cwd(), 'backend', 'netify_data.db'); const db = new Database(dbPath); + // Query the Informatics API dynamically for all active agents + let apiAgents: any[] = []; + try { + const res = await fetch("https://informatics.netify.ai/api/v1/data/stats/top/agent/download?filter_interval=43200&settings_limit=100", { + method: "GET", + headers: { + "Accept": "application/json", + "x-api-key": API_KEY, + "x-net-site": SITE_UUID || "" + }, + cache: "no-store" + }); + if (res.ok) { + const json = await res.json(); + apiAgents = json.data || []; + } + } catch (err) { + console.error("Failed to fetch dynamic agents from Netify API:", err); + } + + // If API query fails or is empty, use the hardcoded agent list as fallback + if (apiAgents.length === 0) { + apiAgents = [ + { agent: { id: 4897042839, uuid: "2F-TF-1D-GK", label: "2F-TF-1D-GK" } }, + { agent: { id: 4895530456, uuid: "8A-V3-PB-85", label: "8A-V3-PB-85" } }, + { agent: { id: 4894730147, uuid: "F6-2V-DT-8A", label: "F6-2V-DT-8A" } }, + { agent: { id: 4895853843, uuid: "1R-79-J9-YE", label: "1R-79-J9-YE" } } + ]; + } + const AGENT_MAC_MAP: Record = { '2F-TF-1D-GK': ['60:be:b4:1f:05:96'], '8A-V3-PB-85': ['04:f4:1c:ce:c2:e6'], @@ -68,41 +98,47 @@ export async function getAgents(): Promise { '70:85:6c:81:50:d4', '70:85:6c:6d:f7:17', '60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32' ], + '1R-79-J9-YE': ['a2:cc:8e:7d:39:51', 'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'], }; const AGENT_LABELS: Record = { "2F-TF-1D-GK": "JRP Cibubur", "8A-V3-PB-85": "IFG LT.18", - "F6-2V-DT-8A": "CPI Balaraja" + "F6-2V-DT-8A": "CPI Balaraja", + "1R-79-J9-YE": "CPI Balaraja WAN" }; const latestBwTime = db.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get()?.t; - const agents: Agent[] = Object.keys(AGENT_MAC_MAP).map((uuid, idx) => { - const macs = AGENT_MAC_MAP[uuid]; - const placeholders = macs.map(() => '?').join(','); + const agents: Agent[] = apiAgents.map((item: any, idx: number) => { + const uuid = item.agent?.uuid || ""; + const id = item.agent?.id || idx; + const macs = AGENT_MAC_MAP[uuid] || []; - const bw = latestBwTime - ? db.prepare(` - SELECT SUM(download) as dl, SUM(upload) as ul - FROM mac_bandwidth - WHERE mac_address IN (${placeholders}) AND fetched_at = ? - `).get(...macs, latestBwTime) - : { dl: 0, ul: 0 }; + let dl = 0; + let ul = 0; + if (macs.length > 0 && latestBwTime) { + const placeholders = macs.map(() => '?').join(','); + const bw = db.prepare(` + SELECT SUM(download) as dl, SUM(upload) as ul + FROM mac_bandwidth + WHERE mac_address IN (${placeholders}) AND fetched_at = ? + `).get(...macs, latestBwTime); + dl = bw?.dl ?? 0; + ul = bw?.ul ?? 0; + } - const dl = bw?.dl ?? 0; - const ul = bw?.ul ?? 0; const totalMB = ((dl + ul) / (1024 * 1024)).toFixed(2); return { - id: idx, + id: id, uuid: uuid, serial: uuid, site_uuid: SITE_UUID || "", organization_uuid: "", provisioned: true, activated: true, - label: AGENT_LABELS[uuid] || uuid, + label: AGENT_LABELS[uuid] || item.agent?.label || uuid, created_at: { human: "N/A", date: "", unix_time: 0 }, updated_at: { human: "N/A", date: "", unix_time: 0 }, last_seen_at: {