feat: complete data segregation per agent matching Netify Portal with zero dummy data
This commit is contained in:
1 parent
179e13f849
commit
a73640b531
3 files changed
+193
-635
No files matched your search
+148
-631
@@ -104,7 +104,7 @@ function initSchema() {
|
|||||||
const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get();
|
const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get();
|
||||||
if (adminExists.count === 0) {
|
if (adminExists.count === 0) {
|
||||||
const hash = bcrypt.hashSync('admin123', 10);
|
const hash = bcrypt.hashSync('admin123', 10);
|
||||||
d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?, ?)").run('admin', hash, 'SUPER_ADMIN');
|
d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN');
|
||||||
console.log('[DB] Created default admin user (admin / admin123)');
|
console.log('[DB] Created default admin user (admin / admin123)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +564,27 @@ function initSchema() {
|
|||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
|
// Ensure agent_uuid exists in all core data tables
|
||||||
|
const tables = [
|
||||||
|
'bandwidth_apps', 'devices', 'flows', 'threats', 'bandwidth_protocols', 'bandwidth_countries',
|
||||||
|
'dns_queries', 'events', 'bandwidth_timeline',
|
||||||
|
'app_categories', 'continents', 'regions', 'cities',
|
||||||
|
'vlans', 'interfaces', 'flow_types', 'flow_origins',
|
||||||
|
'ip_versions', 'remote_ips', 'mac_bandwidth',
|
||||||
|
'tls_versions', 'tls_ciphers', 'tls_security', 'netbios_hostnames',
|
||||||
|
'dhcp_fingerprints', 'http_user_agents', 'sni_hostnames', 'ssl_server_cn',
|
||||||
|
'quic_hostnames', 'bittorrent_hashes', 'ssh_versions', 'mdns_hostnames',
|
||||||
|
'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit',
|
||||||
|
'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery',
|
||||||
|
'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection',
|
||||||
|
'discovery_os'
|
||||||
|
];
|
||||||
|
for (const table of tables) {
|
||||||
|
try {
|
||||||
|
d.exec(`ALTER TABLE ${table} ADD COLUMN agent_uuid TEXT DEFAULT NULL`);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DB] Schema siap.');
|
console.log('[DB] Schema siap.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,71 +805,18 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid, agentUuid) {
|
|||||||
|
|
||||||
function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) {
|
function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
if (!latest?.t) return [];
|
|
||||||
|
const appsLatest = agentUuid
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
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 [];
|
if (!appsLatest?.t) return [];
|
||||||
const rows = d.prepare(`
|
|
||||||
SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
const rows = agentUuid
|
||||||
`).all({ fetched_at: appsLatest.t, limit, siteUuid, agentUuid });
|
? d.prepare(`SELECT * FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: appsLatest.t, limit })
|
||||||
|
: d.prepare(`SELECT * FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: appsLatest.t, limit });
|
||||||
|
|
||||||
return correlateAppLabels(rows);
|
return correlateAppLabels(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,228 +940,45 @@ function deviceMatchesAgent(ip, mac, agentUuid) {
|
|||||||
|
|
||||||
function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) {
|
function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
|
|
||||||
|
const latest = agentUuid
|
||||||
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
if (!latest?.t) return [];
|
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 (@agentUuid IS NULL OR agent_uuid = @agentUuid) 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 (@agentUuid IS NULL OR agent_uuid = @agentUuid) 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 intelList = d.prepare("SELECT * FROM intel_device_discovery").all();
|
||||||
const intelMap = new Map(intelList.map(i => [i.ip_address, i]));
|
const intelMap = new Map(intelList.map(i => [i.ip_address, i]));
|
||||||
|
|
||||||
// Get all devices in latest snapshot from devices table
|
// Get active flow bandwidth in latest flows snapshot for this agent
|
||||||
const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t);
|
const latestFlowFetch = agentUuid
|
||||||
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
// Get active flow bandwidth in latest flows snapshot
|
|
||||||
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
|
||||||
let flowsBandwidth = [];
|
let flowsBandwidth = [];
|
||||||
if (latestFlowFetch?.t) {
|
if (latestFlowFetch?.t) {
|
||||||
flowsBandwidth = d.prepare(`
|
flowsBandwidth = agentUuid
|
||||||
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
? d.prepare(`
|
||||||
FROM flows
|
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
||||||
WHERE fetched_at = ?
|
FROM flows
|
||||||
GROUP BY src_ip
|
WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at
|
||||||
`).all(latestFlowFetch.t);
|
GROUP BY src_ip
|
||||||
|
`).all({ siteUuid, agentUuid, fetched_at: latestFlowFetch.t })
|
||||||
|
: d.prepare(`
|
||||||
|
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
||||||
|
FROM flows
|
||||||
|
WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at
|
||||||
|
GROUP BY src_ip
|
||||||
|
`).all({ siteUuid, fetched_at: latestFlowFetch.t });
|
||||||
}
|
}
|
||||||
const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f]));
|
const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f]));
|
||||||
|
|
||||||
|
// Get all devices in latest snapshot from devices table
|
||||||
|
const devices = agentUuid
|
||||||
|
? d.prepare(`SELECT * FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at`).all({ siteUuid, agentUuid, fetched_at: latest.t })
|
||||||
|
: d.prepare(`SELECT * FROM devices WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).all({ siteUuid, fetched_at: latest.t });
|
||||||
|
|
||||||
const resolved = [];
|
const resolved = [];
|
||||||
const seenIps = new Set();
|
const seenIps = new Set();
|
||||||
|
|
||||||
@@ -1235,23 +1020,21 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search
|
|||||||
resolved.push(processDevice(dev.ip_address, dev.mac_address, dev, flowInfo));
|
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)
|
// 2. Process any active flow IPs that are NOT present in the devices table
|
||||||
for (const flow of flowsBandwidth) {
|
for (const flow of flowsBandwidth) {
|
||||||
if (!flow.src_ip || seenIps.has(flow.src_ip)) continue;
|
if (!flow.src_ip || seenIps.has(flow.src_ip)) continue;
|
||||||
seenIps.add(flow.src_ip);
|
seenIps.add(flow.src_ip);
|
||||||
resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow));
|
resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Filter list based on role (Admin vs Agent)
|
resolved.sort((a, b) => b.download - a.download);
|
||||||
|
|
||||||
|
// If agent is specified, only return devices that have some traffic or are active
|
||||||
let filtered = resolved;
|
let filtered = resolved;
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
if (agentUuid) {
|
||||||
filtered = resolved.filter(dev => deviceMatchesAgent(dev.ip_address, dev.mac_address, agentUuid));
|
filtered = resolved.filter(d => d.total > 0);
|
||||||
} 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);
|
return filtered.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1530,24 +1313,17 @@ function correlateAppLabels(apps) {
|
|||||||
|
|
||||||
function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
|
|
||||||
|
const latest = agentUuid
|
||||||
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
if (!latest?.t) return [];
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
let rows = [];
|
const rows = agentUuid
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
? d.prepare(`SELECT * FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: latest.t, limit })
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
: d.prepare(`SELECT * FROM flows WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit`).all({ siteUuid, fetched_at: latest.t, limit });
|
||||||
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 (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid, agentUuid });
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapped = rows.map(r => ({
|
const mapped = rows.map(r => ({
|
||||||
...r,
|
...r,
|
||||||
@@ -1560,308 +1336,110 @@ function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
|||||||
|
|
||||||
function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) {
|
function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
return agentUuid
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
? d.prepare(`SELECT * FROM threats WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit })
|
||||||
return d.prepare(`
|
: d.prepare(`SELECT * FROM threats WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit });
|
||||||
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) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) {
|
function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
if (!latest?.t) return [];
|
|
||||||
|
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const protoLatest = agentUuid
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
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 [];
|
if (!protoLatest?.t) return [];
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
return agentUuid
|
||||||
`).all({ fetched_at: protoLatest.t, limit, siteUuid, agentUuid });
|
? d.prepare(`SELECT * FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: protoLatest.t, limit })
|
||||||
|
: d.prepare(`SELECT * FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: protoLatest.t, limit });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) {
|
function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
|
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const countryLatest = agentUuid
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
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 [];
|
if (!countryLatest?.t) return [];
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
return agentUuid
|
||||||
`).all({ fetched_at: countryLatest.t, limit, siteUuid, agentUuid });
|
? d.prepare(`SELECT * FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: countryLatest.t, limit })
|
||||||
|
: d.prepare(`SELECT * FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: countryLatest.t, limit });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) {
|
function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
if (!latest?.t) return [];
|
|
||||||
|
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const dnsLatest = agentUuid
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
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 [];
|
if (!dnsLatest?.t) return [];
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit
|
return agentUuid
|
||||||
`).all({ fetched_at: dnsLatest.t, limit, siteUuid, agentUuid });
|
? d.prepare(`SELECT * FROM dns_queries WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: dnsLatest.t, limit })
|
||||||
|
: d.prepare(`SELECT * FROM dns_queries WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit`).all({ siteUuid, fetched_at: dnsLatest.t, limit });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) {
|
function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
return agentUuid
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
? d.prepare(`SELECT * FROM events WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit })
|
||||||
return d.prepare(`
|
: d.prepare(`SELECT * FROM events WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit });
|
||||||
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) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid, agentUuid });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) {
|
function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
const rows = agentUuid
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
? d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit: points })
|
||||||
return d.prepare(`
|
: d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit: points });
|
||||||
SELECT mb.fetched_at,
|
return rows.reverse();
|
||||||
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) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT @limit
|
|
||||||
`).all({ limit: points, siteUuid }).reverse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStats(siteUuid = null, agentUuid = null) {
|
function getStats(siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
|
|
||||||
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
const latestDevFetch = agentUuid
|
||||||
const macs = AGENT_MAC_MAP[agentUuid];
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
const placeholders = macs.map(() => '?').join(',');
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
// 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
|
const totalDevices = latestDevFetch?.t
|
||||||
? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0)
|
? (agentUuid
|
||||||
|
? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at AND download > 0`).get({ siteUuid, agentUuid, fetched_at: latestDevFetch.t })?.n ?? 0)
|
||||||
|
: (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).get({ siteUuid, fetched_at: latestDevFetch.t })?.n ?? 0))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const latestFlowFetch = agentUuid
|
||||||
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
const activeFlows = latestFlowFetch?.t
|
const activeFlows = latestFlowFetch?.t
|
||||||
? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0)
|
? (agentUuid
|
||||||
|
? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at`).get({ siteUuid, agentUuid, fetched_at: latestFlowFetch.t })?.n ?? 0)
|
||||||
|
: (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).get({ siteUuid, fetched_at: latestFlowFetch.t })?.n ?? 0))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const totalThreats = d.prepare(`SELECT COUNT(*) as n FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0;
|
const totalThreats = agentUuid
|
||||||
const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid)`).get({ siteUuid, agentUuid })?.n ?? 0;
|
? (d.prepare(`SELECT COUNT(*) as n FROM threats WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })?.n ?? 0)
|
||||||
|
: (d.prepare(`SELECT COUNT(*) as n FROM threats WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid })?.n ?? 0);
|
||||||
|
|
||||||
|
const totalEvents = agentUuid
|
||||||
|
? (d.prepare(`SELECT COUNT(*) as n FROM events WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })?.n ?? 0)
|
||||||
|
: (d.prepare(`SELECT COUNT(*) as n FROM events WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid })?.n ?? 0);
|
||||||
|
|
||||||
const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_timeline`).get()?.t ?? null;
|
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) AND (@agentUuid IS NULL OR agent_uuid = @agentUuid) ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid, agentUuid });
|
const latestBw = agentUuid
|
||||||
|
? d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid });
|
||||||
|
|
||||||
return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw };
|
return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw };
|
||||||
}
|
}
|
||||||
@@ -1991,78 +1569,17 @@ function insertMACBandwidth(rows, fetchedAt, siteUuid, agentUuid) {
|
|||||||
// ─── QUERY FITUR BARU ─────────────────────────────────────────────────────────
|
// ─── QUERY FITUR BARU ─────────────────────────────────────────────────────────
|
||||||
function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) {
|
function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get();
|
const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1';
|
||||||
|
|
||||||
|
const latest = agentUuid
|
||||||
|
? d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })
|
||||||
|
: d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid });
|
||||||
|
|
||||||
if (!latest?.t) return [];
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
let rows = [];
|
const rows = agentUuid
|
||||||
|
? d.prepare(`SELECT * FROM ${table} WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: latest.t, limit })
|
||||||
// If agentUuid is provided, handle direct MAC or interface filtering
|
: d.prepare(`SELECT * FROM ${table} WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ siteUuid, fetched_at: latest.t, limit });
|
||||||
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 (@agentUuid IS NULL OR agent_uuid = @agentUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid, agentUuid });
|
|
||||||
|
|
||||||
// If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint
|
|
||||||
if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) {
|
|
||||||
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;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-4
@@ -6,6 +6,7 @@ const axios = require('axios');
|
|||||||
const BASE_URL = 'https://informatics.netify.ai/api/v1';
|
const BASE_URL = 'https://informatics.netify.ai/api/v1';
|
||||||
const JWT_TOKEN = process.env.NETIFY_JWT_TOKEN;
|
const JWT_TOKEN = process.env.NETIFY_JWT_TOKEN;
|
||||||
const SITE_UUID = process.env.NETIFY_SITE_UUID;
|
const SITE_UUID = process.env.NETIFY_SITE_UUID;
|
||||||
|
const agentMap = {};
|
||||||
|
|
||||||
function headersSite(useApiKey = false) {
|
function headersSite(useApiKey = false) {
|
||||||
const token = process.env.NETIFY_API_KEY || JWT_TOKEN;
|
const token = process.env.NETIFY_API_KEY || JWT_TOKEN;
|
||||||
@@ -354,7 +355,14 @@ function getMockDataForPath(path, params = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function netifyFetch(path, params = {}, useApiKey = false, agentUuid = null) {
|
async function netifyFetch(path, params = {}, useApiKey = false, agentUuid = null) {
|
||||||
if (agentUuid) params.settings_agent = agentUuid;
|
if (agentUuid) {
|
||||||
|
const agentId = agentMap[agentUuid];
|
||||||
|
if (agentId) {
|
||||||
|
params.filter_agents = `[${agentId}]`;
|
||||||
|
} else {
|
||||||
|
params.settings_agent = agentUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) {
|
if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) {
|
||||||
return getMockDataForPath(path, params);
|
return getMockDataForPath(path, params);
|
||||||
}
|
}
|
||||||
@@ -718,7 +726,7 @@ async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return intelRaw.map(r => {
|
const resolvedList = intelRaw.map(r => {
|
||||||
const ip = r.ip?.address ?? null;
|
const ip = r.ip?.address ?? null;
|
||||||
const mac = r.mac_address ?? r.discovery_mac?.address ?? null;
|
const mac = r.mac_address ?? r.discovery_mac?.address ?? null;
|
||||||
const oui = mac ? mac.substring(0, 8).toUpperCase() : null;
|
const oui = mac ? mac.substring(0, 8).toUpperCase() : null;
|
||||||
@@ -751,7 +759,34 @@ async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid =
|
|||||||
upload : ul,
|
upload : ul,
|
||||||
last_seen : r.last_seen_at?.date || r.discovery_mac?.last_seen_at?.date || null,
|
last_seen : r.last_seen_at?.date || r.discovery_mac?.last_seen_at?.date || null,
|
||||||
};
|
};
|
||||||
}).sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
});
|
||||||
|
|
||||||
|
const seenIps = new Set(resolvedList.map(d => d.ip_address).filter(Boolean));
|
||||||
|
const allActiveIps = new Set([
|
||||||
|
...Object.keys(dlMap),
|
||||||
|
...Object.keys(ulMap)
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const ip of allActiveIps) {
|
||||||
|
if (!seenIps.has(ip)) {
|
||||||
|
seenIps.add(ip);
|
||||||
|
const dl = dlMap[ip] ?? 0;
|
||||||
|
const ul = ulMap[ip] ?? 0;
|
||||||
|
resolvedList.push({
|
||||||
|
ip_address : ip,
|
||||||
|
mac_address : null,
|
||||||
|
device_label : ip,
|
||||||
|
device_type : 'LAN Client',
|
||||||
|
os_label : 'Windows/Linux',
|
||||||
|
manufacturer : 'Unknown',
|
||||||
|
download : dl,
|
||||||
|
upload : ul,
|
||||||
|
last_seen : new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedList.sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// OUI lookup — manufacturer dari 3 oktet pertama MAC
|
// OUI lookup — manufacturer dari 3 oktet pertama MAC
|
||||||
@@ -2852,9 +2887,15 @@ async function fetchVPNDetection(limit = 50, agentUuid = null) {
|
|||||||
async function fetchAgents() {
|
async function fetchAgents() {
|
||||||
const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, false, null);
|
const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, false, null);
|
||||||
if (!data || !Array.isArray(data)) return [];
|
if (!data || !Array.isArray(data)) return [];
|
||||||
return data.map(r => ({
|
const list = data.map(r => ({
|
||||||
id: r.agent?.id,
|
id: r.agent?.id,
|
||||||
uuid: r.agent?.uuid,
|
uuid: r.agent?.uuid,
|
||||||
label: r.agent?.label,
|
label: r.agent?.label,
|
||||||
}));
|
}));
|
||||||
|
for (const a of list) {
|
||||||
|
if (a.uuid && a.id) {
|
||||||
|
agentMap[a.uuid] = a.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
Binary file not shown.
Reference in new issue
Block a user