2718 lines
103 KiB
JavaScript
2718 lines
103 KiB
JavaScript
// backend/netify.js
|
|
const path = require('path');
|
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
|
|
const axios = require('axios');
|
|
|
|
const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
|
const JWT_TOKEN = process.env.BACKONE_JWT_TOKEN || process.env.NETIFY_TOKEN || process.env.NETIFY_JWT_TOKEN;
|
|
const SITE_UUID = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID;
|
|
const agentMap = {};
|
|
|
|
function headersSite(useApiKey = false) {
|
|
const token = process.env.BACKONE_API_KEY || process.env.NETIFY_API_KEY || JWT_TOKEN;
|
|
const headers = { 'x-api-key': token, 'Accept': 'application/json' };
|
|
if (SITE_UUID) {
|
|
headers['x-net-site'] = SITE_UUID;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
|
|
|
|
function fixDates(obj) {
|
|
if (Array.isArray(obj)) {
|
|
for (let i = 0; i < obj.length; i++) fixDates(obj[i]);
|
|
} else if (obj !== null && typeof obj === 'object') {
|
|
for (const key in obj) {
|
|
if ((key === 'first_seen_at' || key === 'last_seen_at' || key.endsWith('_at')) && obj[key] && typeof obj[key].date === 'string') {
|
|
let d = obj[key].date;
|
|
if (d.includes(' ') && !d.endsWith('Z')) {
|
|
obj[key].date = d.replace(' ', 'T') + 'Z';
|
|
} else if (!d.endsWith('Z') && !d.includes('+') && d.includes('T')) {
|
|
obj[key].date = d + 'Z';
|
|
}
|
|
} else if (typeof obj[key] === 'object') {
|
|
fixDates(obj[key]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function netifyFetch(path, params = {}, useApiKey = false, agentUuid = null) {
|
|
if (agentUuid) {
|
|
const agentId = agentMap[agentUuid];
|
|
if (agentId) {
|
|
params.filter_agents = `[${agentId}]`;
|
|
} else {
|
|
params.settings_agent = agentUuid;
|
|
}
|
|
}
|
|
if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) {
|
|
console.error('[Netify] Failed due to invalid config', {hasToken: !!JWT_TOKEN, SITE_UUID});
|
|
return null;
|
|
}
|
|
try {
|
|
const res = await axios.get(`${BASE_URL}${path}`, {
|
|
headers: headersSite(useApiKey), params, timeout: 30000,
|
|
});
|
|
const json = res.data;
|
|
if (json?.status_code !== 0) {
|
|
console.error(`[Netify] API Error ${json?.status_code} pada ${path}: ${json?.status_message}`);
|
|
return null;
|
|
}
|
|
if (json && json.data) fixDates(json.data);
|
|
return json?.data ?? null;
|
|
} catch (err) {
|
|
const s = err.response?.status;
|
|
const msg = JSON.stringify(err.response?.data ?? err.message);
|
|
console.error(`[Netify] ${s ?? 'ERR'} ${path}: ${msg}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ─── TOP APPS: download + upload digabung ─────────────────────────────────────
|
|
async function fetchTopApps(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
|
|
// Buat map upload berdasarkan app_id
|
|
const ulMap = {};
|
|
if (ulData) {
|
|
for (const r of ulData) {
|
|
const id = r.application?.id;
|
|
if (id) ulMap[id] = r.upload ?? 0;
|
|
}
|
|
}
|
|
|
|
return dlData.map(r => ({
|
|
app_id : r.application?.id ?? null,
|
|
app_label : r.application?.label ?? 'Unknown',
|
|
app_tag : r.application?.tag ?? null,
|
|
category : r.application?.category?.label ?? null,
|
|
favicon : r.application?.favicon ?? null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.application?.id] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[r.application?.id] ?? 0),
|
|
flows : r.flows ?? 0,
|
|
}));
|
|
}
|
|
|
|
// ─── TOP DEVICES: download + upload digabung ──────────────────────────────────
|
|
async function fetchTopDevices(interval = 1440, limit = 50, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
|
|
// Map upload berdasarkan IP address
|
|
const ulMap = {};
|
|
if (ulData) {
|
|
for (const r of ulData) {
|
|
const ip = r.local_ip?.address ?? String(r.local_ip);
|
|
ulMap[ip] = r.upload ?? 0;
|
|
}
|
|
}
|
|
|
|
return dlData.map(r => {
|
|
const ip = r.local_ip?.address ?? String(r.local_ip ?? '');
|
|
return {
|
|
ip_address : ip,
|
|
mac_address : null,
|
|
device_label : ip,
|
|
device_type : null,
|
|
os_label : null,
|
|
manufacturer : null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[ip] ?? 0,
|
|
last_seen : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
// ─── PORT-TO-SERVICE MAPPING — untuk enrichment flows ────────────────────────
|
|
const PORT_SERVICE_MAP = {
|
|
80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt',
|
|
53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS',
|
|
25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP',
|
|
22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC',
|
|
21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control',
|
|
3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB',
|
|
1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T',
|
|
1723: 'PPTP', 1701: 'L2TP',
|
|
67: 'DHCP', 68: 'DHCP Client', 123: 'NTP',
|
|
161: 'SNMP', 162: 'SNMP Trap', 514: 'Syslog',
|
|
6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent',
|
|
9993: 'ZeroTier VPN', 3478: 'STUN/TURN', 5004: 'RTP Media',
|
|
5060: 'SIP', 5061: 'SIP TLS',
|
|
8883: 'MQTT TLS', 1883: 'MQTT',
|
|
179: 'BGP', 520: 'RIP',
|
|
};
|
|
|
|
// ─── FLOWS: endpoint /data/flows dengan enrichment app/domain ─────────────────
|
|
// NOTE: API flows tidak punya field app/domain — enrichment dilakukan via:
|
|
// 1. PORT_SERVICE_MAP (reliable, berdasarkan dst port)
|
|
// 2. tls_sni field (satu-satunya SNI field valid, tapi nilai sering kosong)
|
|
async function fetchFlows(limit = 500, agentUuid = null) {
|
|
// Hanya fetch flows + tls_sni (satu-satunya SNI field yang valid = bukan 422)
|
|
const [raw, tslSniRaw] = await Promise.all([
|
|
netifyFetch('/data/flows', { settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid),
|
|
]);
|
|
|
|
if (!raw || !Array.isArray(raw)) return null;
|
|
|
|
// Build TLS SNI lookup — filter yang tidak kosong
|
|
const sniList = [];
|
|
if (tslSniRaw && Array.isArray(tslSniRaw)) {
|
|
for (const r of tslSniRaw) {
|
|
const sni = r.tls_sni;
|
|
if (sni && sni.trim() !== '') {
|
|
sniList.push(sni.replace(/^\*\./, '').trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
return raw.map(r => {
|
|
const port = r.remote_port ?? null;
|
|
// Primary enrichment: port → service name
|
|
const portService = port ? (PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
|
|
|
|
let domain = null;
|
|
let appLabel = portService;
|
|
|
|
// Secondary enrichment: for HTTPS flows, use SNI list if available
|
|
if ((port === 443 || port === 8443) && sniList.length > 0) {
|
|
domain = sniList[0];
|
|
}
|
|
|
|
return {
|
|
flow_id : String(r.flow_id ?? ''),
|
|
src_ip : r.local_ip?.address ?? null,
|
|
src_mac : r.local_mac ?? r.mac?.address ?? null,
|
|
dst_ip : r.remote_ip?.address ?? null,
|
|
dst_port : port,
|
|
protocol : r.ip_protocol?.label ?? null,
|
|
app_label : appLabel,
|
|
domain : domain,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
first_seen : r.first_seen_at?.date ?? null,
|
|
last_seen : r.last_seen_at?.date ?? null,
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
// ─── PROTOCOLS ────────────────────────────────────────────────────────────────
|
|
async function fetchTopProtocols(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
|
|
const ulMap = {};
|
|
if (ulData) {
|
|
for (const r of ulData) {
|
|
const id = r.protocol?.id;
|
|
if (id) ulMap[id] = r.upload ?? 0;
|
|
}
|
|
}
|
|
|
|
return dlData.map(r => ({
|
|
protocol_id : r.protocol?.id ?? null,
|
|
protocol_label : r.protocol?.label ?? 'Unknown',
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.protocol?.id] ?? 0,
|
|
flows : r.flows ?? 0,
|
|
}));
|
|
}
|
|
|
|
// ─── COUNTRIES ────────────────────────────────────────────────────────────────
|
|
async function fetchTopCountries(interval = 1440, limit = 15, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
|
|
const ulMap = {};
|
|
if (ulData) {
|
|
for (const r of ulData) {
|
|
const code = r.country?.code;
|
|
if (code) ulMap[code] = r.upload ?? 0;
|
|
}
|
|
}
|
|
|
|
return dlData.map(r => ({
|
|
country_code : r.country?.code ?? null,
|
|
country_name : r.country?.label ?? 'Unknown',
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.country?.code] ?? 0,
|
|
flows : r.flows ?? 0,
|
|
}));
|
|
}
|
|
|
|
// ─── DNS/HOSTNAME — gunakan tls_sni karena hostname endpoint timeout ──────────
|
|
async function fetchTopDomains(interval = 1440, limit = 50, agentUuid = null) {
|
|
// NOTE: /data/stats/top/hostname/download selalu timeout
|
|
// Gunakan tls_sni yang confirmed bekerja di API ini
|
|
const raw = await netifyFetch('/data/stats/top/tls_sni/download', {
|
|
filter_interval: interval, settings_limit: limit,
|
|
}, false, agentUuid);
|
|
if (!raw) return null;
|
|
|
|
return raw
|
|
.filter(r => r.tls_sni && r.tls_sni.trim() !== '')
|
|
.map(r => ({
|
|
domain : r.tls_sni.replace(/^\*\./, '').trim(),
|
|
app_label : null,
|
|
category : 'HTTPS/TLS',
|
|
download : r.download ?? 0,
|
|
query_count : r.download ?? 0,
|
|
}));
|
|
}
|
|
|
|
|
|
// ─── BANDWIDTH SUMMARY untuk timeline ─────────────────────────────────────────
|
|
async function fetchBandwidthSummary(interval = 30, agentUuid = null) {
|
|
const rawSummary = await netifyFetch('/data/stats/summary', { filter_interval: interval }, false, agentUuid);
|
|
if (rawSummary) {
|
|
return {
|
|
download: rawSummary.download ?? 0,
|
|
upload: rawSummary.upload ?? 0,
|
|
flows: rawSummary.flow_hourly_count ?? 0,
|
|
download_speed: rawSummary.download_speed ?? 0,
|
|
upload_speed: rawSummary.upload_speed ?? 0,
|
|
flow_speed: rawSummary.flow_speed ?? 0,
|
|
devices: 0,
|
|
};
|
|
}
|
|
|
|
// Fallback to old way (aggregate top 100 application stats)
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid),
|
|
]);
|
|
|
|
const download = (dlData ?? []).reduce((s, r) => s + (r.download ?? 0), 0);
|
|
const upload = (ulData ?? []).reduce((s, r) => s + (r.upload ?? 0), 0);
|
|
const flows = (dlData ?? []).reduce((s, r) => s + (r.flows ?? 0), 0);
|
|
|
|
return {
|
|
download,
|
|
upload,
|
|
flows,
|
|
download_speed: 0,
|
|
upload_speed: 0,
|
|
flow_speed: 0,
|
|
devices: dlData?.length ?? 0
|
|
};
|
|
}
|
|
|
|
// ─── THREATS — gunakan flows dengan filter anomali sebagai fallback ────────────
|
|
async function fetchCyberThreats(limit = 50, agentUuid = null) {
|
|
// Events/threats belum ada di v1 — return array kosong agar tidak error
|
|
// Akan diisi saat endpoint ditemukan
|
|
return [];
|
|
}
|
|
|
|
// ─── EVENTS ───────────────────────────────────────────────────────────────────
|
|
async function fetchEvents(limit = 50, agentUuid = null) {
|
|
const raw = await netifyFetch('/event/events', { settings_limit: limit }, false, agentUuid);
|
|
if (!raw || !Array.isArray(raw)) return [];
|
|
|
|
return raw.map(r => {
|
|
let msg = r.label || '';
|
|
if (r.description) {
|
|
try {
|
|
const descObj = JSON.parse(r.description);
|
|
msg = descObj.default || r.label || '';
|
|
if (descObj.tags) {
|
|
for (const k in descObj.tags) {
|
|
const val = Array.isArray(descObj.tags[k]) ? descObj.tags[k][0] : descObj.tags[k];
|
|
msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
msg = r.description;
|
|
}
|
|
}
|
|
|
|
let sevLabel = 'Info';
|
|
if (r.severity >= 30) sevLabel = 'Critical';
|
|
else if (r.severity >= 20) sevLabel = 'High';
|
|
else if (r.severity >= 10) sevLabel = 'Warning';
|
|
|
|
let srcIp = null;
|
|
if (r.description) {
|
|
try {
|
|
const descObj = JSON.parse(r.description);
|
|
srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null;
|
|
} catch {}
|
|
}
|
|
|
|
return {
|
|
event_id: r.id || null,
|
|
event_type: r.basename || 'unknown',
|
|
severity: sevLabel,
|
|
mac_address: r.additional?.device?.mac?.address || null,
|
|
ip_address: srcIp,
|
|
description: msg,
|
|
event_at: r.created_at?.date || new Date().toISOString()
|
|
};
|
|
});
|
|
}
|
|
|
|
async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null) {
|
|
// Ambil lebih banyak bandwidth data (limit x2) supaya coverage IP lebih luas
|
|
const [intelRaw, dlData, ulData, flowsRaw] = await Promise.all([
|
|
netifyFetch('/intelligence/discovery/devices', { settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
]);
|
|
|
|
if (!intelRaw || !Array.isArray(intelRaw)) return [];
|
|
|
|
// Map bandwidth per IP
|
|
const dlMap = {};
|
|
const ulMap = {};
|
|
if (dlData) {
|
|
for (const r of dlData) {
|
|
const ip = r.local_ip?.address ?? String(r.local_ip);
|
|
if (ip) dlMap[ip] = r.download ?? 0;
|
|
}
|
|
}
|
|
if (ulData) {
|
|
for (const r of ulData) {
|
|
const ip = r.local_ip?.address ?? String(r.local_ip);
|
|
if (ip) ulMap[ip] = r.upload ?? 0;
|
|
}
|
|
}
|
|
|
|
// Enrich bandwidth from flows — aggregate per local_ip as fallback
|
|
// Bagi device yang IP-nya tidak ada di top stats (traffic kecil)
|
|
const dlFlowMap = {};
|
|
const ulFlowMap = {};
|
|
const macFlowMap = {};
|
|
const lastSeenFlowMap = {};
|
|
|
|
if (flowsRaw && Array.isArray(flowsRaw)) {
|
|
for (const f of flowsRaw) {
|
|
const ip = f.local_ip?.address;
|
|
if (!ip) continue;
|
|
|
|
if (!dlMap[ip]) {
|
|
dlFlowMap[ip] = (dlFlowMap[ip] ?? 0) + (f.download ?? 0);
|
|
}
|
|
if (!ulMap[ip]) {
|
|
ulFlowMap[ip] = (ulFlowMap[ip] ?? 0) + (f.upload ?? 0);
|
|
}
|
|
|
|
// Correlate MAC Address from flow
|
|
if (!macFlowMap[ip]) {
|
|
const flowMac = f.local_mac ?? f.mac?.address;
|
|
if (flowMac && flowMac !== '00:00:00:00:00:00') {
|
|
macFlowMap[ip] = flowMac;
|
|
}
|
|
}
|
|
|
|
// Correlate Timestamp from flow
|
|
const flowTime = f.last_seen_at?.date || f.created_at?.date;
|
|
if (flowTime) {
|
|
if (!lastSeenFlowMap[ip] || new Date(flowTime) > new Date(lastSeenFlowMap[ip])) {
|
|
lastSeenFlowMap[ip] = flowTime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const resolvedList = intelRaw.map(r => {
|
|
const ip = r.ip?.address ?? null;
|
|
let mac = r.mac_address ?? r.discovery_mac?.address ?? null;
|
|
if (!mac && ip && macFlowMap[ip]) mac = macFlowMap[ip];
|
|
|
|
const oui = mac ? mac.substring(0, 8).toUpperCase() : null;
|
|
const mfr = r.mac_vendor !== 'Unknown' && r.mac_vendor !== 'Local' ? r.mac_vendor : (OUI_MAP[oui] ?? r.mac_vendor ?? null);
|
|
|
|
// Get device_type — prefer discovery_type if meaningful, else device_type
|
|
const rawType = r.discovery_type?.label;
|
|
const deviceType = (rawType && rawType !== 'Unknown' && rawType !== 'Unclassified')
|
|
? rawType
|
|
: (r.device_type?.label && r.device_type.label !== 'Unclassified' ? r.device_type.label : rawType ?? null);
|
|
|
|
// Get OS — prefer discovery_os if meaningful, else device_os
|
|
const rawOs = r.discovery_os?.label;
|
|
const osLabel = (rawOs && rawOs !== 'Unknown' && rawOs !== 'Unclassified')
|
|
? rawOs
|
|
: (r.device_os?.label && r.device_os.label !== 'Unclassified' ? r.device_os.label : rawOs ?? null);
|
|
|
|
// Bandwidth: prioritize top stats, fallback to flows aggregation
|
|
const dl = ip ? (dlMap[ip] ?? dlFlowMap[ip] ?? 0) : 0;
|
|
const ul = ip ? (ulMap[ip] ?? ulFlowMap[ip] ?? 0) : 0;
|
|
|
|
let last_seen = r.last_seen_at?.date || r.discovery_mac?.last_seen_at?.date || null;
|
|
if (!last_seen && ip && lastSeenFlowMap[ip]) last_seen = lastSeenFlowMap[ip];
|
|
|
|
return {
|
|
ip_address : ip,
|
|
mac_address : mac,
|
|
device_label : r.device?.label || r.discovery_mac?.discovery_hardware || ip || 'Unknown',
|
|
device_type : deviceType,
|
|
os_label : osLabel,
|
|
manufacturer : mfr,
|
|
download : dl,
|
|
upload : ul,
|
|
last_seen : last_seen,
|
|
};
|
|
});
|
|
|
|
const seenIps = new Set(resolvedList.map(d => d.ip_address).filter(Boolean));
|
|
const allActiveIps = new Set([
|
|
...Object.keys(dlMap),
|
|
...Object.keys(ulMap)
|
|
]);
|
|
|
|
for (const ip of allActiveIps) {
|
|
if (!seenIps.has(ip)) {
|
|
seenIps.add(ip);
|
|
const dl = dlMap[ip] ?? dlFlowMap[ip] ?? 0;
|
|
const ul = ulMap[ip] ?? ulFlowMap[ip] ?? 0;
|
|
resolvedList.push({
|
|
ip_address : ip,
|
|
mac_address : macFlowMap[ip] || null,
|
|
device_label : ip,
|
|
device_type : 'LAN Client',
|
|
os_label : 'Windows/Linux',
|
|
manufacturer : 'Unknown',
|
|
download : dl,
|
|
upload : ul,
|
|
last_seen : lastSeenFlowMap[ip] || new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
|
|
return resolvedList.sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
|
|
}
|
|
|
|
// OUI lookup — manufacturer dari 3 oktet pertama MAC
|
|
const OUI_MAP = {
|
|
'60:BE:B4' : 'Ruckus Networks',
|
|
'74:6F:88' : 'Ruckus Networks',
|
|
'04:F4:1C' : 'MikroTik',
|
|
'F4:6D:3F' : 'TP-Link',
|
|
'B8:27:EB' : 'Raspberry Pi',
|
|
'DC:A6:32' : 'Raspberry Pi',
|
|
'E4:5F:01' : 'Raspberry Pi',
|
|
'00:50:56' : 'VMware',
|
|
'08:00:27' : 'VirtualBox',
|
|
'AC:17:02' : 'ASUSTek',
|
|
'B4:2E:99' : 'ASUSTek',
|
|
'18:31:BF' : 'ASUSTek',
|
|
'74:D0:2B' : 'Cisco',
|
|
'F8:72:EA' : 'Cisco',
|
|
'00:1A:A0' : 'Cisco',
|
|
'FC:FB:FB' : 'Cisco Meraki',
|
|
'88:15:44' : 'Cisco Meraki',
|
|
'00:18:0A' : 'Ubiquiti',
|
|
'04:18:D6' : 'Ubiquiti',
|
|
'24:A4:3C' : 'Ubiquiti',
|
|
'78:8A:20' : 'Ubiquiti',
|
|
'DC:9F:DB' : 'Ubiquiti',
|
|
'80:2A:A8' : 'Ubiquiti',
|
|
'FC:EC:DA' : 'Ubiquiti',
|
|
'00:27:22' : 'Ubiquiti',
|
|
'B4:FB:E4' : 'Samsung',
|
|
'8C:79:F0' : 'Samsung',
|
|
'F0:25:B7' : 'Samsung',
|
|
'78:BD:BC' : 'Samsung',
|
|
'3C:28:6D' : 'Apple',
|
|
'A4:C3:F0' : 'Apple',
|
|
'F8:FF:C2' : 'Apple',
|
|
'98:01:A7' : 'Apple',
|
|
'3C:22:FB' : 'Apple',
|
|
'F0:DB:F8' : 'Huawei',
|
|
'00:E0:FC' : 'Huawei',
|
|
'54:89:98' : 'Huawei',
|
|
'28:31:52' : 'Xiaomi',
|
|
'64:09:80' : 'Xiaomi',
|
|
'AC:C1:EE' : 'Xiaomi',
|
|
'50:64:2B' : 'Xiaomi',
|
|
'00:0C:29' : 'VMware',
|
|
'00:15:5D' : 'Microsoft Hyper-V',
|
|
'52:54:00' : 'QEMU/KVM',
|
|
};
|
|
|
|
// ─── TAMBAHAN FITUR 1-11 ──────────────────────────────────────────────────────
|
|
|
|
// 1. Top Application Category
|
|
async function fetchTopAppCategories(interval = 1440, limit = 15, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.application_category?.label ?? r.application_category;
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.application_category?.label ?? String(r.application_category ?? 'Unknown');
|
|
return {
|
|
category_label : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 2. Top Continent
|
|
async function fetchTopContinents(interval = 1440, limit = 10, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/continent/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/continent/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.continent?.label ?? String(r.continent ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.continent?.label ?? String(r.continent ?? 'Unknown');
|
|
return {
|
|
continent_name : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 3. Top Region
|
|
async function fetchTopRegions(interval = 1440, limit = 20, agentUuid = null) {
|
|
const raw = await netifyFetch('/data/stats/top/region/download', {
|
|
filter_interval: interval, settings_limit: limit,
|
|
}, false, agentUuid);
|
|
if (!raw) return null;
|
|
return raw.map(r => ({
|
|
region_name : r.region?.region_name?.trim() || '(Unknown Region)',
|
|
region_code : r.region?.region_code?.trim() || null,
|
|
country_name : r.region?.country_name ?? null,
|
|
country_code : r.region?.country_code ?? null,
|
|
download : r.download ?? 0,
|
|
}));
|
|
}
|
|
|
|
// 4. Top City
|
|
async function fetchTopCities(interval = 1440, limit = 20, agentUuid = null) {
|
|
const raw = await netifyFetch('/data/stats/top/city/download', {
|
|
filter_interval: interval, settings_limit: limit,
|
|
}, false, agentUuid);
|
|
if (!raw) return null;
|
|
return raw.map(r => ({
|
|
city_name : r.city?.city?.trim() || '(Unknown City)',
|
|
region_name : r.city?.region_name?.trim() || null,
|
|
country_name : r.city?.country_name ?? null,
|
|
country_code : r.city?.country_code ?? null,
|
|
download : r.download ?? 0,
|
|
}));
|
|
}
|
|
|
|
// 5. Top VLAN
|
|
async function fetchTopVLANs(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/vlan/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/vlan/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.vlan?.id ?? 0;
|
|
ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => ({
|
|
vlan_id : r.vlan?.id ?? 0,
|
|
vlan_label : r.vlan?.label ?? `VLAN ${r.vlan?.id ?? 0}`,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.vlan?.id ?? 0] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[r.vlan?.id ?? 0] ?? 0),
|
|
}));
|
|
}
|
|
|
|
// 6. Top Interface
|
|
async function fetchTopInterfaces(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/interface/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/interface/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.interface?.id;
|
|
if (key !== undefined) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => ({
|
|
iface_id : r.interface?.id ?? null,
|
|
iface_name : r.interface?.name ?? 'Unknown',
|
|
iface_role : r.interface?.role ?? null,
|
|
agent_id : r.interface?.agent_id ?? null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.interface?.id] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[r.interface?.id] ?? 0),
|
|
}));
|
|
}
|
|
|
|
// 7. Top Flow Type
|
|
async function fetchTopFlowTypes(interval = 1440, limit = 10, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/flow_type/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/flow_type/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.flow_type?.label ?? String(r.flow_type ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.flow_type?.label ?? String(r.flow_type ?? 'Unknown');
|
|
return {
|
|
flow_type_label : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 8. Top Flow Origin
|
|
async function fetchTopFlowOrigins(interval = 1440, limit = 10, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/flow_origin/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.flow_origin?.label ?? String(r.flow_origin ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.flow_origin?.label ?? String(r.flow_origin ?? 'Unknown');
|
|
return {
|
|
flow_origin_label : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 9. Top IP Version
|
|
async function fetchTopIPVersions(interval = 1440, limit = 5, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/ip_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/ip_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.ip_version?.label ?? String(r.ip_version ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.ip_version?.label ?? String(r.ip_version ?? 'Unknown');
|
|
return {
|
|
ip_version_label : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 10. Top Remote IP
|
|
async function fetchTopRemoteIPs(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/remote_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.remote_ip?.address ?? String(r.remote_ip ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const addr = r.remote_ip?.address ?? String(r.remote_ip ?? 'Unknown');
|
|
return {
|
|
remote_ip : addr,
|
|
ip_version : r.remote_ip?.version ?? null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[addr] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[addr] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 11. Top Local MAC + Discovery OS
|
|
async function fetchTopLocalMACs(interval = 1440, limit = 50, agentUuid = null) {
|
|
const [dlData, ulData, osData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/local_mac/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/local_mac/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: 20 }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
if (r.local_mac) ulMap[r.local_mac] = r.upload ?? 0;
|
|
}
|
|
// OS summary untuk info panel
|
|
const osList = (osData ?? []).map(r => ({
|
|
os_label : r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown'),
|
|
download : r.download ?? 0,
|
|
}));
|
|
return dlData.map(r => {
|
|
const mac = r.local_mac ?? 'Unknown';
|
|
const oui = mac.substring(0, 8).toUpperCase();
|
|
return {
|
|
mac_address : mac,
|
|
manufacturer : OUI_MAP[oui] ?? null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[mac] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[mac] ?? 0),
|
|
_os_summary : osList, // disertakan di item pertama saja
|
|
};
|
|
});
|
|
}
|
|
|
|
// ─── DISCOVERY OS (standalone) ───────────────────────────────────────────────
|
|
async function fetchTopDiscoveryOS(interval = 1440, limit = 20, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/discovery_os/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.discovery_os?.label ?? String(r.discovery_os ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown');
|
|
return {
|
|
os_label : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// ─── DPI FIELDS ───────────────────────────────────────────────────────────────
|
|
|
|
// TLS Version
|
|
async function fetchTLSVersions(interval = 1440, limit = 10, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.tls_version?.label ?? String(r.tls_version ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.tls_version?.label ?? String(r.tls_version ?? 'Unknown');
|
|
return {
|
|
tls_version : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// TLS Cipher
|
|
async function fetchTLSCiphers(interval = 1440, limit = 15, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.tls_cipher?.label ?? String(r.tls_cipher ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.tls_cipher?.label ?? String(r.tls_cipher ?? 'Unknown');
|
|
return {
|
|
tls_cipher : label,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// TLS Security Level
|
|
async function fetchTLSSecurity(interval = 1440, limit = 10, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.tls_security?.label ?? String(r.tls_security ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const label = r.tls_security?.label ?? String(r.tls_security ?? 'Unknown');
|
|
// Assign warna berdasarkan label untuk UI
|
|
const color = label === 'Recommended' ? 'green'
|
|
: label === 'Secure' ? 'blue'
|
|
: label === 'Weak' ? 'orange'
|
|
: label === 'Insecure' ? 'red'
|
|
: 'gray';
|
|
return {
|
|
tls_security : label,
|
|
color : color,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// NetBIOS Hostname (nama PC Windows)
|
|
async function fetchNetBIOSHostnames(interval = 1440, limit = 30, agentUuid = null) {
|
|
const [dlData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/netbios_hostname/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/netbios_hostname/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dlData) return null;
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const key = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return dlData.map(r => {
|
|
const name = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? 'Unknown');
|
|
return {
|
|
hostname : name,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[name] ?? 0,
|
|
total : (r.download ?? 0) + (ulMap[name] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
async function fetchLookupApplications(page = 1, limit = 50, search = '', agentUuid = null) {
|
|
if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) {
|
|
return {
|
|
applications: [
|
|
{ id: 1, label: 'YouTube', tag: 'video', category: { label: 'Streaming' } },
|
|
{ id: 2, label: 'Netflix', tag: 'video', category: { label: 'Streaming' } },
|
|
{ id: 3, label: 'Web Browsing', tag: 'web', category: { label: 'General' } },
|
|
{ id: 4, label: 'Google Services', tag: 'google', category: { label: 'Tech' } },
|
|
{ id: 5, label: 'WhatsApp', tag: 'im', category: { label: 'Chat' } }
|
|
],
|
|
pagination: { total_records: 5, current_page: 1, total_pages: 1 }
|
|
};
|
|
}
|
|
const params = {
|
|
settings_page: page,
|
|
settings_limit: limit,
|
|
};
|
|
if (search) {
|
|
params.filter_application = search;
|
|
}
|
|
try {
|
|
const res = await axios.get(`${BASE_URL}/lookup/applications`, {
|
|
headers: headersSite(true), params, timeout: 30000,
|
|
});
|
|
const json = res.data;
|
|
if (json?.status_code !== 0) {
|
|
console.error(`[Netify] API Error ${json?.status_code}: ${json?.status_message}`);
|
|
return { applications: [], pagination: {} };
|
|
}
|
|
return {
|
|
applications: json.data || [],
|
|
pagination: json.data_info || {}
|
|
};
|
|
} catch (err) {
|
|
console.error('[Netify] Lookup error:', err.message);
|
|
return { applications: [], pagination: {} };
|
|
}
|
|
}
|
|
|
|
// ─── DETAIL FETCHERS FOR INTERACTIVE MODALS (DB-BASED — API ignores filter params) ─────────
|
|
//
|
|
// NOTE: Netify Informatics API does NOT filter by filter_agent / filter_local_ip —
|
|
// all filter params are silently ignored and global data is returned.
|
|
// All detail queries therefore use the local SQLite DB (flows, devices tables).
|
|
//
|
|
// Agent ↔ src_mac mapping (determined empirically from flows data):
|
|
// 2F-TF-1D-GK (JRP Cibubur) → src_mac '60:be:b4:1f:05:96' (IPs 10.1.x.x, 10.0.x.x)
|
|
// 8A-V3-PB-85 (IFG LT.18) → src_mac '04:f4:1c:ce:c2:e6' (IPs 10.6.x.x, 192.168.9.x)
|
|
// F6-2V-DT-8A (CPI Balaraja) → src_mac '2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', etc (IPs 10.250.x.x)
|
|
|
|
const AGENT_LABELS = {
|
|
'2F-TF-1D-GK': 'JRP Cibubur',
|
|
'8A-V3-PB-85': 'IFG LT.18',
|
|
'F6-2V-DT-8A': 'CPI Balaraja',
|
|
'1R-79-J9-YE': 'CPI Balaraja WAN',
|
|
};
|
|
|
|
// Maps each agent UUID to its gateway/interface MAC address(es) in flows
|
|
const AGENT_MAC_MAP = {
|
|
'2F-TF-1D-GK': ['60:be:b4:1f:05:96'],
|
|
'8A-V3-PB-85': ['04:f4:1c:ce:c2:e6'],
|
|
'F6-2V-DT-8A': ['2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be',
|
|
'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36',
|
|
'60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32'],
|
|
'1R-79-J9-YE': ['70:85:6c:6d:f7:17', '70:85:6c:81:50:d4', 'a2:cc:8e:7d:39:51',
|
|
'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'],
|
|
};
|
|
|
|
// Build SQL IN clause placeholders
|
|
function inClause(arr) {
|
|
return arr.map(() => '?').join(',');
|
|
}
|
|
|
|
// Fetch all data for a specific agent (by UUID) — from local DB flows
|
|
async function fetchAgentDetails(agentUuid) {
|
|
const db = require('./database');
|
|
const d = db.getDB();
|
|
|
|
const label = AGENT_LABELS[agentUuid] || agentUuid;
|
|
const macs = AGENT_MAC_MAP[agentUuid];
|
|
|
|
const emptyResult = { agent_uuid: agentUuid, agent_label: label, summary: null, devices: [], flows: [], top_apps: [], security: { encryption_audit: [], insecure_protocols: [], unencrypted_passwords: [], ip_reputation: [], tor_detections: [], vpn_detections: [] }, events: [], mac_bandwidth: [], server_discovery: [] };
|
|
|
|
if (!macs || macs.length === 0) return emptyResult;
|
|
|
|
const ph = inClause(macs);
|
|
|
|
// ── 1. Top apps by this agent (from flows) ─────────────────────────────────
|
|
const appRows = db.getLatestBandwidthApps(15, null, agentUuid);
|
|
|
|
const top_apps = appRows.map(r => ({
|
|
app_id : null,
|
|
app_label : r.app_label,
|
|
category : null,
|
|
favicon : null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
}));
|
|
|
|
// ── 2. Distinct devices for this agent (using aligned subnet and MAC mapping) ─
|
|
let resolvedDevices = db.getLatestDevices(100, null, agentUuid);
|
|
|
|
// FALLBACK: If agent-specific API failed to return devices, extract from global using MACs
|
|
if (resolvedDevices.length === 0 && macs.length > 0) {
|
|
const latestDevGlobal = d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE agent_uuid IS NULL`).get()?.t;
|
|
if (latestDevGlobal) {
|
|
resolvedDevices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ? AND agent_uuid IS NULL AND mac_address IN (${ph})`).all(latestDevGlobal, ...macs);
|
|
}
|
|
}
|
|
|
|
const agentIPs = resolvedDevices.map(d => d.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 = resolvedDevices.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.fetched_at || null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 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 (using aligned flows list) ───────────────
|
|
let flows = db.getLatestFlows(100, null, agentUuid);
|
|
|
|
// FALLBACK: Extrapolate flows from global using MACs/IPs if agent-specific fails
|
|
if (flows.length === 0 && (macs.length > 0 || agentIPs.length > 0)) {
|
|
const latestFlowGlobal = d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE agent_uuid IS NULL`).get()?.t;
|
|
if (latestFlowGlobal) {
|
|
if (phIPs) {
|
|
flows = d.prepare(`SELECT * FROM flows WHERE fetched_at = ? AND agent_uuid IS NULL AND (local_mac IN (${ph}) OR local_ip IN (${phIPs})) ORDER BY download DESC LIMIT 100`).all(latestFlowGlobal, ...macs, ...agentIPs);
|
|
} else {
|
|
flows = d.prepare(`SELECT * FROM flows WHERE fetched_at = ? AND agent_uuid IS NULL AND local_mac IN (${ph}) ORDER BY download DESC LIMIT 100`).all(latestFlowGlobal, ...macs);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 4. Summary stats (aligned with dashboard stats query) ───────────────────
|
|
const stats = db.getStats(null, agentUuid);
|
|
|
|
// ACCURATE BANDWIDTH CALCULATION FROM MAC BANDWIDTH (Overrides broken Netify Summary endpoint)
|
|
let totalDown = 0;
|
|
let totalUp = 0;
|
|
const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t;
|
|
if (latestMacSnap && macs.length > 0) {
|
|
const macRows = d.prepare(`SELECT download, upload FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${ph})`).all(latestMacSnap, ...macs);
|
|
for (const r of macRows) {
|
|
totalDown += (r.download || 0);
|
|
totalUp += (r.upload || 0);
|
|
}
|
|
}
|
|
|
|
const summary = {
|
|
total_devices : stats.totalDevices > 0 ? stats.totalDevices : devices.length,
|
|
active_flows : stats.activeFlows > 0 ? stats.activeFlows : flows.length,
|
|
bandwidth_down : totalDown > 0 ? totalDown : (stats.latestBw?.total_download ?? 0),
|
|
bandwidth_up : totalUp > 0 ? totalUp : (stats.latestBw?.total_upload ?? 0),
|
|
};
|
|
|
|
// ── 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 'Vulnerable' THEN 1 WHEN 'Moderate' 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 (aligned with events page) ───────────────
|
|
const events = db.getLatestEvents(200, null, agentUuid);
|
|
|
|
// ── 7. MAC bandwidth for this agent's MACs ──────────────────────────────────
|
|
// latestMacSnap was already declared above, reusing it.
|
|
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,
|
|
};
|
|
}
|
|
|
|
// Fetch data for a specific device IP — from local DB (all 10 correlated tables)
|
|
async function fetchDeviceDetails(ip, agentUuid = null) {
|
|
const db = require('./database');
|
|
const d = db.getDB();
|
|
|
|
// ── 0. Resolve MAC from flows (most recent) ──────────────────────────────
|
|
const macRow = d.prepare(`SELECT src_mac FROM flows WHERE src_ip = ? AND src_mac IS NOT NULL ORDER BY last_seen DESC LIMIT 1`).get(ip);
|
|
const mac = macRow?.src_mac || null;
|
|
|
|
// ── 1. Device info from devices table ────────────────────────────────────
|
|
const deviceRow = d.prepare(`
|
|
SELECT device_label, device_type, os_label, manufacturer, download, upload, last_seen
|
|
FROM devices
|
|
WHERE ip_address = ?
|
|
ORDER BY fetched_at DESC
|
|
LIMIT 1
|
|
`).get(ip);
|
|
|
|
// ── 2. Discovery info (may differ from devices table) ────────────────────
|
|
const discRow = d.prepare(`
|
|
SELECT device_type, os_label, manufacturer, device_label, is_new
|
|
FROM intel_device_discovery
|
|
WHERE ip_address = ?
|
|
ORDER BY fetched_at DESC
|
|
LIMIT 1
|
|
`).get(ip);
|
|
|
|
const device_info = {
|
|
device_label : deviceRow?.device_label || discRow?.device_label || null,
|
|
device_type : deviceRow?.device_type || discRow?.device_type || null,
|
|
os_label : deviceRow?.os_label || discRow?.os_label || null,
|
|
manufacturer : deviceRow?.manufacturer || discRow?.manufacturer || null,
|
|
mac_address : mac,
|
|
is_new : discRow?.is_new ?? null,
|
|
};
|
|
|
|
// ── 3. Named apps & correlated ports ─────────────────────────────────────
|
|
const rawAppRows = d.prepare(`
|
|
SELECT app_label,
|
|
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 30
|
|
`).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 ─────────────────────────────
|
|
const domainRows = d.prepare(`
|
|
SELECT domain,
|
|
-- use app_label that appeared most with this domain
|
|
(SELECT app_label FROM flows
|
|
WHERE src_ip = f.src_ip AND domain = f.domain
|
|
AND app_label IS NOT NULL
|
|
ORDER BY bytes_download DESC LIMIT 1) AS app_label,
|
|
-- extract root domain for display
|
|
domain AS display_name,
|
|
SUM(bytes_download) AS download,
|
|
SUM(bytes_upload) AS upload,
|
|
COUNT(*) AS flow_count
|
|
FROM flows f
|
|
WHERE src_ip = ?
|
|
AND domain IS NOT NULL
|
|
GROUP BY domain
|
|
ORDER BY download DESC
|
|
LIMIT 30
|
|
`).all(ip);
|
|
|
|
// ── 5. Smart combined display list ──────────────────────────────────────
|
|
const combinedMap = new Map();
|
|
|
|
// Add domains first (higher priority)
|
|
for (const r of domainRows) {
|
|
combinedMap.set('domain:' + r.domain, {
|
|
label : r.domain, // the actual website/domain
|
|
sub_label : r.app_label || null, // protocol (HTTPS/TLS etc)
|
|
type : 'domain',
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
flow_count : r.flow_count,
|
|
});
|
|
}
|
|
|
|
// Add named & correlated apps
|
|
for (const r of rawAppRows) {
|
|
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)) {
|
|
combinedMap.set(key, {
|
|
label : label,
|
|
sub_label : sub_label,
|
|
type : type,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
flow_count : r.flow_count,
|
|
});
|
|
}
|
|
}
|
|
|
|
const top_apps = [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25);
|
|
|
|
// top_domains: keep simple list for Info tab
|
|
const top_domains = domainRows.map(r => ({
|
|
domain : r.domain,
|
|
app_label : r.app_label,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
flow_count : r.flow_count,
|
|
}));
|
|
|
|
// ── 5. Recent flows ────────────────────────────────────────────────────
|
|
const flowRows = d.prepare(`
|
|
SELECT dst_ip, dst_port, protocol, app_label, domain,
|
|
bytes_download AS download, bytes_upload AS upload, last_seen
|
|
FROM flows
|
|
WHERE src_ip = ?
|
|
ORDER BY last_seen DESC
|
|
LIMIT 100
|
|
`).all(ip);
|
|
|
|
const flows = flowRows.map(r => ({
|
|
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,
|
|
}));
|
|
|
|
// ── 6. Totals ─────────────────────────────────────────────────────────
|
|
const sumRow = d.prepare(`
|
|
SELECT SUM(bytes_download) AS total_download,
|
|
SUM(bytes_upload) AS total_upload,
|
|
COUNT(*) AS flow_count
|
|
FROM flows
|
|
WHERE src_ip = ?
|
|
`).get(ip);
|
|
|
|
// ── 7. Encryption audit (latest snapshot) ─────────────────────────────
|
|
const latestAudit = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t;
|
|
const encRow = latestAudit
|
|
? d.prepare(`
|
|
SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, mac_address
|
|
FROM intel_encryption_audit
|
|
WHERE fetched_at = ? AND ip_address = ?
|
|
LIMIT 1
|
|
`).get(latestAudit, ip)
|
|
: null;
|
|
|
|
// Also try fallback by MAC if not found by IP
|
|
const encRowMac = (!encRow && mac && latestAudit)
|
|
? d.prepare(`
|
|
SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, ip_address
|
|
FROM intel_encryption_audit
|
|
WHERE fetched_at = ? AND mac_address = ?
|
|
ORDER BY detected_at DESC
|
|
LIMIT 1
|
|
`).get(latestAudit, mac)
|
|
: null;
|
|
|
|
const encFinal = encRow || encRowMac;
|
|
|
|
const encryption = encFinal ? {
|
|
encrypted_pct : encFinal.encrypted_pct ?? null,
|
|
encrypted_bytes : encFinal.encrypted ?? null,
|
|
unencrypted_bytes: encFinal.unencrypted ?? null,
|
|
total_bytes : encFinal.total ?? null,
|
|
risk_level : encFinal.risk_level ?? null,
|
|
} : null;
|
|
|
|
// ── 8. Server discovery (servers this device accessed) ─────────────────
|
|
const serverRows = d.prepare(`
|
|
SELECT DISTINCT server_type, hostname, port, protocol, os_label,
|
|
MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at
|
|
FROM intel_server_discovery
|
|
WHERE ip_address = ?
|
|
GROUP BY server_type, port, protocol
|
|
ORDER BY download DESC
|
|
LIMIT 50
|
|
`).all(ip);
|
|
|
|
// fallback by MAC if no rows by IP
|
|
const serverRowsMac = (serverRows.length === 0 && mac)
|
|
? d.prepare(`
|
|
SELECT DISTINCT server_type, hostname, port, protocol, os_label,
|
|
MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at
|
|
FROM intel_server_discovery
|
|
WHERE mac_address = ?
|
|
GROUP BY server_type, port, protocol
|
|
ORDER BY download DESC
|
|
LIMIT 50
|
|
`).all(mac)
|
|
: [];
|
|
|
|
const server_discovery = (serverRows.length > 0 ? serverRows : serverRowsMac).map(r => ({
|
|
server_type : r.server_type,
|
|
hostname : r.hostname || null,
|
|
port : r.port,
|
|
protocol : r.protocol,
|
|
os_label : r.os_label || null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
detected_at : r.detected_at,
|
|
}));
|
|
|
|
// ── 9. Unencrypted passwords ───────────────────────────────────────────
|
|
let pwdRows = d.prepare(`
|
|
SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at
|
|
FROM intel_unencrypted_passwords
|
|
WHERE ip_address = ?
|
|
ORDER BY detected_at DESC
|
|
LIMIT 50
|
|
`).all(ip);
|
|
|
|
if (pwdRows.length === 0 && mac) {
|
|
pwdRows = d.prepare(`
|
|
SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at
|
|
FROM intel_unencrypted_passwords
|
|
WHERE mac_address = ?
|
|
ORDER BY detected_at DESC
|
|
LIMIT 50
|
|
`).all(mac);
|
|
}
|
|
|
|
const unencrypted_passwords = pwdRows.map(r => ({
|
|
dst_ip : r.dst_ip,
|
|
dst_port : r.dst_port,
|
|
protocol : r.protocol,
|
|
username : r.username,
|
|
severity : r.severity,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
detected_at : r.detected_at,
|
|
}));
|
|
|
|
// ── 10. IP Reputation (latest snapshot, this device's local_ip) ─────────
|
|
const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t;
|
|
const repRows = latestRepSnap
|
|
? d.prepare(`
|
|
SELECT ip_address, local_ip, reputation, score, country, app_label, blacklisted, download, upload
|
|
FROM intel_ip_reputation
|
|
WHERE fetched_at = ? AND (local_ip = ? OR ip_address = ?)
|
|
ORDER BY score DESC NULLS LAST
|
|
LIMIT 30
|
|
`).all(latestRepSnap, ip, ip)
|
|
: [];
|
|
|
|
const ip_reputation = repRows.map(r => ({
|
|
remote_ip : r.ip_address,
|
|
local_ip : r.local_ip,
|
|
reputation : r.reputation,
|
|
score : r.score,
|
|
country : r.country,
|
|
app_label : r.app_label,
|
|
blacklisted : !!r.blacklisted,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
}));
|
|
|
|
// ── 11. VPN detection ─────────────────────────────────────────────────
|
|
let vpnRows = d.prepare(`
|
|
SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at
|
|
FROM intel_vpn_detection
|
|
WHERE ip_address = ?
|
|
ORDER BY detected_at DESC
|
|
LIMIT 20
|
|
`).all(ip);
|
|
|
|
if (vpnRows.length === 0 && mac) {
|
|
vpnRows = d.prepare(`
|
|
SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at
|
|
FROM intel_vpn_detection
|
|
WHERE mac_address = ?
|
|
ORDER BY detected_at DESC
|
|
LIMIT 20
|
|
`).all(mac);
|
|
}
|
|
|
|
const vpn_detections = vpnRows.map(r => ({
|
|
vpn_type : r.vpn_type,
|
|
remote_ip : r.remote_ip,
|
|
protocol : r.protocol,
|
|
country : r.country,
|
|
confidence : r.confidence,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
detected_at : r.detected_at,
|
|
}));
|
|
|
|
// ── 12. Events ────────────────────────────────────────────────────────
|
|
const evtByIP = d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE ip_address = ? ORDER BY event_at DESC LIMIT 50`).all(ip);
|
|
const evtByMAC = mac ? d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE mac_address = ? ORDER BY event_at DESC LIMIT 50`).all(mac) : [];
|
|
|
|
const seenEvt = new Set();
|
|
const evtMerged = [];
|
|
for (const r of [...evtByIP, ...evtByMAC]) {
|
|
const key = `${r.event_type}:${r.event_at}`;
|
|
if (!seenEvt.has(key)) {
|
|
seenEvt.add(key);
|
|
evtMerged.push({ 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 });
|
|
}
|
|
}
|
|
evtMerged.sort((a, b) => (b.event_at || '').localeCompare(a.event_at || ''));
|
|
const events = evtMerged.slice(0, 100);
|
|
|
|
// ── 13. MAC bandwidth (latest snapshot) ───────────────────────────────
|
|
const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t;
|
|
const macBwRow = (latestMacSnap && mac)
|
|
? d.prepare(`SELECT manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address = ? LIMIT 1`).get(latestMacSnap, mac)
|
|
: null;
|
|
|
|
const mac_bandwidth = macBwRow ? {
|
|
mac_address : mac,
|
|
manufacturer : macBwRow.manufacturer,
|
|
download : macBwRow.download ?? 0,
|
|
upload : macBwRow.upload ?? 0,
|
|
total : macBwRow.total ?? 0,
|
|
} : null;
|
|
|
|
return {
|
|
ip,
|
|
mac_address : mac,
|
|
total_download : deviceRow?.download || sumRow?.total_download || 0,
|
|
total_upload : deviceRow?.upload || sumRow?.total_upload || 0,
|
|
flow_count : sumRow?.flow_count ?? 0,
|
|
device_info,
|
|
top_apps,
|
|
top_domains : domainRows.map(r => ({ domain: r.domain, download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count })),
|
|
flows,
|
|
encryption,
|
|
server_discovery,
|
|
unencrypted_passwords,
|
|
ip_reputation,
|
|
vpn_detections,
|
|
events,
|
|
mac_bandwidth,
|
|
};
|
|
}
|
|
|
|
// Fetch data for a specific application// Fetch data for a specific application — from local DB
|
|
async function fetchAppDetails(appLabel, agentUuid = null) {
|
|
const db = require('./database');
|
|
const d = db.getDB();
|
|
|
|
// ── Totals: from flows (protocol-level) OR bandwidth_apps (brand-level) ──
|
|
// IMPORTANT: use MAX(download) from latest snapshot — NOT SUM() of all history!
|
|
let flowQuery = `
|
|
SELECT SUM(bytes_download) AS download,
|
|
SUM(bytes_upload) AS upload,
|
|
COUNT(*) AS flow_count
|
|
FROM flows
|
|
WHERE app_label = @appLabel
|
|
`;
|
|
if (agentUuid) flowQuery += ` AND agent_uuid = @agentUuid`;
|
|
const flowTotalRow = d.prepare(flowQuery).get({ appLabel, agentUuid });
|
|
|
|
let total_download = flowTotalRow?.download ?? 0;
|
|
let total_upload = flowTotalRow?.upload ?? 0;
|
|
let data_source = 'flows'; // track where totals came from
|
|
|
|
// If no flows data (brand-name app like YouTube), use LATEST bandwidth_apps snapshot
|
|
if (total_download === 0 && total_upload === 0) {
|
|
let snapQuery = `SELECT MAX(fetched_at) AS t FROM bandwidth_apps WHERE app_label = @appLabel`;
|
|
snapQuery += agentUuid ? ` AND agent_uuid = @agentUuid` : ` AND agent_uuid IS NULL`;
|
|
const latestSnap = d.prepare(snapQuery).get({ appLabel, agentUuid })?.t;
|
|
|
|
if (latestSnap) {
|
|
let bwQuery = `
|
|
SELECT download, upload
|
|
FROM bandwidth_apps
|
|
WHERE app_label = @appLabel AND fetched_at = @latestSnap
|
|
`;
|
|
bwQuery += agentUuid ? ` AND agent_uuid = @agentUuid` : ` AND agent_uuid IS NULL`;
|
|
bwQuery += ` LIMIT 1`;
|
|
|
|
const bwRow = d.prepare(bwQuery).get({ appLabel, latestSnap, agentUuid });
|
|
if (bwRow) {
|
|
total_download = bwRow.download ?? 0;
|
|
total_upload = bwRow.upload ?? 0;
|
|
data_source = 'bandwidth_apps';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Top 5 Flow Records accessing this app (Raw Flows) ──
|
|
let ipQuery = `
|
|
SELECT src_ip AS ip_address,
|
|
dst_ip,
|
|
domain,
|
|
last_seen,
|
|
bytes_download AS download,
|
|
bytes_upload AS upload
|
|
FROM flows
|
|
WHERE app_label = @appLabel
|
|
`;
|
|
if (agentUuid) ipQuery += ` AND agent_uuid = @agentUuid`;
|
|
ipQuery += ` ORDER BY download DESC LIMIT 5`;
|
|
const ipRows = d.prepare(ipQuery).all({ appLabel, agentUuid });
|
|
|
|
// ── Domain-based per-IP breakdown (bridges HTTPS/TLS → brand name) ──
|
|
// Build keyword map for common brand names
|
|
const DOMAIN_KEYWORDS = {
|
|
'YouTube' : ['youtube', 'googlevideo', 'ytimg', 'yt3.ggpht'],
|
|
'Facebook' : ['facebook', 'fbcdn', 'fb.com', 'fbsbx'],
|
|
'WhatsApp' : ['whatsapp', 'wa.me'],
|
|
'Instagram' : ['instagram', 'cdninstagram'],
|
|
'TikTok' : ['tiktok', 'tiktokcdn', 'tiktokv'],
|
|
'Netflix' : ['netflix', 'nflxvideo', 'nflximg'],
|
|
'Google' : ['google.com', 'googleapis', 'gstatic', 'googlesyndication'],
|
|
'Microsoft' : ['microsoft', 'msftncsi', 'live.com', 'office365', 'sharepoint'],
|
|
'Zoom' : ['zoom.us', 'zoomgov'],
|
|
'Spotify' : ['spotify', 'scdn.co'],
|
|
};
|
|
|
|
let domain_breakdown = [];
|
|
const kws = DOMAIN_KEYWORDS[appLabel];
|
|
if (kws && kws.length > 0) {
|
|
// Build WHERE clause for domain keywords
|
|
const likeClause = kws.map(() => `LOWER(domain) LIKE ?`).join(' OR ');
|
|
const likeParams = kws.map(k => `%${k}%`);
|
|
let domQuery = `
|
|
SELECT src_ip AS ip_address,
|
|
dst_ip,
|
|
domain,
|
|
last_seen,
|
|
bytes_download AS download,
|
|
bytes_upload AS upload
|
|
FROM flows
|
|
WHERE domain IS NOT NULL AND (${likeClause})
|
|
`;
|
|
const domParams = [...likeParams];
|
|
if (agentUuid) {
|
|
domQuery += ` AND agent_uuid = ?`;
|
|
domParams.push(agentUuid);
|
|
}
|
|
domQuery += ` ORDER BY download DESC LIMIT 5`;
|
|
const domRows = d.prepare(domQuery).all(...domParams);
|
|
|
|
domain_breakdown = domRows.map(r => ({
|
|
ip_address : r.ip_address,
|
|
dst_ip : r.dst_ip,
|
|
domain : r.domain,
|
|
last_seen : r.last_seen,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
}));
|
|
}
|
|
|
|
// ── Threat flags: cross-reference top IPs with intel_ip_reputation ──
|
|
const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t;
|
|
const threatMap = {};
|
|
if (latestRepSnap) {
|
|
const repRows = d.prepare(`
|
|
SELECT local_ip, ip_address, reputation, blacklisted
|
|
FROM intel_ip_reputation
|
|
WHERE fetched_at = ?
|
|
`).all(latestRepSnap);
|
|
for (const r of repRows) {
|
|
const key = r.local_ip || r.ip_address;
|
|
if (key) threatMap[key] = { reputation: r.reputation, blacklisted: r.blacklisted };
|
|
}
|
|
}
|
|
|
|
const allIpRows = domain_breakdown.length > 0 ? domain_breakdown : ipRows;
|
|
const top_ips = allIpRows.map(r => ({
|
|
ip_address : r.ip_address,
|
|
dst_ip : r.dst_ip,
|
|
domain : r.domain,
|
|
last_seen : r.last_seen,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
reputation : threatMap[r.ip_address]?.reputation ?? null,
|
|
blacklisted : threatMap[r.ip_address]?.blacklisted ?? false,
|
|
}));
|
|
|
|
// ── Per-agent breakdown using agent_uuid grouping in flows OR local_mac mapping ──
|
|
let allRelevantFlows = [];
|
|
if (kws && kws.length > 0) {
|
|
const likeClause2 = kws.map(() => `LOWER(domain) LIKE ?`).join(' OR ');
|
|
const likeParams2 = kws.map(k => `%${k}%`);
|
|
let scoreQuery = `
|
|
SELECT agent_uuid, src_mac AS local_mac,
|
|
bytes_download AS download,
|
|
bytes_upload AS upload
|
|
FROM flows
|
|
WHERE domain IS NOT NULL AND (${likeClause2})
|
|
`;
|
|
const scoreParams = [...likeParams2];
|
|
if (agentUuid) {
|
|
scoreQuery += ` AND (agent_uuid = ? OR agent_uuid IS NULL)`;
|
|
scoreParams.push(agentUuid);
|
|
}
|
|
allRelevantFlows = d.prepare(scoreQuery).all(...scoreParams);
|
|
} else {
|
|
let scoreQuery = `
|
|
SELECT agent_uuid, src_mac AS local_mac,
|
|
bytes_download AS download,
|
|
bytes_upload AS upload
|
|
FROM flows
|
|
WHERE app_label = ?
|
|
`;
|
|
const scoreParams = [appLabel];
|
|
if (agentUuid) {
|
|
scoreQuery += ` AND (agent_uuid = ? OR agent_uuid IS NULL)`;
|
|
scoreParams.push(agentUuid);
|
|
}
|
|
allRelevantFlows = d.prepare(scoreQuery).all(...scoreParams);
|
|
}
|
|
|
|
// Reverse map MAC to Agent UUID
|
|
const macToAgent = {};
|
|
for (const [auid, macs] of Object.entries(AGENT_MAC_MAP)) {
|
|
for (const m of macs) {
|
|
macToAgent[m] = auid;
|
|
}
|
|
}
|
|
|
|
const agentScoreMap = {};
|
|
for (const row of allRelevantFlows) {
|
|
let auid = row.agent_uuid;
|
|
if (!auid && row.local_mac && macToAgent[row.local_mac]) {
|
|
auid = macToAgent[row.local_mac]; // Fallback to MAC mapping
|
|
}
|
|
if (auid) {
|
|
if (agentUuid && auid !== agentUuid) continue; // skip if filtering by a specific agent
|
|
if (!agentScoreMap[auid]) {
|
|
agentScoreMap[auid] = { download: 0, upload: 0, flow_count: 0 };
|
|
}
|
|
agentScoreMap[auid].download += (row.download ?? 0);
|
|
agentScoreMap[auid].upload += (row.upload ?? 0);
|
|
agentScoreMap[auid].flow_count += 1;
|
|
}
|
|
}
|
|
|
|
let agent_scorecard = Object.keys(agentScoreMap).map(auid => ({
|
|
agent_uuid : auid,
|
|
agent_label : AGENT_LABELS[auid] || auid,
|
|
download : agentScoreMap[auid].download,
|
|
upload : agentScoreMap[auid].upload,
|
|
flow_count : agentScoreMap[auid].flow_count,
|
|
})).filter(a => a.download > 0 || a.upload > 0);
|
|
|
|
// Fallback to bandwidth_apps if flow-based scorecard is empty
|
|
if (agent_scorecard.length === 0) {
|
|
const snapQuery = `SELECT MAX(fetched_at) AS t FROM bandwidth_apps WHERE app_label = @appLabel AND agent_uuid IS NOT NULL`;
|
|
const latestAppSnap = d.prepare(snapQuery).get({ appLabel })?.t;
|
|
if (latestAppSnap) {
|
|
let bwQuery = `SELECT agent_uuid, download, upload FROM bandwidth_apps WHERE app_label = @appLabel AND fetched_at = @latestAppSnap AND agent_uuid IS NOT NULL`;
|
|
if (agentUuid) bwQuery += ` AND agent_uuid = @agentUuid`;
|
|
const bwRows = d.prepare(bwQuery).all({ appLabel, latestAppSnap, agentUuid });
|
|
for (const row of bwRows) {
|
|
agent_scorecard.push({
|
|
agent_uuid: row.agent_uuid,
|
|
agent_label: AGENT_LABELS[row.agent_uuid] || row.agent_uuid,
|
|
download: row.download,
|
|
upload: row.upload,
|
|
flow_count: 0
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
app_label : appLabel,
|
|
total_download,
|
|
total_upload,
|
|
data_source,
|
|
agent_scorecard,
|
|
top_ips,
|
|
has_domain_breakdown: domain_breakdown.length > 0,
|
|
};
|
|
}
|
|
|
|
|
|
// Fetch security device risk overview — encryption audit + insecure protocols per device
|
|
async function fetchSecurityDevices(siteUuid = null, agentUuid = null) {
|
|
const db = require('./database');
|
|
const d = db.getDB();
|
|
|
|
// Get active IPs and MACs for filtering if agentUuid is provided
|
|
let agentIPs = null;
|
|
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 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));
|
|
|
|
// Compute risk per device
|
|
const deviceMap = {};
|
|
for (const r of encryptRows) {
|
|
const ip = r.ip_address;
|
|
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;
|
|
let riskLevel;
|
|
if (encPct < 50 || insecureIPs.has(ip)) {
|
|
riskLevel = 'Vulnerable';
|
|
} else if (encPct < 80) {
|
|
riskLevel = 'Moderate';
|
|
} else {
|
|
riskLevel = 'Safe';
|
|
}
|
|
if (!deviceMap[ip] || deviceMap[ip].encrypted_pct < encPct) {
|
|
deviceMap[ip] = {
|
|
ip_address : ip,
|
|
mac_address : r.mac_address ?? null,
|
|
device_label : r.device_label ?? null,
|
|
encrypted_pct : encPct,
|
|
unencrypted : r.unencrypted ?? 0,
|
|
encrypted : r.encrypted ?? 0,
|
|
total : r.total ?? 0,
|
|
risk_level : riskLevel,
|
|
has_insecure : insecureIPs.has(ip),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Get device details (type, OS) from discovery data
|
|
const discMap = {};
|
|
try {
|
|
const discRows = db.getLatestDevices(1000, siteUuid, agentUuid);
|
|
for (const r of discRows) {
|
|
if (r.ip_address) discMap[r.ip_address] = r;
|
|
}
|
|
} catch (_) {
|
|
// skip enrichment if error
|
|
}
|
|
|
|
const devices = Object.values(deviceMap).map(dev => ({
|
|
...dev,
|
|
device_type : discMap[dev.ip_address]?.device_type ?? null,
|
|
os_label : discMap[dev.ip_address]?.os_label ?? null,
|
|
manufacturer : discMap[dev.ip_address]?.manufacturer ?? null,
|
|
}));
|
|
|
|
// Sort: Vulnerable first, then Moderate, then Safe
|
|
const ORDER = { Vulnerable: 0, Moderate: 1, Safe: 2 };
|
|
devices.sort((a, b) => (ORDER[a.risk_level] ?? 3) - (ORDER[b.risk_level] ?? 3));
|
|
|
|
return devices;
|
|
}
|
|
|
|
module.exports = {
|
|
fetchLookupApplications,
|
|
fetchAgents,
|
|
fetchTopApps, fetchTopDevices, fetchTopProtocols, fetchTopCountries,
|
|
fetchTopDomains, fetchBandwidthSummary, fetchDiscoveredDevices,
|
|
fetchCyberThreats, fetchFlows, fetchEvents,
|
|
fetchTopAppCategories, fetchTopContinents, fetchTopRegions, fetchTopCities,
|
|
fetchTopVLANs, fetchTopInterfaces, fetchTopFlowTypes, fetchTopFlowOrigins,
|
|
fetchTopIPVersions, fetchTopRemoteIPs, fetchTopLocalMACs,
|
|
fetchTopDiscoveryOS,
|
|
fetchTLSVersions, fetchTLSCiphers, fetchTLSSecurity, fetchNetBIOSHostnames,
|
|
// DPI 12-21 (field name sudah diperbaiki sesuai dokumentasi resmi)
|
|
fetchDHCPClassFingerprints,
|
|
fetchHTTPUserAgents,
|
|
fetchSNIHostnames,
|
|
fetchSSLServerCN,
|
|
fetchQUICHostnames,
|
|
fetchBitTorrentInfoHashes,
|
|
fetchSSHClients,
|
|
fetchSSHServers,
|
|
fetchMDNSHostnames,
|
|
// Intelligence 22-30 (derive dari data yang tersedia)
|
|
fetchCryptoMining,
|
|
fetchDeviceDiscovery,
|
|
fetchEncryptionAudit,
|
|
fetchInsecureProtocols,
|
|
fetchIPReputation,
|
|
fetchServerDiscovery,
|
|
fetchTorDetection,
|
|
fetchUnencryptedPasswords,
|
|
fetchVPNDetection,
|
|
// Interactive detail fetchers
|
|
fetchAgentDetails,
|
|
fetchDeviceDetails,
|
|
fetchAppDetails,
|
|
fetchSecurityDevices,
|
|
};
|
|
|
|
// ─── DPI FIELDS 12-21 — FIELD NAMES DIPERBAIKI SESUAI DOCS ──────────────────
|
|
// Sumber: https://www.netify.ai/documentation/informatics/v2/data/fields
|
|
//
|
|
// Field yang SALAH sebelumnya → yang BENAR:
|
|
// dhcp_fingerprint → dhcp_class
|
|
// user_agent → http_useragent
|
|
// ssl_cn → https_sni_hostname
|
|
// ssl_server_cn → ssl_server_cn ✓ (sudah benar)
|
|
// quic_hostname → quic_hostname ✓ (sudah benar, mungkin belum aktif di akun)
|
|
// bt_info_hash → bittorrent_info_hash
|
|
// ssh_version → ssh_client / ssh_server (2 field terpisah)
|
|
// mdns_hostname → mdns_hostname ✓ (sudah benar, mungkin belum aktif di akun)
|
|
|
|
// Helper builder untuk DPI single-field
|
|
async function _dpiTopField(fieldName, interval, limit, agentUuid = null) {
|
|
const [dl, ul] = await Promise.all([
|
|
netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }, false, agentUuid),
|
|
]);
|
|
if (!dl) return null;
|
|
const ulMap = {};
|
|
if (ul) for (const r of ul) {
|
|
const key = r[fieldName]?.name ?? r[fieldName]?.label ?? String(r[fieldName] ?? '');
|
|
if (key) ulMap[key] = r.upload ?? 0;
|
|
}
|
|
return { dl, ulMap };
|
|
}
|
|
|
|
// 12. DHCP Class (field: dhcp_class)
|
|
async function fetchDHCPClassFingerprints(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('dhcp_class', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const label = r.dhcp_class?.name ?? r.dhcp_class?.label ?? String(r.dhcp_class ?? 'Unknown');
|
|
return {
|
|
fingerprint : label,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 13. HTTP User-Agent (field: http_useragent)
|
|
async function fetchHTTPUserAgents(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('http_useragent', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const label = r.http_useragent?.name ?? r.http_useragent?.label ?? String(r.http_useragent ?? 'Unknown');
|
|
return {
|
|
user_agent : label,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 14. HTTPS SNI Hostname (field: https_sni_hostname)
|
|
async function fetchSNIHostnames(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('https_sni_hostname', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const name = r.https_sni_hostname?.name ?? r.https_sni_hostname?.label ?? String(r.https_sni_hostname ?? 'Unknown');
|
|
return {
|
|
sni_hostname : name,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[name] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[name] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 15. SSL Server Common Name (field: ssl_server_cn)
|
|
async function fetchSSLServerCN(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('ssl_server_cn', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const name = r.ssl_server_cn?.name ?? r.ssl_server_cn?.label ?? String(r.ssl_server_cn ?? 'Unknown');
|
|
return {
|
|
ssl_server_cn : name,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[name] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[name] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 17. QUIC Hostname (field: quic_hostname)
|
|
async function fetchQUICHostnames(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('quic_hostname', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const name = r.quic_hostname?.name ?? r.quic_hostname?.label ?? String(r.quic_hostname ?? 'Unknown');
|
|
return {
|
|
quic_hostname : name,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[name] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[name] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 18. BitTorrent Info Hash (field: bittorrent_info_hash)
|
|
async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('bittorrent_info_hash', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const hash = r.bittorrent_info_hash?.hash ?? r.bittorrent_info_hash?.name ?? String(r.bittorrent_info_hash ?? 'Unknown');
|
|
const label = r.bittorrent_info_hash?.label ?? hash;
|
|
return {
|
|
info_hash : hash,
|
|
label : label,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[hash] ?? res.ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[hash] ?? res.ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 19a. SSH Client (field: ssh_client)
|
|
async function fetchSSHClients(interval = 1440, limit = 20, agentUuid = null) {
|
|
const res = await _dpiTopField('ssh_client', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const label = r.ssh_client?.name ?? r.ssh_client?.label ?? String(r.ssh_client ?? 'Unknown');
|
|
return {
|
|
ssh_version : label,
|
|
direction : 'client',
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 19b. SSH Server (field: ssh_server)
|
|
async function fetchSSHServers(interval = 1440, limit = 20, agentUuid = null) {
|
|
const res = await _dpiTopField('ssh_server', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const label = r.ssh_server?.name ?? r.ssh_server?.label ?? String(r.ssh_server ?? 'Unknown');
|
|
return {
|
|
ssh_version : label,
|
|
direction : 'server',
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[label] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[label] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// 21. mDNS Hostname (field: mdns_hostname)
|
|
async function fetchMDNSHostnames(interval = 1440, limit = 30, agentUuid = null) {
|
|
const res = await _dpiTopField('mdns_hostname', interval, limit, agentUuid);
|
|
if (!res) return null;
|
|
return res.dl.map(r => {
|
|
const name = r.mdns_hostname?.name ?? r.mdns_hostname?.label ?? String(r.mdns_hostname ?? 'Unknown');
|
|
return {
|
|
mdns_hostname : name,
|
|
download : r.download ?? 0,
|
|
upload : res.ulMap[name] ?? 0,
|
|
total : (r.download ?? 0) + (res.ulMap[name] ?? 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
// ─── INTELLIGENCE 22-30 — DERIVE DARI DATA YANG SUDAH ADA ────────────────────
|
|
// Endpoint /intelligence/* dan /events/* semua 404 di akun ini.
|
|
// Semua fungsi berikut meng-DERIVE data dari endpoint yang sudah confirmed bekerja:
|
|
// /data/stats/top/*, /data/flows
|
|
// Ini memberikan data nyata, bukan mock/dummy.
|
|
|
|
// 22. Cryptocurrency Mining — derive dari apps dengan nama mengandung "crypto" / "mining"
|
|
// + flows ke port mining pool (3333, 4444, 8333, 9999, 14444)
|
|
async function fetchCryptoMining(interval = 1440, limit = 50, agentUuid = null) {
|
|
const MINING_POOLS_PORTS = new Set([3333, 4444, 5555, 7777, 8333, 9332, 9999, 14444, 45560, 45700]);
|
|
const MINING_APPS = ['bitcoin', 'crypto', 'mining', 'monero', 'ethereum', 'nicehash', 'nanopool', 'f2pool', 'antpool', 'slushpool'];
|
|
|
|
const [appData, flowsRaw] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
]);
|
|
|
|
const results = [];
|
|
|
|
// Derive dari aplikasi
|
|
if (appData) {
|
|
for (const r of appData) {
|
|
const name = (r.application?.label ?? '').toLowerCase();
|
|
const tag = (r.application?.tag ?? '').toLowerCase();
|
|
if (MINING_APPS.some(k => name.includes(k) || tag.includes(k))) {
|
|
results.push({
|
|
detected_at : null,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
pool_host : r.application?.label ?? null,
|
|
pool_ip : null,
|
|
protocol : null,
|
|
app_label : r.application?.label ?? null,
|
|
confidence : 0.7,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
source : 'derived:app',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Derive dari flows ke port mining
|
|
if (flowsRaw) {
|
|
for (const r of flowsRaw) {
|
|
const port = r.remote_port ?? 0;
|
|
if (MINING_POOLS_PORTS.has(port)) {
|
|
results.push({
|
|
detected_at : r.first_seen_at?.date ?? null,
|
|
ip_address : r.local_ip?.address ?? null,
|
|
mac_address : r.local_mac ?? null,
|
|
pool_host : null,
|
|
pool_ip : r.remote_ip?.address ?? null,
|
|
protocol : r.ip_protocol?.label ?? null,
|
|
app_label : null,
|
|
confidence : 0.85,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
source : 'derived:flow',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
// 23. Device Discovery — derive dari flows (perangkat unik dengan MAC)
|
|
async function fetchDeviceDiscovery(limit = 100, agentUuid = null) {
|
|
const [flowsRaw, dlData] = await Promise.all([
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/local_ip/download', { filter_interval: 1440, settings_limit: 200 }, false, agentUuid),
|
|
]);
|
|
|
|
const seen = new Map();
|
|
if (flowsRaw) {
|
|
for (const r of flowsRaw) {
|
|
const ip = r.local_ip?.address;
|
|
const mac = r.local_mac;
|
|
if (!ip) continue;
|
|
if (!seen.has(ip)) {
|
|
seen.set(ip, {
|
|
detected_at : r.first_seen_at?.date ?? null,
|
|
ip_address : ip,
|
|
mac_address : mac ?? null,
|
|
device_label : r.device?.label ?? null,
|
|
device_type : r.mac?.discovery_hardware ?? null,
|
|
os_label : r.discovery_os?.label ?? null,
|
|
manufacturer : mac ? (OUI_MAP[mac.substring(0,8).toUpperCase()] ?? null) : null,
|
|
is_new : true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tambah IP yang punya bandwidth tapi tidak ada di flows
|
|
if (dlData) {
|
|
for (const r of dlData) {
|
|
const ip = r.local_ip?.address ?? String(r.local_ip ?? '');
|
|
if (ip && !seen.has(ip)) {
|
|
seen.set(ip, {
|
|
detected_at : null,
|
|
ip_address : ip,
|
|
mac_address : null,
|
|
device_label : null,
|
|
device_type : null,
|
|
os_label : null,
|
|
manufacturer : null,
|
|
is_new : true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(seen.values()).slice(0, limit);
|
|
}
|
|
|
|
// 24. Encryption Audit — derive dari TLS security per-IP dari flows
|
|
async function fetchEncryptionAudit(limit = 50, agentUuid = null) {
|
|
const [tlsSec, flowsRaw] = await Promise.all([
|
|
netifyFetch('/data/stats/top/tls_security/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid),
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
]);
|
|
|
|
// Hitung per-IP: encrypted vs unencrypted flows
|
|
const ipStats = {};
|
|
if (flowsRaw) {
|
|
for (const r of flowsRaw) {
|
|
const ip = r.local_ip?.address;
|
|
const port = r.remote_port ?? 0;
|
|
const mac = r.local_mac ?? null;
|
|
if (!ip) continue;
|
|
if (!ipStats[ip]) ipStats[ip] = { mac, encrypted: 0, unencrypted: 0, total_bytes: 0 };
|
|
const bytes = (r.download ?? 0) + (r.upload ?? 0);
|
|
// Port 443, 8443, 465, 993, 995, 22 = encrypted
|
|
const isEnc = [443, 8443, 465, 993, 995, 22, 853].includes(port);
|
|
if (isEnc) ipStats[ip].encrypted += bytes;
|
|
else ipStats[ip].unencrypted += bytes;
|
|
ipStats[ip].total_bytes += bytes;
|
|
}
|
|
}
|
|
|
|
return Object.entries(ipStats)
|
|
.map(([ip, s]) => {
|
|
const total = s.encrypted + s.unencrypted;
|
|
const enc_pct = total > 0 ? (s.encrypted / total) * 100 : null;
|
|
const risk = enc_pct === null ? null
|
|
: enc_pct >= 90 ? 'Low'
|
|
: enc_pct >= 60 ? 'Medium'
|
|
: enc_pct >= 30 ? 'High'
|
|
: 'Critical';
|
|
const oui = s.mac ? s.mac.substring(0,8).toUpperCase() : null;
|
|
return {
|
|
ip_address : ip,
|
|
mac_address : s.mac,
|
|
device_label : OUI_MAP[oui] ?? null,
|
|
encrypted_pct : enc_pct != null ? Math.round(enc_pct * 10) / 10 : null,
|
|
encrypted : s.encrypted,
|
|
unencrypted : s.unencrypted,
|
|
total : total,
|
|
risk_level : risk,
|
|
detected_at : null,
|
|
};
|
|
})
|
|
.sort((a, b) => (a.encrypted_pct ?? 101) - (b.encrypted_pct ?? 101))
|
|
.slice(0, limit);
|
|
}
|
|
|
|
// 25. Insecure Protocols — derive dari top protocols (confirmed bekerja)
|
|
async function fetchInsecureProtocols(interval = 1440, limit = 50, agentUuid = null) {
|
|
const INSECURE = {
|
|
'HTTP' : { port: 80, risk: 'High' },
|
|
'FTP' : { port: 21, risk: 'Critical' },
|
|
'Telnet' : { port: 23, risk: 'Critical' },
|
|
'SMTP' : { port: 25, risk: 'Medium' },
|
|
'POP3' : { port: 110, risk: 'Medium' },
|
|
'IMAP' : { port: 143, risk: 'Medium' },
|
|
'DNS' : { port: 53, risk: 'Low' },
|
|
'SNMP' : { port: 161, risk: 'High' },
|
|
'LDAP' : { port: 389, risk: 'High' },
|
|
'RDP' : { port: 3389, risk: 'High' },
|
|
'NTP' : { port: 123, risk: 'Low' },
|
|
'TFTP' : { port: 69, risk: 'High' },
|
|
'rsh' : { port: 514, risk: 'Critical' },
|
|
'rlogin' : { port: 513, risk: 'Critical' },
|
|
};
|
|
|
|
const [protoData, ulData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid),
|
|
]);
|
|
if (!protoData) return [];
|
|
|
|
const ulMap = {};
|
|
if (ulData) for (const r of ulData) {
|
|
const id = r.protocol?.id;
|
|
if (id) ulMap[id] = r.upload ?? 0;
|
|
}
|
|
|
|
return protoData
|
|
.filter(r => INSECURE[r.protocol?.label])
|
|
.map(r => {
|
|
const label = r.protocol?.label ?? 'Unknown';
|
|
const info = INSECURE[label];
|
|
return {
|
|
protocol : label,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
dst_ip : null,
|
|
dst_port : info.port,
|
|
app_label : null,
|
|
download : r.download ?? 0,
|
|
upload : ulMap[r.protocol?.id] ?? 0,
|
|
risk : info.risk,
|
|
detected_at : null,
|
|
source : 'derived',
|
|
};
|
|
})
|
|
.slice(0, limit);
|
|
}
|
|
|
|
// 26. IP Reputation — derive dari top remote_ip + cross-check negara berisiko tinggi
|
|
async function fetchIPReputation(limit = 50, agentUuid = null) {
|
|
// Negara dengan risiko tinggi berdasarkan threat intel umum
|
|
const HIGH_RISK_COUNTRIES = new Set([
|
|
'China', 'Russia', 'Iran', 'North Korea', 'Nigeria', 'Romania',
|
|
'Brazil', 'Ukraine', 'Vietnam', 'Indonesia',
|
|
]);
|
|
|
|
const [ipData, countryData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/country/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid),
|
|
]);
|
|
|
|
const results = [];
|
|
|
|
if (ipData) {
|
|
// Tandai IP dari negara berisiko (informasi negara tidak ada per-IP dari API,
|
|
// jadi kita pakai country data untuk konteks)
|
|
for (const r of ipData) {
|
|
const ip = r.remote_ip?.address ?? String(r.remote_ip ?? '');
|
|
if (!ip) continue;
|
|
results.push({
|
|
ip_address : ip,
|
|
local_ip : null,
|
|
mac_address : null,
|
|
reputation : 'Unknown',
|
|
score : null,
|
|
country : null,
|
|
app_label : null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
detected_at : null,
|
|
blacklisted : false,
|
|
source : 'derived:top_ip',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Tambah entri untuk negara berisiko yang terdeteksi
|
|
if (countryData) {
|
|
for (const r of countryData) {
|
|
const country = r.country?.label ?? '';
|
|
if (HIGH_RISK_COUNTRIES.has(country)) {
|
|
results.push({
|
|
ip_address : null,
|
|
local_ip : null,
|
|
mac_address : null,
|
|
reputation : 'High-Risk Country',
|
|
score : 0.7,
|
|
country : country,
|
|
app_label : null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
detected_at : null,
|
|
blacklisted : false,
|
|
source : 'derived:country',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
// 27. Server Discovery — derive dari flows dengan flow_origin=Server + port well-known server
|
|
async function fetchServerDiscovery(limit = 100, agentUuid = null) {
|
|
const SERVER_PORTS = {
|
|
80: 'HTTP', 443: 'HTTPS', 22: 'SSH', 21: 'FTP', 25: 'SMTP',
|
|
110: 'POP3', 143: 'IMAP', 3306: 'MySQL', 5432: 'PostgreSQL',
|
|
6379: 'Redis', 27017: 'MongoDB', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt',
|
|
53: 'DNS', 3389: 'RDP', 5900: 'VNC', 161: 'SNMP', 123: 'NTP',
|
|
389: 'LDAP', 636: 'LDAPS', 5060: 'SIP', 1194: 'OpenVPN',
|
|
};
|
|
|
|
const [flowOriginData, flowsRaw] = await Promise.all([
|
|
netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid),
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
]);
|
|
|
|
const serverMap = {};
|
|
if (flowsRaw) {
|
|
for (const r of flowsRaw) {
|
|
const localIP = r.local_ip?.address;
|
|
const localPort = r.local_port ?? r.remote_port;
|
|
const proto = r.ip_protocol?.label ?? null;
|
|
const mac = r.local_mac ?? null;
|
|
if (!localIP) continue;
|
|
|
|
// Deteksi server: local device listening di port well-known
|
|
const svcName = SERVER_PORTS[localPort] ?? SERVER_PORTS[r.remote_port];
|
|
if (svcName) {
|
|
const key = `${localIP}:${localPort ?? r.remote_port}`;
|
|
if (!serverMap[key]) {
|
|
const oui = mac ? mac.substring(0,8).toUpperCase() : null;
|
|
serverMap[key] = {
|
|
detected_at : r.first_seen_at?.date ?? null,
|
|
ip_address : localIP,
|
|
mac_address : mac,
|
|
server_type : svcName,
|
|
hostname : r.device?.label ?? null,
|
|
port : localPort ?? r.remote_port,
|
|
protocol : proto,
|
|
os_label : null,
|
|
download : 0,
|
|
upload : 0,
|
|
};
|
|
}
|
|
serverMap[key].download += r.download ?? 0;
|
|
serverMap[key].upload += r.upload ?? 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Object.values(serverMap)
|
|
.sort((a, b) => (b.download + b.upload) - (a.download + a.upload))
|
|
.slice(0, limit);
|
|
}
|
|
|
|
// 28. Tor Detection — derive dari top remote_ip + app/hostname matching Tor
|
|
async function fetchTorDetection(limit = 50, agentUuid = null) {
|
|
const TOR_INDICATORS = ['tor', '.onion', 'torproject', 'torbrowser'];
|
|
|
|
const [appData, domainData] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/hostname/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid),
|
|
]);
|
|
|
|
const results = [];
|
|
|
|
if (appData) {
|
|
for (const r of appData) {
|
|
const name = (r.application?.label ?? '').toLowerCase();
|
|
const tag = (r.application?.tag ?? '').toLowerCase();
|
|
if (TOR_INDICATORS.some(k => name.includes(k) || tag.includes(k))) {
|
|
results.push({
|
|
detected_at : null,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
exit_node : null,
|
|
circuit_id : null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
country : null,
|
|
source : 'derived:app',
|
|
label : r.application?.label,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (domainData) {
|
|
for (const r of domainData) {
|
|
const host = (r.hostname?.name ?? '').toLowerCase();
|
|
if (TOR_INDICATORS.some(k => host.includes(k))) {
|
|
results.push({
|
|
detected_at : null,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
exit_node : null,
|
|
circuit_id : null,
|
|
download : r.download ?? 0,
|
|
upload : 0,
|
|
country : null,
|
|
source : 'derived:hostname',
|
|
label : r.hostname?.name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
// 29. Unencrypted Passwords — derive dari flows ke port cleartext auth
|
|
async function fetchUnencryptedPasswords(limit = 50, agentUuid = null) {
|
|
// Port yang dikenal mengirim kredensial cleartext
|
|
const CLEARTEXT_AUTH_PORTS = {
|
|
21 : { protocol: 'FTP', severity: 'Critical' },
|
|
23 : { protocol: 'Telnet', severity: 'Critical' },
|
|
25 : { protocol: 'SMTP', severity: 'High' },
|
|
80 : { protocol: 'HTTP', severity: 'High' },
|
|
110 : { protocol: 'POP3', severity: 'High' },
|
|
143 : { protocol: 'IMAP', severity: 'High' },
|
|
389 : { protocol: 'LDAP', severity: 'Critical' },
|
|
512 : { protocol: 'rexec', severity: 'Critical' },
|
|
513 : { protocol: 'rlogin', severity: 'Critical' },
|
|
514 : { protocol: 'rsh', severity: 'Critical' },
|
|
};
|
|
|
|
const flowsRaw = await netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid);
|
|
if (!flowsRaw) return [];
|
|
|
|
const seen = new Map();
|
|
for (const r of flowsRaw) {
|
|
const port = r.remote_port ?? 0;
|
|
const info = CLEARTEXT_AUTH_PORTS[port];
|
|
if (!info) continue;
|
|
|
|
const key = `${r.local_ip?.address}:${r.remote_ip?.address}:${port}`;
|
|
if (!seen.has(key)) {
|
|
seen.set(key, {
|
|
detected_at : r.first_seen_at?.date ?? null,
|
|
ip_address : r.local_ip?.address ?? null,
|
|
mac_address : r.local_mac ?? null,
|
|
dst_ip : r.remote_ip?.address ?? null,
|
|
dst_port : port,
|
|
protocol : info.protocol,
|
|
username : null,
|
|
download : 0,
|
|
upload : 0,
|
|
severity : info.severity,
|
|
});
|
|
}
|
|
seen.get(key).download += r.download ?? 0;
|
|
seen.get(key).upload += r.upload ?? 0;
|
|
}
|
|
|
|
return Array.from(seen.values())
|
|
.sort((a, b) => (b.download + b.upload) - (a.download + a.upload))
|
|
.slice(0, limit);
|
|
}
|
|
|
|
// 30. VPN Detection — derive dari apps/protocols/hostnames yang mengindikasikan VPN
|
|
async function fetchVPNDetection(limit = 50, agentUuid = null) {
|
|
const VPN_APPS = [
|
|
'openvpn', 'wireguard', 'nordvpn', 'expressvpn', 'surfshark', 'tunnelbear',
|
|
'mullvad', 'protonvpn', 'ipvanish', 'pia', 'private internet access',
|
|
'hotspot shield', 'cyberghost', 'vpn', 'pptp', 'l2tp', 'ipsec', 'sstp',
|
|
'shadowsocks', 'v2ray', 'trojan', 'outline',
|
|
];
|
|
const VPN_PROTOCOLS = new Set(['OpenVPN', 'WireGuard', 'IPSec', 'PPTP', 'L2TP', 'GRE', 'SSTP']);
|
|
// Port yang umum digunakan VPN
|
|
const VPN_PORTS = new Set([1194, 51820, 500, 4500, 1701, 1723, 8388, 443]);
|
|
|
|
const [appData, protoData, flowsRaw] = await Promise.all([
|
|
netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid),
|
|
netifyFetch('/data/stats/top/protocol/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid),
|
|
netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid),
|
|
]);
|
|
|
|
const results = [];
|
|
|
|
if (appData) {
|
|
for (const r of appData) {
|
|
const name = (r.application?.label ?? '').toLowerCase();
|
|
const tag = (r.application?.tag ?? '').toLowerCase();
|
|
if (VPN_APPS.some(k => name.includes(k) || tag.includes(k))) {
|
|
results.push({
|
|
detected_at : null,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
vpn_type : r.application?.label ?? 'Unknown VPN',
|
|
remote_ip : null,
|
|
protocol : null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
country : null,
|
|
confidence : 0.9,
|
|
source : 'derived:app',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (protoData) {
|
|
for (const r of protoData) {
|
|
const label = r.protocol?.label ?? '';
|
|
if (VPN_PROTOCOLS.has(label)) {
|
|
results.push({
|
|
detected_at : null,
|
|
ip_address : null,
|
|
mac_address : null,
|
|
vpn_type : label,
|
|
remote_ip : null,
|
|
protocol : label,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
country : null,
|
|
confidence : 0.85,
|
|
source : 'derived:protocol',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (flowsRaw) {
|
|
const portSeen = new Set();
|
|
for (const r of flowsRaw) {
|
|
const port = r.remote_port ?? 0;
|
|
if (VPN_PORTS.has(port) && !portSeen.has(port)) {
|
|
portSeen.add(port);
|
|
results.push({
|
|
detected_at : r.first_seen_at?.date ?? null,
|
|
ip_address : r.local_ip?.address ?? null,
|
|
mac_address : r.local_mac ?? null,
|
|
vpn_type : `Port ${port}`,
|
|
remote_ip : r.remote_ip?.address ?? null,
|
|
protocol : r.ip_protocol?.label ?? null,
|
|
download : r.download ?? 0,
|
|
upload : r.upload ?? 0,
|
|
country : null,
|
|
confidence : 0.75,
|
|
source : 'derived:port',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
async function fetchAgents() {
|
|
const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, false, null);
|
|
if (!data || !Array.isArray(data)) return [];
|
|
const list = data.map(r => ({
|
|
id: r.agent?.id,
|
|
uuid: r.agent?.uuid,
|
|
label: r.agent?.label,
|
|
}));
|
|
for (const a of list) {
|
|
if (a.uuid && a.id) {
|
|
agentMap[a.uuid] = a.id;
|
|
}
|
|
}
|
|
return list;
|
|
} |