feat: implement dynamic agent detection, automatic default account seeding, and strict data isolation filtering
This commit is contained in:
1 parent
8cd4f95856
commit
53008983b3
7 files changed
+208
-17
No files matched your search
+62
-2
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ const AGENT_LABELS: Record<string, string> = {
|
||||
'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 {
|
||||
|
||||
+51
-15
@@ -59,6 +59,36 @@ export async function getAgents(): Promise<Agent[]> {
|
||||
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<string, string[]> = {
|
||||
'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<Agent[]> {
|
||||
'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<string, string> = {
|
||||
"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: {
|
||||
|
||||
Reference in new issue
Block a user