feat: implement password changes, agent headers, MAC routing tags, GeoIP caching lookups, port descriptions, and network infra agent isolation
This commit is contained in:
1 parent
72ded25e4d
commit
93d4002b27
10 files changed
+1193
-126
No files matched your search
+582
-53
@@ -6,6 +6,40 @@ const bcrypt = require('bcryptjs');
|
|||||||
const DB_PATH = path.join(__dirname, 'netify_data.db');
|
const DB_PATH = path.join(__dirname, 'netify_data.db');
|
||||||
let db;
|
let db;
|
||||||
|
|
||||||
|
// Agent UUID mappings to MAC addresses and numeric interface IDs
|
||||||
|
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',
|
||||||
|
'70:85:6c:81:50:d4', '70:85:6c:6d:f7:17', '60:be:b4:29:d3:33',
|
||||||
|
'60:be:b4:26:4c:d6', '60:be:b4:29:d3:32'
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const AGENT_NUMERIC_IDS = {
|
||||||
|
'2F-TF-1D-GK': ['4894730147'],
|
||||||
|
'8A-V3-PB-85': ['4895530456'],
|
||||||
|
'F6-2V-DT-8A': ['4895853843', '4897042839'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAgentTrafficRatio(agentUuid) {
|
||||||
|
const d = getDB();
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
if (!macs || macs.length === 0) return 0;
|
||||||
|
|
||||||
|
const latest = d.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get();
|
||||||
|
if (!latest?.t) return 0;
|
||||||
|
|
||||||
|
const siteTotal = d.prepare("SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ?").get(latest.t)?.val || 1;
|
||||||
|
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
const agentTotal = d.prepare(`SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders})`).get(latest.t, ...macs)?.val || 0;
|
||||||
|
|
||||||
|
return siteTotal > 0 ? (agentTotal / siteTotal) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function getDB() {
|
function getDB() {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
db = new Database(DB_PATH);
|
db = new Database(DB_PATH);
|
||||||
@@ -26,9 +60,17 @@ function initSchema() {
|
|||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role TEXT NOT NULL DEFAULT 'AGENT_VIEWER',
|
role TEXT NOT NULL DEFAULT 'AGENT_VIEWER',
|
||||||
site_uuid TEXT DEFAULT NULL,
|
site_uuid TEXT DEFAULT NULL,
|
||||||
|
agent_uuid TEXT DEFAULT NULL,
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
|
// Alter users table to add agent_uuid if it was created on an older schema
|
||||||
|
try {
|
||||||
|
d.exec("ALTER TABLE users ADD COLUMN agent_uuid TEXT DEFAULT NULL");
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, safe to ignore
|
||||||
|
}
|
||||||
|
|
||||||
const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get();
|
const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get();
|
||||||
if (adminExists.count === 0) {
|
if (adminExists.count === 0) {
|
||||||
const hash = bcrypt.hashSync('admin123', 10);
|
const hash = bcrypt.hashSync('admin123', 10);
|
||||||
@@ -39,11 +81,32 @@ function initSchema() {
|
|||||||
const agentExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent1'").get();
|
const agentExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent1'").get();
|
||||||
if (agentExists.count === 0) {
|
if (agentExists.count === 0) {
|
||||||
const hash = bcrypt.hashSync('agent123', 10);
|
const hash = bcrypt.hashSync('agent123', 10);
|
||||||
const site_uuid = process.env.NETIFY_SITE_UUID || 'dummy_site_uuid';
|
const site_uuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e';
|
||||||
d.prepare("INSERT INTO users (username, password_hash, role, site_uuid) VALUES (?, ?, ?, ?)").run('agent1', hash, 'AGENT_VIEWER', site_uuid);
|
d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent1', hash, 'AGENT_VIEWER', site_uuid, '8A-V3-PB-85');
|
||||||
console.log('[DB] Created default agent user (agent1 / agent123)');
|
console.log('[DB] Created default agent user (agent1 / agent123)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed specific agent accounts for testing isolation
|
||||||
|
const agentJrpExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_jrp'").get();
|
||||||
|
if (agentJrpExists.count === 0) {
|
||||||
|
const hash = bcrypt.hashSync('agent123', 10);
|
||||||
|
d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_jrp', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '2F-TF-1D-GK');
|
||||||
|
console.log('[DB] Created JRP Cibubur agent user (agent_jrp / agent123)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentIfgExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_ifg'").get();
|
||||||
|
if (agentIfgExists.count === 0) {
|
||||||
|
const hash = bcrypt.hashSync('agent123', 10);
|
||||||
|
d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_ifg', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', '8A-V3-PB-85');
|
||||||
|
console.log('[DB] Created IFG LT.18 agent user (agent_ifg / agent123)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentCpiExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'agent_cpi'").get();
|
||||||
|
if (agentCpiExists.count === 0) {
|
||||||
|
const hash = bcrypt.hashSync('agent123', 10);
|
||||||
|
d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)").run('agent_cpi', hash, 'AGENT_VIEWER', '6681452d_9cae_4ff4_8ae8_0d504774265e', 'F6-2V-DT-8A');
|
||||||
|
console.log('[DB] Created CPI Balaraja agent user (agent_cpi / agent123)');
|
||||||
|
}
|
||||||
|
|
||||||
d.exec(`CREATE TABLE IF NOT EXISTS tls_versions (
|
d.exec(`CREATE TABLE IF NOT EXISTS tls_versions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -490,6 +553,15 @@ function initSchema() {
|
|||||||
total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0
|
total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
|
d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache (
|
||||||
|
ip_address TEXT PRIMARY KEY,
|
||||||
|
isp TEXT,
|
||||||
|
country TEXT,
|
||||||
|
city TEXT,
|
||||||
|
as_org TEXT,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
console.log('[DB] Schema siap.');
|
console.log('[DB] Schema siap.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,86 +770,441 @@ function insertBandwidthTimeline(summary, fetchedAt, siteUuid) {
|
|||||||
|
|
||||||
// ─── QUERY FUNCTIONS ──────────────────────────────────────────────────────────
|
// ─── QUERY FUNCTIONS ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getLatestBandwidthApps(limit = 20, siteUuid = null) {
|
function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get();
|
|
||||||
if (!latest?.t) return [];
|
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestDevices(limit = 100, siteUuid = null) {
|
|
||||||
const d = getDB();
|
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
|
|
||||||
if (!latest?.t) return [];
|
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestFlows(limit = 100, siteUuid = null) {
|
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
if (!latest?.t) return [];
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT app_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload,
|
||||||
|
(SUM(bytes_download) + SUM(bytes_upload)) AS total, COUNT(*) AS flow_count
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND app_label IS NOT NULL
|
||||||
|
GROUP BY app_label
|
||||||
|
ORDER BY download DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, latest.t, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get();
|
||||||
|
if (!appsLatest?.t) return [];
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM bandwidth_apps WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
||||||
|
`).all({ fetched_at: appsLatest.t, limit, siteUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) {
|
||||||
|
let label = dbLabel || ip;
|
||||||
|
let manufacturer = dbManufacturer || 'Unknown';
|
||||||
|
let type = dbType || 'Generic Client';
|
||||||
|
let os = 'Unknown';
|
||||||
|
|
||||||
|
if (manufacturer.includes('Routerboard') || manufacturer.includes('MikroTik')) {
|
||||||
|
manufacturer = 'MikroTik';
|
||||||
|
type = 'Router/Network';
|
||||||
|
os = 'RouterOS';
|
||||||
|
} else if (manufacturer.includes('Fortinet')) {
|
||||||
|
manufacturer = 'Fortinet';
|
||||||
|
type = 'Firewall/Network';
|
||||||
|
os = 'FortiOS';
|
||||||
|
} else if (manufacturer.includes('WatchGuard')) {
|
||||||
|
manufacturer = 'WatchGuard';
|
||||||
|
type = 'Firewall/Network';
|
||||||
|
os = 'Fireware';
|
||||||
|
} else if (manufacturer.includes('Juniper')) {
|
||||||
|
manufacturer = 'Juniper';
|
||||||
|
type = 'Switch/Network';
|
||||||
|
os = 'Junos';
|
||||||
|
} else if (manufacturer.includes('Apple')) {
|
||||||
|
manufacturer = 'Apple';
|
||||||
|
type = 'Smart Device';
|
||||||
|
os = 'iOS/macOS';
|
||||||
|
} else if (manufacturer.includes('Samsung')) {
|
||||||
|
manufacturer = 'Samsung';
|
||||||
|
type = 'Smart TV';
|
||||||
|
os = 'Tizen OS';
|
||||||
|
} else if (manufacturer.includes('LCFC') || manufacturer.includes('Lenovo')) {
|
||||||
|
manufacturer = 'Lenovo';
|
||||||
|
type = 'Workstation';
|
||||||
|
os = 'Windows/Linux';
|
||||||
|
} else if (manufacturer.includes('Huawei')) {
|
||||||
|
manufacturer = 'Huawei';
|
||||||
|
type = 'Mobile';
|
||||||
|
os = 'Android';
|
||||||
|
} else if (manufacturer.includes('Dahua')) {
|
||||||
|
manufacturer = 'Dahua';
|
||||||
|
type = 'IP Camera';
|
||||||
|
os = 'Embedded OS';
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelLower = label.toLowerCase();
|
||||||
|
if (labelLower.includes('windows') || labelLower.includes('microsoft')) {
|
||||||
|
os = 'Windows';
|
||||||
|
type = 'Workstation';
|
||||||
|
manufacturer = manufacturer === 'Unknown' ? 'Microsoft' : manufacturer;
|
||||||
|
} else if (labelLower.includes('apple') || labelLower.includes('iphone') || labelLower.includes('ipad') || labelLower.includes('mac')) {
|
||||||
|
os = labelLower.includes('mac') ? 'macOS' : 'Apple iOS';
|
||||||
|
type = labelLower.includes('mac') ? 'Workstation' : 'Mobile';
|
||||||
|
manufacturer = 'Apple';
|
||||||
|
} else if (labelLower.includes('android') || labelLower.includes('oppo') || labelLower.includes('samsung phone')) {
|
||||||
|
os = 'Android';
|
||||||
|
type = 'Mobile';
|
||||||
|
if (labelLower.includes('oppo')) manufacturer = 'Oppo';
|
||||||
|
if (labelLower.includes('samsung')) manufacturer = 'Samsung';
|
||||||
|
} else if (labelLower.includes('samsung tv') || labelLower.includes('tizen')) {
|
||||||
|
os = 'Tizen OS';
|
||||||
|
type = 'Smart TV';
|
||||||
|
manufacturer = 'Samsung';
|
||||||
|
} else if (labelLower.includes('agent device')) {
|
||||||
|
os = 'Linux';
|
||||||
|
type = 'Security Agent';
|
||||||
|
manufacturer = 'S-Bluetech';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRouterIP = ['10.6.50.25', '10.6.12.242', '192.168.9.1', '10.6.11.208', '10.6.10.4'].includes(ip);
|
||||||
|
if (type === 'Router/Network' && !isRouterIP) {
|
||||||
|
type = 'LAN Client';
|
||||||
|
manufacturer = 'Unknown';
|
||||||
|
os = 'Windows/Linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { label, manufacturer, type, os };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) {
|
||||||
|
const d = getDB();
|
||||||
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
|
||||||
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
// Load intelligence tables to assist in type resolving
|
||||||
|
const intelList = d.prepare("SELECT * FROM intel_device_discovery").all();
|
||||||
|
const intelMap = new Map(intelList.map(i => [i.ip_address, i]));
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
|
||||||
|
// Find latest fetched_at in flows
|
||||||
|
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
|
|
||||||
|
// Get unique flow client IPs with bandwidth aggregation
|
||||||
|
let flowClients = [];
|
||||||
|
if (latestFlowFetch?.t) {
|
||||||
|
flowClients = d.prepare(`
|
||||||
|
SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
|
GROUP BY src_ip
|
||||||
|
ORDER BY flow_download DESC
|
||||||
|
`).all(...macs, latestFlowFetch.t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all devices in latest snapshot
|
||||||
|
const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t);
|
||||||
|
const devicesMap = new Map(devices.map(dev => [dev.ip_address, dev]));
|
||||||
|
|
||||||
|
const resolved = [];
|
||||||
|
const seenIps = new Set();
|
||||||
|
|
||||||
|
// First, add all devices that are explicitly in flows (active)
|
||||||
|
for (const fc of flowClients) {
|
||||||
|
if (!fc.src_ip) continue;
|
||||||
|
seenIps.add(fc.src_ip);
|
||||||
|
|
||||||
|
const devInfo = devicesMap.get(fc.src_ip);
|
||||||
|
const intelInfo = intelMap.get(fc.src_ip);
|
||||||
|
|
||||||
|
const dbLabel = devInfo ? devInfo.device_label : (intelInfo ? intelInfo.device_label : null);
|
||||||
|
const dbMan = devInfo ? devInfo.manufacturer : (intelInfo ? intelInfo.manufacturer : null);
|
||||||
|
const dbType = intelInfo ? intelInfo.device_type : null;
|
||||||
|
|
||||||
|
const meta = resolveDeviceMetadata(fc.src_ip, fc.src_mac, dbLabel, dbMan, dbType);
|
||||||
|
|
||||||
|
const isRouted = fc.src_mac === '04:f4:1c:ce:c2:e6' && fc.src_ip !== '10.6.50.25' && fc.src_ip !== '10.6.12.242';
|
||||||
|
|
||||||
|
resolved.push({
|
||||||
|
id: devInfo ? devInfo.id : null,
|
||||||
|
site_uuid: devInfo ? devInfo.site_uuid : siteUuid,
|
||||||
|
fetched_at: latest.t,
|
||||||
|
mac_address: fc.src_mac,
|
||||||
|
ip_address: fc.src_ip,
|
||||||
|
device_label: meta.label,
|
||||||
|
device_type: meta.type,
|
||||||
|
os_label: meta.os,
|
||||||
|
manufacturer: meta.manufacturer,
|
||||||
|
download: fc.flow_download,
|
||||||
|
upload: fc.flow_upload,
|
||||||
|
total: fc.flow_download + fc.flow_upload,
|
||||||
|
is_gateway_routed: isRouted ? 1 : 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, add other devices in devices table belonging to the agent's subnet (e.g. inactive/idle)
|
||||||
|
for (const dev of devices) {
|
||||||
|
if (seenIps.has(dev.ip_address)) continue;
|
||||||
|
|
||||||
|
// Check if this device belongs to the agent
|
||||||
|
let isAgentDevice = macs.includes(dev.mac_address);
|
||||||
|
if (!isAgentDevice && dev.ip_address) {
|
||||||
|
const devOctets = dev.ip_address.split('.');
|
||||||
|
if (devOctets.length === 4) {
|
||||||
|
const prefix3 = devOctets.slice(0, 3).join('.');
|
||||||
|
isAgentDevice = flowClients.some(fc => fc.src_ip && fc.src_ip.startsWith(prefix3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAgentDevice) {
|
||||||
|
const intelInfo = intelMap.get(dev.ip_address);
|
||||||
|
const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null);
|
||||||
|
|
||||||
|
const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242';
|
||||||
|
|
||||||
|
resolved.push({
|
||||||
|
...dev,
|
||||||
|
device_label: meta.label,
|
||||||
|
device_type: meta.type,
|
||||||
|
os_label: meta.os,
|
||||||
|
manufacturer: meta.manufacturer,
|
||||||
|
download: dev.download || 0,
|
||||||
|
upload: dev.upload || 0,
|
||||||
|
total: dev.total || 0,
|
||||||
|
is_gateway_routed: isRouted ? 1 : 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by download desc
|
||||||
|
resolved.sort((a, b) => b.download - a.download);
|
||||||
|
return resolved.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin view: apply metadata parsing and gateway routed check for all devices
|
||||||
|
const rows = d.prepare(`
|
||||||
|
SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
||||||
|
`).all({ fetched_at: latest.t, limit, siteUuid });
|
||||||
|
|
||||||
|
return rows.map(dev => {
|
||||||
|
const intelInfo = intelMap.get(dev.ip_address);
|
||||||
|
const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null);
|
||||||
|
|
||||||
|
const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...dev,
|
||||||
|
mac_address: dev.mac_address,
|
||||||
|
device_label: meta.label,
|
||||||
|
device_type: meta.type,
|
||||||
|
os_label: meta.os,
|
||||||
|
manufacturer: meta.manufacturer,
|
||||||
|
is_gateway_routed: isRouted ? 1 : 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) {
|
||||||
|
const d = getDB();
|
||||||
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
|
ORDER BY bytes_download DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, latest.t, limit);
|
||||||
|
}
|
||||||
|
|
||||||
return d.prepare(`
|
return d.prepare(`
|
||||||
SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit
|
SELECT * FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
`).all({ fetched_at: latest.t, limit, siteUuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestThreats(limit = 50, siteUuid = null) {
|
function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM threats
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
ORDER BY fetched_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, ...macs, limit);
|
||||||
|
}
|
||||||
return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
return d.prepare(`SELECT * FROM threats WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestProtocols(limit = 20, siteUuid = null) {
|
function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get();
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
if (!latest?.t) return [];
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
return d.prepare(`
|
return d.prepare(`
|
||||||
SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
SELECT protocol AS protocol_label, SUM(bytes_download) AS download, SUM(bytes_upload) AS upload, COUNT(*) AS flow_count
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
FROM flows
|
||||||
}
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
function getLatestCountries(limit = 15, siteUuid = null) {
|
GROUP BY protocol
|
||||||
const d = getDB();
|
ORDER BY download DESC
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get();
|
LIMIT ?
|
||||||
if (!latest?.t) return [];
|
`).all(...macs, latest.t, limit);
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
}
|
|
||||||
function getLatestDNS(limit = 20, siteUuid = null) {
|
|
||||||
const d = getDB();
|
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get();
|
|
||||||
if (!latest?.t) return [];
|
|
||||||
return d.prepare(`
|
|
||||||
SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit
|
|
||||||
`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestEvents(limit = 50, siteUuid = null) {
|
const protoLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get();
|
||||||
|
if (!protoLatest?.t) return [];
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM bandwidth_protocols WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
||||||
|
`).all({ fetched_at: protoLatest.t, limit, siteUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM intel_ip_reputation`).get();
|
||||||
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT country AS country_name, SUM(download) AS download, SUM(upload) AS upload, COUNT(*) AS flow_count
|
||||||
|
FROM intel_ip_reputation
|
||||||
|
WHERE (mac_address IN (${placeholders}) OR local_ip IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})))
|
||||||
|
AND country IS NOT NULL AND fetched_at = ?
|
||||||
|
GROUP BY country
|
||||||
|
ORDER BY download DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, ...macs, latest.t, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get();
|
||||||
|
if (!countryLatest?.t) return [];
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM bandwidth_countries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit
|
||||||
|
`).all({ fetched_at: countryLatest.t, limit, siteUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) {
|
||||||
|
const d = getDB();
|
||||||
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
|
if (!latest?.t) return [];
|
||||||
|
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT domain, COUNT(*) AS query_count, app_label, 'Web' AS category
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND domain IS NOT NULL AND fetched_at = ?
|
||||||
|
GROUP BY domain
|
||||||
|
ORDER BY query_count DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, latest.t, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get();
|
||||||
|
if (!dnsLatest?.t) return [];
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM dns_queries WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit
|
||||||
|
`).all({ fetched_at: dnsLatest.t, limit, siteUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
|
const d = getDB();
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM events
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
ORDER BY fetched_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, ...macs, limit);
|
||||||
|
}
|
||||||
return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
return d.prepare(`SELECT * FROM events WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBandwidthTimeline(points = 60, siteUuid = null) {
|
function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT fetched_at, SUM(download) AS total_download, SUM(upload) AS total_upload,
|
||||||
|
0 AS total_flows, 0 AS active_devices
|
||||||
|
FROM mac_bandwidth
|
||||||
|
WHERE mac_address IN (${placeholders})
|
||||||
|
GROUP BY fetched_at
|
||||||
|
ORDER BY fetched_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, points).reverse();
|
||||||
|
}
|
||||||
return d.prepare(`
|
return d.prepare(`
|
||||||
SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit
|
SELECT * FROM bandwidth_timeline WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit
|
||||||
`).all({ limit: points, siteUuid }).reverse();
|
`).all({ limit: points, siteUuid }).reverse();
|
||||||
}
|
}
|
||||||
function getStats(siteUuid = null) { console.log('getStats called with siteUuid:', siteUuid, typeof siteUuid);
|
|
||||||
|
function getStats(siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
|
||||||
// Hitung devices aktif dari polling terakhir (bukan DISTINCT mac karena null)
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
|
||||||
|
// Count active devices for this agent
|
||||||
|
const totalDevices = getLatestDevices(1000, siteUuid, agentUuid).length;
|
||||||
|
|
||||||
|
// Count active flows for this agent
|
||||||
|
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
|
const activeFlows = latestFlowFetch?.t
|
||||||
|
? (d.prepare(`
|
||||||
|
SELECT COUNT(*) as n FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
|
`).get(...macs, latestFlowFetch.t)?.n ?? 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Count threats for this agent
|
||||||
|
const totalThreats = d.prepare(`
|
||||||
|
SELECT COUNT(*) as n FROM threats
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
`).get(...macs, ...macs)?.n ?? 0;
|
||||||
|
|
||||||
|
// Count events for this agent
|
||||||
|
const totalEvents = d.prepare(`
|
||||||
|
SELECT COUNT(*) as n FROM events
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
`).get(...macs, ...macs)?.n ?? 0;
|
||||||
|
|
||||||
|
const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get()?.t ?? null;
|
||||||
|
|
||||||
|
// Construct latest bandwidth timeline summary for this agent
|
||||||
|
const latestBw = latestFlowFetch?.t
|
||||||
|
? d.prepare(`
|
||||||
|
SELECT SUM(bytes_download) AS total_download, SUM(bytes_upload) AS total_upload,
|
||||||
|
COUNT(*) AS total_flows, COUNT(DISTINCT src_ip) AS active_devices, ? as fetched_at
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
|
`).get(latestFlowFetch.t, ...macs, latestFlowFetch.t)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to site-wide stats if no agentUuid
|
||||||
const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
|
const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get();
|
||||||
console.log('latestDevFetch:', latestDevFetch); const totalDevices = latestDevFetch?.t
|
const totalDevices = latestDevFetch?.t
|
||||||
? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0)
|
? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestDevFetch.t, siteUuid })?.n ?? 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Hitung flows aktif dari polling terakhir
|
|
||||||
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get();
|
||||||
console.log('latestFlowFetch:', latestFlowFetch); const activeFlows = latestFlowFetch?.t
|
const activeFlows = latestFlowFetch?.t
|
||||||
? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0)
|
? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at`).get({ fetched_at: latestFlowFetch.t, siteUuid })?.n ?? 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
@@ -890,11 +1317,82 @@ function insertMACBandwidth(rows, fetchedAt, siteUuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── QUERY FITUR BARU ─────────────────────────────────────────────────────────
|
// ─── QUERY FITUR BARU ─────────────────────────────────────────────────────────
|
||||||
function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null) {
|
function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get();
|
const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get();
|
||||||
if (!latest?.t) return [];
|
if (!latest?.t) return [];
|
||||||
return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid });
|
|
||||||
|
let rows = [];
|
||||||
|
|
||||||
|
// If agentUuid is provided, handle direct MAC or interface filtering
|
||||||
|
const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null;
|
||||||
|
if (agentUuid && macs) {
|
||||||
|
if (table === 'mac_bandwidth') {
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
rows = d.prepare(`SELECT * FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...macs, limit);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
if (table === 'interfaces') {
|
||||||
|
const numericIds = AGENT_NUMERIC_IDS[agentUuid] || [];
|
||||||
|
if (numericIds.length > 0) {
|
||||||
|
const placeholders = numericIds.map(() => '?').join(',');
|
||||||
|
rows = d.prepare(`SELECT * FROM interfaces WHERE fetched_at = ? AND agent_id IN (${placeholders}) ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, ...numericIds, limit);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (table === 'remote_ips') {
|
||||||
|
const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get();
|
||||||
|
if (latestFlowsFetch?.t) {
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
rows = d.prepare(`
|
||||||
|
SELECT dst_ip AS remote_ip,
|
||||||
|
CASE WHEN dst_ip LIKE '%:%' THEN 6 ELSE 4 END AS ip_version,
|
||||||
|
SUM(bytes_download) AS download,
|
||||||
|
SUM(bytes_upload) AS upload,
|
||||||
|
SUM(bytes_download + bytes_upload) AS total
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ?
|
||||||
|
GROUP BY dst_ip
|
||||||
|
ORDER BY download DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, latestFlowsFetch.t, limit);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (table === 'dns_queries') {
|
||||||
|
const latestFlowsFetch = d.prepare("SELECT MAX(fetched_at) as t FROM flows").get();
|
||||||
|
if (latestFlowsFetch?.t) {
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
rows = d.prepare(`
|
||||||
|
SELECT domain, COUNT(*) AS query_count
|
||||||
|
FROM flows
|
||||||
|
WHERE src_mac IN (${placeholders}) AND fetched_at = ? AND domain IS NOT NULL
|
||||||
|
GROUP BY domain
|
||||||
|
ORDER BY query_count DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, latestFlowsFetch.t, limit);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard site-wide select
|
||||||
|
rows = d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ fetched_at: latest.t, limit, siteUuid });
|
||||||
|
|
||||||
|
// If agentUuid is provided, scale down numerical values by traffic ratio to match the agent's footprint
|
||||||
|
if (agentUuid && !['mac_bandwidth', 'interfaces'].includes(table)) {
|
||||||
|
const ratio = getAgentTrafficRatio(agentUuid);
|
||||||
|
return rows.map(r => ({
|
||||||
|
...r,
|
||||||
|
download: Math.round((r.download || 0) * ratio),
|
||||||
|
upload: Math.round((r.upload || 0) * ratio),
|
||||||
|
total: Math.round((r.total || 0) * ratio),
|
||||||
|
query_count: Math.round((r.query_count || 0) * ratio),
|
||||||
|
flow_count: Math.round((r.flow_count || 0) * ratio),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DPI Fields
|
// DPI Fields
|
||||||
@@ -1172,12 +1670,26 @@ function insertVPNDetection(rows, fetchedAt, siteUuid) {
|
|||||||
|
|
||||||
// ─── QUERY Intelligence — ambil semua row tanpa batasan fetched_at ────────────
|
// ─── QUERY Intelligence — ambil semua row tanpa batasan fetched_at ────────────
|
||||||
// (karena event ini tidak diposting ulang tiap menit, simpan kumulatif)
|
// (karena event ini tidak diposting ulang tiap menit, simpan kumulatif)
|
||||||
function getIntelData(table, limit = 100, siteUuid = null) {
|
function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
|
if (agentUuid && AGENT_MAC_MAP[agentUuid]) {
|
||||||
|
const macs = AGENT_MAC_MAP[agentUuid];
|
||||||
|
const placeholders = macs.map(() => '?').join(',');
|
||||||
|
|
||||||
|
// For reputation, the column is local_ip, not ip_address
|
||||||
|
const ipField = table === 'intel_ip_reputation' ? 'local_ip' : 'ip_address';
|
||||||
|
|
||||||
|
return d.prepare(`
|
||||||
|
SELECT * FROM ${table}
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
ORDER BY fetched_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(...macs, ...macs, limit);
|
||||||
|
}
|
||||||
return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIntelStats(siteUuid = null) {
|
function getIntelStats(siteUuid = null, agentUuid = null) {
|
||||||
const d = getDB();
|
const d = getDB();
|
||||||
const tables = [
|
const tables = [
|
||||||
'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit',
|
'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit',
|
||||||
@@ -1185,9 +1697,21 @@ function getIntelStats(siteUuid = null) {
|
|||||||
'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection',
|
'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection',
|
||||||
];
|
];
|
||||||
const counts = {};
|
const counts = {};
|
||||||
|
|
||||||
|
const macs = agentUuid ? AGENT_MAC_MAP[agentUuid] : null;
|
||||||
|
const placeholders = macs ? macs.map(() => '?').join(',') : '';
|
||||||
|
|
||||||
for (const t of tables) {
|
for (const t of tables) {
|
||||||
try {
|
try {
|
||||||
|
if (agentUuid && macs) {
|
||||||
|
const ipField = t === 'intel_ip_reputation' ? 'local_ip' : 'ip_address';
|
||||||
|
counts[t] = d.prepare(`
|
||||||
|
SELECT COUNT(*) as n FROM ${t}
|
||||||
|
WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))
|
||||||
|
`).get(...macs, ...macs)?.n ?? 0;
|
||||||
|
} else {
|
||||||
counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0;
|
counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid)`).get({ siteUuid })?.n ?? 0;
|
||||||
|
}
|
||||||
} catch { counts[t] = 0; }
|
} catch { counts[t] = 0; }
|
||||||
}
|
}
|
||||||
return counts;
|
return counts;
|
||||||
@@ -1195,6 +1719,7 @@ function getIntelStats(siteUuid = null) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getUserByUsername,
|
getUserByUsername,
|
||||||
|
updateUserPassword,
|
||||||
getDB,
|
getDB,
|
||||||
insertBandwidthApps, insertDevices, insertFlows, insertThreats,
|
insertBandwidthApps, insertDevices, insertFlows, insertThreats,
|
||||||
insertProtocols, insertCountries, insertDNS, insertEvents,
|
insertProtocols, insertCountries, insertDNS, insertEvents,
|
||||||
@@ -1224,3 +1749,7 @@ module.exports = {
|
|||||||
function getUserByUsername(username) {
|
function getUserByUsername(username) {
|
||||||
return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateUserPassword(userId, newPasswordHash) {
|
||||||
|
return getDB().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newPasswordHash, userId);
|
||||||
|
}
|
||||||
+132
-3
@@ -1,7 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { getUserByUsername } = require('../database');
|
const { getUserByUsername, updateUserPassword, getDB } = require('../database');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
|
||||||
@@ -23,7 +23,7 @@ router.post('/login', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid },
|
{ id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid },
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: '1d' }
|
{ expiresIn: '1d' }
|
||||||
);
|
);
|
||||||
@@ -38,7 +38,7 @@ router.post('/login', (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
user: { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid }
|
user: { id: user.id, username: user.username, role: user.role, site_uuid: user.site_uuid, agent_uuid: user.agent_uuid }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,9 +56,138 @@ router.get('/me', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/change-password', (req, res) => {
|
||||||
|
const token = req.cookies?.token;
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET);
|
||||||
|
const { currentPassword, newPassword } = req.body;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
return res.status(400).json({ error: 'Current password and new password are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get user details from database
|
||||||
|
const user = getUserByUsername(decoded.username);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Verify current password
|
||||||
|
const isCurrentValid = bcrypt.compareSync(currentPassword, user.password_hash);
|
||||||
|
if (!isCurrentValid) {
|
||||||
|
return res.status(400).json({ error: 'Password saat ini salah' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate new password strength
|
||||||
|
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).{8,}$/;
|
||||||
|
if (!passwordRegex.test(newPassword)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Password baru tidak memenuhi kriteria: minimal 8 karakter, serta mengandung huruf besar, huruf kecil, angka, dan karakter spesial.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Hash new password and save to DB
|
||||||
|
const newHash = bcrypt.hashSync(newPassword, 10);
|
||||||
|
updateUserPassword(user.id, newHash);
|
||||||
|
|
||||||
|
return res.json({ ok: true, message: 'Password berhasil diubah!' });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
res.clearCookie('token');
|
res.clearCookie('token');
|
||||||
res.json({ message: 'Logged out successfully' });
|
res.json({ message: 'Logged out successfully' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/geoip', async (req, res) => {
|
||||||
|
const ip = req.query.ip;
|
||||||
|
if (!ip) {
|
||||||
|
return res.status(400).json({ error: 'IP is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = getDB();
|
||||||
|
try {
|
||||||
|
// 1. Check local cache
|
||||||
|
let cached = d.prepare("SELECT * FROM geoip_cache WHERE ip_address = ?").get(ip);
|
||||||
|
if (cached) {
|
||||||
|
return res.json(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if private IP (IPv4 and IPv6 link local)
|
||||||
|
const parts = ip.split('.');
|
||||||
|
let isPrivate = false;
|
||||||
|
if (parts.length === 4) {
|
||||||
|
const o1 = parseInt(parts[0], 10);
|
||||||
|
const o2 = parseInt(parts[1], 10);
|
||||||
|
if (o1 === 10) isPrivate = true;
|
||||||
|
else if (o1 === 192 && o2 === 168) isPrivate = true;
|
||||||
|
else if (o1 === 172 && (o2 >= 16 && o2 <= 31)) isPrivate = true;
|
||||||
|
else if (o1 === 127) isPrivate = true;
|
||||||
|
else if (o1 === 169 && o2 === 254) isPrivate = true;
|
||||||
|
} else if (ip.startsWith('fe80:') || ip === '::1' || ip.startsWith('fd')) {
|
||||||
|
isPrivate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrivate) {
|
||||||
|
const privateInfo = {
|
||||||
|
ip_address: ip,
|
||||||
|
isp: 'Intranet / Private Network',
|
||||||
|
country: 'Local',
|
||||||
|
city: 'Local',
|
||||||
|
as_org: 'RFC 1918 Private Range'
|
||||||
|
};
|
||||||
|
d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run(
|
||||||
|
privateInfo.ip_address, privateInfo.isp, privateInfo.country, privateInfo.city, privateInfo.as_org
|
||||||
|
);
|
||||||
|
return res.json(privateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Query public GeoIP API (ip-api.com) with timeout
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3-second timeout
|
||||||
|
|
||||||
|
const response = await fetch(`http://ip-api.com/json/${ip}`, { signal: controller.signal });
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
const geo = await response.json();
|
||||||
|
if (geo && geo.status === 'success') {
|
||||||
|
const publicInfo = {
|
||||||
|
ip_address: ip,
|
||||||
|
isp: geo.isp || 'Unknown ISP',
|
||||||
|
country: geo.country || 'Unknown Country',
|
||||||
|
city: geo.city || 'Unknown City',
|
||||||
|
as_org: geo.as || geo.org || 'Data Center'
|
||||||
|
};
|
||||||
|
|
||||||
|
d.prepare("INSERT OR IGNORE INTO geoip_cache (ip_address, isp, country, city, as_org) VALUES (?, ?, ?, ?, ?)").run(
|
||||||
|
publicInfo.ip_address, publicInfo.isp, publicInfo.country, publicInfo.city, publicInfo.as_org
|
||||||
|
);
|
||||||
|
return res.json(publicInfo);
|
||||||
|
} else {
|
||||||
|
// Return temporary/fallback details for lookup failures without caching
|
||||||
|
return res.json({
|
||||||
|
ip_address: ip,
|
||||||
|
isp: 'Public IP',
|
||||||
|
country: 'Remote',
|
||||||
|
city: 'Remote',
|
||||||
|
as_org: 'Public Network'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return res.json({
|
||||||
|
ip_address: ip,
|
||||||
|
isp: 'Public IP',
|
||||||
|
country: 'Remote',
|
||||||
|
city: 'Remote',
|
||||||
|
as_org: 'Public Network'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
+45
-45
@@ -6,8 +6,8 @@ const { runPoll } = require('../scheduler');
|
|||||||
|
|
||||||
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
||||||
router.get('/summary', (req, res) => {
|
router.get('/summary', (req, res) => {
|
||||||
const stats = db.getStats(req.user?.site_uuid);
|
const stats = db.getStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid);
|
const timeline = db.getBandwidthTimeline(1, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const latest = timeline[0] ?? {};
|
const latest = timeline[0] ?? {};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -27,54 +27,54 @@ router.get('/summary', (req, res) => {
|
|||||||
// GET /api/dashboard/apps — top aplikasi
|
// GET /api/dashboard/apps — top aplikasi
|
||||||
router.get('/apps', (req, res) => {
|
router.get('/apps', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 10);
|
const limit = parseInt(req.query.limit ?? 10);
|
||||||
const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid);
|
const data = db.getLatestBandwidthApps(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/devices — daftar device
|
// GET /api/dashboard/devices — daftar device
|
||||||
router.get('/devices', (req, res) => {
|
router.get('/devices', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
const data = db.getLatestDevices(limit, req.user?.site_uuid);
|
const data = db.getLatestDevices(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/flows — flow aktif
|
// GET /api/dashboard/flows — flow aktif
|
||||||
router.get('/flows', (req, res) => {
|
router.get('/flows', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
const data = db.getLatestFlows(limit, req.user?.site_uuid);
|
const data = db.getLatestFlows(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/threats — ancaman keamanan
|
// GET /api/dashboard/threats — ancaman keamanan
|
||||||
router.get('/threats', (req, res) => {
|
router.get('/threats', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const data = db.getLatestThreats(limit, req.user?.site_uuid);
|
const data = db.getLatestThreats(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/protocols — top protokol
|
// GET /api/dashboard/protocols — top protokol
|
||||||
router.get('/protocols', (req, res) => {
|
router.get('/protocols', (req, res) => {
|
||||||
const data = db.getLatestProtocols(20, req.user?.site_uuid);
|
const data = db.getLatestProtocols(20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/countries — top negara
|
// GET /api/dashboard/countries — top negara
|
||||||
router.get('/countries', (req, res) => {
|
router.get('/countries', (req, res) => {
|
||||||
const data = db.getLatestCountries(15, req.user?.site_uuid);
|
const data = db.getLatestCountries(15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/dns — top DNS queries
|
// GET /api/dashboard/dns — top DNS queries
|
||||||
router.get('/dns', (req, res) => {
|
router.get('/dns', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const data = db.getLatestDNS(limit, req.user?.site_uuid);
|
const data = db.getLatestDNS(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/dashboard/events — events terbaru
|
// GET /api/dashboard/events — events terbaru
|
||||||
router.get('/events', (req, res) => {
|
router.get('/events', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
const rawData = db.getLatestEvents(limit, req.user?.site_uuid);
|
const rawData = db.getLatestEvents(limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
const mappedData = rawData.map(r => ({
|
const mappedData = rawData.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
event_id: r.event_id,
|
event_id: r.event_id,
|
||||||
@@ -90,7 +90,7 @@ router.get('/events', (req, res) => {
|
|||||||
// GET /api/dashboard/timeline — bandwidth timeline (grafik)
|
// GET /api/dashboard/timeline — bandwidth timeline (grafik)
|
||||||
router.get('/timeline', (req, res) => {
|
router.get('/timeline', (req, res) => {
|
||||||
const points = parseInt(req.query.points ?? 60);
|
const points = parseInt(req.query.points ?? 60);
|
||||||
const data = db.getBandwidthTimeline(points, req.user?.site_uuid);
|
const data = db.getBandwidthTimeline(points, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null);
|
||||||
res.json({ ok: true, data });
|
res.json({ ok: true, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,67 +102,67 @@ router.post('/refresh', async (req, res) => {
|
|||||||
|
|
||||||
// ─── ROUTES FITUR BARU 1-11 ───────────────────────────────────────────────────
|
// ─── ROUTES FITUR BARU 1-11 ───────────────────────────────────────────────────
|
||||||
router.get('/app-categories', (req, res) => {
|
router.get('/app-categories', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/continents', (req, res) => {
|
router.get('/continents', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('continents', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/regions', (req, res) => {
|
router.get('/regions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('regions', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/cities', (req, res) => {
|
router.get('/cities', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('cities', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/vlans', (req, res) => {
|
router.get('/vlans', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('vlans', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/interfaces', (req, res) => {
|
router.get('/interfaces', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/flow-types', (req, res) => {
|
router.get('/flow-types', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/flow-origins', (req, res) => {
|
router.get('/flow-origins', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/ip-versions', (req, res) => {
|
router.get('/ip-versions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/remote-ips', (req, res) => {
|
router.get('/remote-ips', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
router.get('/mac-bandwidth', (req, res) => {
|
router.get('/mac-bandwidth', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ──────────────────────────────────────
|
// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ──────────────────────────────────────
|
||||||
|
|
||||||
// TLS Versions
|
// TLS Versions
|
||||||
router.get('/tls-versions', (req, res) => {
|
router.get('/tls-versions', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// TLS Ciphers
|
// TLS Ciphers
|
||||||
router.get('/tls-ciphers', (req, res) => {
|
router.get('/tls-ciphers', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// TLS Security Level
|
// TLS Security Level
|
||||||
router.get('/tls-security', (req, res) => {
|
router.get('/tls-security', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// NetBIOS Hostnames (Windows devices)
|
// NetBIOS Hostnames (Windows devices)
|
||||||
router.get('/netbios', (req, res) => {
|
router.get('/netbios', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Discovery OS (sistem operasi yang terdeteksi)
|
// Discovery OS (sistem operasi yang terdeteksi)
|
||||||
router.get('/discovery-os', (req, res) => {
|
router.get('/discovery-os', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── ROUTES DPI 12-21 ─────────────────────────────────────────────────────────
|
// ─── ROUTES DPI 12-21 ─────────────────────────────────────────────────────────
|
||||||
@@ -170,110 +170,110 @@ router.get('/discovery-os', (req, res) => {
|
|||||||
// 12. DHCP Class Fingerprint
|
// 12. DHCP Class Fingerprint
|
||||||
router.get('/dhcp-fingerprints', (req, res) => {
|
router.get('/dhcp-fingerprints', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 13. HTTP User-Agent
|
// 13. HTTP User-Agent
|
||||||
router.get('/http-user-agents', (req, res) => {
|
router.get('/http-user-agents', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 14. HTTPS SNI Hostname
|
// 14. HTTPS SNI Hostname
|
||||||
router.get('/sni-hostnames', (req, res) => {
|
router.get('/sni-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 15. SSL Server Common Name
|
// 15. SSL Server Common Name
|
||||||
router.get('/ssl-server-cn', (req, res) => {
|
router.get('/ssl-server-cn', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 17. QUIC Hostname
|
// 17. QUIC Hostname
|
||||||
router.get('/quic-hostnames', (req, res) => {
|
router.get('/quic-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 18. BitTorrent Info Hash
|
// 18. BitTorrent Info Hash
|
||||||
router.get('/bittorrent-hashes', (req, res) => {
|
router.get('/bittorrent-hashes', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 19. SSH Version
|
// 19. SSH Version
|
||||||
router.get('/ssh-versions', (req, res) => {
|
router.get('/ssh-versions', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 20);
|
const limit = parseInt(req.query.limit ?? 20);
|
||||||
res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 21. mDNS Hostname (Chromecast, Apple TV, etc.)
|
// 21. mDNS Hostname (Chromecast, Apple TV, etc.)
|
||||||
router.get('/mdns-hostnames', (req, res) => {
|
router.get('/mdns-hostnames', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 30);
|
const limit = parseInt(req.query.limit ?? 30);
|
||||||
res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── INTELLIGENCE ROUTES 22-30 ────────────────────────────────────────────────
|
// ─── INTELLIGENCE ROUTES 22-30 ────────────────────────────────────────────────
|
||||||
|
|
||||||
// Stats ringkasan semua intelligence (untuk badge count di tab)
|
// Stats ringkasan semua intelligence (untuk badge count di tab)
|
||||||
router.get('/intelligence/stats', (req, res) => {
|
router.get('/intelligence/stats', (req, res) => {
|
||||||
res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelStats(req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 22. Cryptocurrency Mining
|
// 22. Cryptocurrency Mining
|
||||||
router.get('/intelligence/crypto-mining', (req, res) => {
|
router.get('/intelligence/crypto-mining', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 23. Device Discovery
|
// 23. Device Discovery
|
||||||
router.get('/intelligence/device-discovery', (req, res) => {
|
router.get('/intelligence/device-discovery', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 100);
|
const limit = parseInt(req.query.limit ?? 100);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 24. Encryption Audit
|
// 24. Encryption Audit
|
||||||
router.get('/intelligence/encryption-audit', (req, res) => {
|
router.get('/intelligence/encryption-audit', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 25. Insecure Protocols
|
// 25. Insecure Protocols
|
||||||
router.get('/intelligence/insecure-protocols', (req, res) => {
|
router.get('/intelligence/insecure-protocols', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 26. IP Reputation
|
// 26. IP Reputation
|
||||||
router.get('/intelligence/ip-reputation', (req, res) => {
|
router.get('/intelligence/ip-reputation', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 27. Server Discovery
|
// 27. Server Discovery
|
||||||
router.get('/intelligence/server-discovery', (req, res) => {
|
router.get('/intelligence/server-discovery', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 100);
|
const limit = parseInt(req.query.limit ?? 100);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 28. Tor Detection
|
// 28. Tor Detection
|
||||||
router.get('/intelligence/tor', (req, res) => {
|
router.get('/intelligence/tor', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 29. Unencrypted Passwords
|
// 29. Unencrypted Passwords
|
||||||
router.get('/intelligence/unencrypted-passwords', (req, res) => {
|
router.get('/intelligence/unencrypted-passwords', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 30. VPN Detection
|
// 30. VPN Detection
|
||||||
router.get('/intelligence/vpn', (req, res) => {
|
router.get('/intelligence/vpn', (req, res) => {
|
||||||
const limit = parseInt(req.query.limit ?? 50);
|
const limit = parseInt(req.query.limit ?? 50);
|
||||||
res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid) });
|
res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit, req.user?.site_uuid, req.user?.role === 'AGENT_VIEWER' ? req.user?.agent_uuid : null) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── LOOKUP ROUTES ────────────────────────────────────────────────────────────
|
// ─── LOOKUP ROUTES ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents";
|
import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||||
@@ -10,6 +11,8 @@ import { AgentDetailModal } from "@/components/ui/AgentDetailModal";
|
|||||||
import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle, ChevronRight } from "lucide-react";
|
import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function AgentsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [role, setRole] = useState<string | null>(null);
|
||||||
const [agents, setAgents] = useState<Agent[]>([]);
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -46,8 +49,25 @@ export default function AgentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetch('/api/auth/me')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.user) {
|
||||||
|
setRole(data.user.role || null);
|
||||||
|
if (data.user.role === 'AGENT_VIEWER') {
|
||||||
|
router.push('/');
|
||||||
|
} else {
|
||||||
loadAgents();
|
loadAgents();
|
||||||
}, []);
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Auth check failed:", err);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setFormData({ agentId: "", label: "" });
|
setFormData({ agentId: "", label: "" });
|
||||||
|
|||||||
@@ -40,9 +40,16 @@ export default function DevicesPage() {
|
|||||||
header: "MAC Address",
|
header: "MAC Address",
|
||||||
accessor: (row) =>
|
accessor: (row) =>
|
||||||
row.mac_address ? (
|
row.mac_address ? (
|
||||||
<code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground">
|
<div className="flex flex-col gap-1">
|
||||||
|
<code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground w-fit">
|
||||||
{row.mac_address}
|
{row.mac_address}
|
||||||
</code>
|
</code>
|
||||||
|
{row.is_gateway_routed === 1 && (
|
||||||
|
<span className="text-[10px] text-amber-500/80 font-sans italic">
|
||||||
|
Via Routed Gateway
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,48 @@ import { DataTable, Column } from "@/components/ui/DataTable";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { fmtBytes } from "@/lib/utils";
|
import { fmtBytes } from "@/lib/utils";
|
||||||
|
import { IpDetails } from "@/components/ui/IpDetails";
|
||||||
|
|
||||||
|
function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) {
|
||||||
|
const label = appLabel || domain || "";
|
||||||
|
const portStr = String(port);
|
||||||
|
|
||||||
|
const matches: Record<string, string> = {
|
||||||
|
"1433": "MSSQL Database Server",
|
||||||
|
"1434": "MSSQL Monitor Server",
|
||||||
|
"3306": "MySQL/MariaDB Database Server",
|
||||||
|
"5432": "PostgreSQL Database Server",
|
||||||
|
"1521": "Oracle Database Server",
|
||||||
|
"27017": "MongoDB Database Server",
|
||||||
|
"6379": "Redis Key-Value Cache",
|
||||||
|
"80": "Unencrypted Web Traffic (HTTP)",
|
||||||
|
"443": "Encrypted Web Traffic (HTTPS/TLS)",
|
||||||
|
"22": "SSH Remote Management / SFTP",
|
||||||
|
"21": "FTP File Upload/Download (Unencrypted)",
|
||||||
|
"23": "Telnet Command Shell (Insecure)",
|
||||||
|
"25": "SMTP Mail Delivery",
|
||||||
|
"587": "Secure SMTP Mail Delivery",
|
||||||
|
"110": "POP3 Mail Storage Retrieval",
|
||||||
|
"993": "Secure IMAP Mail Service",
|
||||||
|
"53": "DNS Server Query (Domain Lookup)",
|
||||||
|
"123": "NTP Network Clock Synchronizer",
|
||||||
|
"161": "SNMP Network Device Monitoring",
|
||||||
|
"3389": "RDP Remote Desktop Management",
|
||||||
|
"445": "SMB Windows File Sharing",
|
||||||
|
"137": "NetBIOS Name Service",
|
||||||
|
"138": "NetBIOS Datagram Service",
|
||||||
|
"139": "NetBIOS Session Service",
|
||||||
|
"1812": "RADIUS Authentication Server",
|
||||||
|
"1813": "RADIUS Accounting Server"
|
||||||
|
};
|
||||||
|
|
||||||
|
const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
main: label || `Port ${port}`,
|
||||||
|
sub: explanation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function FlowsPage() {
|
export default function FlowsPage() {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@@ -21,10 +63,35 @@ export default function FlowsPage() {
|
|||||||
{ header: "#", accessor: (row, i) => i + 1 },
|
{ header: "#", accessor: (row, i) => i + 1 },
|
||||||
{ header: "Flow ID", accessor: (row) => row.flow_id },
|
{ header: "Flow ID", accessor: (row) => row.flow_id },
|
||||||
{ header: "Src IP", accessor: (row) => <span className="text-blue-400">{row.src_ip}</span> },
|
{ header: "Src IP", accessor: (row) => <span className="text-blue-400">{row.src_ip}</span> },
|
||||||
{ header: "Dst IP", accessor: (row) => <span className="text-blue-400">{row.dst_ip}</span> },
|
{
|
||||||
|
header: "Dst IP",
|
||||||
|
accessor: (row) => (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-mono text-blue-400">{row.dst_ip}</span>
|
||||||
|
<IpDetails ip={row.dst_ip} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
{ header: "Dst Port", accessor: (row) => row.dst_port },
|
{ header: "Dst Port", accessor: (row) => row.dst_port },
|
||||||
{ header: "Protocol", accessor: (row) => row.protocol },
|
{ header: "Protocol", accessor: (row) => row.protocol },
|
||||||
{ header: "App / Domain", accessor: (row) => row.app_label || row.domain || "-" },
|
{
|
||||||
|
header: "App / Domain",
|
||||||
|
accessor: (row) => {
|
||||||
|
const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-semibold text-white truncate max-w-[200px]" title={exp.main}>
|
||||||
|
{exp.main}
|
||||||
|
</span>
|
||||||
|
{exp.sub && (
|
||||||
|
<span className="text-[10px] text-slate-400 italic">
|
||||||
|
{exp.sub}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: "Download",
|
header: "Download",
|
||||||
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
|
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { Bell, RefreshCw } from "lucide-react";
|
import { Bell, RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
|
const AGENT_LABELS: Record<string, string> = {
|
||||||
|
'2F-TF-1D-GK': 'JRP Cibubur',
|
||||||
|
'8A-V3-PB-85': 'IFG LT.18',
|
||||||
|
'F6-2V-DT-8A': 'CPI Balaraja',
|
||||||
|
};
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
lastUpdate = "—",
|
lastUpdate = "—",
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
@@ -11,6 +18,19 @@ export function Header({
|
|||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [user, setUser] = useState<{ role?: string; agent_uuid?: string } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/auth/me')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.user) {
|
||||||
|
setUser(data.user);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-border bg-background px-6">
|
<header className="flex h-16 shrink-0 items-center justify-between border-b border-border bg-background px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -20,6 +40,16 @@ export function Header({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{user?.role === 'AGENT_VIEWER' && (
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-blue-500/30 bg-blue-950/40 px-3 py-1 text-xs text-blue-300 shadow-[0_0_10px_rgba(59,130,246,0.1)]">
|
||||||
|
<span className="font-semibold">Agent:</span> {AGENT_LABELS[user.agent_uuid || ''] || user.agent_uuid} ({user.agent_uuid})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user?.role === 'SUPER_ADMIN' && (
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-red-500/30 bg-red-950/40 px-3 py-1 text-xs text-red-300 shadow-[0_0_10px_rgba(239,68,68,0.1)]">
|
||||||
|
<span className="font-semibold">Admin:</span> All Agents
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Update: {lastUpdate}
|
Update: {lastUpdate}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: "Summary", href: "/", icon: LayoutDashboard },
|
{ name: "Summary", href: "/", icon: LayoutDashboard },
|
||||||
@@ -39,6 +41,36 @@ const navigation = [
|
|||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [user, setUser] = useState<{ role?: string; agent_uuid?: string } | null>(null);
|
||||||
|
|
||||||
|
// Change Password state
|
||||||
|
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
||||||
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [successMsg, setSuccessMsg] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Real-time checks for password strength validation
|
||||||
|
const isMinLength = newPassword.length >= 8;
|
||||||
|
const hasUppercase = /[A-Z]/.test(newPassword);
|
||||||
|
const hasLowercase = /[a-z]/.test(newPassword);
|
||||||
|
const hasNumber = /[0-9]/.test(newPassword);
|
||||||
|
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(newPassword);
|
||||||
|
const passwordsMatch = newPassword === confirmPassword && confirmPassword.length > 0;
|
||||||
|
const isPasswordValid = isMinLength && hasUppercase && hasLowercase && hasNumber && hasSpecial && passwordsMatch;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/auth/me')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.user) {
|
||||||
|
setUser(data.user);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +82,46 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isPasswordValid) return;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
setSuccessMsg("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ currentPassword, newPassword }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
setSuccessMsg("Password berhasil diubah!");
|
||||||
|
setCurrentPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsChangePasswordOpen(false);
|
||||||
|
setSuccessMsg("");
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
setErrorMsg(data.error || "Gagal mengubah password");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setErrorMsg("Terjadi kesalahan koneksi");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredNavigation = navigation.filter((item) => {
|
||||||
|
if (user?.role === 'AGENT_VIEWER' && item.name === 'Agents') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
|
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
|
||||||
{/* Subtle glow effect behind sidebar */}
|
{/* Subtle glow effect behind sidebar */}
|
||||||
@@ -63,7 +135,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto relative z-10">
|
<div className="flex flex-1 flex-col overflow-y-auto relative z-10">
|
||||||
<nav className="flex-1 space-y-1.5 px-3 py-6">
|
<nav className="flex-1 space-y-1.5 px-3 py-6">
|
||||||
{navigation.map((item) => {
|
{filteredNavigation.map((item) => {
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -102,7 +174,21 @@ export function Sidebar() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-border/50 relative z-10 shrink-0">
|
<div className="p-4 border-t border-border/50 relative z-10 shrink-0 space-y-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setErrorMsg("");
|
||||||
|
setSuccessMsg("");
|
||||||
|
setCurrentPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
setIsChangePasswordOpen(true);
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-slate-300 hover:bg-secondary/50 hover:text-white transition-all duration-200"
|
||||||
|
>
|
||||||
|
<Lock className="mr-3 h-5 w-5" />
|
||||||
|
Change Password
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200"
|
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200"
|
||||||
@@ -111,6 +197,101 @@ export function Sidebar() {
|
|||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal isOpen={isChangePasswordOpen} onClose={() => setIsChangePasswordOpen(false)} title="Ubah Password">
|
||||||
|
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="rounded-lg bg-red-500/10 p-3 text-xs text-red-400 border border-red-500/20">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{successMsg && (
|
||||||
|
<div className="rounded-lg bg-green-500/10 p-3 text-xs text-green-400 border border-green-500/20">
|
||||||
|
{successMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium text-slate-300">Password Saat Ini</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium text-slate-300">Password Baru</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium text-slate-300">Konfirmasi Password Baru</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-white placeholder-slate-500 focus:border-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Real-time Checklist */}
|
||||||
|
<div className="rounded-lg bg-secondary/30 p-3.5 border border-border/50 space-y-2">
|
||||||
|
<span className="text-xs font-semibold text-slate-300">Syarat Kekuatan Password:</span>
|
||||||
|
<ul className="text-xs space-y-1.5">
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", isMinLength ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{isMinLength ? "✓" : "○"}</span> Minimal 8 karakter
|
||||||
|
</li>
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", hasUppercase ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{hasUppercase ? "✓" : "○"}</span> Mengandung huruf besar (A-Z)
|
||||||
|
</li>
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", hasLowercase ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{hasLowercase ? "✓" : "○"}</span> Mengandung huruf kecil (a-z)
|
||||||
|
</li>
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", hasNumber ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{hasNumber ? "✓" : "○"}</span> Mengandung angka (0-9)
|
||||||
|
</li>
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", hasSpecial ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{hasSpecial ? "✓" : "○"}</span> Mengandung karakter spesial (contoh: @$!%*?&)
|
||||||
|
</li>
|
||||||
|
<li className={cn("flex items-center transition-colors duration-200", passwordsMatch ? "text-green-400 font-medium" : "text-slate-400")}>
|
||||||
|
<span className="mr-2">{passwordsMatch ? "✓" : "○"}</span> Password cocok
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsChangePasswordOpen(false)}
|
||||||
|
className="rounded-lg bg-secondary px-4 py-2 text-sm font-medium text-white hover:bg-secondary/80 transition-colors"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isPasswordValid || isSubmitting}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg px-4 py-2 text-sm font-medium text-white shadow-lg transition-all duration-200",
|
||||||
|
isPasswordValid && !isSubmitting
|
||||||
|
? "bg-primary hover:bg-primary/90 shadow-primary/25 cursor-pointer"
|
||||||
|
: "bg-slate-700 text-slate-400 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Menyimpan..." : "Simpan Password"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
fetchDeviceDetails, DeviceDetails,
|
fetchDeviceDetails, DeviceDetails,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import { fmtBytes } from "@/lib/utils";
|
import { fmtBytes } from "@/lib/utils";
|
||||||
|
import { IpDetails } from "./IpDetails";
|
||||||
import {
|
import {
|
||||||
Loader2, X, Monitor, Globe, Activity, Lock, Server,
|
Loader2, X, Monitor, Globe, Activity, Lock, Server,
|
||||||
AlertTriangle, Eye, Shield, Bell, Wifi, Unlock, Router,
|
AlertTriangle, Eye, Shield, Bell, Wifi, Unlock, Router,
|
||||||
@@ -21,6 +22,47 @@ interface Props {
|
|||||||
type Tab = "info" | "flows" | "apps" | "encryption" | "servers" | "passwords" | "reputation" | "vpn_events";
|
type Tab = "info" | "flows" | "apps" | "encryption" | "servers" | "passwords" | "reputation" | "vpn_events";
|
||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) {
|
||||||
|
const label = appLabel || domain || "";
|
||||||
|
const portStr = String(port);
|
||||||
|
|
||||||
|
const matches: Record<string, string> = {
|
||||||
|
"1433": "MSSQL Database Server",
|
||||||
|
"1434": "MSSQL Monitor Server",
|
||||||
|
"3306": "MySQL/MariaDB Database Server",
|
||||||
|
"5432": "PostgreSQL Database Server",
|
||||||
|
"1521": "Oracle Database Server",
|
||||||
|
"27017": "MongoDB Database Server",
|
||||||
|
"6379": "Redis Key-Value Cache",
|
||||||
|
"80": "Unencrypted Web Traffic (HTTP)",
|
||||||
|
"443": "Encrypted Web Traffic (HTTPS/TLS)",
|
||||||
|
"22": "SSH Remote Management / SFTP",
|
||||||
|
"21": "FTP File Upload/Download (Unencrypted)",
|
||||||
|
"23": "Telnet Command Shell (Insecure)",
|
||||||
|
"25": "SMTP Mail Delivery",
|
||||||
|
"587": "Secure SMTP Mail Delivery",
|
||||||
|
"110": "POP3 Mail Storage Retrieval",
|
||||||
|
"993": "Secure IMAP Mail Service",
|
||||||
|
"53": "DNS Server Query (Domain Lookup)",
|
||||||
|
"123": "NTP Network Clock Synchronizer",
|
||||||
|
"161": "SNMP Network Device Monitoring",
|
||||||
|
"3389": "RDP Remote Desktop Management",
|
||||||
|
"445": "SMB Windows File Sharing",
|
||||||
|
"137": "NetBIOS Name Service",
|
||||||
|
"138": "NetBIOS Datagram Service",
|
||||||
|
"139": "NetBIOS Session Service",
|
||||||
|
"1812": "RADIUS Authentication Server",
|
||||||
|
"1813": "RADIUS Accounting Server"
|
||||||
|
};
|
||||||
|
|
||||||
|
const explanation = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
main: label || `Port ${port}`,
|
||||||
|
sub: explanation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function sevStyle(sev: string | null) {
|
function sevStyle(sev: string | null) {
|
||||||
switch ((sev || "").toLowerCase()) {
|
switch ((sev || "").toLowerCase()) {
|
||||||
case "critical": return "bg-red-500/15 text-red-400 ring-1 ring-red-500/30";
|
case "critical": return "bg-red-500/15 text-red-400 ring-1 ring-red-500/30";
|
||||||
@@ -264,28 +306,21 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop
|
|||||||
{activeTab === "flows" && (
|
{activeTab === "flows" && (
|
||||||
data.flows.length === 0 ? <Empty msg="Tidak ada flows untuk device ini." /> : (
|
data.flows.length === 0 ? <Empty msg="Tidak ada flows untuk device ini." /> : (
|
||||||
<TableWrap head={["Destination IP", "Port", "Domain / App", "Proto", "↓", "↑", "Last Seen"]}>
|
<TableWrap head={["Destination IP", "Port", "Domain / App", "Proto", "↓", "↑", "Last Seen"]}>
|
||||||
{data.flows.map((f, i) => (
|
{data.flows.map((f, i) => {
|
||||||
|
const exp = explainAppOrPort(f.app_label, f.domain, f.dst_port || 0);
|
||||||
|
return (
|
||||||
<tr key={i} className="hover:bg-white/[0.03] transition-colors">
|
<tr key={i} className="hover:bg-white/[0.03] transition-colors">
|
||||||
<td className="px-3 py-2 font-mono text-xs text-blue-400 whitespace-nowrap">{f.dst_ip || "—"}</td>
|
<td className="px-3 py-2 whitespace-nowrap">
|
||||||
|
<span className="font-mono text-xs text-blue-400 block">{f.dst_ip || "—"}</span>
|
||||||
|
{f.dst_ip && <IpDetails ip={f.dst_ip} />}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2 text-xs text-slate-400">{f.dst_port ?? "—"}</td>
|
<td className="px-3 py-2 text-xs text-slate-400">{f.dst_port ?? "—"}</td>
|
||||||
<td className="px-3 py-2 max-w-[180px]">
|
<td className="px-3 py-2 max-w-[180px]">
|
||||||
{f.domain ? (
|
<span className="text-xs text-teal-300 font-mono truncate block" title={exp.main}>
|
||||||
/* Domain name takes priority — shown in teal monospace */
|
{exp.main}
|
||||||
<span className="text-xs text-teal-300 font-mono truncate block" title={f.domain}>
|
|
||||||
{f.domain}
|
|
||||||
</span>
|
</span>
|
||||||
) : f.app_label && !f.app_label.startsWith('Port ') ? (
|
{exp.sub && (
|
||||||
/* Named protocol — purple */
|
<span className="text-[10px] text-slate-500 block italic">{exp.sub}</span>
|
||||||
<span className="text-xs text-purple-300 truncate block">{f.app_label}</span>
|
|
||||||
) : f.app_label ? (
|
|
||||||
/* Port-only entry — muted */
|
|
||||||
<span className="text-xs text-slate-500 truncate block">{f.app_label}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-slate-600">—</span>
|
|
||||||
)}
|
|
||||||
{/* Show app_label as secondary if domain is set */}
|
|
||||||
{f.domain && f.app_label && (
|
|
||||||
<span className="text-[10px] text-slate-500 block">{f.app_label}</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td>
|
<td className="px-3 py-2 text-xs text-slate-400">{f.protocol || "—"}</td>
|
||||||
@@ -293,7 +328,8 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop
|
|||||||
<td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td>
|
<td className="px-3 py-2 text-right text-xs text-green-400 whitespace-nowrap">{fmtBytes(f.upload)}</td>
|
||||||
<td className="px-3 py-2 text-xs text-slate-500 whitespace-nowrap">{f.last_seen?.slice(0, 16) || "—"}</td>
|
<td className="px-3 py-2 text-xs text-slate-500 whitespace-nowrap">{f.last_seen?.slice(0, 16) || "—"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TableWrap>
|
</TableWrap>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
// Client-side cache to prevent duplicate fetches for the same IP
|
||||||
|
const geoIpCache: Record<string, { isp: string; country: string; as_org: string }> = {};
|
||||||
|
|
||||||
|
export function IpDetails({ ip }: { ip: string }) {
|
||||||
|
const [details, setDetails] = useState<{ isp: string; country: string; as_org: string } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ip) return;
|
||||||
|
|
||||||
|
// Check memory cache first
|
||||||
|
if (geoIpCache[ip]) {
|
||||||
|
setDetails(geoIpCache[ip]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
fetch(`/api/auth/geoip?ip=${encodeURIComponent(ip)}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
const resolved = {
|
||||||
|
isp: data.isp || "Unknown ISP",
|
||||||
|
country: data.country || "Remote",
|
||||||
|
as_org: data.as_org || "Public Network",
|
||||||
|
};
|
||||||
|
geoIpCache[ip] = resolved;
|
||||||
|
setDetails(resolved);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("GeoIP lookup client error:", err);
|
||||||
|
if (isMounted) {
|
||||||
|
setDetails({
|
||||||
|
isp: "Public IP",
|
||||||
|
country: "Remote",
|
||||||
|
as_org: "Public Network",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [ip]);
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return <span className="text-[10px] text-slate-500 animate-pulse">loading...</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLocal = details.country === "Local";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-[10px] leading-tight mt-0.5 max-w-[180px] truncate">
|
||||||
|
<span className={isLocal ? "text-emerald-500/90 font-medium" : "text-cyan-400/90 font-medium"}>
|
||||||
|
{details.isp}
|
||||||
|
</span>
|
||||||
|
{!isLocal && (
|
||||||
|
<span className="text-slate-400 font-normal">
|
||||||
|
{" • "}{details.country}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user