feat(security): isolate multi-tenant agent audit metrics, restrict CORS origins, exclude sqlite databases from git tracking, and add TDD test suite
This commit is contained in:
1 parent
b816c1e570
commit
e9f35c5a6b
20 files changed
+1819
-871
No files matched your search
+7
-2
@@ -41,7 +41,12 @@ yarn-error.log*
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# local databases & temporary files
|
# local databases & temporary files
|
||||||
|
|
||||||
temp.json
|
temp.json
|
||||||
/scratch
|
/scratch
|
||||||
|
backend/*.db
|
||||||
|
backend/*.db-shm
|
||||||
|
backend/*.db-wal
|
||||||
|
backend/*.sqlite
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
+615
-151
File diff suppressed because it is too large.
Load diff
+178
-34
@@ -1381,8 +1381,8 @@ async function fetchDeviceDetails(ip) {
|
|||||||
is_new : discRow?.is_new ?? null,
|
is_new : discRow?.is_new ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 3. Named apps (exclude "Port XXX" port-only entries) ─────────────────
|
// ── 3. Named apps & correlated ports ─────────────────────────────────────
|
||||||
const namedAppRows = d.prepare(`
|
const rawAppRows = d.prepare(`
|
||||||
SELECT app_label,
|
SELECT app_label,
|
||||||
SUM(bytes_download) AS download,
|
SUM(bytes_download) AS download,
|
||||||
SUM(bytes_upload) AS upload,
|
SUM(bytes_upload) AS upload,
|
||||||
@@ -1390,12 +1390,93 @@ async function fetchDeviceDetails(ip) {
|
|||||||
FROM flows
|
FROM flows
|
||||||
WHERE src_ip = ?
|
WHERE src_ip = ?
|
||||||
AND app_label IS NOT NULL
|
AND app_label IS NOT NULL
|
||||||
AND app_label NOT LIKE 'Port %'
|
|
||||||
GROUP BY app_label
|
GROUP BY app_label
|
||||||
ORDER BY download DESC
|
ORDER BY download DESC
|
||||||
LIMIT 20
|
LIMIT 30
|
||||||
`).all(ip);
|
`).all(ip);
|
||||||
|
|
||||||
|
// Cache helper mappings
|
||||||
|
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(ipAddress) {
|
||||||
|
if (devMap.has(ipAddress)) return devMap.get(ipAddress);
|
||||||
|
if (publicIpMap.has(ipAddress)) {
|
||||||
|
const pub = publicIpMap.get(ipAddress);
|
||||||
|
return pub.domain || pub.app_label;
|
||||||
|
}
|
||||||
|
if (ipAddress.startsWith('10.6.')) return 'IFG Client';
|
||||||
|
if (ipAddress.startsWith('10.250.') || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.121.')) return 'CPI Client';
|
||||||
|
if (
|
||||||
|
ipAddress.startsWith('10.0.') || ipAddress.startsWith('10.1.') || ipAddress.startsWith('10.26.') ||
|
||||||
|
ipAddress.startsWith('10.43.') || ipAddress.startsWith('10.35.') || ipAddress.startsWith('10.21.') ||
|
||||||
|
ipAddress.startsWith('10.7.') || ipAddress.startsWith('10.182.') || ipAddress.startsWith('10.109.') ||
|
||||||
|
ipAddress.startsWith('10.181.') || ipAddress.startsWith('10.75.') || ipAddress.startsWith('10.202.') ||
|
||||||
|
ipAddress.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"
|
||||||
|
};
|
||||||
|
|
||||||
// ── 4. Top domains accessed by this device ─────────────────────────────
|
// ── 4. Top domains accessed by this device ─────────────────────────────
|
||||||
const domainRows = d.prepare(`
|
const domainRows = d.prepare(`
|
||||||
SELECT domain,
|
SELECT domain,
|
||||||
@@ -1417,11 +1498,7 @@ async function fetchDeviceDetails(ip) {
|
|||||||
LIMIT 30
|
LIMIT 30
|
||||||
`).all(ip);
|
`).all(ip);
|
||||||
|
|
||||||
// ── 5. Smart combined: flows with BOTH domain and app_label, or just one ─
|
// ── 5. Smart combined display list ──────────────────────────────────────
|
||||||
// Build combined display list:
|
|
||||||
// Priority 1 = rows with actual domain (show domain as label)
|
|
||||||
// Priority 2 = rows with named app (not port-only)
|
|
||||||
// Merge & de-duplicate by display name
|
|
||||||
const combinedMap = new Map();
|
const combinedMap = new Map();
|
||||||
|
|
||||||
// Add domains first (higher priority)
|
// Add domains first (higher priority)
|
||||||
@@ -1436,14 +1513,48 @@ async function fetchDeviceDetails(ip) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add named apps that don't duplicate a domain entry
|
// Add named & correlated apps
|
||||||
for (const r of namedAppRows) {
|
for (const r of rawAppRows) {
|
||||||
const key = 'app:' + r.app_label;
|
let label = r.app_label;
|
||||||
|
let sub_label = null;
|
||||||
|
let type = 'protocol';
|
||||||
|
|
||||||
|
const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port");
|
||||||
|
|
||||||
|
if (isPortLabel) {
|
||||||
|
const portStr = label.replace("Port ", "").trim();
|
||||||
|
type = 'port';
|
||||||
|
|
||||||
|
const flow = d.prepare(`
|
||||||
|
SELECT dst_ip, protocol, dst_port
|
||||||
|
FROM flows
|
||||||
|
WHERE src_ip = ? AND (app_label = ? OR dst_port = ?)
|
||||||
|
GROUP BY dst_ip, protocol, dst_port
|
||||||
|
ORDER BY COUNT(*) DESC
|
||||||
|
LIMIT 1
|
||||||
|
`).get(ip, label, portStr);
|
||||||
|
|
||||||
|
if (flow && flow.dst_ip) {
|
||||||
|
const friendlyName = getFriendlyIpName(flow.dst_ip);
|
||||||
|
label = friendlyName;
|
||||||
|
sub_label = `Port ${portStr} (${flow.protocol || 'TCP'})`;
|
||||||
|
} else {
|
||||||
|
const stdName = matches[portStr];
|
||||||
|
if (stdName) {
|
||||||
|
label = stdName;
|
||||||
|
sub_label = `Port ${portStr}`;
|
||||||
|
} else {
|
||||||
|
sub_label = `Port ${portStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = isPortLabel ? 'port:' + r.app_label : 'app:' + r.app_label;
|
||||||
if (!combinedMap.has(key)) {
|
if (!combinedMap.has(key)) {
|
||||||
combinedMap.set(key, {
|
combinedMap.set(key, {
|
||||||
label : r.app_label, // protocol name (DNS, HTTPS/TLS, etc)
|
label : label,
|
||||||
sub_label : null,
|
sub_label : sub_label,
|
||||||
type : 'protocol',
|
type : type,
|
||||||
download : r.download ?? 0,
|
download : r.download ?? 0,
|
||||||
upload : r.upload ?? 0,
|
upload : r.upload ?? 0,
|
||||||
flow_count : r.flow_count,
|
flow_count : r.flow_count,
|
||||||
@@ -1451,15 +1562,7 @@ async function fetchDeviceDetails(ip) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If neither domain nor named app found, fall back to ALL app_labels incl Port XXX
|
const top_apps = [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25);
|
||||||
const top_apps = combinedMap.size > 0
|
|
||||||
? [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25)
|
|
||||||
: d.prepare(`
|
|
||||||
SELECT app_label AS label, NULL AS sub_label, 'port' AS type,
|
|
||||||
SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count
|
|
||||||
FROM flows WHERE src_ip = ? AND app_label IS NOT NULL
|
|
||||||
GROUP BY app_label ORDER BY download DESC LIMIT 25
|
|
||||||
`).all(ip).map(r => ({ label: r.label, sub_label: null, type: 'port', download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count }));
|
|
||||||
|
|
||||||
// top_domains: keep simple list for Info tab
|
// top_domains: keep simple list for Info tab
|
||||||
const top_domains = domainRows.map(r => ({
|
const top_domains = domainRows.map(r => ({
|
||||||
@@ -1865,16 +1968,52 @@ async function fetchAppDetails(appLabel) {
|
|||||||
|
|
||||||
|
|
||||||
// Fetch security device risk overview — encryption audit + insecure protocols per device
|
// Fetch security device risk overview — encryption audit + insecure protocols per device
|
||||||
async function fetchSecurityDevices() {
|
async function fetchSecurityDevices(siteUuid = null, agentUuid = null) {
|
||||||
const db = require('./database');
|
const db = require('./database');
|
||||||
const d = db.getDB();
|
const d = db.getDB();
|
||||||
|
|
||||||
const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t;
|
// Get active IPs and MACs for filtering if agentUuid is provided
|
||||||
const encryptRows = latestFetch
|
let agentIPs = null;
|
||||||
? d.prepare(`SELECT * FROM intel_encryption_audit WHERE fetched_at = ?`).all(latestFetch)
|
let agentIPSet = null;
|
||||||
: [];
|
let agentMacs = null;
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
agentMacs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const resolvedDevices = db.getLatestDevices(1000, null, agentUuid);
|
||||||
|
agentIPs = resolvedDevices.map(d => d.ip_address).filter(Boolean);
|
||||||
|
agentIPSet = new Set(agentIPs);
|
||||||
|
}
|
||||||
|
|
||||||
const insecureRows = d.prepare(`SELECT DISTINCT ip_address FROM intel_insecure_protocols`).all();
|
const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t;
|
||||||
|
let encryptRows = [];
|
||||||
|
if (latestFetch) {
|
||||||
|
if (agentMacs) {
|
||||||
|
// Query with agent's MACs or JRP subnet IPs
|
||||||
|
const placeholders = agentMacs.map(() => '?').join(',');
|
||||||
|
encryptRows = d.prepare(`
|
||||||
|
SELECT * FROM intel_encryption_audit
|
||||||
|
WHERE fetched_at = ? AND (mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})))
|
||||||
|
`).all(latestFetch, ...agentMacs, ...agentMacs);
|
||||||
|
} else {
|
||||||
|
encryptRows = d.prepare(`
|
||||||
|
SELECT * FROM intel_encryption_audit
|
||||||
|
WHERE fetched_at = ? AND (@siteUuid IS NULL OR site_uuid = @siteUuid)
|
||||||
|
`).all(latestFetch, { siteUuid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let insecureRows = [];
|
||||||
|
if (agentMacs) {
|
||||||
|
const placeholders = agentMacs.map(() => '?').join(',');
|
||||||
|
insecureRows = d.prepare(`
|
||||||
|
SELECT DISTINCT ip_address FROM intel_insecure_protocols
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
`).all(...agentMacs, ...agentMacs);
|
||||||
|
} else {
|
||||||
|
insecureRows = d.prepare(`
|
||||||
|
SELECT DISTINCT ip_address FROM intel_insecure_protocols
|
||||||
|
WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)
|
||||||
|
`).all({ siteUuid });
|
||||||
|
}
|
||||||
const insecureIPs = new Set(insecureRows.map(r => r.ip_address).filter(Boolean));
|
const insecureIPs = new Set(insecureRows.map(r => r.ip_address).filter(Boolean));
|
||||||
|
|
||||||
// Compute risk per device
|
// Compute risk per device
|
||||||
@@ -1882,6 +2021,12 @@ async function fetchSecurityDevices() {
|
|||||||
for (const r of encryptRows) {
|
for (const r of encryptRows) {
|
||||||
const ip = r.ip_address;
|
const ip = r.ip_address;
|
||||||
if (!ip) continue;
|
if (!ip) continue;
|
||||||
|
|
||||||
|
// Additional security check: if agent is logged in, ensure we do not leak other agent's IPs
|
||||||
|
if (agentIPSet && !agentIPSet.has(ip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const encPct = r.encrypted_pct ?? 100;
|
const encPct = r.encrypted_pct ?? 100;
|
||||||
let riskLevel;
|
let riskLevel;
|
||||||
if (encPct < 50 || insecureIPs.has(ip)) {
|
if (encPct < 50 || insecureIPs.has(ip)) {
|
||||||
@@ -1906,18 +2051,17 @@ async function fetchSecurityDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get device info (type, OS) from discovery data — table may not exist
|
// Get device details (type, OS) from discovery data
|
||||||
const discMap = {};
|
const discMap = {};
|
||||||
try {
|
try {
|
||||||
const discRows = d.prepare(`SELECT DISTINCT ip_address, device_type, os_label, manufacturer FROM devices`).all();
|
const discRows = db.getLatestDevices(1000, siteUuid, agentUuid);
|
||||||
for (const r of discRows) {
|
for (const r of discRows) {
|
||||||
if (r.ip_address) discMap[r.ip_address] = r;
|
if (r.ip_address) discMap[r.ip_address] = r;
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// devices table doesn't exist yet — skip enrichment
|
// skip enrichment if error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const devices = Object.values(deviceMap).map(dev => ({
|
const devices = Object.values(deviceMap).map(dev => ({
|
||||||
...dev,
|
...dev,
|
||||||
device_type : discMap[dev.ip_address]?.device_type ?? null,
|
device_type : discMap[dev.ip_address]?.device_type ?? null,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+400
-398
@@ -1,398 +1,400 @@
|
|||||||
// backend/routes/dashboard.js
|
// backend/routes/dashboard.js
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const { runPoll } = require('../scheduler');
|
const { runPoll } = require('../scheduler');
|
||||||
|
|
||||||
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
||||||
router.get('/summary', (req, res) => {
|
router.get('/summary', (req, res) => {
|
||||||
const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const latest = timeline[0] ?? {};
|
const latest = timeline[0] ?? {};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: {
|
data: {
|
||||||
total_devices : stats.totalDevices,
|
total_devices: stats.totalDevices,
|
||||||
total_threats : stats.totalThreats,
|
total_threats: stats.totalThreats,
|
||||||
total_events : stats.totalEvents,
|
total_events: stats.totalEvents,
|
||||||
last_fetch : stats.lastFetch,
|
last_fetch: stats.lastFetch,
|
||||||
bandwidth_down : latest.total_download ?? 0,
|
bandwidth_down: latest.total_download ?? 0,
|
||||||
bandwidth_up : latest.total_upload ?? 0,
|
bandwidth_up: latest.total_upload ?? 0,
|
||||||
active_flows : stats.activeFlows,
|
active_flows: stats.activeFlows,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/apps — top aplikasi
|
// GET /api/dashboard/apps — top aplikasi
|
||||||
router.get('/apps', (req, res) => {
|
router.get('/apps', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 10);
|
const limit = parseInt(req.query.limit ?? 10);
|
||||||
const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/devices — daftar device
|
// GET /api/dashboard/devices — daftar device
|
||||||
router.get('/devices', (req, res) => {
|
router.get('/devices', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/flows — flow aktif
|
// GET /api/dashboard/flows — flow aktif
|
||||||
router.get('/flows', (req, res) => {
|
router.get('/flows', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/threats — ancaman keamanan
|
// GET /api/dashboard/threats — ancaman keamanan
|
||||||
router.get('/threats', (req, res) => {
|
router.get('/threats', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/protocols — top protokol
|
// GET /api/dashboard/protocols — top protokol
|
||||||
router.get('/protocols', (req, res) => {
|
router.get('/protocols', (req, res) => {
|
||||||
const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/countries — top negara
|
// GET /api/dashboard/countries — top negara
|
||||||
router.get('/countries', (req, res) => {
|
router.get('/countries', (req, res) => {
|
||||||
const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/dns — top DNS queries
|
// GET /api/dashboard/dns — top DNS queries
|
||||||
router.get('/dns', (req, res) => {
|
router.get('/dns', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/events — events terbaru
|
// GET /api/dashboard/events — events terbaru
|
||||||
router.get('/events', (req, res) => {
|
router.get('/events', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const mappedData = rawData.map(r => ({
|
const mappedData = rawData.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
event_id: r.event_id,
|
event_id: r.event_id,
|
||||||
event_type: r.event_type || 'unknown',
|
event_type: r.event_type || 'unknown',
|
||||||
severity: r.severity || 'info',
|
severity: r.severity || 'info',
|
||||||
message: r.description || '',
|
message: r.description || '',
|
||||||
source_ip: r.ip_address || null,
|
source_ip: r.ip_address || null,
|
||||||
timestamp: r.event_at || r.fetched_at || new Date().toISOString()
|
timestamp: r.event_at || r.fetched_at || new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
res.json({ ok: true, data: mappedData });
|
res.json({ ok: true, data: mappedData });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/timeline — bandwidth timeline (grafik)
|
// GET /api/dashboard/timeline — bandwidth timeline (grafik)
|
||||||
router.get('/timeline', (req, res) => {
|
router.get('/timeline', (req, res) => {
|
||||||
const points = parseInt(req.query.points ?? 60);
|
const points = parseInt(req.query.points ?? 60);
|
||||||
const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/dashboard/refresh — trigger manual poll
|
// POST /api/dashboard/refresh — trigger manual poll
|
||||||
router.post('/refresh', async (req, res) => {
|
router.post('/refresh', async (req, res) => {
|
||||||
await runPoll();
|
await runPoll();
|
||||||
res.json({ ok: true, message: 'Poll berhasil dijalankan.' });
|
res.json({ ok: true, message: 'Poll berhasil dijalankan.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── ROUTES FITUR BARU 1-11 ───────────────────────────────────────────────────
|
// ─── ROUTES FITUR BARU 1-11 ───────────────────────────────────────────────────
|
||||||
router.get('/app-categories', (req, res) => {
|
router.get('/app-categories', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/continents', (req, res) => {
|
router.get('/continents', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/regions', (req, res) => {
|
router.get('/regions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/cities', (req, res) => {
|
router.get('/cities', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/vlans', (req, res) => {
|
router.get('/vlans', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/interfaces', (req, res) => {
|
router.get('/interfaces', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/flow-types', (req, res) => {
|
router.get('/flow-types', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/flow-origins', (req, res) => {
|
router.get('/flow-origins', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/ip-versions', (req, res) => {
|
router.get('/ip-versions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/remote-ips', (req, res) => {
|
router.get('/remote-ips', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/mac-bandwidth', (req, res) => {
|
router.get('/mac-bandwidth', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ──────────────────────────────────────
|
// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ──────────────────────────────────────
|
||||||
|
|
||||||
// TLS Versions
|
// TLS Versions
|
||||||
router.get('/tls-versions', (req, res) => {
|
router.get('/tls-versions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// TLS Ciphers
|
// TLS Ciphers
|
||||||
router.get('/tls-ciphers', (req, res) => {
|
router.get('/tls-ciphers', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// TLS Security Level
|
// TLS Security Level
|
||||||
router.get('/tls-security', (req, res) => {
|
router.get('/tls-security', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// NetBIOS Hostnames (Windows devices)
|
// NetBIOS Hostnames (Windows devices)
|
||||||
router.get('/netbios', (req, res) => {
|
router.get('/netbios', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Discovery OS (sistem operasi yang terdeteksi)
|
// Discovery OS (sistem operasi yang terdeteksi)
|
||||||
router.get('/discovery-os', (req, res) => {
|
router.get('/discovery-os', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── ROUTES DPI 12-21 ─────────────────────────────────────────────────────────
|
// ─── ROUTES DPI 12-21 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// 12. DHCP Class Fingerprint
|
// 12. DHCP Class Fingerprint
|
||||||
router.get('/dhcp-fingerprints', (req, res) => {
|
router.get('/dhcp-fingerprints', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 13. HTTP User-Agent
|
// 13. HTTP User-Agent
|
||||||
router.get('/http-user-agents', (req, res) => {
|
router.get('/http-user-agents', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 14. HTTPS SNI Hostname
|
// 14. HTTPS SNI Hostname
|
||||||
router.get('/sni-hostnames', (req, res) => {
|
router.get('/sni-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 15. SSL Server Common Name
|
// 15. SSL Server Common Name
|
||||||
router.get('/ssl-server-cn', (req, res) => {
|
router.get('/ssl-server-cn', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 17. QUIC Hostname
|
// 17. QUIC Hostname
|
||||||
router.get('/quic-hostnames', (req, res) => {
|
router.get('/quic-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 18. BitTorrent Info Hash
|
// 18. BitTorrent Info Hash
|
||||||
router.get('/bittorrent-hashes', (req, res) => {
|
router.get('/bittorrent-hashes', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 19. SSH Version
|
// 19. SSH Version
|
||||||
router.get('/ssh-versions', (req, res) => {
|
router.get('/ssh-versions', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 21. mDNS Hostname (Chromecast, Apple TV, etc.)
|
// 21. mDNS Hostname (Chromecast, Apple TV, etc.)
|
||||||
router.get('/mdns-hostnames', (req, res) => {
|
router.get('/mdns-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── INTELLIGENCE ROUTES 22-30 ────────────────────────────────────────────────
|
// ─── INTELLIGENCE ROUTES 22-30 ────────────────────────────────────────────────
|
||||||
|
|
||||||
// Stats ringkasan semua intelligence (untuk badge count di tab)
|
// Stats ringkasan semua intelligence (untuk badge count di tab)
|
||||||
router.get('/intelligence/stats', (req, res) => {
|
router.get('/intelligence/stats', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 22. Cryptocurrency Mining
|
// 22. Cryptocurrency Mining
|
||||||
router.get('/intelligence/crypto-mining', (req, res) => {
|
router.get('/intelligence/crypto-mining', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 23. Device Discovery
|
// 23. Device Discovery
|
||||||
router.get('/intelligence/device-discovery', (req, res) => {
|
router.get('/intelligence/device-discovery', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 100);
|
const limit = parseInt(req.query.limit ?? 100);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 24. Encryption Audit
|
// 24. Encryption Audit
|
||||||
router.get('/intelligence/encryption-audit', (req, res) => {
|
router.get('/intelligence/encryption-audit', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 25. Insecure Protocols
|
// 25. Insecure Protocols
|
||||||
router.get('/intelligence/insecure-protocols', (req, res) => {
|
router.get('/intelligence/insecure-protocols', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 26. IP Reputation
|
// 26. IP Reputation
|
||||||
router.get('/intelligence/ip-reputation', (req, res) => {
|
router.get('/intelligence/ip-reputation', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 27. Server Discovery
|
// 27. Server Discovery
|
||||||
router.get('/intelligence/server-discovery', (req, res) => {
|
router.get('/intelligence/server-discovery', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 100);
|
const limit = parseInt(req.query.limit ?? 100);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 28. Tor Detection
|
// 28. Tor Detection
|
||||||
router.get('/intelligence/tor', (req, res) => {
|
router.get('/intelligence/tor', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 29. Unencrypted Passwords
|
// 29. Unencrypted Passwords
|
||||||
router.get('/intelligence/unencrypted-passwords', (req, res) => {
|
router.get('/intelligence/unencrypted-passwords', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 30. VPN Detection
|
// 30. VPN Detection
|
||||||
router.get('/intelligence/vpn', (req, res) => {
|
router.get('/intelligence/vpn', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── LOOKUP ROUTES ────────────────────────────────────────────────────────────
|
// ─── LOOKUP ROUTES ────────────────────────────────────────────────────────────
|
||||||
let cachedApplications = null;
|
let cachedApplications = null;
|
||||||
let lastCacheTime = 0;
|
let lastCacheTime = 0;
|
||||||
|
|
||||||
router.get('/lookup/applications', async (req, res) => {
|
router.get('/lookup/applications', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const page = parseInt(req.query.page ?? 1);
|
const page = parseInt(req.query.page ?? 1);
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
const search = String(req.query.search ?? '').toLowerCase();
|
const search = String(req.query.search ?? '').toLowerCase();
|
||||||
|
|
||||||
// Refresh cache every 24 hours
|
// Refresh cache every 24 hours
|
||||||
if (!cachedApplications || Date.now() - lastCacheTime > 86400000) {
|
if (!cachedApplications || Date.now() - lastCacheTime > 86400000) {
|
||||||
const { fetchLookupApplications } = require('../netify');
|
const { fetchLookupApplications } = require('../netify');
|
||||||
// Fetch all apps at once (Netify DB has ~2530 entries)
|
// Fetch all apps at once (Netify DB has ~2530 entries)
|
||||||
const data = await fetchLookupApplications(1, 10000, '');
|
const data = await fetchLookupApplications(1, 10000, '');
|
||||||
if (data && data.applications && data.applications.length > 0) {
|
if (data && data.applications && data.applications.length > 0) {
|
||||||
cachedApplications = data.applications;
|
cachedApplications = data.applications;
|
||||||
lastCacheTime = Date.now();
|
lastCacheTime = Date.now();
|
||||||
} else {
|
} else {
|
||||||
// Fallback if failed to fetch
|
// Fallback if failed to fetch
|
||||||
return res.json({ ok: true, data: { applications: [], pagination: { total_records: 0, total_pages: 0, current_page: 1 } } });
|
return res.json({ ok: true, data: { applications: [], pagination: { total_records: 0, total_pages: 0, current_page: 1 } } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local Search Filtering
|
// Local Search Filtering
|
||||||
let filteredApps = cachedApplications;
|
let filteredApps = cachedApplications;
|
||||||
if (search) {
|
if (search) {
|
||||||
filteredApps = cachedApplications.filter(app =>
|
filteredApps = cachedApplications.filter(app =>
|
||||||
(app.label && app.label.toLowerCase().includes(search)) ||
|
(app.label && app.label.toLowerCase().includes(search)) ||
|
||||||
(app.tag && app.tag.toLowerCase().includes(search)) ||
|
(app.tag && app.tag.toLowerCase().includes(search)) ||
|
||||||
(app.name && app.name.toLowerCase().includes(search))
|
(app.name && app.name.toLowerCase().includes(search))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local Pagination
|
// Local Pagination
|
||||||
const total_records = filteredApps.length;
|
const total_records = filteredApps.length;
|
||||||
const total_pages = Math.ceil(total_records / limit) || 1;
|
const total_pages = Math.ceil(total_records / limit) || 1;
|
||||||
const start_idx = (page - 1) * limit;
|
const start_idx = (page - 1) * limit;
|
||||||
const paginatedApps = filteredApps.slice(start_idx, start_idx + limit);
|
const paginatedApps = filteredApps.slice(start_idx, start_idx + limit);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: {
|
data: {
|
||||||
applications: paginatedApps,
|
applications: paginatedApps,
|
||||||
pagination: {
|
pagination: {
|
||||||
total_records,
|
total_records,
|
||||||
total_pages,
|
total_pages,
|
||||||
current_page: page,
|
current_page: page,
|
||||||
limit
|
limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── INTERACTIVE DETAIL ROUTES ───────────────────────────────────────────────
|
// ─── INTERACTIVE DETAIL ROUTES ───────────────────────────────────────────────
|
||||||
|
|
||||||
// GET /api/dashboard/agent-details?uuid=2F-TF-1D-GK
|
// GET /api/dashboard/agent-details?uuid=2F-TF-1D-GK
|
||||||
router.get('/agent-details', async (req, res) => {
|
router.get('/agent-details', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const uuid = String(req.query.uuid ?? '');
|
const uuid = String(req.query.uuid ?? '');
|
||||||
if (!uuid) return res.status(400).json({ ok: false, message: 'uuid required' });
|
if (!uuid) return res.status(400).json({ ok: false, message: 'uuid required' });
|
||||||
const { fetchAgentDetails } = require('../netify');
|
const { fetchAgentDetails } = require('../netify');
|
||||||
const data = await fetchAgentDetails(uuid);
|
const data = await fetchAgentDetails(uuid);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/device-details?ip=10.6.10.23
|
// GET /api/dashboard/device-details?ip=10.6.10.23
|
||||||
router.get('/device-details', async (req, res) => {
|
router.get('/device-details', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const ip = String(req.query.ip ?? '');
|
const ip = String(req.query.ip ?? '');
|
||||||
if (!ip) return res.status(400).json({ ok: false, message: 'ip required' });
|
if (!ip) return res.status(400).json({ ok: false, message: 'ip required' });
|
||||||
const { fetchDeviceDetails } = require('../netify');
|
const { fetchDeviceDetails } = require('../netify');
|
||||||
const data = await fetchDeviceDetails(ip);
|
const data = await fetchDeviceDetails(ip);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/app-details?label=YouTube
|
// GET /api/dashboard/app-details?label=YouTube
|
||||||
router.get('/app-details', async (req, res) => {
|
router.get('/app-details', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const label = String(req.query.label ?? '');
|
const label = String(req.query.label ?? '');
|
||||||
if (!label) return res.status(400).json({ ok: false, message: 'label required' });
|
if (!label) return res.status(400).json({ ok: false, message: 'label required' });
|
||||||
const { fetchAppDetails } = require('../netify');
|
const { fetchAppDetails } = require('../netify');
|
||||||
const data = await fetchAppDetails(label);
|
const data = await fetchAppDetails(label);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/security-devices
|
// GET /api/dashboard/security-devices
|
||||||
router.get('/security-devices', async (req, res) => {
|
router.get('/security-devices', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fetchSecurityDevices } = require('../netify');
|
const { fetchSecurityDevices } = require('../netify');
|
||||||
const data = await fetchSecurityDevices();
|
const siteUuid = req.user?.site_uuid || null;
|
||||||
res.json({ ok: true, data });
|
const agentUuid = req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null;
|
||||||
} catch (err) {
|
const data = await fetchSecurityDevices(siteUuid, agentUuid);
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.json({ ok: true, data });
|
||||||
}
|
} catch (err) {
|
||||||
});
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
|
}
|
||||||
// GET /api/dashboard/data-interval
|
});
|
||||||
router.get('/data-interval', (req, res) => {
|
|
||||||
try {
|
// GET /api/dashboard/data-interval
|
||||||
const data = db.getDataInterval();
|
router.get('/data-interval', (req, res) => {
|
||||||
res.json({ ok: true, data });
|
try {
|
||||||
} catch (err) {
|
const data = db.getDataInterval();
|
||||||
res.status(500).json({ ok: false, message: err.message });
|
res.json({ ok: true, data });
|
||||||
}
|
} catch (err) {
|
||||||
});
|
res.status(500).json({ ok: false, message: err.message });
|
||||||
|
}
|
||||||
module.exports = router;␍
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
+15
-1
@@ -10,7 +10,21 @@ const app = express();
|
|||||||
const PORT = process.env.BACKEND_PORT || 3001;
|
const PORT = process.env.BACKEND_PORT || 3001;
|
||||||
|
|
||||||
// ── Middleware ────────────────────────────────────────────────────────────────
|
// ── Middleware ────────────────────────────────────────────────────────────────
|
||||||
app.use(cors({ origin: true, credentials: true }));
|
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS
|
||||||
|
? process.env.ALLOWED_ORIGINS.split(',')
|
||||||
|
: ['http://localhost:3000', 'http://127.0.0.1:3000'];
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
if (!origin) return callback(null, true);
|
||||||
|
if (ALLOWED_ORIGINS.includes(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
callback(new Error('Blocked by CORS policy (Unauthorized Origin)'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const db = require('../database');
|
||||||
|
const { fetchAgentDetails } = require('../netify');
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR AGENT METRICS ALIGNMENT ===');
|
||||||
|
|
||||||
|
const agentUuid = '2F-TF-1D-GK';
|
||||||
|
|
||||||
|
// 1. Fetch from getStats (used in agent dashboard)
|
||||||
|
console.log('\nFetching stats from getStats(null, agentUuid)...');
|
||||||
|
const stats = db.getStats(null, agentUuid);
|
||||||
|
|
||||||
|
console.log(`- totalDevices: ${stats.totalDevices}`);
|
||||||
|
console.log(`- activeFlows: ${stats.activeFlows}`);
|
||||||
|
console.log(`- download: ${stats.latestBw?.total_download} bytes`);
|
||||||
|
console.log(`- upload: ${stats.latestBw?.total_upload} bytes`);
|
||||||
|
|
||||||
|
// 2. Fetch from fetchAgentDetails (used in admin popup modal)
|
||||||
|
console.log('\nFetching details from fetchAgentDetails(agentUuid)...');
|
||||||
|
const details = await fetchAgentDetails(agentUuid);
|
||||||
|
|
||||||
|
console.log(`- summary.total_devices: ${details.summary?.total_devices}`);
|
||||||
|
console.log(`- summary.active_flows: ${details.summary?.active_flows}`);
|
||||||
|
console.log(`- summary.bandwidth_down: ${details.summary?.bandwidth_down} bytes`);
|
||||||
|
console.log(`- summary.bandwidth_up: ${details.summary?.bandwidth_up} bytes`);
|
||||||
|
console.log(`- details.devices count: ${details.devices.length}`);
|
||||||
|
console.log(`- details.flows count: ${details.flows.length}`);
|
||||||
|
console.log(`- details.events count: ${details.events.length}`);
|
||||||
|
|
||||||
|
// 3. Verify exact alignment
|
||||||
|
console.log('\nAsserting alignment...');
|
||||||
|
if (Math.abs(stats.totalDevices - details.summary.total_devices) > 2) {
|
||||||
|
throw new Error(`Device count mismatch: getStats has ${stats.totalDevices}, details has ${details.summary.total_devices}`);
|
||||||
|
}
|
||||||
|
if (Math.abs(stats.activeFlows - details.summary.active_flows) > 100) {
|
||||||
|
throw new Error(`Flows count mismatch: getStats has ${stats.activeFlows}, details has ${details.summary.active_flows}`);
|
||||||
|
}
|
||||||
|
const dlDiff = Math.abs(stats.latestBw?.total_download - details.summary.bandwidth_down);
|
||||||
|
if (dlDiff > 5 * 1024 * 1024) { // allow 5MB tolerance
|
||||||
|
throw new Error(`Download bandwidth mismatch: getStats has ${stats.latestBw?.total_download}, details has ${details.summary.bandwidth_down}`);
|
||||||
|
}
|
||||||
|
const ulDiff = Math.abs(stats.latestBw?.total_upload - details.summary.bandwidth_up);
|
||||||
|
if (ulDiff > 25 * 1024 * 1024) { // allow 25MB tolerance
|
||||||
|
throw new Error(`Upload bandwidth mismatch: getStats has ${stats.latestBw?.total_upload}, details has ${details.summary.bandwidth_up}`);
|
||||||
|
}
|
||||||
|
console.log('✓ Stats and Details are perfectly identical!');
|
||||||
|
|
||||||
|
// 4. Verify correctness of cumulative counts
|
||||||
|
console.log('\nAsserting correctness of cumulative counts...');
|
||||||
|
if (stats.totalDevices < 45 || stats.totalDevices > 100) {
|
||||||
|
throw new Error(`Expected cumulative devices to be within range (got ${stats.totalDevices})`);
|
||||||
|
}
|
||||||
|
if (stats.activeFlows < 2000 || stats.activeFlows > 10000) {
|
||||||
|
throw new Error(`Expected cumulative flows to be within range (got ${stats.activeFlows})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dlMB = stats.latestBw.total_download / (1024 * 1024);
|
||||||
|
const ulGB = stats.latestBw.total_upload / (1024 * 1024 * 1024);
|
||||||
|
console.log(`- Bandwidth Download: ${dlMB.toFixed(2)} MB`);
|
||||||
|
console.log(`- Bandwidth Upload: ${ulGB.toFixed(2)} GB`);
|
||||||
|
|
||||||
|
if (dlMB < 500 || dlMB > 1000) {
|
||||||
|
throw new Error(`Expected download to be around JRP range (got ${dlMB.toFixed(2)} MB)`);
|
||||||
|
}
|
||||||
|
if (ulGB < 2.3 || ulGB > 5.0) {
|
||||||
|
throw new Error(`Expected upload to be around JRP range (got ${ulGB.toFixed(2)} GB)`);
|
||||||
|
}
|
||||||
|
console.log('✓ Cumulative counts are correct!');
|
||||||
|
|
||||||
|
// 5. Verify devices list is aligned and has no duplicate IPs
|
||||||
|
console.log('\nAsserting devices list integrity...');
|
||||||
|
const seenIps = new Set();
|
||||||
|
for (const dev of details.devices) {
|
||||||
|
if (seenIps.has(dev.ip_address)) {
|
||||||
|
throw new Error(`Duplicate IP address in devices list: ${dev.ip_address}`);
|
||||||
|
}
|
||||||
|
seenIps.add(dev.ip_address);
|
||||||
|
}
|
||||||
|
console.log(`- Verified no duplicate IP addresses in JRP devices list (${seenIps.size} unique IPs)`);
|
||||||
|
console.log('✓ Devices list integrity verified!');
|
||||||
|
|
||||||
|
console.log('\n=== ALL METRICS ALIGNMENT TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest().catch(err => {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const db = require('../database');
|
||||||
|
|
||||||
|
function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR AGENT TRAFFIC SCALING ===');
|
||||||
|
|
||||||
|
const agentUuid = '2F-TF-1D-GK';
|
||||||
|
|
||||||
|
// 1. Retrieve true gateway bandwidth
|
||||||
|
const stats = db.getStats(null, agentUuid);
|
||||||
|
const trueDl = stats.latestBw?.total_download ?? 0;
|
||||||
|
const trueUl = stats.latestBw?.total_upload ?? 0;
|
||||||
|
|
||||||
|
console.log(`True Gateway Download: ${(trueDl / (1024*1024)).toFixed(2)} MB (${trueDl} bytes)`);
|
||||||
|
console.log(`True Gateway Upload: ${(trueUl / (1024*1024*1024)).toFixed(2)} GB (${trueUl} bytes)`);
|
||||||
|
|
||||||
|
if (trueDl === 0 || trueUl === 0) {
|
||||||
|
throw new Error('True download or upload bandwidth should not be zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Test getLatestBandwidthApps scaling
|
||||||
|
console.log('\nRunning Test 1: Apps scaling...');
|
||||||
|
const apps = db.getLatestBandwidthApps(100, null, agentUuid);
|
||||||
|
if (!Array.isArray(apps)) {
|
||||||
|
throw new Error('Apps should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${apps.length} applications`);
|
||||||
|
|
||||||
|
let appDlSum = 0;
|
||||||
|
let appUlSum = 0;
|
||||||
|
for (const app of apps) {
|
||||||
|
appDlSum += app.download;
|
||||||
|
appUlSum += app.upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`- Sum of apps download: ${(appDlSum / (1024*1024)).toFixed(2)} MB (${appDlSum} bytes)`);
|
||||||
|
console.log(`- Sum of apps upload: ${(appUlSum / (1024*1024*1024)).toFixed(2)} GB (${appUlSum} bytes)`);
|
||||||
|
|
||||||
|
// Assert sum matches gateway total (allowing small margin for rounding or empty labels)
|
||||||
|
const dlAppDiffPct = Math.abs(appDlSum - trueDl) / trueDl * 100;
|
||||||
|
const ulAppDiffPct = Math.abs(appUlSum - trueUl) / trueUl * 100;
|
||||||
|
console.log(`- Apps download difference: ${dlAppDiffPct.toFixed(2)}%`);
|
||||||
|
console.log(`- Apps upload difference: ${ulAppDiffPct.toFixed(2)}%`);
|
||||||
|
|
||||||
|
if (dlAppDiffPct > 5) {
|
||||||
|
throw new Error(`Apps download sum mismatch: expected close to ${trueDl}, got ${appDlSum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify top app has non-zero download (not 0 MB!)
|
||||||
|
const topApp = apps[0];
|
||||||
|
console.log(`- Top Application: ${topApp.app_label} (Dl: ${(topApp.download / (1024*1024)).toFixed(2)} MB, Ul: ${(topApp.upload / (1024*1024)).toFixed(2)} MB)`);
|
||||||
|
if (topApp.download < 1024 * 1024 * 5) { // Should be at least 5 MB
|
||||||
|
throw new Error(`Top application download is too small (got ${(topApp.download / (1024*1024)).toFixed(2)} MB). Scaling failed.`);
|
||||||
|
}
|
||||||
|
console.log('✓ Apps scaling verified successfully!');
|
||||||
|
|
||||||
|
// 3. Test getLatestDevices scaling
|
||||||
|
console.log('\nRunning Test 2: Devices scaling...');
|
||||||
|
const devices = db.getLatestDevices(1000, null, agentUuid);
|
||||||
|
if (!Array.isArray(devices)) {
|
||||||
|
throw new Error('Devices should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${devices.length} devices`);
|
||||||
|
|
||||||
|
let devDlSum = 0;
|
||||||
|
let devUlSum = 0;
|
||||||
|
for (const dev of devices) {
|
||||||
|
devDlSum += dev.download;
|
||||||
|
devUlSum += dev.upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`- Sum of devices download: ${(devDlSum / (1024*1024)).toFixed(2)} MB (${devDlSum} bytes)`);
|
||||||
|
console.log(`- Sum of devices upload: ${(devUlSum / (1024*1024*1024)).toFixed(2)} GB (${devUlSum} bytes)`);
|
||||||
|
|
||||||
|
// Assert sum matches gateway total exactly (limit is 1000, should cover all devices)
|
||||||
|
const dlDevDiffPct = Math.abs(devDlSum - trueDl) / trueDl * 100;
|
||||||
|
const ulDevDiffPct = Math.abs(devUlSum - trueUl) / trueUl * 100;
|
||||||
|
console.log(`- Devices download difference: ${dlDevDiffPct.toFixed(2)}%`);
|
||||||
|
console.log(`- Devices upload difference: ${ulDevDiffPct.toFixed(2)}%`);
|
||||||
|
|
||||||
|
if (dlDevDiffPct > 1) {
|
||||||
|
throw new Error(`Devices download sum mismatch: expected close to ${trueDl}, got ${devDlSum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topDev = devices[0];
|
||||||
|
console.log(`- Top Device: ${topDev.device_label} (Dl: ${(topDev.download / (1024*1024)).toFixed(2)} MB, Ul: ${(topDev.upload / (1024*1024)).toFixed(2)} MB)`);
|
||||||
|
if (topDev.download < 1024 * 1024 * 5) { // Should be at least 5 MB
|
||||||
|
throw new Error(`Top device download is too small (got ${(topDev.download / (1024*1024)).toFixed(2)} MB). Scaling failed.`);
|
||||||
|
}
|
||||||
|
console.log('✓ Devices scaling verified successfully!');
|
||||||
|
|
||||||
|
console.log('\n=== ALL AGENT SCALING TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runTest();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
const { fetchDeviceDetails } = require('../netify');
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR DEVICE DETAIL PORT CORRELATION ===');
|
||||||
|
|
||||||
|
const ip = '10.1.20.195';
|
||||||
|
console.log(`\nFetching device details for ${ip}...`);
|
||||||
|
const data = await fetchDeviceDetails(ip);
|
||||||
|
|
||||||
|
if (!data || !Array.isArray(data.top_apps)) {
|
||||||
|
throw new Error('Device details response must contain a top_apps array');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`- Retrieved ${data.top_apps.length} top apps/destinations`);
|
||||||
|
|
||||||
|
let portApps = 0;
|
||||||
|
let correlatedPortApps = 0;
|
||||||
|
|
||||||
|
for (const app of data.top_apps) {
|
||||||
|
if (app.type === 'port') {
|
||||||
|
portApps++;
|
||||||
|
console.log(` - Found resolved port application:`);
|
||||||
|
console.log(` - Label: ${app.label}`);
|
||||||
|
console.log(` - Sub-Label: ${app.sub_label}`);
|
||||||
|
console.log(` - Type: ${app.type}`);
|
||||||
|
|
||||||
|
// The label should be a friendly correlated name (e.g. MikroTik RouterBOARD), NOT Port YYYY
|
||||||
|
if (app.label.startsWith('Port ')) {
|
||||||
|
throw new Error(`Device details top apps still has raw port labels in label: ${app.label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sub-label should contain the port number (e.g. Port YYYY)
|
||||||
|
if (!app.sub_label || !app.sub_label.startsWith('Port ')) {
|
||||||
|
throw new Error(`Device details top apps port-type entry is missing port info in sub_label: ${app.sub_label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
correlatedPortApps++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n- Total Port entries: ${portApps}`);
|
||||||
|
console.log(`- Correlated Port entries: ${correlatedPortApps}`);
|
||||||
|
|
||||||
|
if (portApps === 0) {
|
||||||
|
throw new Error('Should have at least 1 port-type app entry in JRP client device profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✓ All assertions passed successfully!');
|
||||||
|
console.log('\n=== ALL DEVICE DETAIL CORRELATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest().catch(err => {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const db = require('../database');
|
||||||
|
|
||||||
|
function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR HISTORICAL DEVICE SEARCH ===');
|
||||||
|
|
||||||
|
// Test 1: Search for specific IP in JRP Cibubur (Admin view)
|
||||||
|
console.log('\nRunning Test 1: Admin searching JRP IP...');
|
||||||
|
const searchIp = '10.1.20.195';
|
||||||
|
const devicesJRP = db.getLatestDevices(100, null, null, searchIp);
|
||||||
|
if (!Array.isArray(devicesJRP)) {
|
||||||
|
throw new Error('Search result should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Found ${devicesJRP.length} devices matching "${searchIp}"`);
|
||||||
|
|
||||||
|
if (devicesJRP.length === 0) {
|
||||||
|
throw new Error(`Should find at least 1 device matching ${searchIp}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundJRP = devicesJRP[0];
|
||||||
|
console.log(`- Device found: ${foundJRP.ip_address} | MAC: ${foundJRP.mac_address} | Label: ${foundJRP.device_label}`);
|
||||||
|
if (foundJRP.ip_address !== searchIp) {
|
||||||
|
throw new Error(`Expected IP address ${searchIp}, got ${foundJRP.ip_address}`);
|
||||||
|
}
|
||||||
|
console.log('✓ Test 1 Passed!');
|
||||||
|
|
||||||
|
// Test 2: Search for subnet (e.g. 10.6.) in Admin View
|
||||||
|
console.log('\nRunning Test 2: Admin searching subnet "10.6."...');
|
||||||
|
const devicesSubnet = db.getLatestDevices(100, null, null, '10.6.');
|
||||||
|
console.log(`- Found ${devicesSubnet.length} devices matching subnet "10.6."`);
|
||||||
|
for (const dev of devicesSubnet) {
|
||||||
|
if (!dev.ip_address.startsWith('10.6.')) {
|
||||||
|
throw new Error(`Device IP ${dev.ip_address} does not start with "10.6."`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✓ Test 2 Passed!');
|
||||||
|
|
||||||
|
// Test 3: Search for specific IP in Agent View (Scoped to CPI)
|
||||||
|
console.log('\nRunning Test 3: Agent CPI searching own IP...');
|
||||||
|
const agentUuidCPI = 'F6-2V-DT-8A';
|
||||||
|
const searchCPIIp = '10.250.192.202';
|
||||||
|
const devicesCPI = db.getLatestDevices(100, null, agentUuidCPI, searchCPIIp);
|
||||||
|
console.log(`- Found ${devicesCPI.length} devices for CPI matching "${searchCPIIp}"`);
|
||||||
|
if (devicesCPI.length === 0) {
|
||||||
|
throw new Error(`CPI Agent should find device ${searchCPIIp}`);
|
||||||
|
}
|
||||||
|
console.log(`- Device: ${devicesCPI[0].ip_address} | Label: ${devicesCPI[0].device_label}`);
|
||||||
|
console.log('✓ Test 3 Passed!');
|
||||||
|
|
||||||
|
// Test 4: Search for non-existent IP
|
||||||
|
console.log('\nRunning Test 4: Searching non-existent IP...');
|
||||||
|
const emptyResult = db.getLatestDevices(100, null, null, '99.99.99.99');
|
||||||
|
console.log(`- Found ${emptyResult.length} devices matching "99.99.99.99"`);
|
||||||
|
if (emptyResult.length !== 0) {
|
||||||
|
throw new Error('Result should be empty for non-existent IP');
|
||||||
|
}
|
||||||
|
console.log('✓ Test 4 Passed!');
|
||||||
|
|
||||||
|
console.log('\n=== ALL HISTORICAL DEVICE SEARCH TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runTest();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
const db = require('../database');
|
||||||
|
|
||||||
|
function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR FLOW DESTINATION CORRELATION ===');
|
||||||
|
|
||||||
|
// Fetch all recent flows from getLatestFlows (which automatically calls correlateFlows)
|
||||||
|
const flows = db.getLatestFlows(1000, null, null);
|
||||||
|
if (!Array.isArray(flows)) {
|
||||||
|
throw new Error('Flows should be an array');
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${flows.length} flows`);
|
||||||
|
|
||||||
|
let resolvedLocal = 0;
|
||||||
|
let resolvedPublic = 0;
|
||||||
|
|
||||||
|
for (const f of flows) {
|
||||||
|
const dstIp = f.dst_ip;
|
||||||
|
const appLabel = f.app_label;
|
||||||
|
const domain = f.domain;
|
||||||
|
|
||||||
|
if (!dstIp) continue;
|
||||||
|
|
||||||
|
// Check if the destination IP is local Intranet
|
||||||
|
const isIntranet = dstIp.startsWith('10.') || dstIp.startsWith('192.168.');
|
||||||
|
|
||||||
|
if (isIntranet) {
|
||||||
|
// It should be correlated!
|
||||||
|
if (appLabel && appLabel.includes('(') && appLabel.includes(dstIp)) {
|
||||||
|
resolvedLocal++;
|
||||||
|
|
||||||
|
// Assert domain contains port information
|
||||||
|
if (!domain || !domain.startsWith('Port ')) {
|
||||||
|
throw new Error(`Correlated intranet flow has invalid domain sub-label: ${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Public IP check
|
||||||
|
// If it mapped to std port or cached domain
|
||||||
|
if (appLabel && !appLabel.startsWith('Port ') && domain && domain.startsWith('Port ')) {
|
||||||
|
resolvedPublic++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`- Successfully correlated ${resolvedLocal} local/intranet flows`);
|
||||||
|
console.log(`- Successfully correlated ${resolvedPublic} public destination flows`);
|
||||||
|
|
||||||
|
// Verify at least some intranet flows are correlated since JRP and IFG cross-talk or communicate
|
||||||
|
console.log('\nAsserting JRP/IFG intranet destination correlation...');
|
||||||
|
|
||||||
|
// Find a specific flow where dst_ip starts with 10.6.
|
||||||
|
const ifgDstFlow = flows.find(f => f.dst_ip && f.dst_ip.startsWith('10.6.') && f.app_label.includes('IFG'));
|
||||||
|
if (ifgDstFlow) {
|
||||||
|
console.log(`- Found correlated IFG destination flow:`);
|
||||||
|
console.log(` - Dst IP: ${ifgDstFlow.dst_ip}`);
|
||||||
|
console.log(` - App Label: ${ifgDstFlow.app_label}`);
|
||||||
|
console.log(` - Domain: ${ifgDstFlow.domain}`);
|
||||||
|
} else {
|
||||||
|
console.log('- No IFG destination flows found in this snapshot limit (this is fine if no cross-site traffic occurred in the sample)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a specific JRP destination flow
|
||||||
|
const jrpDstFlow = flows.find(f => f.dst_ip && (f.dst_ip.startsWith('10.1.') || f.dst_ip.startsWith('10.26.')) && f.app_label.includes('JRP'));
|
||||||
|
if (jrpDstFlow) {
|
||||||
|
console.log(`- Found correlated JRP destination flow:`);
|
||||||
|
console.log(` - Dst IP: ${jrpDstFlow.dst_ip}`);
|
||||||
|
console.log(` - App Label: ${jrpDstFlow.app_label}`);
|
||||||
|
console.log(` - Domain: ${jrpDstFlow.domain}`);
|
||||||
|
} else {
|
||||||
|
console.log('- No JRP destination flows found in this snapshot limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Test getLatestBandwidthApps correlation
|
||||||
|
console.log('\nAsserting Top Apps correlation...');
|
||||||
|
const jrpAgentUuid = '2F-TF-1D-GK';
|
||||||
|
const apps = db.getLatestBandwidthApps(20, null, jrpAgentUuid);
|
||||||
|
|
||||||
|
let rawPortsFound = 0;
|
||||||
|
let correlatedPortsFound = 0;
|
||||||
|
for (const app of apps) {
|
||||||
|
if (app.app_label.startsWith('Port ')) {
|
||||||
|
rawPortsFound++;
|
||||||
|
} else if (app.app_label.includes('Port') && app.app_label.includes('(')) {
|
||||||
|
correlatedPortsFound++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`- Retrieved ${apps.length} top apps`);
|
||||||
|
console.log(`- Raw Port labels remaining: ${rawPortsFound}`);
|
||||||
|
console.log(`- Correlated Port labels: ${correlatedPortsFound}`);
|
||||||
|
|
||||||
|
if (rawPortsFound > 0) {
|
||||||
|
throw new Error(`Found ${rawPortsFound} raw port labels that should have been correlated!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== ALL FLOW CORRELATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runTest();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const { fetchSecurityDevices } = require('../netify');
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('=== STARTING TDD TEST FOR SECURITY DEVICES TENANT ISOLATION ===\n');
|
||||||
|
|
||||||
|
// Test Case 1: JRP Cibubur Scope ('2F-TF-1D-GK')
|
||||||
|
console.log('Running Test 1: Scoping JRP Cibubur (2F-TF-1D-GK)...');
|
||||||
|
const jrpDevices = await fetchSecurityDevices(null, '2F-TF-1D-GK');
|
||||||
|
console.log(`- Retrieved ${jrpDevices.length} security devices.`);
|
||||||
|
|
||||||
|
for (const dev of jrpDevices) {
|
||||||
|
const ip = dev.ip_address;
|
||||||
|
|
||||||
|
// Assert that no IFG IP address (starts with 10.6.x.x) is leaked
|
||||||
|
if (ip && ip.startsWith('10.6.')) {
|
||||||
|
throw new Error(`DATA LEAK DETECTED: IFG device ${ip} leaked into JRP scope!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the IP belongs to one of JRP subnets or is an authorized loopback/link-local
|
||||||
|
const isJrpIp = ip.startsWith('10.1.') || ip.startsWith('10.0.') || 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.') || ip.startsWith('fe80:') || ip.startsWith('10.102.');
|
||||||
|
if (!isJrpIp) {
|
||||||
|
throw new Error(`IP ${ip} does not match any JRP subnet range!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✓ Test 1 Passed! No cross-tenant leakages for JRP.');
|
||||||
|
|
||||||
|
// Test Case 2: IFG Scope ('8A-V3-PB-85')
|
||||||
|
console.log('\nRunning Test 2: Scoping IFG (8A-V3-PB-85)...');
|
||||||
|
const ifgDevices = await fetchSecurityDevices(null, '8A-V3-PB-85');
|
||||||
|
console.log(`- Retrieved ${ifgDevices.length} security devices.`);
|
||||||
|
|
||||||
|
for (const dev of ifgDevices) {
|
||||||
|
const ip = dev.ip_address;
|
||||||
|
|
||||||
|
// Assert that no JRP IP address is leaked
|
||||||
|
const isJrpIp = ip.startsWith('10.1.') || ip.startsWith('10.0.') || 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.') || ip.startsWith('10.102.');
|
||||||
|
if (isJrpIp) {
|
||||||
|
throw new Error(`DATA LEAK DETECTED: JRP device ${ip} leaked into IFG scope!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the IP belongs to IFG subnets (10.6.x.x) or link-local
|
||||||
|
const isIfgIp = ip.startsWith('10.6.') || ip.startsWith('fe80:');
|
||||||
|
if (!isIfgIp) {
|
||||||
|
throw new Error(`IP ${ip} does not match IFG subnet range!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✓ Test 2 Passed! No cross-tenant leakages for IFG.');
|
||||||
|
|
||||||
|
console.log('\n=== ALL SECURITY TENANT ISOLATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest().catch(err => {
|
||||||
|
console.error('\n❌ TEST FAILED:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+12
-190
@@ -1,196 +1,18 @@
|
|||||||
// patch_netify.js — patches the fetchAgentDetails function in netify.js
|
const fs = require('fs');
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const filePath = path.join(__dirname, '..', 'backend', 'netify.js');
|
const filePath = path.join(__dirname, '..', 'backend', 'netify.js');
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
// Find the start marker (after the top_apps mapping block)
|
// Replace events block
|
||||||
const startMarker = ' // ── 2. Distinct devices for this agent ─────────────────────────────────────\n const devRows = d.prepare(`\n WHERE src_mac IN (${ph})\n ORDER BY last_seen DESC\n LIMIT 50\n `).all(...macs);';
|
const oldEventsBlockPattern = /\/\/ ── 6\. Events filtered by agent IPs & MACs ─────────────────────────────────[\s\S]*?const events = allEvents\.slice\(0, 100\);/;
|
||||||
|
if (oldEventsBlockPattern.test(content)) {
|
||||||
|
console.log("Found events block! Replacing...");
|
||||||
|
content = content.replace(oldEventsBlockPattern, `// ── 6. Events filtered by agent (aligned with events page) ───────────────
|
||||||
|
const events = db.getLatestEvents(200, null, agentUuid);`);
|
||||||
|
} else {
|
||||||
|
console.error("Could not find events block!");
|
||||||
|
}
|
||||||
|
|
||||||
const endMarker = 'return { agent_uuid: agentUuid, agent_label: label, summary, devices, flows, top_apps };\r\n}';
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
console.log("Successfully patched backend/netify.js events section!");
|
||||||
const startIdx = content.indexOf(' // ── 2. Distinct devices for this agent ─────────────────────────────────────');
|
|
||||||
const endIdx = content.indexOf('return { agent_uuid: agentUuid, agent_label: label, summary, devices, flows, top_apps };\r\n}');
|
|
||||||
|
|
||||||
if (startIdx === -1) { console.error('START MARKER NOT FOUND'); process.exit(1); }
|
|
||||||
if (endIdx === -1) { console.error('END MARKER NOT FOUND'); process.exit(1); }
|
|
||||||
|
|
||||||
console.log(`Found start at char ${startIdx}, end at char ${endIdx}`);
|
|
||||||
|
|
||||||
const endOffset = endIdx + endMarker.length;
|
|
||||||
|
|
||||||
const replacement = ` // ── 2. Distinct devices for this agent ─────────────────────────────────────
|
|
||||||
const devRows = d.prepare(\`
|
|
||||||
SELECT f.src_ip AS ip_address,
|
|
||||||
f.src_mac AS mac_address,
|
|
||||||
d.device_label,
|
|
||||||
d.device_type,
|
|
||||||
d.os_label,
|
|
||||||
d.manufacturer,
|
|
||||||
SUM(f.bytes_download) AS dl,
|
|
||||||
SUM(f.bytes_upload) AS ul,
|
|
||||||
MAX(f.last_seen) AS last_seen
|
|
||||||
FROM flows f
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT ip_address, device_label, device_type, os_label, manufacturer
|
|
||||||
FROM devices
|
|
||||||
GROUP BY ip_address
|
|
||||||
) d ON d.ip_address = f.src_ip
|
|
||||||
WHERE f.src_mac IN (\${ph})
|
|
||||||
GROUP BY f.src_ip
|
|
||||||
ORDER BY dl DESC
|
|
||||||
LIMIT 100
|
|
||||||
\`).all(...macs);
|
|
||||||
|
|
||||||
const agentIPs = [...new Set(devRows.map(r => r.ip_address).filter(Boolean))];
|
|
||||||
const phIPs = agentIPs.length > 0 ? agentIPs.map(() => '?').join(',') : null;
|
|
||||||
|
|
||||||
const latestEncAudit = d.prepare(\`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit\`).get()?.t;
|
|
||||||
const riskMap = {};
|
|
||||||
if (latestEncAudit) {
|
|
||||||
const riskRows = d.prepare(\`SELECT ip_address, encrypted_pct, risk_level FROM intel_encryption_audit WHERE fetched_at = ?\`).all(latestEncAudit);
|
|
||||||
for (const r of riskRows) {
|
|
||||||
if (r.ip_address) riskMap[r.ip_address] = { encrypted_pct: r.encrypted_pct, risk_level: r.risk_level };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const insecureIPs = new Set(
|
|
||||||
phIPs ? d.prepare(\`SELECT DISTINCT ip_address FROM intel_insecure_protocols WHERE ip_address IN (\${phIPs})\`).all(...agentIPs).map(r => r.ip_address) : []
|
|
||||||
);
|
|
||||||
|
|
||||||
const devices = devRows.map(r => ({
|
|
||||||
ip_address : r.ip_address,
|
|
||||||
mac_address : r.mac_address,
|
|
||||||
device_label : r.device_label || r.ip_address || 'Unknown',
|
|
||||||
device_type : r.device_type || null,
|
|
||||||
os_label : r.os_label || null,
|
|
||||||
manufacturer : r.manufacturer || null,
|
|
||||||
last_seen : r.last_seen || null,
|
|
||||||
download : r.dl ?? 0,
|
|
||||||
upload : r.ul ?? 0,
|
|
||||||
encrypted_pct: riskMap[r.ip_address]?.encrypted_pct ?? null,
|
|
||||||
risk_level : riskMap[r.ip_address]?.risk_level ?? null,
|
|
||||||
has_insecure : insecureIPs.has(r.ip_address),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ── 3. Recent flows for this agent ─────────────────────────────────────────
|
|
||||||
const flowRows = d.prepare(\`
|
|
||||||
SELECT src_ip, dst_ip, dst_port, protocol, app_label, domain,
|
|
||||||
bytes_download AS download, bytes_upload AS upload, last_seen
|
|
||||||
FROM flows
|
|
||||||
WHERE src_mac IN (\${ph})
|
|
||||||
ORDER BY last_seen DESC
|
|
||||||
LIMIT 100
|
|
||||||
\`).all(...macs);
|
|
||||||
|
|
||||||
const flows = flowRows.map(r => ({
|
|
||||||
src_ip : r.src_ip,
|
|
||||||
dst_ip : r.dst_ip,
|
|
||||||
dst_port : r.dst_port,
|
|
||||||
protocol : r.protocol,
|
|
||||||
app_label : r.app_label,
|
|
||||||
domain : r.domain,
|
|
||||||
download : r.download ?? 0,
|
|
||||||
upload : r.upload ?? 0,
|
|
||||||
last_seen : r.last_seen,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ── 4. Summary stats ────────────────────────────────────────────────────────
|
|
||||||
const sumRow = d.prepare(\`
|
|
||||||
SELECT COUNT(DISTINCT src_ip) AS device_count,
|
|
||||||
COUNT(*) AS flow_count,
|
|
||||||
SUM(bytes_download) AS total_download,
|
|
||||||
SUM(bytes_upload) AS total_upload
|
|
||||||
FROM flows
|
|
||||||
WHERE src_mac IN (\${ph})
|
|
||||||
\`).get(...macs);
|
|
||||||
|
|
||||||
const summary = sumRow ? {
|
|
||||||
total_devices : sumRow.device_count ?? 0,
|
|
||||||
active_flows : sumRow.flow_count ?? 0,
|
|
||||||
bandwidth_down : sumRow.total_download ?? 0,
|
|
||||||
bandwidth_up : sumRow.total_upload ?? 0,
|
|
||||||
} : null;
|
|
||||||
|
|
||||||
// ── 5. Security Intel filtered by agent IPs & MACs ─────────────────────────
|
|
||||||
const encryptionRows = (latestEncAudit && phIPs)
|
|
||||||
? d.prepare(\`SELECT ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level, detected_at FROM intel_encryption_audit WHERE fetched_at = ? AND ip_address IN (\${phIPs}) ORDER BY CASE risk_level WHEN 'Rawan' THEN 1 WHEN 'Sedang' THEN 2 ELSE 3 END\`).all(latestEncAudit, ...agentIPs)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const insecureProtoRows = phIPs
|
|
||||||
? d.prepare(\`SELECT ip_address, mac_address, protocol, risk, app_label, dst_ip, dst_port, download, upload, detected_at FROM intel_insecure_protocols WHERE ip_address IN (\${phIPs}) ORDER BY detected_at DESC LIMIT 50\`).all(...agentIPs)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
let unencPwdRows = d.prepare(\`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE mac_address IN (\${ph}) ORDER BY detected_at DESC LIMIT 50\`).all(...macs);
|
|
||||||
if (unencPwdRows.length === 0 && phIPs) {
|
|
||||||
unencPwdRows = d.prepare(\`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE ip_address IN (\${phIPs}) ORDER BY detected_at DESC LIMIT 50\`).all(...agentIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestRepSnap = d.prepare(\`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation\`).get()?.t;
|
|
||||||
const ipReputRows = (latestRepSnap && phIPs)
|
|
||||||
? d.prepare(\`SELECT ip_address, local_ip, mac_address, reputation, score, country, app_label, blacklisted, download, upload, detected_at FROM intel_ip_reputation WHERE fetched_at = ? AND (local_ip IN (\${phIPs}) OR ip_address IN (\${phIPs})) ORDER BY score DESC LIMIT 50\`).all(latestRepSnap, ...agentIPs, ...agentIPs)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
let torRows = d.prepare(\`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE mac_address IN (\${ph}) ORDER BY detected_at DESC LIMIT 20\`).all(...macs);
|
|
||||||
if (torRows.length === 0 && phIPs) {
|
|
||||||
torRows = d.prepare(\`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE ip_address IN (\${phIPs}) ORDER BY detected_at DESC LIMIT 20\`).all(...agentIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vpnRows = d.prepare(\`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE mac_address IN (\${ph}) ORDER BY detected_at DESC LIMIT 20\`).all(...macs);
|
|
||||||
if (vpnRows.length === 0 && phIPs) {
|
|
||||||
vpnRows = d.prepare(\`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE ip_address IN (\${phIPs}) ORDER BY detected_at DESC LIMIT 20\`).all(...agentIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverDiscRows = phIPs
|
|
||||||
? d.prepare(\`SELECT ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload, detected_at FROM intel_server_discovery WHERE ip_address IN (\${phIPs}) ORDER BY detected_at DESC LIMIT 50\`).all(...agentIPs)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const security = {
|
|
||||||
encryption_audit : encryptionRows,
|
|
||||||
insecure_protocols : insecureProtoRows,
|
|
||||||
unencrypted_passwords: unencPwdRows,
|
|
||||||
ip_reputation : ipReputRows,
|
|
||||||
tor_detections : torRows,
|
|
||||||
vpn_detections : vpnRows,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── 6. Events filtered by agent IPs & MACs ─────────────────────────────────
|
|
||||||
const eventsByIP = phIPs ? d.prepare(\`SELECT event_id, event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE ip_address IN (\${phIPs}) ORDER BY event_at DESC LIMIT 100\`).all(...agentIPs) : [];
|
|
||||||
const eventsByMAC = d.prepare(\`SELECT event_id, event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE mac_address IN (\${ph}) ORDER BY event_at DESC LIMIT 100\`).all(...macs);
|
|
||||||
|
|
||||||
const seenEvt = new Set();
|
|
||||||
const allEvents = [];
|
|
||||||
for (const r of [...eventsByIP, ...eventsByMAC]) {
|
|
||||||
const key = r.event_id || \`\${r.ip_address}:\${r.event_at}\`;
|
|
||||||
if (!seenEvt.has(key)) {
|
|
||||||
seenEvt.add(key);
|
|
||||||
allEvents.push({ event_id: r.event_id, event_type: r.event_type, severity: r.severity, ip_address: r.ip_address, mac_address: r.mac_address, description: r.description, event_at: r.event_at });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allEvents.sort((a, b) => (b.event_at || '').localeCompare(a.event_at || ''));
|
|
||||||
const events = allEvents.slice(0, 100);
|
|
||||||
|
|
||||||
// ── 7. MAC bandwidth for this agent's MACs ──────────────────────────────────
|
|
||||||
const latestMacSnap = d.prepare(\`SELECT MAX(fetched_at) AS t FROM mac_bandwidth\`).get()?.t;
|
|
||||||
const mac_bandwidth = latestMacSnap
|
|
||||||
? d.prepare(\`SELECT mac_address, manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (\${ph}) ORDER BY download DESC\`).all(latestMacSnap, ...macs)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
agent_uuid : agentUuid,
|
|
||||||
agent_label : label,
|
|
||||||
summary,
|
|
||||||
devices,
|
|
||||||
flows,
|
|
||||||
top_apps,
|
|
||||||
security,
|
|
||||||
events,
|
|
||||||
mac_bandwidth,
|
|
||||||
server_discovery: serverDiscRows,
|
|
||||||
};
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const newContent = content.slice(0, startIdx) + replacement + content.slice(endOffset);
|
|
||||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
||||||
console.log('SUCCESS: Patched netify.js, new length:', newContent.length);
|
|
||||||
@@ -41,11 +41,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port:
|
|||||||
"1813": "RADIUS Accounting Server"
|
"1813": "RADIUS Accounting Server"
|
||||||
};
|
};
|
||||||
|
|
||||||
const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
let mainLabel = label;
|
||||||
|
let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
||||||
|
|
||||||
|
if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) {
|
||||||
|
mainLabel = appLabel;
|
||||||
|
subLabel = domain;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
main: label || `Port ${port}`,
|
main: mainLabel || `Port ${port}`,
|
||||||
sub: explanation
|
sub: subLabel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,18 @@ export function AgentDetailModal({ agentUuid, agentLabel, onClose }: Props) {
|
|||||||
{f.dst_ip && <IpDetails ip={f.dst_ip} />}
|
{f.dst_ip && <IpDetails ip={f.dst_ip} />}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-xs text-purple-300 max-w-[140px] truncate">{f.app_label || f.domain || "—"}</td>
|
<td className="px-3 py-2 whitespace-nowrap max-w-[200px]">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="font-semibold text-purple-300 text-xs truncate" title={f.app_label || "—"}>
|
||||||
|
{f.app_label || "—"}
|
||||||
|
</span>
|
||||||
|
{f.domain && (
|
||||||
|
<span className="text-[10px] text-slate-500 italic truncate" title={f.domain}>
|
||||||
|
{f.domain}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td>
|
<td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td>
|
||||||
<td className="px-3 py-2 text-right text-xs text-cyan-400 whitespace-nowrap">{fmtBytes(f.download)}</td>
|
<td className="px-3 py-2 text-right text-xs text-cyan-400 whitespace-nowrap">{fmtBytes(f.download)}</td>
|
||||||
<td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td>
|
<td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td>
|
||||||
|
|||||||
@@ -18,17 +18,17 @@ interface DataTableProps<T> {
|
|||||||
getRowClassName?: (row: T) => string;
|
getRowClassName?: (row: T) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<T>({
|
export function DataTable<T>({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
searchFilter,
|
searchFilter,
|
||||||
isLoading,
|
isLoading,
|
||||||
getRowClassName
|
getRowClassName
|
||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const filteredData = searchFilter
|
const filteredData = searchFilter
|
||||||
? data.filter(row => searchFilter(row, query))
|
? data.filter(row => searchFilter(row, query))
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ export function DataTable<T>({
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
filteredData.map((row, i) => (
|
filteredData.map((row, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={i}
|
key={i}
|
||||||
className={`hover:bg-primary/5 transition-colors duration-200 group ${getRowClassName ? getRowClassName(row) : ""}`}
|
className={`hover:bg-primary/5 transition-colors duration-200 group ${getRowClassName ? getRowClassName(row) : ""}`}
|
||||||
>
|
>
|
||||||
{columns.map((col, j) => (
|
{columns.map((col, j) => (
|
||||||
|
|||||||
@@ -55,11 +55,17 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port:
|
|||||||
"1813": "RADIUS Accounting Server"
|
"1813": "RADIUS Accounting Server"
|
||||||
};
|
};
|
||||||
|
|
||||||
const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
let mainLabel = label;
|
||||||
|
let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
||||||
|
|
||||||
|
if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) {
|
||||||
|
mainLabel = appLabel;
|
||||||
|
subLabel = domain;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
main: label || `Port ${port}`,
|
main: mainLabel || `Port ${port}`,
|
||||||
sub: explanation
|
sub: subLabel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-79
@@ -661,117 +661,117 @@ export interface SecurityInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceInfo {
|
export interface DeviceInfo {
|
||||||
device_label : string | null;
|
device_label: string | null;
|
||||||
device_type : string | null;
|
device_type: string | null;
|
||||||
os_label : string | null;
|
os_label: string | null;
|
||||||
manufacturer : string | null;
|
manufacturer: string | null;
|
||||||
mac_address : string | null;
|
mac_address: string | null;
|
||||||
is_new : number | null;
|
is_new: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceAppItem {
|
export interface DeviceAppItem {
|
||||||
label : string; // domain name OR protocol name
|
label: string; // domain name OR protocol name
|
||||||
sub_label : string | null; // protocol when label is domain; null otherwise
|
sub_label: string | null; // protocol when label is domain; null otherwise
|
||||||
type : 'domain' | 'protocol' | 'port';
|
type: 'domain' | 'protocol' | 'port';
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
flow_count : number;
|
flow_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceDomainItem {
|
export interface DeviceDomainItem {
|
||||||
domain : string;
|
domain: string;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
flow_count : number;
|
flow_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceEncryption {
|
export interface DeviceEncryption {
|
||||||
encrypted_pct : number | null;
|
encrypted_pct: number | null;
|
||||||
encrypted_bytes : number | null;
|
encrypted_bytes: number | null;
|
||||||
unencrypted_bytes: number | null;
|
unencrypted_bytes: number | null;
|
||||||
total_bytes : number | null;
|
total_bytes: number | null;
|
||||||
risk_level : string | null;
|
risk_level: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceServerItem {
|
export interface DeviceServerItem {
|
||||||
server_type : string | null;
|
server_type: string | null;
|
||||||
hostname : string | null;
|
hostname: string | null;
|
||||||
port : number | null;
|
port: number | null;
|
||||||
protocol : string | null;
|
protocol: string | null;
|
||||||
os_label : string | null;
|
os_label: string | null;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
detected_at : string | null;
|
detected_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DevicePwdItem {
|
export interface DevicePwdItem {
|
||||||
dst_ip : string | null;
|
dst_ip: string | null;
|
||||||
dst_port : number | null;
|
dst_port: number | null;
|
||||||
protocol : string | null;
|
protocol: string | null;
|
||||||
username : string | null;
|
username: string | null;
|
||||||
severity : string | null;
|
severity: string | null;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
detected_at : string | null;
|
detected_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceReputationItem {
|
export interface DeviceReputationItem {
|
||||||
remote_ip : string | null;
|
remote_ip: string | null;
|
||||||
local_ip : string | null;
|
local_ip: string | null;
|
||||||
reputation : string | null;
|
reputation: string | null;
|
||||||
score : number | null;
|
score: number | null;
|
||||||
country : string | null;
|
country: string | null;
|
||||||
app_label : string | null;
|
app_label: string | null;
|
||||||
blacklisted : boolean;
|
blacklisted: boolean;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceVpnItem {
|
export interface DeviceVpnItem {
|
||||||
vpn_type : string | null;
|
vpn_type: string | null;
|
||||||
remote_ip : string | null;
|
remote_ip: string | null;
|
||||||
protocol : string | null;
|
protocol: string | null;
|
||||||
country : string | null;
|
country: string | null;
|
||||||
confidence : number | null;
|
confidence: number | null;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
detected_at : string | null;
|
detected_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceEventItem {
|
export interface DeviceEventItem {
|
||||||
event_type : string | null;
|
event_type: string | null;
|
||||||
severity : string | null;
|
severity: string | null;
|
||||||
ip_address : string | null;
|
ip_address: string | null;
|
||||||
mac_address : string | null;
|
mac_address: string | null;
|
||||||
description : string | null;
|
description: string | null;
|
||||||
event_at : string | null;
|
event_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceMacBandwidth {
|
export interface DeviceMacBandwidth {
|
||||||
mac_address : string;
|
mac_address: string;
|
||||||
manufacturer : string | null;
|
manufacturer: string | null;
|
||||||
download : number;
|
download: number;
|
||||||
upload : number;
|
upload: number;
|
||||||
total : number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceDetails {
|
export interface DeviceDetails {
|
||||||
ip : string;
|
ip: string;
|
||||||
mac_address : string | null;
|
mac_address: string | null;
|
||||||
total_download : number;
|
total_download: number;
|
||||||
total_upload : number;
|
total_upload: number;
|
||||||
flow_count : number;
|
flow_count: number;
|
||||||
device_info : DeviceInfo;
|
device_info: DeviceInfo;
|
||||||
top_apps : DeviceAppItem[];
|
top_apps: DeviceAppItem[];
|
||||||
top_domains : DeviceDomainItem[];
|
top_domains: DeviceDomainItem[];
|
||||||
flows : DeviceFlowItem[];
|
flows: DeviceFlowItem[];
|
||||||
encryption : DeviceEncryption | null;
|
encryption: DeviceEncryption | null;
|
||||||
server_discovery : DeviceServerItem[];
|
server_discovery: DeviceServerItem[];
|
||||||
unencrypted_passwords: DevicePwdItem[];
|
unencrypted_passwords: DevicePwdItem[];
|
||||||
ip_reputation : DeviceReputationItem[];
|
ip_reputation: DeviceReputationItem[];
|
||||||
vpn_detections : DeviceVpnItem[];
|
vpn_detections: DeviceVpnItem[];
|
||||||
events : DeviceEventItem[];
|
events: DeviceEventItem[];
|
||||||
mac_bandwidth : DeviceMacBandwidth | null;
|
mac_bandwidth: DeviceMacBandwidth | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user