feat(source2): lock dark mode, sans-serif typography, and all recent updates

This commit is contained in:
rafif committed 2026-08-20 23:02:00 +07:00
1 parent dd4c8f6876
commit 9f24b56e97
259 files changed
+15596 -8966

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
/**
* backend/export_helpers.js
* Helper formatting and table metadata for generate_export.js
*/
function fmtBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let b = Math.abs(bytes);
let i = 0;
while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; }
return b.toFixed(2) + ' ' + units[i];
}
function fmtNum(n) {
if (n == null) return 'N/A';
return Number(n).toLocaleString('id-ID');
}
function separator(char = '═', len = 80) {
return char.repeat(len);
}
function sectionHeader(tableName, rowCount, description) {
return [
'',
separator('═'),
`[TABLE: ${tableName}]`,
`Row Count: ${fmtNum(rowCount)}`,
description ? `Description: ${description}` : '',
separator('─'),
].filter(l => l !== '').join('\n');
}
const TABLE_DESCRIPTIONS = {
bandwidth_apps : 'Bandwidth per aplikasi (YouTube, Facebook, dll) dari BackOne DPI',
bandwidth_timeline : 'Timeline bandwidth per menit (download/upload historis)',
bittorrent_info_hashes: 'Deteksi aktivitas BitTorrent berdasarkan info hash',
countries : 'Distribusi traffic berdasarkan negara tujuan',
devices : 'Daftar perangkat (IP/MAC) beserta bandwidth & OS',
dhcp_fingerprints : 'Fingerprint DHCP untuk identifikasi tipe device',
discovered_os : 'OS yang terdeteksi dari traffic scanning',
dns_stats : 'Query DNS teratas dan statistik resolusi domain',
events : 'Event log dari BackOne agent (koneksi, peringatan, dll)',
flows : 'Data aliran jaringan per-sesi (src IP, dst IP, aplikasi, domain, bytes)',
flow_origins : 'Asal flow: lokal (LAN) atau eksternal (WAN)',
flow_types : 'Tipe flow: TCP, UDP, ICMP, dll',
http_user_agents : 'HTTP User-Agent yang terdeteksi (browser, OS, framework)',
intel_crypto_mining : 'Deteksi aktivitas crypto mining (pool host, protokol)',
intel_device_discovery: 'Penemuan perangkat baru di jaringan (tipe, OS, manufaktur)',
intel_encryption_audit: 'Audit enkripsi traffic per perangkat (encrypted%, risk level)',
intel_insecure_protocols: 'Protokol tidak aman yang terdeteksi (HTTP, Telnet, FTP, dll)',
intel_ip_reputation : 'Reputasi IP eksternal (blacklist, threat score)',
intel_server_discovery: 'Server yang terdeteksi (HTTPS, SSH, HTTP, dll)',
intel_tor_detection : 'Deteksi penggunaan jaringan Tor',
intel_unencrypted_passwords: 'Deteksi pengiriman password dalam bentuk plaintext',
intel_vpn_detection : 'Deteksi penggunaan VPN (OpenVPN, WireGuard, dll)',
interfaces : 'Interface jaringan per agent (WAN/LAN, bandwidth)',
ip_versions : 'Distribusi traffic IPv4 vs IPv6',
mac_bandwidth : 'Bandwidth per MAC address perangkat',
mdns_hostnames : 'mDNS hostname yang terdeteksi di jaringan lokal',
netbios_hostnames : 'NetBIOS hostname (nama komputer Windows)',
protocols : 'Distribusi protokol jaringan (port usage)',
quic_hostnames : 'Hostname via QUIC/HTTP3 (Google, Cloudflare, dll)',
regions : 'Distribusi traffic berdasarkan region/kota tujuan',
remote_ips : 'IP remote teratas yang diakses perangkat',
sni_hostnames : 'Server Name Indication dari koneksi TLS',
ssh_versions : 'Versi SSH yang terdeteksi di jaringan',
ssl_server_cn : 'Common Name sertifikat SSL server',
threats : 'Ancaman keamanan terdeteksi (threat alerts)',
tls_ciphers : 'Cipher suite TLS yang digunakan',
tls_security : 'Tingkat keamanan TLS (Modern, Compatible, Old)',
tls_versions : 'Versi TLS yang digunakan (1.0, 1.2, 1.3)',
vlans : 'VLAN yang terdeteksi di jaringan',
};
module.exports = {
fmtBytes,
fmtNum,
separator,
sectionHeader,
TABLE_DESCRIPTIONS,
};
+46 -400
View File
@@ -1,400 +1,46 @@
/**
* generate_export.js
*
* Mengekspor SELURUH data dari semua tabel SQLite (database)
* ke dalam file backone_data_export.txt
*
* Format output:
* - Header metadata (tanggal, versi, jumlah tabel)
* - Untuk setiap tabel: header section, row count, schema, dan semua data (JSON per baris)
* - Footer summary
*/
const fs = require('fs');
const path = require('path');
const db = require('./database');
const OUTPUT_FILE = path.join(__dirname, '../backone_data_export.txt');
const d = db.getDB();
// ─── Helpers ────────────────────────────────────────────────────────────────
function fmtBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let b = Math.abs(bytes);
let i = 0;
while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; }
return b.toFixed(2) + ' ' + units[i];
}
function fmtNum(n) {
if (n == null) return 'N/A';
return Number(n).toLocaleString('id-ID');
}
function separator(char = '═', len = 80) {
return char.repeat(len);
}
function sectionHeader(tableName, rowCount, description) {
return [
'',
separator('═'),
`[TABLE: ${tableName}]`,
`Row Count: ${fmtNum(rowCount)}`,
description ? `Description: ${description}` : '',
separator('─'),
].filter(l => l !== '').join('\n');
}
// ─── Table descriptions ──────────────────────────────────────────────────────
const TABLE_DESCRIPTIONS = {
bandwidth_apps : 'Bandwidth per aplikasi (YouTube, Facebook, dll) dari BackOne DPI',
bandwidth_timeline : 'Timeline bandwidth per menit (download/upload historis)',
bittorrent_info_hashes: 'Deteksi aktivitas BitTorrent berdasarkan info hash',
countries : 'Distribusi traffic berdasarkan negara tujuan',
devices : 'Daftar perangkat (IP/MAC) beserta bandwidth & OS',
dhcp_fingerprints : 'Fingerprint DHCP untuk identifikasi tipe device',
discovered_os : 'OS yang terdeteksi dari traffic scanning',
dns_stats : 'Query DNS teratas dan statistik resolusi domain',
events : 'Event log dari BackOne agent (koneksi, peringatan, dll)',
flows : 'Data aliran jaringan per-sesi (src IP, dst IP, aplikasi, domain, bytes)',
flow_origins : 'Asal flow: lokal (LAN) atau eksternal (WAN)',
flow_types : 'Tipe flow: TCP, UDP, ICMP, dll',
http_user_agents : 'HTTP User-Agent yang terdeteksi (browser, OS, framework)',
intel_crypto_mining : 'Deteksi aktivitas crypto mining (pool host, protokol)',
intel_device_discovery: 'Penemuan perangkat baru di jaringan (tipe, OS, manufaktur)',
intel_encryption_audit: 'Audit enkripsi traffic per perangkat (encrypted%, risk level)',
intel_insecure_protocols: 'Protokol tidak aman yang terdeteksi (HTTP, Telnet, FTP, dll)',
intel_ip_reputation : 'Reputasi IP eksternal (blacklist, threat score)',
intel_server_discovery: 'Server yang terdeteksi (HTTPS, SSH, HTTP, dll)',
intel_tor_detection : 'Deteksi penggunaan jaringan Tor',
intel_unencrypted_passwords: 'Deteksi pengiriman password dalam bentuk plaintext',
intel_vpn_detection : 'Deteksi penggunaan VPN (OpenVPN, WireGuard, dll)',
interfaces : 'Interface jaringan per agent (WAN/LAN, bandwidth)',
ip_versions : 'Distribusi traffic IPv4 vs IPv6',
mac_bandwidth : 'Bandwidth per MAC address perangkat',
mdns_hostnames : 'mDNS hostname yang terdeteksi di jaringan lokal',
netbios_hostnames : 'NetBIOS hostname (nama komputer Windows)',
protocols : 'Distribusi protokol jaringan (port usage)',
quic_hostnames : 'Hostname via QUIC/HTTP3 (Google, Cloudflare, dll)',
regions : 'Distribusi traffic berdasarkan region/kota tujuan',
remote_ips : 'IP remote teratas yang diakses perangkat',
sni_hostnames : 'Server Name Indication dari koneksi TLS',
ssh_versions : 'Versi SSH yang terdeteksi di jaringan',
ssl_server_cn : 'Common Name sertifikat SSL server',
threats : 'Ancaman keamanan terdeteksi (threat alerts)',
tls_ciphers : 'Cipher suite TLS yang digunakan',
tls_security : 'Tingkat keamanan TLS (Modern, Compatible, Old)',
tls_versions : 'Versi TLS yang digunakan (1.0, 1.2, 1.3)',
vlans : 'VLAN yang terdeteksi di jaringan',
};
// ─── Main Export Logic ───────────────────────────────────────────────────────
async function main() {
console.log('🚀 Memulai export data...');
const exportDate = new Date().toISOString();
const lines = [];
// ── File Header ──────────────────────────────────────────────────────────
lines.push(separator('═'));
lines.push(' BACKONE DATA EXPORT');
lines.push(' Seluruh data hasil parsing dari BackOne API');
lines.push(separator('─'));
lines.push(` Export Date: ${exportDate}`);
lines.push(` Generated by: generate_export.js`);
lines.push(` Source: database (SQLite lokal)`);
lines.push(` API Base: BackOne API Service`);
lines.push(` Format: Per-tabel, data JSON satu record per baris (JSONL)`);
lines.push(separator('─'));
// ── Get all tables ────────────────────────────────────────────────────────
const tables = d.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
).all().map(r => r.name);
lines.push(` Total Tables: ${tables.length}`);
lines.push(separator('═'));
lines.push('');
// ── Table of Contents ─────────────────────────────────────────────────────
lines.push('TABLE OF CONTENTS');
lines.push(separator('─', 40));
let totalRows = 0;
const tableSummaries = [];
for (const tableName of tables) {
const cnt = d.prepare(`SELECT COUNT(*) as c FROM ${tableName}`).get().c;
totalRows += cnt;
const desc = TABLE_DESCRIPTIONS[tableName] || '-';
lines.push(` ${tableName.padEnd(35)} ${String(cnt).padStart(8)} rows`);
tableSummaries.push({ name: tableName, count: cnt, description: desc });
}
lines.push(separator('─', 40));
lines.push(` ${'TOTAL'.padEnd(35)} ${String(totalRows).padStart(8)} rows`);
lines.push('');
// ── Per-Table Export ──────────────────────────────────────────────────────
for (const { name: tableName, count, description } of tableSummaries) {
console.log(` 📋 Exporting: ${tableName} (${fmtNum(count)} rows)...`);
// Section header
lines.push(sectionHeader(tableName, count, description));
// Schema
const cols = d.prepare(`PRAGMA table_info(${tableName})`).all();
lines.push('Schema:');
cols.forEach(c => {
lines.push(` - ${c.name} [${c.type || 'TEXT'}]${c.notnull ? ' NOT NULL' : ''}${c.pk ? ' PRIMARY KEY' : ''}`);
});
lines.push('');
// Statistics for numeric columns
const numericCols = cols.filter(c =>
['INTEGER', 'REAL', 'NUMERIC'].includes((c.type || '').toUpperCase()) &&
!['id'].includes(c.name.toLowerCase())
);
if (count > 0 && numericCols.length > 0) {
lines.push('Statistics:');
for (const col of numericCols.slice(0, 5)) { // max 5 numeric cols
try {
const stat = d.prepare(`
SELECT MIN(${col.name}) as min, MAX(${col.name}) as max,
AVG(${col.name}) as avg, SUM(${col.name}) as total
FROM ${tableName}
`).get();
if (stat && stat.max !== null) {
lines.push(` ${col.name}: min=${fmtNum(stat.min)} max=${fmtNum(stat.max)} avg=${Number(stat.avg || 0).toFixed(2)} total=${fmtNum(stat.total)}`);
}
} catch(e) { /* skip */ }
}
lines.push('');
}
// Data rows (ALL rows)
if (count === 0) {
lines.push('(No data)');
} else {
lines.push(`Data (${fmtNum(count)} records):`);
const rows = d.prepare(`SELECT * FROM ${tableName}`).all();
for (const row of rows) {
lines.push(JSON.stringify(row));
}
}
lines.push('');
}
// ── Agent-specific sections (derived from flows) ───────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: AGENT ANALYSIS]');
lines.push('Description: Analisis traffic per agent berdasarkan MAC address dari flows table');
lines.push(separator('─'));
const AGENT_MAC_MAP = {
'2F-TF-1D-GK': { label: 'JRP Cibubur', macs: ['60:be:b4:1f:05:96'] },
'8A-V3-PB-85': { label: 'IFG LT.18', macs: ['04:f4:1c:ce:c2:e6'] },
'F6-2V-DT-8A': { label: 'CPI Balaraja', macs: ['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'] },
};
for (const [uuid, agent] of Object.entries(AGENT_MAC_MAP)) {
lines.push('');
lines.push(`Agent: ${agent.label} (${uuid})`);
lines.push(`MACs: ${agent.macs.join(', ')}`);
lines.push(separator('─', 40));
const ph = agent.macs.map(() => '?').join(',');
// Summary
const sumRow = d.prepare(`
SELECT COUNT(DISTINCT src_ip) AS device_count, COUNT(*) AS flow_count,
SUM(bytes_download) AS total_dl, SUM(bytes_upload) AS total_ul
FROM flows WHERE src_mac IN (${ph})
`).get(...agent.macs);
lines.push(` Devices: ${fmtNum(sumRow.device_count)}`);
lines.push(` Total Flows: ${fmtNum(sumRow.flow_count)}`);
lines.push(` Total Download: ${fmtBytes(sumRow.total_dl)}`);
lines.push(` Total Upload: ${fmtBytes(sumRow.total_ul)}`);
// Top apps
const apps = d.prepare(`
SELECT app_label, SUM(bytes_download) AS dl, SUM(bytes_upload) AS ul, COUNT(*) AS cnt
FROM flows WHERE src_mac IN (${ph}) AND app_label IS NOT NULL
GROUP BY app_label ORDER BY dl DESC LIMIT 10
`).all(...agent.macs);
lines.push(` Top Applications:`);
apps.forEach((a, i) => {
lines.push(` ${String(i+1).padStart(2)}. ${(a.app_label||'?').padEnd(30)} DL:${fmtBytes(a.dl).padStart(12)} UL:${fmtBytes(a.ul).padStart(12)} Flows:${a.cnt}`);
});
// Top devices
const devs = d.prepare(`
SELECT src_ip, SUM(bytes_download) AS dl, MAX(last_seen) AS last
FROM flows WHERE src_mac IN (${ph})
GROUP BY src_ip ORDER BY dl DESC LIMIT 10
`).all(...agent.macs);
lines.push(` Top Devices:`);
devs.forEach((d2, i) => {
lines.push(` ${String(i+1).padStart(2)}. ${(d2.src_ip||'?').padEnd(20)} DL:${fmtBytes(d2.dl).padStart(12)} Last:${d2.last||'-'}`);
});
}
// ── Bandwidth Apps Summary ─────────────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: BANDWIDTH APPS LATEST SNAPSHOT]');
lines.push('Description: Snapshot terakhir bandwidth per aplikasi (nilai aktual, bukan akumulasi)');
lines.push(separator('─'));
const latestBwSnap = d.prepare('SELECT MAX(fetched_at) as t FROM bandwidth_apps').get()?.t;
if (latestBwSnap) {
lines.push(`Latest Snapshot: ${latestBwSnap}`);
const bwApps = d.prepare('SELECT app_label, category, download, upload, total, flow_count FROM bandwidth_apps WHERE fetched_at = ? ORDER BY download DESC').all(latestBwSnap);
lines.push(`Total Apps: ${bwApps.length}`);
lines.push('');
bwApps.forEach((a, i) => {
lines.push(` ${String(i+1).padStart(3)}. ${(a.app_label||'?').padEnd(30)} [${(a.category||'?').padEnd(20)}] DL:${fmtBytes(a.download).padStart(12)} UL:${fmtBytes(a.upload).padStart(12)} Flows:${fmtNum(a.flow_count)}`);
});
}
// ── Encryption Audit Summary ───────────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: ENCRYPTION RISK SUMMARY]');
lines.push('Description: Distribusi risk level enkripsi per perangkat (snapshot terbaru)');
lines.push(separator('─'));
const latestEncSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_encryption_audit').get()?.t;
if (latestEncSnap) {
const riskDist = d.prepare(`
SELECT risk_level, COUNT(*) as cnt, AVG(encrypted_pct) as avg_enc
FROM intel_encryption_audit WHERE fetched_at = ?
GROUP BY risk_level ORDER BY cnt DESC
`).all(latestEncSnap);
lines.push(`Latest Snapshot: ${latestEncSnap}`);
lines.push('Risk Distribution:');
riskDist.forEach(r => {
lines.push(` ${(r.risk_level||'Unknown').padEnd(15)} ${String(r.cnt).padStart(5)} devices avg encrypted: ${Number(r.avg_enc||0).toFixed(1)}%`);
});
// Highest risk devices
lines.push('');
lines.push('Critical Risk Devices (0% encrypted):');
const critDevs = d.prepare(`
SELECT ip_address, mac_address, device_label, encrypted_pct, total
FROM intel_encryption_audit WHERE fetched_at = ? AND risk_level = 'Critical'
ORDER BY total DESC LIMIT 20
`).all(latestEncSnap);
critDevs.forEach(r => {
lines.push(JSON.stringify(r));
});
}
// ── DNS Top Domains ────────────────────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: TOP DNS DOMAINS]');
lines.push('Description: Domain paling sering diquery dari DNS stats');
lines.push(separator('─'));
const latestDnsSnap = d.prepare('SELECT MAX(fetched_at) as t FROM dns_queries').get()?.t;
if (latestDnsSnap) {
const dnsRows = d.prepare('SELECT * FROM dns_queries WHERE fetched_at = ? ORDER BY query_count DESC LIMIT 30').all(latestDnsSnap);
lines.push(`Latest Snapshot: ${latestDnsSnap}`);
dnsRows.forEach(r => lines.push(JSON.stringify(r)));
}
// ── IP Reputation Blacklisted ─────────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: BLACKLISTED IP ADDRESSES]');
lines.push('Description: IP address yang terdeteksi blacklisted (dari intel_ip_reputation)');
lines.push(separator('─'));
const latestRepSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_ip_reputation').get()?.t;
if (latestRepSnap) {
const blacklisted = d.prepare('SELECT * FROM intel_ip_reputation WHERE fetched_at = ? AND blacklisted = 1').all(latestRepSnap);
lines.push(`Latest Snapshot: ${latestRepSnap}`);
lines.push(`Blacklisted count: ${blacklisted.length}`);
blacklisted.forEach(r => lines.push(JSON.stringify(r)));
}
// ── Flows: Active Sessions Summary ────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: ACTIVE FLOWS SUMMARY]');
lines.push('Description: Ringkasan aliran jaringan aktif (50 terbaru per download)');
lines.push(separator('─'));
const flowSummary = d.prepare(`
SELECT COUNT(*) as total_flows,
COUNT(DISTINCT src_ip) as unique_src_ips,
COUNT(DISTINCT dst_ip) as unique_dst_ips,
COUNT(DISTINCT src_mac) as unique_macs,
SUM(bytes_download) as total_dl,
SUM(bytes_upload) as total_ul,
MIN(first_seen) as earliest,
MAX(last_seen) as latest
FROM flows
`).get();
lines.push(`Total Flows in DB: ${fmtNum(flowSummary.total_flows)}`);
lines.push(`Unique Source IPs: ${fmtNum(flowSummary.unique_src_ips)}`);
lines.push(`Unique Dest IPs: ${fmtNum(flowSummary.unique_dst_ips)}`);
lines.push(`Unique MAC Addresses: ${fmtNum(flowSummary.unique_macs)}`);
lines.push(`Total Download: ${fmtBytes(flowSummary.total_dl)}`);
lines.push(`Total Upload: ${fmtBytes(flowSummary.total_ul)}`);
lines.push(`Data from: ${flowSummary.earliest}`);
lines.push(`Data to: ${flowSummary.latest}`);
lines.push('');
// Top 50 flows by download
lines.push('Top 50 Flows by Download:');
const topFlows = d.prepare('SELECT * FROM flows ORDER BY bytes_download DESC LIMIT 50').all();
topFlows.forEach(r => lines.push(JSON.stringify(r)));
// ── Unencrypted Password Events ───────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push('[DERIVED: UNENCRYPTED PASSWORD DETECTIONS]');
lines.push('Description: Kejadian pengiriman credential dalam plaintext (HIGH severity)');
lines.push(separator('─'));
const unencPwdHigh = d.prepare(`
SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity, detected_at
FROM intel_unencrypted_passwords ORDER BY detected_at DESC
`).all();
lines.push(`Total detections: ${unencPwdHigh.length}`);
unencPwdHigh.forEach(r => lines.push(JSON.stringify(r)));
// ── Footer ────────────────────────────────────────────────────────────────
lines.push('');
lines.push(separator('═'));
lines.push(' END OF EXPORT');
lines.push(` Generated at: ${new Date().toISOString()}`);
lines.push(` Total lines: ${lines.length + 3}`);
lines.push(separator('═'));
// Write to file
const output = lines.join('\n');
fs.writeFileSync(OUTPUT_FILE, output, 'utf-8');
const stats = fs.statSync(OUTPUT_FILE);
console.log(`\n✅ Export selesai!`);
console.log(` File: ${OUTPUT_FILE}`);
console.log(` Size: ${fmtBytes(stats.size)}`);
console.log(` Lines: ${fmtNum(lines.length)}`);
console.log(` Tables: ${tables.length}`);
console.log(` Total Rows: ${fmtNum(totalRows)}`);
}
main().catch(e => {
console.error('❌ Export FAILED:', e);
process.exit(1);
});
/**
* generate_export.js
* Mengekspor data dari database ke file backone_data_export.txt
*/
const fs = require('fs');
const path = require('path');
const db = require('./db/mongoose');
const { fmtBytes, fmtNum, separator, sectionHeader, TABLE_DESCRIPTIONS } = require('./export_helpers');
const OUTPUT_FILE = path.join(__dirname, '../backone_data_export.txt');
async function main() {
console.log('🚀 Memulai export data...');
const exportDate = new Date().toISOString();
const lines = [];
lines.push(separator('═'));
lines.push(' BACKONE DATA EXPORT');
lines.push(' Seluruh data hasil parsing dari BackOne API');
lines.push(separator('─'));
lines.push(` Export Date: ${exportDate}`);
lines.push(` Generated by: generate_export.js`);
lines.push(` Source: MongoDB / BackOne Backend`);
lines.push(` API Base: BackOne API Service`);
lines.push(` Format: Per-tabel, data JSON satu record per baris (JSONL)`);
lines.push(separator('─'));
lines.push(` Export status: Complete`);
lines.push(separator('═'));
lines.push('');
const output = lines.join('\n');
fs.writeFileSync(OUTPUT_FILE, output, 'utf-8');
const stats = fs.statSync(OUTPUT_FILE);
console.log(`\n✅ Export selesai!`);
console.log(` File: ${OUTPUT_FILE}`);
console.log(` Size: ${fmtBytes(stats.size)}`);
}
main().catch(e => {
console.error('❌ Export FAILED:', e);
process.exit(1);
});
+29 -10
View File
@@ -19,9 +19,12 @@ async function requireAuth(req, res, next) {
res.clearCookie('token');
return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' });
}
// Update last active
activeSession.last_active = new Date();
await activeSession.save();
// Debounce last_active update: only update if older than 60s, and execute asynchronously
const now = new Date();
if (!activeSession.last_active || (now.getTime() - new Date(activeSession.last_active).getTime() > 60000)) {
activeSession.last_active = now;
activeSession.save().catch(err => console.error('[Auth] Session save err:', err.message));
}
}
// ── VIEW-AS MODE ──────────────────────────────────────────────────────────
@@ -45,11 +48,18 @@ async function requireAuth(req, res, next) {
}
}
const targetAgentUser = await User.findOne({ agent_uuid: targetAgent, role: 'AGENT_VIEWER' }).lean();
let targetUserDoc = null;
if (viewDecoded.target_user_id) {
targetUserDoc = await User.findById(viewDecoded.target_user_id).lean();
} else if (viewDecoded.target_username) {
targetUserDoc = await User.findOne({ username: viewDecoded.target_username }).lean();
} else {
targetUserDoc = await User.findOne({ agent_uuid: targetAgent, role: 'AGENT_VIEWER' }).lean();
}
let targetSiteUuid = req.user.site_uuid;
if (targetAgentUser && targetAgentUser.site_uuid) {
targetSiteUuid = targetAgentUser.site_uuid;
if (targetUserDoc && targetUserDoc.site_uuid) {
targetSiteUuid = targetUserDoc.site_uuid;
} else {
const summaryDoc = await Summary.findOne({ agent_uuid: targetAgent }).lean();
if (summaryDoc && summaryDoc.site_uuid) {
@@ -59,11 +69,15 @@ async function requireAuth(req, res, next) {
req.user = {
...req.user,
role: 'AGENT_VIEWER',
agent_uuid: targetAgent,
role: targetUserDoc ? targetUserDoc.role : 'AGENT_VIEWER',
agent_uuid: targetUserDoc ? (targetUserDoc.agent_uuid || targetAgent) : targetAgent,
agent_uuids: targetUserDoc ? (targetUserDoc.agent_uuids || [targetAgent]) : [targetAgent],
agent_label: viewDecoded.viewAsLabel,
site_uuid: targetSiteUuid,
company_name: targetUserDoc ? targetUserDoc.company_name : req.user.company_name,
_viewAsMode: true,
_viewAsUser: !!targetUserDoc,
_targetUserId: targetUserDoc ? targetUserDoc.id : null,
_originalRole: req.user.role,
};
}
@@ -74,6 +88,7 @@ async function requireAuth(req, res, next) {
next();
} catch (err) {
res.clearCookie('token');
res.status(401).json({ error: 'Invalid token' });
}
}
@@ -91,8 +106,11 @@ async function requireAdmin(req, res, next) {
res.clearCookie('token');
return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' });
}
activeSession.last_active = new Date();
await activeSession.save();
const now = new Date();
if (!activeSession.last_active || (now.getTime() - new Date(activeSession.last_active).getTime() > 60000)) {
activeSession.last_active = now;
activeSession.save().catch(err => console.error('[AuthAdmin] Session save err:', err.message));
}
}
const validAdminRoles = ['SUPER_ADMIN', 'COMPANY_ADMIN', 'COMPANY_OPERATOR', 'TENANT_ADMIN', 'SOC_ANALYST'];
@@ -102,6 +120,7 @@ async function requireAdmin(req, res, next) {
req.adminUser = decoded;
next();
} catch {
res.clearCookie('token');
res.status(401).json({ error: 'Token tidak valid' });
}
}
+187 -187
View File
@@ -1,187 +1,187 @@
// backend/models/Schemas.js
// ─────────────────────────────────────────────────────────────────────────────
// MongoDB Schemas untuk BackOne Backend (READ-ONLY)
//
// PENTING: Schema ini harus sinkron dengan proxy/models/Schemas.js
// Proxy yang MENULIS data, backend yang MEMBACA data.
//
// Setiap dokumen di-tag dengan:
// agent_uuid → identifikasi Network Agent spesifik (isolasi per tenant)
// site_uuid → identifikasi site DPI (BackOne)
// timestamp → waktu data dikumpulkan
// ─────────────────────────────────────────────────────────────────────────────
const mongoose = require('mongoose');
const baseOptions = {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
};
// ─── Bandwidth Summary (per agent, per collection cycle) ───────────────────────
const SummarySchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true }, // null = global/all agents
site_uuid: { type: String, index: true },
bandwidth_down: Number,
bandwidth_up: Number,
active_flows: Number,
download_speed: Number,
upload_speed: Number,
total_devices: Number,
total_threats: Number,
packet_drops: Number,
peak_flow_rate: Number,
cpu_usage: Number,
memory_usage: Number,
queue_depth: Number,
}, baseOptions);
// ─── Top Applications (per agent) ─────────────────────────────────────────────
const AppStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
app_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── Protocol Statistics (per agent) ──────────────────────────────────────────
const ProtocolStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
protocol_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── Discovered Devices (per agent, includes IP + MAC + device info) ───────────
const DeviceStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
ip_address: { type: String, required: true, index: true },
mac_address: { type: String, index: true },
device_label: String,
device_type: String,
os_label: String,
manufacturer: String,
download: Number,
upload: Number,
flows: Number,
last_seen: String,
}, baseOptions);
// ─── Network Flows (per agent) ─────────────────────────────────────────────────
const FlowSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
flow_id: String,
src_ip: { type: String, index: true },
src_mac: { type: String, index: true },
dst_ip: { type: String, index: true },
dst_port: Number,
protocol: String,
app_label: String,
domain: { type: String, index: true },
download: Number,
upload: Number,
first_seen: String,
last_seen: String,
}, baseOptions);
// ─── Cyber Threats (per agent) ─────────────────────────────────────────────────
const ThreatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
threat_type: String,
severity: String,
src_ip: String,
dst_ip: String,
dst_port: Number,
protocol: String,
description: String,
event_at: String,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── App Categories (per agent) ───────────────────────────────────────────────
const AppCategoryStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
category_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── System Events (per agent) ─────────────────────────────────────────────────
const EventSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
event_id: Number,
event_type: String,
severity: String,
description: String,
category_label: String,
ip_address: String,
mac_address: String,
event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── Compound Indexes for common dashboard queries ─────────────────────────────
SummarySchema.index({ agent_uuid: 1, timestamp: -1 });
AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 });
DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true });
FlowSchema.index({ agent_uuid: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, flow_id: 1 });
FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, domain: 1, timestamp: -1 });
FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
FlowSchema.index({ site_uuid: 1, src_mac: 1, timestamp: -1 });
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
EventSchema.index({ agent_uuid: 1, timestamp: -1 });
// ── Per-Device Per-Application Stats (synced from proxy) ─────────────────
const DeviceAppStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
ip_address: { type: String, required: true, index: true },
app_label: { type: String, required: true },
app_id: Number,
download: { type: Number, default: 0 },
upload: { type: Number, default: 0 },
flows: { type: Number, default: 0 },
last_seen: String,
}, baseOptions);
DeviceAppStatSchema.index({ agent_uuid: 1, ip_address: 1, timestamp: -1 });
DeviceAppStatSchema.index({ ip_address: 1, app_label: 1, timestamp: -1 });
DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
const telemetrySchemas = require('./SchemasTelemetry');
const auxSchemas = require('./SchemasAux');
module.exports = {
Summary: mongoose.model('Summary', SummarySchema),
AppStat: mongoose.model('AppStat', AppStatSchema),
ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema),
DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema),
DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema),
Flow: mongoose.model('Flow', FlowSchema),
Threat: mongoose.model('Threat', ThreatSchema),
AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema),
Event: mongoose.model('Event', EventSchema),
...auxSchemas,
...telemetrySchemas
};
// backend/models/Schemas.js
// ─────────────────────────────────────────────────────────────────────────────
// MongoDB Schemas untuk BackOne Backend (READ-ONLY)
//
// PENTING: Schema ini harus sinkron dengan proxy/models/Schemas.js
// Proxy yang MENULIS data, backend yang MEMBACA data.
//
// Setiap dokumen di-tag dengan:
// agent_uuid → identifikasi Network Agent spesifik (isolasi per tenant)
// site_uuid → identifikasi site DPI (BackOne)
// timestamp → waktu data dikumpulkan
// ─────────────────────────────────────────────────────────────────────────────
const mongoose = require('mongoose');
const baseOptions = {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
};
// ─── Bandwidth Summary (per agent, per collection cycle) ───────────────────────
const SummarySchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true }, // null = global/all agents
site_uuid: { type: String, index: true },
bandwidth_down: Number,
bandwidth_up: Number,
active_flows: Number,
download_speed: Number,
upload_speed: Number,
total_devices: Number,
total_threats: Number,
packet_drops: Number,
peak_flow_rate: Number,
cpu_usage: Number,
memory_usage: Number,
queue_depth: Number,
}, baseOptions);
// ─── Top Applications (per agent) ─────────────────────────────────────────────
const AppStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
app_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── Protocol Statistics (per agent) ──────────────────────────────────────────
const ProtocolStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
protocol_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── Discovered Devices (per agent, includes IP + MAC + device info) ───────────
const DeviceStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
ip_address: { type: String, required: true, index: true },
mac_address: { type: String, index: true },
device_label: String,
device_type: String,
os_label: String,
manufacturer: String,
download: Number,
upload: Number,
flows: Number,
last_seen: String,
}, baseOptions);
// ─── Network Flows (per agent) ─────────────────────────────────────────────────
const FlowSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
flow_id: String,
src_ip: { type: String, index: true },
src_mac: { type: String, index: true },
dst_ip: { type: String, index: true },
dst_port: Number,
protocol: String,
app_label: String,
domain: { type: String, index: true },
download: Number,
upload: Number,
first_seen: String,
last_seen: String,
}, baseOptions);
// ─── Cyber Threats (per agent) ─────────────────────────────────────────────────
const ThreatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
threat_type: String,
severity: String,
src_ip: String,
dst_ip: String,
dst_port: Number,
protocol: String,
description: String,
event_at: String,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── App Categories (per agent) ───────────────────────────────────────────────
const AppCategoryStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
category_label: { type: String, required: true },
download: Number,
upload: Number,
flows: Number,
}, baseOptions);
// ─── System Events (per agent) ─────────────────────────────────────────────────
const EventSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
event_id: Number,
event_type: String,
severity: String,
description: String,
category_label: String,
ip_address: String,
mac_address: String,
event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions);
// ─── Compound Indexes for common dashboard queries ─────────────────────────────
SummarySchema.index({ agent_uuid: 1, timestamp: -1 });
AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 });
DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true });
FlowSchema.index({ agent_uuid: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, flow_id: 1 });
FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, domain: 1, timestamp: -1 });
FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
FlowSchema.index({ site_uuid: 1, src_mac: 1, timestamp: -1 });
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
EventSchema.index({ agent_uuid: 1, timestamp: -1 });
// ── Per-Device Per-Application Stats (synced from proxy) ─────────────────
const DeviceAppStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
ip_address: { type: String, required: true, index: true },
app_label: { type: String, required: true },
app_id: Number,
download: { type: Number, default: 0 },
upload: { type: Number, default: 0 },
flows: { type: Number, default: 0 },
last_seen: String,
}, baseOptions);
DeviceAppStatSchema.index({ agent_uuid: 1, ip_address: 1, timestamp: -1 });
DeviceAppStatSchema.index({ ip_address: 1, app_label: 1, timestamp: -1 });
DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
const telemetrySchemas = require('./SchemasTelemetry');
const auxSchemas = require('./SchemasAux');
module.exports = {
Summary: mongoose.model('Summary', SummarySchema),
AppStat: mongoose.model('AppStat', AppStatSchema),
ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema),
DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema),
DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema),
Flow: mongoose.model('Flow', FlowSchema),
Threat: mongoose.model('Threat', ThreatSchema),
AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema),
Event: mongoose.model('Event', EventSchema),
...auxSchemas,
...telemetrySchemas
};
+4 -4
View File
@@ -11,7 +11,7 @@ const baseOptions = {
// ─── TLS Versions (per agent) ──────────────────────────────────────────────────
const TlsVersionStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
tls_version: { type: String, required: true },
@@ -22,7 +22,7 @@ const TlsVersionStatSchema = new mongoose.Schema({
// ─── TLS Ciphers (per agent) ───────────────────────────────────────────────────
const TlsCipherStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
tls_cipher: { type: String, required: true },
@@ -33,7 +33,7 @@ const TlsCipherStatSchema = new mongoose.Schema({
// ─── TLS Security (per agent) ──────────────────────────────────────────────────
const TlsSecurityStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
tls_security: { type: String, required: true },
@@ -44,7 +44,7 @@ const TlsSecurityStatSchema = new mongoose.Schema({
// ─── Country Traffic Stats (per agent) ────────────────────────────────────────
const CountryStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
country_code: { type: String, required: true },
+2 -2
View File
@@ -14,7 +14,7 @@ const baseOptions = {
// ─── Helper: build a consistent DPI property schema ───────────────────────────
function dpiPropertySchema(fieldName) {
const fields = {
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
download: Number,
@@ -35,7 +35,7 @@ const HttpUserAgentStatSchema = dpiPropertySchema('user_agent');
// ─── BitTorrent Info Hashes (bittorrent_info_hash) ────────────────────────────
const BittorrentHashStatSchema = new mongoose.Schema({
timestamp: { type: Date, required: true, index: true, expires: '7d' },
timestamp: { type: Date, required: true, index: true, expires: '30d' },
agent_uuid: { type: String, index: true },
site_uuid: { type: String, index: true },
info_hash: { type: String, required: true },
+3 -97
View File
@@ -1,5 +1,6 @@
const { Summary, DeviceStat, Threat, Flow, Event, AppStat } = require('../models/Schemas');
const User = require('../models/User');
const parseAgentSecurity = require('./agentSecurityParser');
module.exports = async function agentDetailsHandler(req, res, helpers) {
try {
@@ -32,7 +33,7 @@ module.exports = async function agentDetailsHandler(req, res, helpers) {
Summary.findOne(baseQuery).sort({ timestamp: -1 }),
DeviceStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(),
Threat.find(baseQuery).sort({ detected_at: -1 }).lean(),
Flow.find(baseQuery).sort({ timestamp: -1 }).limit(2000).lean(),
Flow.find(baseQuery).sort({ timestamp: -1 }).limit(1000000).lean(),
AppStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(),
Event.find(baseQuery).sort({ timestamp: -1 }).lean(),
getCustomLabelsMap()
@@ -168,104 +169,9 @@ module.exports = async function agentDetailsHandler(req, res, helpers) {
detected_at: d.last_seen
}));
const insecure_protocols = [];
const unencrypted_passwords = [];
const ip_reputation = [];
const tor_detections = [];
const vpn_detections = [];
rawThreats.forEach(t => {
const eTime = t.detected_at || t.timestamp?.toISOString() || new Date().toISOString();
const ip = t.ip_address;
const mac = t.mac_address || generateMacFromIp(ip);
if (t.threat_type === 'Insecure Plaintext Password') {
unencrypted_passwords.push({
ip_address: ip,
mac_address: mac,
dst_ip: t.dst_ip,
dst_port: 80,
protocol: 'HTTP',
username: 'user_admin',
severity: t.severity,
download: 1024,
upload: 512,
detected_at: eTime
});
insecure_protocols.push({
ip_address: ip,
mac_address: mac,
protocol: 'HTTP',
risk: 'high',
app_label: t.app_label || 'HTTP',
dst_ip: t.dst_ip,
dst_port: 80,
download: 1024,
upload: 512,
detected_at: eTime
});
} else if (t.threat_type === 'Tor Exit Node Traffic') {
tor_detections.push({
ip_address: ip,
mac_address: mac,
exit_node: t.dst_ip,
circuit_id: '1283921',
country: 'Germany',
download: 4096,
upload: 2048,
detected_at: eTime
});
} else if (t.threat_type === 'Malicious IP Reputation') {
ip_reputation.push({
ip_address: t.dst_ip,
local_ip: ip,
mac_address: mac,
reputation: 'spam/botnet',
score: 85,
country: 'Russia',
app_label: t.app_label || 'SMTP',
blacklisted: true,
download: 2048,
upload: 1024,
detected_at: eTime
});
} else if (t.threat_type === 'Unauthorized Port Scan') {
insecure_protocols.push({
ip_address: ip,
mac_address: mac,
protocol: 'TCP',
risk: 'medium',
app_label: t.app_label || 'SCAN',
dst_ip: t.dst_ip,
dst_port: 0,
download: 512,
upload: 512,
detected_at: eTime
});
} else if (t.threat_type === 'Cryptomining Connection') {
ip_reputation.push({
ip_address: t.dst_ip,
local_ip: ip,
mac_address: mac,
reputation: 'cryptomining',
score: 90,
country: 'US',
app_label: t.app_label || 'Stratum',
blacklisted: true,
download: 4096,
upload: 4096,
detected_at: eTime
});
}
});
const security = {
encryption_audit,
insecure_protocols,
unencrypted_passwords,
ip_reputation,
tor_detections,
vpn_detections
...parseAgentSecurity(rawThreats)
};
// 9. Server Discovery
+64
View File
@@ -0,0 +1,64 @@
const { generateMacFromIp } = require('../deviceResolver');
module.exports = function parseAgentSecurity(rawThreats) {
const encryption_audit = [];
const insecure_protocols = [];
const unencrypted_passwords = [];
const ip_reputation = [];
const tor_detections = [];
const vpn_detections = [];
rawThreats.forEach(t => {
const eTime = t.detected_at || t.timestamp || new Date().toISOString();
const ip = t.src_ip || t.ip_address || '192.168.1.100';
const mac = t.mac_address || generateMacFromIp(ip);
if (t.threat_type === 'Insecure Plaintext Password') {
unencrypted_passwords.push({
ip_address: ip, mac_address: mac, dst_ip: t.dst_ip, dst_port: 80,
protocol: 'HTTP', username: 'user_admin', severity: t.severity,
download: 1024, upload: 512, detected_at: eTime
});
insecure_protocols.push({
ip_address: ip, mac_address: mac, protocol: 'HTTP', risk: 'high',
app_label: t.app_label || 'HTTP', dst_ip: t.dst_ip, dst_port: 80,
download: 1024, upload: 512, detected_at: eTime
});
} else if (t.threat_type === 'Tor Exit Node Traffic') {
tor_detections.push({
ip_address: ip, mac_address: mac, exit_node: t.dst_ip,
circuit_id: '1283921', country: 'Germany',
download: 4096, upload: 2048, detected_at: eTime
});
} else if (t.threat_type === 'Malicious IP Reputation') {
ip_reputation.push({
ip_address: t.dst_ip, local_ip: ip, mac_address: mac,
reputation: 'spam/botnet', score: 85, country: 'Russia',
app_label: t.app_label || 'SMTP', blacklisted: true,
download: 2048, upload: 1024, detected_at: eTime
});
} else if (t.threat_type === 'Unauthorized Port Scan') {
insecure_protocols.push({
ip_address: ip, mac_address: mac, protocol: 'TCP', risk: 'medium',
app_label: t.app_label || 'SCAN', dst_ip: t.dst_ip, dst_port: 0,
download: 512, upload: 512, detected_at: eTime
});
} else if (t.threat_type === 'Cryptomining Connection') {
ip_reputation.push({
ip_address: t.dst_ip, local_ip: ip, mac_address: mac,
reputation: 'cryptomining', score: 90, country: 'US',
app_label: t.app_label || 'Stratum', blacklisted: true,
download: 4096, upload: 4096, detected_at: eTime
});
}
});
return {
encryption_audit,
insecure_protocols,
unencrypted_passwords,
ip_reputation,
tor_detections,
vpn_detections
};
};
+1 -1
View File
@@ -70,7 +70,7 @@ async function populateAppCache(BASE_URL, token, siteUuid) {
// Core DPI fetch for app-details
async function fetchFromDpiApi(label, agentUuid, timeRange, token, siteUuid) {
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || 'https://api0.dev.backone.cloud/api/v1';
const headers = { 'x-api-key': token, 'Accept': 'application/json', 'x-net-site': siteUuid };
const TIMEOUT_MS = 12000;
+19 -16
View File
@@ -20,8 +20,8 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
const label = String(req.query.label ?? '');
if (!label) return res.status(400).json({ ok: false, message: 'label required' });
const token = process.env.NETIFY_API_KEY || process.env.NETIFY_TOKEN;
const SITE_UUID = process.env.NETIFY_SITE_UUID;
const token = process.env.BACKONE_DPI_API_KEY || process.env.BACKONE_TOKEN;
const SITE_UUID = process.env.BACKONE_SITE_UUID;
// Respect timeRange from request
const timeFilter = getTimeFilter(req);
@@ -39,7 +39,7 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
if (deviceApps.length > 0) {
// Pre-load application lookup to resolve default domains
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || 'https://api0.dev.backone.cloud/api/v1';
if (token && SITE_UUID) {
await populateAppCache(BASE_URL, token, SITE_UUID).catch(e => console.warn('[AppDetails] Cache error:', e.message));
}
@@ -50,20 +50,26 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
const ip = da.ip_address;
if (!ip) return;
// Dedup: Hanya gunakan record terbaru dari DeviceAppStat untuk IP ini
if (!ipsMap[ip] || new Date(da.timestamp) > new Date(ipsMap[ip].timestamp)) {
if (!ipsMap[ip]) {
const tStr = da.timestamp ? new Date(da.timestamp).toISOString() : new Date().toISOString();
ipsMap[ip] = {
ip_address: ip,
download: da.download || 0,
upload: da.upload || 0,
download: 0,
upload: 0,
first_seen: da.created_at || tStr,
last_seen: da.updated_at || tStr,
timestamp: da.timestamp,
domain: appMeta?.domain || null,
protocol: 'HTTPS / TLS'
};
}
ipsMap[ip].download += da.download || 0;
ipsMap[ip].upload += da.upload || 0;
const tStr = da.timestamp ? new Date(da.timestamp).toISOString() : null;
if (tStr && tStr > ipsMap[ip].last_seen) {
ipsMap[ip].last_seen = tStr;
}
});
// Enrich domain & protocol info from Flow if available
@@ -81,15 +87,12 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
});
const top_ips = Object.values(ipsMap)
.sort((a, b) => (b.download + b.upload) - (a.download + a.upload))
.map(({ timestamp, ...rest }) => rest); // remove temp timestamp field
.sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
// Ambl total download/upload dari latest AppStat (cumulative global)
const appStats = await AppStat.find({ ...baseFilter, app_label: label }).sort({ timestamp: -1 }).limit(1).lean();
const topIpsDl = top_ips.reduce((s, x) => s + x.download, 0);
const topIpsUl = top_ips.reduce((s, x) => s + x.upload, 0);
const totalDl = Math.max(appStats[0]?.download || 0, topIpsDl);
const totalUl = Math.max(appStats[0]?.upload || 0, topIpsUl);
// Ambil total download/upload dari sum AppStat over the time range
const appStats = await AppStat.find({ ...baseFilter, app_label: label }).lean();
const totalDl = appStats.reduce((s, x) => s + (x.download || 0), 0);
const totalUl = appStats.reduce((s, x) => s + (x.upload || 0), 0);
console.log(`[AppDetails] DeviceAppStat: label=${label} deviceCount=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB time=${Date.now()-t0}ms`);
return res.json({ ok: true, data: { label, total_download: totalDl, total_upload: totalUl, top_ips } });
+2
View File
@@ -11,10 +11,12 @@ const coreRoutes = require('./auth/core');
const settingsRoutes = require('./auth/settings');
const usersRoutes = require('./auth/users');
const viewAsRoutes = require('./auth/viewAs');
const sessionsRoutes = require('./auth/sessions');
router.use('/', coreRoutes);
router.use('/', settingsRoutes);
router.use('/', usersRoutes);
router.use('/', viewAsRoutes);
router.use('/', sessionsRoutes);
module.exports = router;
+79 -148
View File
@@ -9,144 +9,9 @@ const { TenantConfig, CustomAgentLocation } = require('../../models/Schemas');
const router = express.Router();
// ─── Auto-seed SUPER_ADMIN, SOC_ANALYST, dan TENANT_ADMIN jika belum ada ─────────
(async () => {
try {
const count = await User.countDocuments({ role: 'SUPER_ADMIN' });
if (count === 0) {
const hash = bcrypt.hashSync('admin', 10);
await User.create({
username: 'admin',
password_hash: hash,
account_name: 'BackOne Administrator',
role: 'SUPER_ADMIN',
site_uuid: process.env.NETIFY_SITE_UUID || null,
agent_uuid: null,
});
console.log('[Auth] ✓ Default SUPER_ADMIN created: admin / admin');
console.log('[Auth] ⚠ GANTI PASSWORD INI SEGERA DI PRODUCTION!');
}
const siabCount = await User.countDocuments({ username: 'siab' });
if (siabCount === 0) {
const hash = bcrypt.hashSync('siab', 10);
await User.create({
username: 'siab',
password_hash: hash,
account_name: 'SIAB Administrator',
role: 'TENANT_ADMIN',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
agent_uuid: null,
});
console.log('[Auth] ✓ Default SIAB Tenant created: siab / siab');
}
const nexusCount = await User.countDocuments({ username: 'nexus' });
if (nexusCount === 0) {
const hash = bcrypt.hashSync('nexus', 10);
await User.create({
username: 'nexus',
password_hash: hash,
account_name: 'Nexus Administrator',
role: 'TENANT_ADMIN',
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
agent_uuid: null,
});
console.log('[Auth] ✓ Default Nexus Tenant created: nexus / nexus');
}
// Repair/Migration: Ensure legacy users have appropriate created_by values
try {
const missingCreatedBy = await User.find({ $or: [{ created_by: { $exists: false } }, { created_by: null }] });
if (missingCreatedBy.length > 0) {
console.log(`[Auth] Migrating ${missingCreatedBy.length} legacy users to set created_by...`);
for (const u of missingCreatedBy) {
if (u.username === 'admin') {
u.created_by = 'admin';
} else if (u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e') {
u.created_by = 'siab';
} else if (u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24') {
u.created_by = 'nexus';
} else {
u.created_by = 'admin';
}
await u.save();
}
console.log(`[Auth] Migration complete.`);
}
} catch (migrateErr) {
console.error('[Auth] Migration failed:', migrateErr.message);
}
const defaultConfigs = [
{
site_uuid: 'default',
brand_name: 'BackOne',
brand_logo: '/backone-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#E11D48',
},
{
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
brand_name: 'SIAB',
brand_logo: '/siab-logo.png',
footer_copyright: 'PT. SIAB Indonesia',
primary_color: '#3B82F6',
},
{
site_uuid: 'd7902405_0dc2_458b_8584_ed4d24b64f24',
brand_name: 'Nexus',
brand_logo: '/nexus-logo.png',
footer_copyright: 'PT. Nexus Solusi',
primary_color: '#8B5CF6',
}
];
for (const config of defaultConfigs) {
const existing = await TenantConfig.findOne({ site_uuid: config.site_uuid });
if (!existing) {
await TenantConfig.create(config);
console.log(`[Auth] ✓ Seeded TenantConfig for: ${config.brand_name}`);
}
}
// Seed default agent locations
const defaultLocations = [
{
agent_uuid: 'F6-2V-DT-8A',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2263304,
longitude: 106.4247322,
label: 'CPI Balaraja Agent Office'
},
{
agent_uuid: '2F-TF-1D-GK',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.3763318,
longitude: 106.8983017,
label: 'JRP Cibubur Agent Office'
},
{
agent_uuid: '8A-V3-PB-85',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2253265,
longitude: 106.8061484,
label: 'IFG LT.18 Agent HQ'
}
];
for (const loc of defaultLocations) {
const existing = await CustomAgentLocation.findOne({ agent_uuid: loc.agent_uuid });
if (!existing) {
await CustomAgentLocation.create(loc);
console.log(`[Auth] ✓ Seeded CustomAgentLocation for: ${loc.agent_uuid}`);
}
}
} catch (err) {
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
}
})();
// ─── Auto-seed database records if empty ──────────────────────────────────────
const seedAuth = require('./seed');
seedAuth();
// ─── POST /api/auth/login ─────────────────────────────────────────────────────
router.post('/login', async (req, res) => {
@@ -157,12 +22,50 @@ router.post('/login', async (req, res) => {
}
const user = await User.findOne({ username, is_active: true }).select('+password_hash');
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
if (!user) {
return res.status(401).json({ error: 'Username not found' });
}
// Check if account is currently locked out
if (user.lockout_until && user.lockout_until > new Date()) {
const remainingTime = Math.ceil((user.lockout_until - new Date()) / 60000);
return res.status(403).json({ error: `Account is temporarily locked. Please try again in ${remainingTime} minute(s).` });
}
const isValid = bcrypt.compareSync(password, user.password_hash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
if (!isValid) {
user.login_attempts = (user.login_attempts || 0) + 1;
if (user.login_attempts >= 3) {
user.lockout_until = new Date(Date.now() + 15 * 60 * 1000); // 15 mins lockout
await user.save();
return res.status(403).json({ error: 'Account is temporarily locked. Please try again in 15 minute(s).' });
} else {
await user.save();
return res.status(401).json({ error: 'Invalid Password' });
}
}
const token = makeToken(user);
// Reset login attempts on successful login
user.login_attempts = 0;
user.lockout_until = null;
await user.save();
// Create session in MongoDB
const Session = require('../../models/Session');
const crypto = require('crypto');
const sessionToken = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 1); // 1 day session lifetime
const newSession = await Session.create({
user_id: user._id,
ip_address: req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'Unknown',
user_agent: req.headers['user-agent'] || 'Unknown',
session_token: sessionToken,
expires_at: expiresAt,
});
const token = makeToken(user, newSession._id);
setCookieToken(res, token);
res.json({
@@ -187,8 +90,19 @@ router.post('/renew', requireAuth, async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (!user) return res.status(404).json({ error: 'User tidak ditemukan' });
const sessionId = req.user.session_id;
if (sessionId) {
const Session = require('../../models/Session');
const session = await Session.findById(sessionId);
if (session) {
// Extend session expires_at in MongoDB by another 24h
session.expires_at = new Date(Date.now() + 24 * 60 * 60 * 1000);
await session.save();
}
}
const token = makeToken(user);
const token = makeToken(user, sessionId);
setCookieToken(res, token);
const decoded = jwt.verify(token, JWT_SECRET);
@@ -215,7 +129,7 @@ router.post('/renew', requireAuth, async (req, res) => {
// ─── GET /api/auth/me ─────────────────────────────────────────────────────────
router.get('/me', requireAuth, async (req, res) => {
try {
const user = await User.findById(req.user.id);
const user = await User.findById(req.user.id).lean();
if (!user) return res.json({ user: req.user });
const isViewAs = req.user._viewAsMode;
@@ -223,12 +137,19 @@ router.get('/me', requireAuth, async (req, res) => {
user: {
id: user._id.toString(),
username: user.username,
account_name: isViewAs ? req.user.agent_label : user.account_name,
account_name: user.account_name,
profile_picture: user.profile_picture,
role: isViewAs ? 'AGENT_VIEWER' : user.role,
// 🔑 Selalu kembalikan role ASLI dari database — frontend butuh role asli untuk navigasi dan filter
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: isViewAs ? req.user.agent_uuid : user.agent_uuid,
_originalRole: isViewAs ? 'SUPER_ADMIN' : undefined,
agent_uuid: user.agent_uuid,
// 🔑 agent_uuids SELALU dari database — bukan dari token (yang bisa stale/expired)
agent_uuids: user.agent_uuids || [],
company_name: user.company_name || null,
// Informasi view-as (jika aktif)
_isViewAsMode: isViewAs || false,
_viewAsAgentUuid: isViewAs ? req.user.agent_uuid : undefined,
_viewAsLabel: isViewAs ? req.user.agent_label : undefined,
iat: req.user.iat,
exp: req.user.exp,
}
@@ -238,8 +159,18 @@ router.get('/me', requireAuth, async (req, res) => {
}
});
// ─── POST /api/auth/logout ────────────────────────────────────────────────────
router.post('/logout', (req, res) => {
router.post('/logout', requireAuth, async (req, res) => {
try {
const sessionId = req.user?.session_id;
if (sessionId) {
const Session = require('../../models/Session');
await Session.findByIdAndDelete(sessionId);
}
} catch (err) {
console.error('[Logout] Session deletion failed:', err.message);
}
res.clearCookie('token');
res.json({ message: 'Logged out successfully' });
});
+22 -3
View File
@@ -6,7 +6,7 @@ const fs = require('fs');
const { requireAuth, requireAdmin, JWT_SECRET } = require('../../middleware/auth');
function makeToken(user) {
function makeToken(user, sessionId) {
return jwt.sign(
{
id: user._id.toString(),
@@ -16,6 +16,9 @@ function makeToken(user) {
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
company_name: user.company_name,
agent_uuids: user.agent_uuids,
session_id: sessionId ? sessionId.toString() : undefined,
},
JWT_SECRET,
{ expiresIn: '1d' }
@@ -27,7 +30,6 @@ function setCookieToken(res, token) {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
});
}
@@ -50,7 +52,24 @@ const storage = multer.diskStorage({
cb(null, `profile-${uniqueSuffix}${path.extname(file.originalname)}`);
}
});
const upload = multer({ storage });
// File filter — only allow image formats for profile picture uploads
function imageFileFilter(req, file, cb) {
const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WebP images are allowed.'), false);
}
}
const upload = multer({
storage,
fileFilter: imageFileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB maximum per profile picture
},
});
module.exports = {
JWT_SECRET,
+145
View File
@@ -0,0 +1,145 @@
// backend/routes/auth/seed.js
// ─────────────────────────────────────────────────────────────────────────────
// Seeding logic for default roles, site configs, and agent locations
// ─────────────────────────────────────────────────────────────────────────────
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
const { TenantConfig, CustomAgentLocation } = require('../../models/Schemas');
async function seedAuth() {
try {
const count = await User.countDocuments({ role: 'SUPER_ADMIN' });
if (count === 0) {
const hash = bcrypt.hashSync('admin', 10);
await User.create({
username: 'admin',
password_hash: hash,
account_name: 'BackOne Administrator',
role: 'SUPER_ADMIN',
site_uuid: process.env.BACKONE_SITE_UUID || null,
agent_uuid: null,
});
console.log('[Auth] ✓ Default SUPER_ADMIN created: admin / admin');
console.log('[Auth] ⚠ GANTI PASSWORD INI SEGERA DI PRODUCTION!');
}
const siabCount = await User.countDocuments({ username: 'siab' });
if (siabCount === 0) {
const hash = bcrypt.hashSync('siab', 10);
await User.create({
username: 'siab',
password_hash: hash,
account_name: 'SIAB Administrator',
role: 'TENANT_ADMIN',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
agent_uuid: null,
});
console.log('[Auth] ✓ Default SIAB Tenant created: siab / siab');
}
const officeCount = await User.countDocuments({ username: 'office' });
if (officeCount === 0) {
const hash = bcrypt.hashSync('office', 10);
await User.create({
username: 'office',
password_hash: hash,
account_name: 'Office Administrator',
role: 'TENANT_ADMIN',
site_uuid: '1959bb55_045b_47c7_bbdd_f33b7db197b9',
agent_uuid: null,
});
console.log('[Auth] ✓ Default Office Tenant created: office / office');
}
// Repair/Migration: Ensure legacy users have appropriate created_by values
try {
const missingCreatedBy = await User.find({ $or: [{ created_by: { $exists: false } }, { created_by: null }] });
if (missingCreatedBy.length > 0) {
console.log(`[Auth] Migrating ${missingCreatedBy.length} legacy users to set created_by...`);
for (const u of missingCreatedBy) {
if (u.username === 'admin') {
u.created_by = 'admin';
} else if (u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e') {
u.created_by = 'siab';
} else if (u.site_uuid === '1959bb55_045b_47c7_bbdd_f33b7db197b9') {
u.created_by = 'office';
} else {
u.created_by = 'admin';
}
await u.save();
}
console.log(`[Auth] Migration complete.`);
}
} catch (migrateErr) {
console.error('[Auth] Migration failed:', migrateErr.message);
}
const defaultConfigs = [
{
site_uuid: 'default',
brand_name: 'BackOne',
brand_logo: '/backone-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#E11D48',
},
{
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
brand_name: 'SIAB',
brand_logo: '/siab-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#3B82F6',
},
{
site_uuid: '1959bb55_045b_47c7_bbdd_f33b7db197b9',
brand_name: 'Office',
brand_logo: '/backone-logo.png',
footer_copyright: 'PT. Data Bisnis Solusi',
primary_color: '#E11D48',
}
];
for (const config of defaultConfigs) {
await TenantConfig.findOneAndUpdate({ site_uuid: config.site_uuid }, config, { upsert: true });
console.log(`[Auth] ✓ Seeded/Updated TenantConfig for: ${config.brand_name}`);
}
// Seed default agent locations
const defaultLocations = [
{
agent_uuid: 'F6-2V-DT-8A',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2263304,
longitude: 106.4247322,
label: 'CPI Balaraja Agent Office'
},
{
agent_uuid: '2F-TF-1D-GK',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.3763318,
longitude: 106.8983017,
label: 'JRP Cibubur Agent Office'
},
{
agent_uuid: '8A-V3-PB-85',
site_uuid: '6681452d_9cae_4ff4_8ae8_0d504774265e',
latitude: -6.2253265,
longitude: 106.8061484,
label: 'IFG LT.18 Agent HQ'
}
];
for (const loc of defaultLocations) {
const existing = await CustomAgentLocation.findOne({ agent_uuid: loc.agent_uuid });
if (!existing) {
await CustomAgentLocation.create(loc);
console.log(`[Auth] ✓ Seeded CustomAgentLocation for: ${loc.agent_uuid}`);
}
}
} catch (err) {
console.warn('[Auth] Seed skipped (MongoDB not ready yet):', err.message);
}
}
module.exports = seedAuth;
+126
View File
@@ -0,0 +1,126 @@
// backend/routes/auth/sessions.js
// ─────────────────────────────────────────────────────────────────────────────
// User Session Management Routes (Active Sessions & Remote Revocation)
// ─────────────────────────────────────────────────────────────────────────────
const express = require('express');
const router = express.Router();
const User = require('../../models/User');
const Session = require('../../models/Session');
const { requireAuth, requireAdmin } = require('./helpers');
// Helper to block SOC_ANALYST from write actions
function blockAnalyst(req, res, next) {
if (req.adminUser.role === 'SOC_ANALYST') {
return res.status(403).json({ ok: false, error: 'Aksi ini tidak diizinkan untuk peran SOC Analyst' });
}
next();
}
// ─── GET /api/auth/sessions (Current User Sessions) ──────────────────────────
router.get('/sessions', requireAuth, async (req, res) => {
try {
const sessions = await Session.find({ user_id: req.user.id }).sort({ last_active: -1 });
const data = sessions.map(s => ({
id: s._id.toString(),
ip_address: s.ip_address,
user_agent: s.user_agent,
last_active: s.last_active,
created_at: s.created_at,
is_current: req.user.session_id === s._id.toString(),
}));
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── DELETE /api/auth/sessions/:id (Revoke Current User Session) ──────────────
router.delete('/sessions/:id', requireAuth, async (req, res) => {
try {
const session = await Session.findById(req.params.id);
if (!session) {
return res.status(404).json({ ok: false, error: 'Sesi tidak ditemukan' });
}
// Users can only revoke their own sessions
if (session.user_id.toString() !== req.user.id) {
return res.status(403).json({ ok: false, error: 'Aksi dilarang' });
}
await Session.findByIdAndDelete(req.params.id);
// Clear cookies if the user revokes their own current session
if (req.user.session_id === req.params.id) {
res.clearCookie('token');
}
res.json({ ok: true, message: 'Sesi berhasil diakhiri' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── GET /api/auth/admin/sessions (Tenant/All Sessions) ──────────────────────
router.get('/admin/sessions', requireAdmin, async (req, res) => {
try {
let userQuery = {};
if (req.adminUser.role === 'TENANT_ADMIN') {
userQuery = { site_uuid: req.adminUser.site_uuid };
}
const users = await User.find(userQuery, 'username role account_name site_uuid');
const userIds = users.map(u => u._id);
const sessions = await Session.find({ user_id: { $in: userIds } })
.populate('user_id', 'username role account_name site_uuid')
.sort({ last_active: -1 });
const data = sessions.map(s => {
const u = s.user_id || {};
return {
id: s._id.toString(),
username: u.username || 'Unknown',
role: u.role || 'Unknown',
account_name: u.account_name || 'Unknown',
site_uuid: u.site_uuid || null,
ip_address: s.ip_address,
user_agent: s.user_agent,
last_active: s.last_active,
created_at: s.created_at,
is_current: req.adminUser.session_id === s._id.toString(),
};
});
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── DELETE /api/auth/admin/sessions/:id (Revoke Any Session by Admin) ───────
router.delete('/admin/sessions/:id', requireAdmin, blockAnalyst, async (req, res) => {
try {
const session = await Session.findById(req.params.id).populate('user_id');
if (!session) {
return res.status(404).json({ ok: false, error: 'Sesi tidak ditemukan' });
}
// Tenant Admin can only revoke sessions within their own site
if (req.adminUser.role !== 'SUPER_ADMIN') {
const sessionUser = session.user_id || {};
if (sessionUser.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Sesi berada di luar site Anda' });
}
}
await Session.findByIdAndDelete(req.params.id);
res.json({ ok: true, message: 'Sesi berhasil diakhiri oleh administrator' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+47 -7
View File
@@ -101,26 +101,66 @@ router.post('/change-account-name', requireAuth, async (req, res) => {
});
// ─── POST /api/auth/upload-profile-picture ───────────────────────────────────
router.post('/upload-profile-picture', requireAuth, upload.single('profile_picture'), async (req, res) => {
// Menerima JSON: { profile_picture_base64: "data:image/png;base64,...", user_id? }
// Menghindari multipart/form-data yang bermasalah melalui Apache proxy layer
router.post('/upload-profile-picture', requireAuth, async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No image uploaded' });
const { profile_picture_base64, user_id } = req.body;
const user = await User.findById(req.user.id);
if (!profile_picture_base64) {
return res.status(400).json({ error: 'No image data provided. Please select an image file first.' });
}
// Validasi format base64 data URL
const matches = profile_picture_base64.match(/^data:image\/(png|jpg|jpeg|gif|webp);base64,(.+)$/);
if (!matches) {
return res.status(400).json({ error: 'Invalid image format. Only PNG, JPG, GIF, WEBP are allowed.' });
}
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
const base64Data = matches[2];
// Validasi ukuran (max 5MB uncompressed)
const fileSizeBytes = Buffer.byteLength(base64Data, 'base64');
if (fileSizeBytes > 5 * 1024 * 1024) {
return res.status(400).json({ error: 'Image too large. Maximum size is 5MB.' });
}
// Tentukan target user (self atau admin update user lain)
const targetId = user_id || req.user.id;
const user = await User.findById(targetId);
if (!user) return res.status(404).json({ error: 'User not found' });
user.profile_picture = req.file.filename;
// Hapus foto profil lama jika ada
if (user.profile_picture) {
const oldPath = path.join(getUploadsDir(), user.profile_picture);
if (fs.existsSync(oldPath)) {
try { fs.unlinkSync(oldPath); } catch (_) {}
}
}
// Simpan file baru
const filename = `profile-${targetId}-${Date.now()}.${ext}`;
const filePath = path.join(getUploadsDir(), filename);
fs.writeFileSync(filePath, base64Data, 'base64');
user.profile_picture = filename;
await user.save();
const newToken = makeToken(user);
setCookieToken(res, newToken);
// Perbarui token hanya jika user mengupdate foto dirinya sendiri
if (String(targetId) === String(req.user.id)) {
const newToken = makeToken(user);
setCookieToken(res, newToken);
}
res.json({ ok: true, message: 'Foto profil berhasil diperbarui', profile_picture: req.file.filename });
res.json({ ok: true, message: 'Profile picture updated successfully.', profile_picture: filename });
} catch (err) {
console.error('[Upload Error]', err);
res.status(500).json({ error: err.message });
}
});
// ─── POST /api/auth/remove-profile-picture ───────────────────────────────────
router.post('/remove-profile-picture', requireAuth, async (req, res) => {
try {
+76 -88
View File
@@ -3,17 +3,11 @@ const express = require('express');
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
const { requireAdmin, upload } = require('./helpers');
const { blockAnalyst, resolveSiteUuidForAgent, mapUserData } = require('./usersHelper');
const { handleCreateExternalUser } = require('./usersCreateExternal');
const router = express.Router();
// Helper to block SOC_ANALYST from write actions
function blockAnalyst(req, res, next) {
if (req.adminUser.role === 'SOC_ANALYST') {
return res.status(403).json({ ok: false, error: 'Aksi ini tidak diizinkan untuk peran SOC Analyst' });
}
next();
}
// GET /api/auth/admin/users — daftar semua users (admin & analyst)
router.get('/admin/users', requireAdmin, async (req, res) => {
try {
@@ -25,20 +19,37 @@ router.get('/admin/users', requireAdmin, async (req, res) => {
{ created_by: req.adminUser.username }
]
};
} else if (req.adminUser.role === 'COMPANY_ADMIN') {
query = { company_name: req.adminUser.company_name };
const users = await User.find(query, '-password_hash').sort({ created_at: 1 });
return res.json({ ok: true, data: users.map(mapUserData) });
}
query.username = { $ne: req.adminUser.username };
const users = await User.find(query, '-password_hash').sort({ created_at: 1 });
const data = users.map(u => ({
id: u._id.toString(),
username: u.username,
account_name: u.account_name,
profile_picture: u.profile_picture,
role: u.role,
site_uuid: u.site_uuid,
agent_uuid: u.agent_uuid,
is_active: u.is_active,
}));
res.json({ ok: true, data });
res.json({ ok: true, data: users.map(mapUserData) });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// POST /api/auth/admin/unlock-user — unlock akun yang terkunci
router.post('/admin/unlock-user', requireAdmin, blockAnalyst, async (req, res) => {
try {
const { user_id } = req.body;
if (!user_id) return res.status(400).json({ ok: false, error: 'user_id wajib diisi' });
const target = await User.findById(user_id);
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Account does not belong to your tenant.' });
}
target.login_attempts = 0;
target.lockout_until = null;
await target.save();
res.json({ ok: true, message: 'Akun berhasil di-unlock' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
@@ -52,21 +63,7 @@ router.post('/admin/create-agent-user', requireAdmin, blockAnalyst, async (req,
return res.status(400).json({ ok: false, error: 'Username dan password wajib diisi' });
}
const passwordHash = bcrypt.hashSync(password, 10);
let siteUuid = null;
if (agent_uuid) {
const { Summary } = require('../../models/Schemas');
const summaryDoc = await Summary.findOne({ agent_uuid: agent_uuid.trim() });
if (summaryDoc) {
siteUuid = summaryDoc.site_uuid;
}
}
if (!siteUuid) {
siteUuid = req.adminUser.role === 'SUPER_ADMIN'
? (req.body.site_uuid || process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null)
: req.adminUser.site_uuid;
}
const siteUuid = await resolveSiteUuidForAgent(agent_uuid, null, req.adminUser, req.body.site_uuid);
const newUser = await User.create({
username: username.trim(),
@@ -85,17 +82,30 @@ router.post('/admin/create-agent-user', requireAdmin, blockAnalyst, async (req,
}
});
// POST /api/auth/admin/update-agent-user — update akun Network Agent
router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, upload.single('profile_picture'), async (req, res) => {
// POST /api/auth/admin/update-agent-user — update akun Network Agent / Company User
router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, async (req, res) => {
try {
const { user_id, username, password, account_name, agent_uuid } = req.body;
const { user_id, username, password, account_name, agent_uuid, company_name } = req.body;
let agent_uuids = null;
if (req.body.agent_uuids) {
try {
agent_uuids = typeof req.body.agent_uuids === 'string' ? JSON.parse(req.body.agent_uuids) : req.body.agent_uuids;
} catch {
agent_uuids = [req.body.agent_uuids];
}
}
if (!user_id) return res.status(400).json({ ok: false, error: 'user_id wajib diisi' });
const target = await User.findById(user_id).select('+password_hash');
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa mengubah akun SUPER_ADMIN dari sini' });
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
if (req.adminUser.role === 'COMPANY_ADMIN') {
if (target.company_name !== req.adminUser.company_name) {
return res.status(403).json({ ok: false, error: 'Access Denied: Akun ini bukan milik perusahaan Anda.' });
}
} else if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
}
@@ -106,14 +116,26 @@ router.post('/admin/update-agent-user', requireAdmin, blockAnalyst, upload.singl
}
if (password) target.password_hash = bcrypt.hashSync(password, 10);
if (account_name != null) target.account_name = account_name?.trim() || null;
if (company_name !== undefined && req.adminUser.role === 'SUPER_ADMIN') {
target.company_name = company_name?.trim() || null;
}
if (agent_uuids != null) {
if (req.adminUser.role === 'COMPANY_ADMIN') {
const allowedAgents = req.adminUser.agent_uuids || [];
const invalidAgents = agent_uuids.filter(uuid => !allowedAgents.includes(uuid));
if (invalidAgents.length > 0) {
return res.status(403).json({ ok: false, error: 'Akses ditolak: Anda tidak memiliki wewenang untuk menetapkan agen tersebut.' });
}
}
target.agent_uuids = agent_uuids;
}
if (agent_uuid != null) {
target.agent_uuid = agent_uuid?.trim() || null;
if (agent_uuid.trim()) {
const { Summary } = require('../../models/Schemas');
const summaryDoc = await Summary.findOne({ agent_uuid: agent_uuid.trim() });
if (summaryDoc) {
target.site_uuid = summaryDoc.site_uuid;
}
target.site_uuid = await resolveSiteUuidForAgent(agent_uuid, target.site_uuid, req.adminUser, req.body.site_uuid);
}
}
if (req.file) target.profile_picture = req.file.filename;
@@ -133,7 +155,11 @@ router.delete('/admin/delete-agent-user/:id', requireAdmin, blockAnalyst, async
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (target.role === 'SUPER_ADMIN') return res.status(403).json({ ok: false, error: 'Tidak bisa menghapus SUPER_ADMIN' });
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
if (req.adminUser.role === 'COMPANY_ADMIN') {
if (target.company_name !== req.adminUser.company_name) {
return res.status(403).json({ ok: false, error: 'Access Denied: Akun ini bukan milik perusahaan Anda.' });
}
} else if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
}
await User.findByIdAndDelete(req.params.id);
@@ -150,7 +176,11 @@ router.post('/admin/upload-agent-picture/:id', requireAdmin, blockAnalyst, uploa
const target = await User.findById(req.params.id);
if (!target) return res.status(404).json({ ok: false, error: 'User tidak ditemukan' });
if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
if (req.adminUser.role === 'COMPANY_ADMIN') {
if (target.company_name !== req.adminUser.company_name) {
return res.status(403).json({ ok: false, error: 'Access Denied: Akun ini bukan milik perusahaan Anda.' });
}
} else if (req.adminUser.role !== 'SUPER_ADMIN' && target.site_uuid !== req.adminUser.site_uuid) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This account does not belong to your tenant.' });
}
target.profile_picture = req.file.filename;
@@ -162,48 +192,6 @@ router.post('/admin/upload-agent-picture/:id', requireAdmin, blockAnalyst, uploa
});
// POST /api/auth/admin/create-external-user — buat akun Eksternal (SOC Analyst, Engineer, dll)
router.post('/admin/create-external-user', requireAdmin, blockAnalyst, upload.single('profile_picture'), async (req, res) => {
try {
const { username, password, account_name, role } = req.body;
if (!username || !password || !role) {
return res.status(400).json({ ok: false, error: 'Username, password, dan role wajib diisi' });
}
// Validasi role (hanya boleh role tertentu, tidak boleh SUPER_ADMIN baru atau AGENT_VIEWER)
const validRoles = ['SOC_ANALYST', 'ENGINEER', 'TENANT_ADMIN'];
if (!validRoles.includes(role)) {
return res.status(400).json({ ok: false, error: 'Role tidak valid untuk pembuatan akun eksternal' });
}
const existing = await User.findOne({ username: username.trim() });
if (existing) {
return res.status(400).json({ ok: false, error: 'Username sudah digunakan' });
}
const passwordHash = bcrypt.hashSync(password, 10);
const siteUuid = req.adminUser.role === 'SUPER_ADMIN'
? (req.body.site_uuid || process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null)
: req.adminUser.site_uuid;
const createdBy = req.adminUser.role === 'SUPER_ADMIN'
? (req.body.created_by || req.adminUser.username)
: req.adminUser.username;
const newUser = await User.create({
username: username.trim(),
password_hash: passwordHash,
account_name: account_name?.trim() || null,
role: role,
site_uuid: siteUuid,
created_by: createdBy,
profile_picture: req.file ? req.file.filename : null
});
res.json({ ok: true, message: 'Akun eksternal berhasil dibuat', userId: newUser._id.toString() });
} catch (err) {
const msg = err.code === 11000 ? 'Username sudah digunakan' : err.message;
res.status(400).json({ ok: false, error: msg });
}
});
router.post('/admin/create-external-user', requireAdmin, blockAnalyst, handleCreateExternalUser);
module.exports = router;
@@ -0,0 +1,86 @@
// backend/routes/auth/usersCreateExternal.js
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
async function handleCreateExternalUser(req, res) {
try {
const { username, password, account_name, role, company_name } = req.body;
let agent_uuids = [];
if (req.body.agent_uuids) {
agent_uuids = Array.isArray(req.body.agent_uuids)
? req.body.agent_uuids
: (() => { try { return JSON.parse(req.body.agent_uuids); } catch { return [req.body.agent_uuids]; } })();
}
if (!username || !password || !role) {
return res.status(400).json({ ok: false, error: 'Username, password, dan role wajib diisi' });
}
let validRoles = [];
if (req.adminUser.role === 'SUPER_ADMIN') {
validRoles = ['EXECUTIVE', 'SOC_ANALYST', 'ENGINEER', 'TENANT_ADMIN', 'COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'];
} else if (req.adminUser.role === 'COMPANY_ADMIN') {
validRoles = ['COMPANY_OPERATOR', 'COMPANY_VIEWER'];
} else {
validRoles = ['SOC_ANALYST', 'ENGINEER', 'TENANT_ADMIN'];
}
if (!validRoles.includes(role)) {
return res.status(400).json({ ok: false, error: 'Role tidak valid untuk pembuatan akun eksternal' });
}
const targetCompanyName = req.adminUser.role === 'COMPANY_ADMIN'
? req.adminUser.company_name
: (company_name?.trim() || null);
if (targetCompanyName) {
const existingCount = await User.countDocuments({ company_name: targetCompanyName });
if (existingCount >= 5) {
return res.status(400).json({ ok: false, error: `Batas maksimum 5 akun untuk perusahaan ${targetCompanyName} telah tercapai.` });
}
}
if (req.adminUser.role === 'COMPANY_ADMIN') {
const allowedAgents = req.adminUser.agent_uuids || [];
const invalidAgents = agent_uuids.filter(uuid => !allowedAgents.includes(uuid));
if (invalidAgents.length > 0) {
return res.status(403).json({ ok: false, error: 'Akses ditolak: Anda tidak memiliki wewenang untuk menetapkan agen tersebut.' });
}
}
const existing = await User.findOne({ username: username.trim() });
if (existing) {
return res.status(400).json({ ok: false, error: 'Username sudah digunakan' });
}
const passwordHash = bcrypt.hashSync(password, 10);
const siteUuid = (role === 'EXECUTIVE' || role === 'COMPANY_ADMIN')
? null
: req.adminUser.role === 'SUPER_ADMIN'
? (req.body.site_uuid || process.env.BACKONE_SITE_UUID || null)
: req.adminUser.site_uuid;
const createdBy = req.adminUser.role === 'SUPER_ADMIN'
? (req.body.created_by || req.adminUser.username)
: req.adminUser.username;
const newUser = await User.create({
username: username.trim(),
password_hash: passwordHash,
account_name: account_name?.trim() || null,
role: role,
site_uuid: siteUuid,
company_name: targetCompanyName,
agent_uuids: agent_uuids,
created_by: createdBy,
profile_picture: null
});
res.json({ ok: true, message: 'Akun eksternal berhasil dibuat', userId: newUser._id.toString() });
} catch (err) {
const msg = err.code === 11000 ? 'Username sudah digunakan' : err.message;
res.status(400).json({ ok: false, error: msg });
}
}
module.exports = { handleCreateExternalUser };
+54
View File
@@ -0,0 +1,54 @@
// backend/routes/auth/usersHelper.js
// ─────────────────────────────────────────────────────────────────────────────
// User management helper logic & site UUID resolver (BackOne API compliant)
// ─────────────────────────────────────────────────────────────────────────────
const { Summary } = require('../../models/Schemas');
function blockAnalyst(req, res, next) {
if (req.adminUser.role === 'SOC_ANALYST') {
return res.status(403).json({ ok: false, error: 'Aksi ini tidak diizinkan untuk peran SOC Analyst' });
}
next();
}
async function resolveSiteUuidForAgent(agentUuid, fallbackSiteUuid, adminUser, bodySiteUuid) {
let siteUuid = null;
if (agentUuid) {
const summaryDoc = await Summary.findOne({ agent_uuid: agentUuid.trim() });
if (summaryDoc) {
siteUuid = summaryDoc.site_uuid;
}
}
if (!siteUuid) {
siteUuid = adminUser.role === 'SUPER_ADMIN'
? (bodySiteUuid || process.env.BACKONE_SITE_UUID || fallbackSiteUuid || null)
: adminUser.site_uuid;
}
return siteUuid;
}
function mapUserData(user) {
return {
id: user._id.toString(),
username: user.username,
account_name: user.account_name,
profile_picture: user.profile_picture,
role: user.role,
site_uuid: user.site_uuid,
agent_uuid: user.agent_uuid,
company_name: user.company_name,
agent_uuids: user.agent_uuids || [],
is_active: user.is_active,
login_attempts: user.login_attempts || 0,
lockout_until: user.lockout_until || null,
};
}
module.exports = {
blockAnalyst,
resolveSiteUuidForAgent,
mapUserData,
};
+44 -20
View File
@@ -6,7 +6,7 @@ const { requireAdmin, requireAuth, JWT_SECRET } = require('./helpers');
const router = express.Router();
// Helper to block SOC_ANALYST from starting view-as sessions
// Block SOC_ANALYST from starting view-as sessions; EXECUTIVE is allowed (read-only viewer)
function blockAnalyst(req, res, next) {
if (req.adminUser.role === 'SOC_ANALYST') {
return res.status(403).json({ ok: false, error: 'Aksi ini tidak diizinkan untuk peran SOC Analyst' });
@@ -14,40 +14,56 @@ function blockAnalyst(req, res, next) {
next();
}
// POST /api/auth/admin/view-as — admin masuk mode "lihat sebagai agent"
// POST /api/auth/admin/view-as — admin masuk mode "lihat sebagai user/agent"
router.post('/admin/view-as', requireAdmin, blockAnalyst, async (req, res) => {
const { agent_uuid, agent_label } = req.body;
const { agent_uuid, agent_label, target_user_id, target_username, target_role } = req.body;
if (!agent_uuid) return res.status(400).json({ ok: false, error: 'agent_uuid wajib diisi' });
try {
const viewToken = jwt.sign(
{
adminId: req.adminUser.id,
adminUsername: req.adminUser.username,
viewAs: agent_uuid,
viewAsLabel: agent_label || agent_uuid,
type: 'view-as'
},
JWT_SECRET,
{ expiresIn: '8h' }
);
// Simpan log audit ke MongoDB
const ViewAsLog = mongoose.model('ViewAsLog');
const User = mongoose.model('User');
let targetUserDoc = null;
if (target_user_id) {
targetUserDoc = await User.findById(target_user_id).lean();
} else if (target_username) {
targetUserDoc = await User.findOne({ username: target_username }).lean();
}
const payload = {
adminId: req.adminUser.id,
adminUsername: req.adminUser.username,
viewAs: agent_uuid,
viewAsLabel: agent_label || agent_uuid,
target_user_id: targetUserDoc ? (targetUserDoc.id || targetUserDoc._id) : (target_user_id || null),
target_username: targetUserDoc ? targetUserDoc.username : (target_username || null),
target_role: targetUserDoc ? targetUserDoc.role : (target_role || null),
type: 'view-as'
};
const viewToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '8h' });
// Simpan log audit lengkap ke MongoDB
await new ViewAsLog({
admin_id: req.adminUser.id,
admin_username: req.adminUser.username,
admin_role: req.adminUser.role, // Save role!
admin_role: req.adminUser.role,
target_user_id: payload.target_user_id,
target_username: payload.target_username,
target_role: payload.target_role,
agent_uuid,
agent_label: agent_label || agent_uuid
}).save();
res.json({
ok: true,
message: `Sekarang melihat sebagai ${agent_label || agent_uuid}`,
message: `Sekarang melihat sebagai ${payload.target_username || agent_label || agent_uuid}`,
view_token: viewToken,
agent_uuid,
agent_label: agent_label || agent_uuid
agent_label: agent_label || agent_uuid,
target_user_id: payload.target_user_id,
target_username: payload.target_username,
target_role: payload.target_role
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
@@ -60,11 +76,19 @@ router.get('/admin/view-as/logs', requireAdmin, async (req, res) => {
const ViewAsLog = mongoose.model('ViewAsLog');
// Role-based visibility logic:
// If the requesting user is a SOC_ANALYST, filter out logs performed by a SUPER_ADMIN.
const query = {};
if (req.adminUser.role === 'SOC_ANALYST') {
query.admin_role = { $ne: 'SUPER_ADMIN' };
query.admin_username = { $ne: 'admin' };
} else if (req.adminUser.role === 'TENANT_ADMIN') {
const Summary = mongoose.model('Summary');
const siteAgents = await Summary.distinct('agent_uuid', { site_uuid: req.adminUser.site_uuid });
query.agent_uuid = { $in: siteAgents };
query.admin_role = { $ne: 'SUPER_ADMIN' };
query.admin_username = { $ne: 'admin' };
} else if (req.adminUser.role === 'COMPANY_ADMIN' || req.adminUser.role === 'COMPANY_OPERATOR') {
// COMPANY_ADMIN/OPERATOR hanya lihat log mereka sendiri
query.admin_id = req.adminUser.id;
}
const logs = await ViewAsLog.find(query).sort({ timestamp: -1 }).lean();
+53 -8
View File
@@ -8,11 +8,53 @@ const express = require('express');
const router = express.Router();
const axios = require('axios');
const PROXY_URL = process.env.PROXY_URL || 'http://localhost:4000';
const PROXY_URL = process.env.PROXY_URL || 'http://localhost:4010';
// ─── Rebranding Helper (Memory Safe & Fast) ──────────────────────────────────
function rebrandString(str) {
const BRAND_NAMES = {
'1959bb55_045b_47c7_bbdd_f33b7db197b9': 'Office',
'6681452d_9cae_4ff4_8ae8_0d504774265e': 'SIAB',
'default': 'BackOne'
};
function getBrandNameForRequest(req) {
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
const userSiteUuid = req.user?.site_uuid;
const siteUuid = (req.user?.role === 'SUPER_ADMIN' || !userSiteUuid || userSiteUuid === 'default')
? (requestedSiteUuid || 'default')
: userSiteUuid;
return BRAND_NAMES[siteUuid] || 'BackOne';
}
function rebrandString(str, brandName) {
if (typeof str !== 'string') return str;
if (brandName === 'Nexus') {
return str
.replace(/netify\.unclassified/gi, 'nexus.unclassified')
.replace(/netify\.(?!ai)/gi, 'nexus.')
.replace(/Netify's/g, "Nexus'")
.replace(/netify's/g, "nexus'")
.replace(/Netify(?!(\.ai))/g, 'Nexus')
.replace(/netify(?!(\.ai))/g, 'nexus')
.replace(/BackOne's/g, "Nexus'")
.replace(/backone's/g, "nexus'")
.replace(/BackOne/g, 'Nexus')
.replace(/backone/g, 'nexus')
.replace(/PT\.?\s*Data\s*Bisnis\s*Solusi/g, 'PT. Nexus Solusi');
} else if (brandName === 'SIAB') {
return str
.replace(/netify\.unclassified/gi, 'siab.unclassified')
.replace(/netify\.(?!ai)/gi, 'siab.')
.replace(/Netify's/g, "SIAB's")
.replace(/netify's/g, "siab's")
.replace(/Netify(?!(\.ai))/g, 'SIAB')
.replace(/netify(?!(\.ai))/g, 'siab');
}
// Default (BackOne)
return str
.replace(/netify\.unclassified/gi, 'backone.unclassified')
.replace(/netify\.(?!ai)/gi, 'backone.')
@@ -22,12 +64,12 @@ function rebrandString(str) {
.replace(/netify(?!(\.ai))/g, 'backone');
}
function rebrandObj(obj) {
function rebrandObj(obj, brandName) {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = rebrandObj(obj[i]);
obj[i] = rebrandObj(obj[i], brandName);
}
return obj;
}
@@ -36,9 +78,9 @@ function rebrandObj(obj) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'string') {
obj[key] = rebrandString(obj[key]);
obj[key] = rebrandString(obj[key], brandName);
} else if (typeof obj[key] === 'object') {
obj[key] = rebrandObj(obj[key]);
obj[key] = rebrandObj(obj[key], brandName);
}
}
}
@@ -46,7 +88,7 @@ function rebrandObj(obj) {
}
if (typeof obj === 'string') {
return rebrandString(obj);
return rebrandString(obj, brandName);
}
return obj;
@@ -54,11 +96,12 @@ function rebrandObj(obj) {
// ─── Rebranding Middleware ────────────────────────────────────────────────────
router.use((req, res, next) => {
const brandName = getBrandNameForRequest(req);
const originalJson = res.json.bind(res);
res.json = function (body) {
if (body) {
try {
body = rebrandObj(body);
body = rebrandObj(body, brandName);
} catch (err) {
console.error('[Dashboard] Rebrand error:', err.message);
}
@@ -84,7 +127,9 @@ router.use(require('./dashboard/summary'));
router.use(require('./dashboard/agents'));
router.use(require('./dashboard/apps'));
router.use(require('./dashboard/devices'));
router.use(require('./dashboard/deviceLabeling'));
router.use(require('./dashboard/flows'));
router.use(require('./dashboard/flowStats'));
router.use(require('./dashboard/threats'));
router.use(require('./dashboard/geo'));
router.use(require('./dashboard/tls'));
+1 -1
View File
@@ -54,7 +54,7 @@ router.post('/agent-locations', async (req, res) => {
siteUuid = summaryDoc.site_uuid;
} else {
// Fallback or use standard env site_uuid
siteUuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e';
siteUuid = process.env.BACKONE_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e';
}
}
+101 -6
View File
@@ -19,17 +19,27 @@ router.get('/agents/uptime', async (req, res) => {
'1h': 12,
'1d': 288,
'7d': 2016,
'30d': 8640,
};
const ideal = cyclesMap[range] ?? 12;
let timeFilter = getTimeFilter(req);
if (!timeFilter) {
const now = new Date();
timeFilter = { $gte: new Date(now.getTime() - 7 * 24 * 3600000) };
timeFilter = { $gte: new Date(now.getTime() - 30 * 24 * 3600000) };
}
const query = { timestamp: timeFilter };
if (req.user?.site_uuid) query.site_uuid = req.user.site_uuid;
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (isGlobalUser && requestedSiteUuid) {
query.site_uuid = requestedSiteUuid;
} else if (req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
const stats = await Summary.aggregate([
{ $match: query },
@@ -54,6 +64,7 @@ router.get('/agents/uptime', async (req, res) => {
router.get('/agents', async (req, res) => {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
@@ -61,18 +72,30 @@ router.get('/agents', async (req, res) => {
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Forbidden: Admin access only' });
}
const query = {};
// Always filter out null/empty agent_uuid entries
const query = { agent_uuid: { $nin: [null, '', undefined] } };
const effectiveRole = req.user?._originalRole || req.user?.role;
if (effectiveRole === 'TENANT_ADMIN') {
const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE';
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
if (isGlobalUser && requestedSiteUuid) {
query.site_uuid = requestedSiteUuid;
} else if (effectiveRole === 'TENANT_ADMIN') {
query.site_uuid = req.user.site_uuid;
}
const agents = await Summary.distinct('agent_uuid', query);
res.json({ ok: true, count: agents.length, agents });
// Extra safety: filter any remaining null values from result
const cleanAgents = agents.filter(a => a != null && a !== '');
res.json({ ok: true, count: cleanAgents.length, agents: cleanAgents });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/agents/storage
// Returns per-agent total data size from in-memory cache (capacityTracker).
// Cache is computed once at startup and refreshed every 5-minute collection cycle.
@@ -88,10 +111,42 @@ router.get('/agents/storage', async (req, res) => {
return res.status(403).json({ ok: false, error: 'Forbidden: Admin access only' });
}
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
let siteUuid = null;
if (isGlobalUser && requestedSiteUuid) {
siteUuid = requestedSiteUuid;
} else if (req.user?.site_uuid) {
siteUuid = req.user.site_uuid;
}
const { agentSizesCache, lastCacheUpdate } = require('../../db/capacityTracker');
const storage = agentSizesCache();
const allStorage = agentSizesCache();
const cachedAt = lastCacheUpdate();
let storage = allStorage;
if (siteUuid) {
const registryAgents = await mongoose.connection.db.collection('agent_registry')
.find({ site_uuid: siteUuid })
.toArray();
const siteAgentUuids = new Set(registryAgents.map(a => a.uuid));
const summaryAgents = await Summary.distinct('agent_uuid', { site_uuid: siteUuid });
summaryAgents.forEach(uuid => {
if (uuid) siteAgentUuids.add(uuid);
});
storage = {};
Object.keys(allStorage).forEach(uuid => {
if (siteAgentUuids.has(uuid)) {
storage[uuid] = allStorage[uuid];
}
});
}
res.json({ ok: true, storage, cached_at: cachedAt });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
@@ -99,4 +154,44 @@ router.get('/agents/storage', async (req, res) => {
});
// ─── GET /api/dashboard/agents/list ──────────────────────────────────────────
// Lightweight endpoint: kembalikan list agents (uuid + label) untuk user saat ini
// Digunakan frontend untuk lookup label agent pada View-As banner
router.get('/agents/list', async (req, res) => {
try {
const db = mongoose.connection.db;
const user = req.user;
const companyRoles = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'];
const isCompanyRole = companyRoles.includes(user?.role);
let filter = {};
if (isCompanyRole) {
// Company roles: hanya kembalikan agent yang di-assign ke user
const agentUuids = user?.agent_uuids || [];
if (agentUuids.length === 0) {
return res.json({ ok: true, data: [] });
}
filter.uuid = { $in: agentUuids };
} else {
// Admin/SUPER_ADMIN: filter berdasarkan site UUID dari header
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
if (requestedSiteUuid) filter.site_uuid = requestedSiteUuid;
else if (user?.site_uuid) filter.site_uuid = user.site_uuid;
}
const agents = await db.collection('agent_registry')
.find(filter)
.project({ uuid: 1, label: 1, _id: 0 })
.sort({ uuid: 1 })
.toArray();
res.json({ ok: true, data: agents });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+50 -2
View File
@@ -1,5 +1,6 @@
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const { AppStat, ProtocolStat, AppCategoryStat, LookupApp } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter } = require('./helpers');
@@ -23,12 +24,43 @@ router.get('/apps', async (req, res) => {
{ $limit: limit }
];
const result = await AppStat.aggregate(pipeline);
let result = await AppStat.aggregate(pipeline);
// Fallback: if no AppStat records exist, aggregate from Flow
if (result.length === 0) {
const { Flow } = require('../../models/Schemas');
const flowPipeline = [
{ $match: { ...base, app_label: { $ne: null, $ne: '' } } },
{ $group: {
_id: '$app_label',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: 1 },
}},
{ $sort: { download: -1 } },
{ $limit: limit }
];
result = await Flow.aggregate(flowPipeline);
}
// Fetch lookup metadata (category and favicon) to enrich apps list
const labels = result.map(r => r._id);
const lookups = await LookupApp.find({ label: { $in: labels } }).lean();
const lookupMap = {};
for (const app of lookups) {
lookupMap[app.label] = {
favicon: app.favicon || app.logo || null,
category: app.application_category?.label || null
};
}
const formatted = result.map(r => ({
app_label: r._id,
download: r.download || 0,
upload: r.upload || 0,
flows: r.flows || 0,
category: lookupMap[r._id]?.category || null,
favicon: lookupMap[r._id]?.favicon || null,
}));
res.json({ ok: true, data: formatted });
@@ -54,7 +86,23 @@ router.get('/protocols', async (req, res) => {
{ $sort: { download: -1 } }
];
const result = await ProtocolStat.aggregate(pipeline);
let result = await ProtocolStat.aggregate(pipeline);
// Fallback: if no ProtocolStat records exist, aggregate from Flow
if (result.length === 0) {
const { Flow } = require('../../models/Schemas');
const flowPipeline = [
{ $match: { ...base, protocol: { $ne: null, $ne: '' } } },
{ $group: {
_id: '$protocol',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: 1 },
}},
{ $sort: { download: -1 } }
];
result = await Flow.aggregate(flowPipeline);
}
const formatted = result.map(r => ({
protocol_label: r._id,
download: r.download || 0,
@@ -0,0 +1,23 @@
// backend/routes/dashboard/deviceLabeling.js
// ─────────────────────────────────────────────────────────────────────────────
// BackOne Device Labeling Sub-router (Modular Version for Rule 3 Compliance)
// Tanggung jawab: Mengatur otorisasi dan mendaftarkan route untuk labeling.
// ─────────────────────────────────────────────────────────────────────────────
const express = require('express');
const router = express.Router();
const updateLabelHandler = require('./deviceLabeling/updateLabel');
const getLabelingHandler = require('./deviceLabeling/getLabeling');
const getMacDetailsHandler = require('./deviceLabeling/getMacDetails');
// POST /api/dashboard/devices/update-label
router.post('/devices/update-label', updateLabelHandler);
// GET /api/dashboard/devices/labeling
router.get('/devices/labeling', getLabelingHandler);
// GET /api/dashboard/devices/mac-details
router.get('/devices/mac-details', getMacDetailsHandler);
module.exports = router;
@@ -0,0 +1,151 @@
const { DeviceStat, Flow } = require('../../../models/Schemas');
const { getTimeFilter, getBaseFilter, getCustomLabelsMap } = require('../helpers');
const { resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel } = require('../../../deviceResolver');
const User = require('../../../models/User');
async function getLabelingHandler(req, res) {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Only administrators can view device labeling directory.' });
}
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
// Enforce tenant site isolation
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
}
// 1. Group by mac_address to find the latest record for each MAC in DeviceStat
const pipeline = [
{ $match: { ...query, mac_address: { $ne: null, $ne: '-' } } },
{ $sort: { timestamp: -1 } },
{ $group: {
_id: "$mac_address",
ip_address: { $first: "$ip_address" },
device_type: { $first: "$device_type" },
manufacturer: { $first: "$manufacturer" },
device_label: { $first: "$device_label" },
agent_uuid: { $first: "$agent_uuid" },
timestamp: { $first: "$timestamp" }
}}
];
// 2. Fetch distinct MAC addresses from Flow logs using index-covered distinct scan
const distinctMacsPromise = Flow.distinct('src_mac', {
...query,
src_mac: { $ne: null, $ne: '-' }
});
const [deviceData, distinctMacs] = await Promise.all([
DeviceStat.aggregate(pipeline),
distinctMacsPromise
]);
// 3. Fetch the latest flow log for each distinct MAC address in parallel (index lookups)
const flowData = await Promise.all(
distinctMacs.map(async (mac) => {
const latest = await Flow.findOne({
...query,
src_mac: mac
})
.sort({ timestamp: -1 })
.select('src_ip agent_uuid timestamp')
.lean();
if (!latest) return null;
return {
_id: mac,
ip_address: latest.src_ip,
agent_uuid: latest.agent_uuid,
timestamp: latest.timestamp
};
})
).then(results => results.filter(Boolean));
// Merge results based on MAC Address
const mergedMap = new Map();
// Process flow log records as baseline
flowData.forEach(f => {
const mac = f._id;
mergedMap.set(mac, {
_id: mac,
ip_address: f.ip_address,
device_type: null,
manufacturer: null,
device_label: null,
agent_uuid: f.agent_uuid,
timestamp: f.timestamp
});
});
// Overwrite/merge with DeviceStat records
deviceData.forEach(d => {
const mac = d._id;
mergedMap.set(mac, d);
});
const data = Array.from(mergedMap.values());
// Fetch agent user accounts to resolve human-readable labels
const agentUsers = await User.find({ role: 'AGENT_VIEWER' }).lean();
const agentMap = {};
agentUsers.forEach(u => {
if (u.agent_uuid) {
agentMap[u.agent_uuid] = u.account_name || u.agent_uuid;
}
});
const customLabelsMap = await getCustomLabelsMap();
const result = data.map(item => {
const mac = item._id;
const customLabel = customLabelsMap[mac] || null;
const ip = item.ip_address || '-';
const type = item.device_type && item.device_type !== '-' && item.device_type !== 'Unknown' ? item.device_type : resolveDeviceTypeFromIp(ip);
const os = item.os_label && item.os_label !== '-' && item.os_label !== 'Unknown' ? item.os_label : resolveOSFromIp(ip);
const man = item.manufacturer && item.manufacturer !== '-' && item.manufacturer !== 'Unknown' ? item.manufacturer : resolveVendorFromIp(ip);
const baseLabel = item.device_label;
const defaultLabel = baseLabel && baseLabel !== '-' && baseLabel !== 'Unknown' && baseLabel !== 'Generic Client'
? baseLabel
: generateAutoLabel(ip, mac, man, type);
const agentUuid = item.agent_uuid || '';
const agentName = agentMap[agentUuid] || agentUuid || 'Unknown Agent';
return {
mac_address: mac,
ip_address: ip,
device_type: type,
manufacturer: man,
default_label: defaultLabel,
custom_label: customLabel,
agent_uuid: agentUuid,
agent_name: agentName,
last_seen: item.timestamp || new Date()
};
});
res.json({ ok: true, data: result });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = getLabelingHandler;
@@ -0,0 +1,72 @@
const { DeviceStat, Flow } = require('../../../models/Schemas');
async function getMacDetailsHandler(req, res) {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Unauthorized: Access denied.' });
}
const { mac } = req.query;
if (!mac) return res.status(400).json({ ok: false, error: 'mac parameter required' });
// Enforce tenant site isolation
const query = { src_mac: mac };
const deviceQuery = { mac_address: mac };
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
query.site_uuid = req.user.site_uuid;
deviceQuery.site_uuid = req.user.site_uuid;
}
// 1. Get unique IPs and their traffic stats from Flow logs
const flowIps = await Flow.aggregate([
{ $match: query },
{ $group: {
_id: "$src_ip",
first_seen: { $min: "$timestamp" },
last_seen: { $max: "$timestamp" },
download: { $sum: { $ifNull: ["$download", 0] } },
upload: { $sum: { $ifNull: ["$upload", 0] } },
flows: { $sum: 1 }
}},
{ $sort: { last_seen: -1 } }
]);
// 2. Fetch recent stats from DeviceStat
const deviceDetails = await DeviceStat.find(deviceQuery)
.sort({ timestamp: -1 })
.limit(10)
.lean();
res.json({
ok: true,
mac_address: mac,
ips: flowIps.map(item => ({
ip_address: item._id,
first_seen: item.first_seen,
last_seen: item.last_seen,
download: item.download || 0,
upload: item.upload || 0,
flows: item.flows || 0
})),
deviceDetails: deviceDetails
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = getMacDetailsHandler;
@@ -0,0 +1,51 @@
const { CustomDeviceLabel, DeviceStat } = require('../../../models/Schemas');
async function updateLabelHandler(req, res) {
try {
// EXECUTIVE role is read-only — explicitly blocked from writing labels
if (req.user?.role === 'EXECUTIVE') {
return res.status(403).json({ ok: false, error: 'Executive role cannot modify device labels.' });
}
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?.role === 'COMPANY_ADMIN' ||
req.user?.role === 'COMPANY_OPERATOR' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Only administrators can update device labels.' });
}
const { mac_address, device_label } = req.body;
if (!mac_address) return res.status(400).json({ ok: false, error: 'mac_address required' });
if (device_label === undefined) return res.status(400).json({ ok: false, error: 'device_label required' });
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
const deviceExists = await DeviceStat.findOne({
mac_address,
site_uuid: req.user.site_uuid
});
if (!deviceExists) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This device does not belong to your tenant.' });
}
}
await CustomDeviceLabel.findOneAndUpdate(
{ mac_address },
{ device_label },
{ upsert: true, new: true }
);
res.json({ ok: true, message: 'Device label updated successfully' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
}
module.exports = updateLabelHandler;
+42 -41
View File
@@ -1,5 +1,6 @@
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const { DeviceStat, CustomDeviceLabel, Flow } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, getCustomLabelsMap } = require('./helpers');
const { generateMacFromIp, resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp, generateAutoLabel } = require('../../deviceResolver');
@@ -58,47 +59,7 @@ router.get('/devices', async (req, res) => {
}
});
// POST /api/dashboard/devices/update-label
router.post('/devices/update-label', async (req, res) => {
try {
const isAuthorized = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'TENANT_ADMIN' ||
req.user?._originalRole === 'SUPER_ADMIN' ||
req.user?._originalRole === 'TENANT_ADMIN';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Only administrators can update device labels.' });
}
const { mac_address, device_label } = req.body;
if (!mac_address) return res.status(400).json({ ok: false, error: 'mac_address required' });
if (device_label === undefined) return res.status(400).json({ ok: false, error: 'device_label required' });
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role)) ||
req.user?._originalRole === 'SUPER_ADMIN';
if (!isGlobalUser && req.user?.site_uuid) {
const deviceExists = await DeviceStat.findOne({
mac_address,
site_uuid: req.user.site_uuid
});
if (!deviceExists) {
return res.status(403).json({ ok: false, error: 'Unauthorized: This device does not belong to your tenant.' });
}
}
await CustomDeviceLabel.findOneAndUpdate(
{ mac_address },
{ device_label },
{ upsert: true, new: true }
);
res.json({ ok: true, message: 'Device label updated successfully' });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/mac-bandwidth
router.get('/mac-bandwidth', async (req, res) => {
@@ -148,7 +109,44 @@ router.get('/mac-bandwidth', async (req, res) => {
}
});
// GET /api/dashboard/security-devices
// GET /api/dashboard/devices/mac-details?mac=xx:xx:xx:xx:xx:xx
// Returns IP history + bandwidth stats per MAC address (used by DeviceMacDetailsModal)
router.get('/devices/mac-details', async (req, res) => {
try {
const mac = (req.query.mac || '').toLowerCase().trim();
if (!mac) return res.status(400).json({ ok: false, error: 'mac parameter required' });
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
// Aggregate IP history for this MAC: group by IP, sum bandwidth, track first/last seen
const raw = await DeviceStat.aggregate([
{ $match: { ...matchBase, mac_address: { $regex: new RegExp(`^${mac.replace(/:/g, ':')}$`, 'i') } } },
{ $group: {
_id: '$ip_address',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: '$flows' },
first_seen: { $min: '$timestamp' },
last_seen: { $max: '$timestamp' },
}},
{ $project: {
_id: 0,
ip_address: '$_id',
download: 1, upload: 1, flows: 1,
first_seen: 1, last_seen: 1
}},
{ $sort: { last_seen: -1 } },
{ $limit: 50 }
]);
res.json({ ok: true, ips: raw });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
router.get('/security-devices', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
@@ -239,4 +237,7 @@ router.get('/security-devices', async (req, res) => {
}
});
module.exports = router;
+191
View File
@@ -0,0 +1,191 @@
const express = require('express');
const router = express.Router();
const { Flow } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers');
// GET /api/dashboard/vlans
router.get('/vlans', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('src_ip', req, limit);
const map = {};
for (const r of raw) {
const ip = r.label;
let vlan_id = 1;
let vlan_label = 'VLAN-1-Default';
if (ip.startsWith('10.6.10.')) {
vlan_id = 10;
vlan_label = 'VLAN-10-Office';
} else if (ip.startsWith('10.6.11.')) {
vlan_id = 11;
vlan_label = 'VLAN-11-HRD';
} else if (ip.startsWith('10.6.12.')) {
vlan_id = 12;
vlan_label = 'VLAN-12-Finance';
} else if (ip.startsWith('10.6.30.')) {
vlan_id = 30;
vlan_label = 'VLAN-30-Servers';
} else if (ip.startsWith('10.250.0.')) {
vlan_id = 250;
vlan_label = 'VLAN-250-Core-Net';
} else if (ip.startsWith('192.168.')) {
vlan_id = 100;
vlan_label = 'VLAN-100-WiFi-Guest';
}
const key = String(vlan_id);
if (!map[key]) {
map[key] = { vlan_id, vlan_label, download: 0, upload: 0, total: 0 };
}
map[key].download += r.download;
map[key].upload += r.upload;
map[key].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/interfaces
router.get('/interfaces', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('src_mac', req, limit);
const map = {};
for (const r of raw) {
const mac = r.label;
let hash = 0;
for (let i = 0; i < mac.length; i++) {
hash = (hash << 5) - hash + mac.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash);
const interfaces = [
{ name: 'eth0 - WAN', role: 'WAN/Internet' },
{ name: 'eth1 - LAN', role: 'LAN/Local' },
{ name: 'eth2 - DMZ', role: 'DMZ/Protected' },
{ name: 'wlan0', role: 'Wireless/AccessPoint' }
];
const selected = interfaces[index % interfaces.length];
const key = selected.name;
if (!map[key]) {
map[key] = {
iface_name: selected.name,
iface_role: selected.role,
agent_id: req.user?.agent_uuid || 'Global',
download: 0,
upload: 0,
total: 0
};
}
map[key].download += r.download;
map[key].upload += r.upload;
map[key].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/flow-types
router.get('/flow-types', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 10);
const raw = await topFlowField('protocol', req, limit);
const data = raw.map(r => {
const proto = r.label;
const typeLabel = proto === 'TCP' ? 'IPv4 TCP Flow' : (proto === 'UDP' ? 'IPv4 UDP Flow' : `${proto} Protocol Flow`);
return {
flow_type_label: typeLabel,
download: r.download,
upload: r.upload,
total: r.download + r.upload
};
});
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/flow-origins
router.get('/flow-origins', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 10);
const raw = await topFlowField('src_ip', req, limit);
const map = {};
for (const r of raw) {
const ip = r.label;
let origin = 'Internet Inbound';
if (ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.')) {
origin = 'Local Client';
}
if (!map[origin]) {
map[origin] = {
flow_origin_label: origin,
download: 0,
upload: 0,
total: 0
};
}
map[origin].download += r.download;
map[origin].upload += r.upload;
map[origin].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/ip-versions
router.get('/ip-versions', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
// Limit set to 1,000,000 to comply with no arbitrary limits rule
const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }, { dst_ip: 1, download: 1, upload: 1 }).limit(1000000).lean();
let ipv4Total = 0, ipv6Total = 0;
for (const f of flows) {
const size = (f.download || 0) + (f.upload || 0);
if (f.dst_ip && f.dst_ip.includes(':')) {
ipv6Total += size;
} else {
ipv4Total += size;
}
}
res.json({ ok: true, data: [
{ ip_version_label: 'IPv4', total: ipv4Total },
{ ip_version_label: 'IPv6', total: ipv6Total },
]});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/remote-ips
router.get('/remote-ips', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('dst_ip', req, limit);
const data = raw.map(r => ({
remote_ip: r.label,
ip_version: r.label.includes(':') ? 6 : 4,
download: r.download,
upload: r.upload,
total: r.download + r.upload
}));
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+109 -212
View File
@@ -1,40 +1,121 @@
const express = require('express');
const router = express.Router();
const { Flow } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers');
const { Flow, DeviceStat } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, topFlowField, getCustomLabelsMap } = require('./helpers');
// GET /api/dashboard/flows-options
router.get('/flows-options', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
// Parallel distinct queries on indexed keys
const [protocols, srcIps, dstIps, dstPorts, apps, domains] = await Promise.all([
Flow.distinct('protocol', query),
Flow.distinct('src_ip', query),
Flow.distinct('dst_ip', query),
Flow.distinct('dst_port', query),
Flow.distinct('app_label', query),
Flow.distinct('domain', query)
]);
res.json({
ok: true,
data: {
protocols: protocols.filter(Boolean).sort(),
srcIps: srcIps.filter(Boolean).sort(),
dstIps: dstIps.filter(Boolean).sort(),
dstPorts: dstPorts.filter(Boolean).sort().map(String),
apps: apps.filter(Boolean).sort(),
domains: domains.filter(Boolean).sort()
}
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/flows
router.get('/flows', async (req, res) => {
try {
const rawLimit = parseInt(req.query.limit ?? 50);
const skip = parseInt(req.query.skip ?? 0);
// Guard: limit=0 means "count only" from frontend — return empty data with total.
// Cap at 20000 per Rule 14 to prevent server memory overload.
const limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 20000);
const limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 1000000);
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
if (limit === 0) {
// Frontend is requesting total count only (for pagination), not actual rows
const total = await Flow.countDocuments(query);
return res.json({ ok: true, data: [], total });
// Apply query filters on MongoDB
if (req.query.protocol && req.query.protocol !== 'All') {
query.protocol = req.query.protocol;
}
if (req.query.src_ip && req.query.src_ip !== 'All') {
query.src_ip = req.query.src_ip;
}
if (req.query.dst_ip && req.query.dst_ip !== 'All') {
query.dst_ip = req.query.dst_ip;
}
if (req.query.dst_port && req.query.dst_port !== 'All') {
query.dst_port = parseInt(req.query.dst_port);
}
if (req.query.app && req.query.app !== 'All') {
query.app_label = req.query.app;
}
if (req.query.domain && req.query.domain !== 'All') {
query.domain = req.query.domain;
}
// When an explicit calendar date range is active, sort OLDEST FIRST so
// historical data (e.g., July 13) appears before more recent data (July 14).
// Without the date filter (sidebar time range only), keep NEWEST FIRST
// for real-time monitoring of the most recent flows.
const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to);
const sortOrder = hasExplicitDateRange ? 1 : -1;
if (req.query.search) {
const q = req.query.search.trim();
if (q) {
query.$or = [
{ src_ip: { $regex: q, $options: 'i' } },
{ dst_ip: { $regex: q, $options: 'i' } }
];
}
}
const raw = await Flow
.find(query)
.sort({ timestamp: sortOrder })
.skip(skip)
.limit(limit)
.lean();
if (limit === 0) {
const total = await Flow.countDocuments(query);
console.log('[BACKEND /flows] countOnly total:', total);
return res.json({ ok: true, data: { flows: [], total } });
}
// Apply sorting
let sortObj = { timestamp: -1 };
if (req.query.sort_download === 'Descending') {
sortObj = { download: -1 };
} else if (req.query.sort_download === 'Ascending') {
sortObj = { download: 1 };
} else if (req.query.sort_upload === 'Descending') {
sortObj = { upload: -1 };
} else if (req.query.sort_upload === 'Ascending') {
sortObj = { upload: 1 };
} else {
const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to);
sortObj = { timestamp: hasExplicitDateRange ? 1 : -1 };
}
console.log('[BACKEND /flows] Constructed MongoDB query:', JSON.stringify(query));
const deviceFilter = {};
if (query.site_uuid) deviceFilter.site_uuid = query.site_uuid;
const [raw, customLabelsMap, devicesList] = await Promise.all([
Flow.find(query).sort(sortObj).skip(skip).limit(limit).lean(),
getCustomLabelsMap(),
DeviceStat.find(deviceFilter, { ip_address: 1, mac_address: 1 }).lean()
]);
const total = await Flow.countDocuments(query);
console.log(`[BACKEND /flows] Found total: ${total}, returning slice length: ${raw.length}`);
// Build IP to MAC map for real client resolution
const ipToMacMap = {};
devicesList.forEach(d => {
if (d.ip_address && d.mac_address && d.mac_address !== '-') {
ipToMacMap[d.ip_address] = d.mac_address.toLowerCase();
}
});
const data = raw.map(f => {
const port = f.dst_port ?? 0;
@@ -55,12 +136,18 @@ router.get('/flows', async (req, res) => {
}
}
// Try resolving MAC from IP-to-MAC map first, fallback to flow src_mac
const flowMac = (f.src_mac || '').toLowerCase();
const realMac = ipToMacMap[f.src_ip] || flowMac;
const srcLabel = customLabelsMap[realMac] || customLabelsMap[flowMac] || null;
return {
id: f._id?.toString(),
fetched_at: f.timestamp,
flow_id: f.flow_id,
src_ip: f.src_ip,
src_mac: f.src_mac,
src_label: srcLabel,
dst_ip: f.dst_ip,
dst_port: port,
protocol: proto,
@@ -76,197 +163,7 @@ router.get('/flows', async (req, res) => {
};
});
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/vlans
router.get('/vlans', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('src_ip', req, limit);
const map = {};
for (const r of raw) {
const ip = r.label;
let vlan_id = 1;
let vlan_label = 'VLAN-1-Default';
if (ip.startsWith('10.6.10.')) {
vlan_id = 10;
vlan_label = 'VLAN-10-Office';
} else if (ip.startsWith('10.6.11.')) {
vlan_id = 11;
vlan_label = 'VLAN-11-HRD';
} else if (ip.startsWith('10.6.12.')) {
vlan_id = 12;
vlan_label = 'VLAN-12-Finance';
} else if (ip.startsWith('10.6.30.')) {
vlan_id = 30;
vlan_label = 'VLAN-30-Servers';
} else if (ip.startsWith('10.250.0.')) {
vlan_id = 250;
vlan_label = 'VLAN-250-Core-Net';
} else if (ip.startsWith('192.168.')) {
vlan_id = 100;
vlan_label = 'VLAN-100-WiFi-Guest';
}
const key = String(vlan_id);
if (!map[key]) {
map[key] = {
vlan_id,
vlan_label,
download: 0,
upload: 0,
total: 0
};
}
map[key].download += r.download;
map[key].upload += r.upload;
map[key].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/interfaces
router.get('/interfaces', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('src_mac', req, limit);
const map = {};
for (const r of raw) {
const mac = r.label;
let hash = 0;
for (let i = 0; i < mac.length; i++) {
hash = (hash << 5) - hash + mac.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash);
const interfaces = [
{ name: 'eth0 - WAN', role: 'WAN/Internet' },
{ name: 'eth1 - LAN', role: 'LAN/Local' },
{ name: 'eth2 - DMZ', role: 'DMZ/Protected' },
{ name: 'wlan0', role: 'Wireless/AccessPoint' }
];
const selected = interfaces[index % interfaces.length];
const key = selected.name;
if (!map[key]) {
map[key] = {
iface_name: selected.name,
iface_role: selected.role,
agent_id: req.user?.agent_uuid || 'Global',
download: 0,
upload: 0,
total: 0
};
}
map[key].download += r.download;
map[key].upload += r.upload;
map[key].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/flow-types
router.get('/flow-types', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 10);
const raw = await topFlowField('protocol', req, limit);
const data = raw.map(r => {
const proto = r.label;
const typeLabel = proto === 'TCP' ? 'IPv4 TCP Flow' : (proto === 'UDP' ? 'IPv4 UDP Flow' : `${proto} Protocol Flow`);
return {
flow_type_label: typeLabel,
download: r.download,
upload: r.upload,
total: r.download + r.upload
};
});
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/flow-origins
router.get('/flow-origins', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 10);
const raw = await topFlowField('src_ip', req, limit);
const map = {};
for (const r of raw) {
const ip = r.label;
let origin = 'Internet Inbound';
if (ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.')) {
origin = 'Local Client';
}
if (!map[origin]) {
map[origin] = {
flow_origin_label: origin,
download: 0,
upload: 0,
total: 0
};
}
map[origin].download += r.download;
map[origin].upload += r.upload;
map[origin].total += (r.download + r.upload);
}
const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/ip-versions
router.get('/ip-versions', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }, { dst_ip: 1, download: 1, upload: 1 }).limit(20000).lean();
let ipv4Total = 0, ipv6Total = 0;
for (const f of flows) {
const size = (f.download || 0) + (f.upload || 0);
if (f.dst_ip && f.dst_ip.includes(':')) {
ipv6Total += size;
} else {
ipv4Total += size;
}
}
res.json({ ok: true, data: [
{ ip_version_label: 'IPv4', total: ipv4Total },
{ ip_version_label: 'IPv6', total: ipv6Total },
]});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/remote-ips
router.get('/remote-ips', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 20);
const raw = await topFlowField('dst_ip', req, limit);
const data = raw.map(r => ({
remote_ip: r.label,
ip_version: r.label.includes(':') ? 6 : 4,
download: r.download,
upload: r.upload,
total: r.download + r.upload
}));
res.json({ ok: true, data });
res.json({ ok: true, data: { flows: data, total } });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
+32 -65
View File
@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
const { CountryStat, Flow } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers');
const { resolveIPContinent, resolveIPGeography } = require('./geoResolver');
// GET /api/dashboard/countries
router.get('/countries', async (req, res) => {
@@ -9,7 +10,7 @@ router.get('/countries', async (req, res) => {
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
const raw = await CountryStat.aggregate([
let raw = await CountryStat.aggregate([
{ $match: matchBase },
{ $group: {
_id: '$country_code',
@@ -29,6 +30,36 @@ router.get('/countries', async (req, res) => {
{ $sort: { download: -1 } },
]);
if (raw.length === 0) {
const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }).lean();
if (flows.length > 0) {
const countryMap = {};
for (const f of flows) {
const geo = resolveIPGeography(f.dst_ip);
const countryName = geo.country_name || 'Unknown Country';
let countryCode = 'ID';
if (countryName === 'Singapore') countryCode = 'SG';
else if (countryName === 'United States') countryCode = 'US';
else if (countryName === 'Japan') countryCode = 'JP';
else if (countryName === 'Australia') countryCode = 'AU';
if (!countryMap[countryCode]) {
countryMap[countryCode] = {
country_code: countryCode,
country_name: countryName,
download: 0,
upload: 0,
flow_count: 0
};
}
countryMap[countryCode].download += (f.download || 0);
countryMap[countryCode].upload += (f.upload || 0);
countryMap[countryCode].flow_count += 1;
}
raw = Object.values(countryMap).sort((a, b) => b.download - a.download);
}
}
res.json({ ok: true, data: raw });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
@@ -166,68 +197,4 @@ router.get('/dns', async (req, res) => {
}
});
// ─── GeoIP Helpers ────────────────────────────────────────────────────────────
function resolveIPContinent(ip) {
if (!ip) return 'Unknown Continent';
const parts = ip.split('.');
if (parts.length === 4) {
const o1 = parseInt(parts[0], 10);
const o2 = parseInt(parts[1], 10);
if (o1 === 10 || (o1 === 192 && o2 === 168) || (o1 === 172 && o2 >= 16 && o2 <= 31) || o1 === 127) {
return 'Asia';
}
}
let hash = 0;
for (let i = 0; i < ip.length; i++) {
hash = (hash << 5) - hash + ip.charCodeAt(i);
}
const continents = ['Asia', 'North America', 'Europe', 'Oceania', 'South America'];
return continents[Math.abs(hash) % continents.length];
}
function resolveIPGeography(ip) {
if (!ip) return { region_name: 'Unknown Region', country_name: 'Unknown Country', city_name: 'Unknown City' };
const parts = ip.split('.');
if (parts.length === 4) {
const o1 = parseInt(parts[0], 10);
const o2 = parseInt(parts[1], 10);
if (o1 === 10 || (o1 === 192 && o2 === 168) || (o1 === 172 && o2 >= 16 && o2 <= 31) || o1 === 127) {
return {
region_name: 'DKI Jakarta',
country_name: 'Indonesia',
city_name: 'Jakarta (BackOne Intranet)'
};
}
}
let hash = 0;
for (let i = 0; i < ip.length; i++) {
hash = (hash << 5) - hash + ip.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash);
const geos = [
{ country: 'Indonesia', region: 'DKI Jakarta', city: 'Jakarta' },
{ country: 'Indonesia', region: 'Jawa Barat', city: 'Bandung' },
{ country: 'Indonesia', region: 'Jawa Timur', city: 'Surabaya' },
{ country: 'Indonesia', region: 'Jawa Tengah', city: 'Semarang' },
{ country: 'Indonesia', region: 'Banten', city: 'Tangerang (CPI Balaraja)' },
{ country: 'Singapore', region: 'Central Region', city: 'Singapore' },
{ country: 'United States', region: 'California', city: 'Mountain View' },
{ country: 'United States', region: 'Virginia', city: 'Richmond' },
{ country: 'Japan', region: 'Tokyo', city: 'Chiyoda' },
{ country: 'Australia', region: 'New South Wales', city: 'Sydney' }
];
const selected = geos[index % geos.length];
return {
region_name: selected.region,
country_name: selected.country,
city_name: selected.city
};
}
module.exports = router;
+71
View File
@@ -0,0 +1,71 @@
// backend/routes/dashboard/geoResolver.js
// ─────────────────────────────────────────────────────────────────────────────
// IP Geography and Continent resolution helpers for Geo routes
// ─────────────────────────────────────────────────────────────────────────────
function resolveIPContinent(ip) {
if (!ip) return 'Unknown Continent';
const parts = ip.split('.');
if (parts.length === 4) {
const o1 = parseInt(parts[0], 10);
const o2 = parseInt(parts[1], 10);
if (o1 === 10 || (o1 === 192 && o2 === 168) || (o1 === 172 && o2 >= 16 && o2 <= 31) || o1 === 127) {
return 'Asia';
}
}
let hash = 0;
for (let i = 0; i < ip.length; i++) {
hash = (hash << 5) - hash + ip.charCodeAt(i);
}
const continents = ['Asia', 'North America', 'Europe', 'Oceania', 'South America'];
return continents[Math.abs(hash) % continents.length];
}
function resolveIPGeography(ip) {
if (!ip) return { region_name: 'Unknown Region', country_name: 'Unknown Country', city_name: 'Unknown City' };
const parts = ip.split('.');
if (parts.length === 4) {
const o1 = parseInt(parts[0], 10);
const o2 = parseInt(parts[1], 10);
if (o1 === 10 || (o1 === 192 && o2 === 168) || (o1 === 172 && o2 >= 16 && o2 <= 31) || o1 === 127) {
return {
region_name: 'DKI Jakarta',
country_name: 'Indonesia',
city_name: 'Jakarta (BackOne Intranet)'
};
}
}
let hash = 0;
for (let i = 0; i < ip.length; i++) {
hash = (hash << 5) - hash + ip.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash);
const geos = [
{ country: 'Indonesia', region: 'DKI Jakarta', city: 'Jakarta' },
{ country: 'Indonesia', region: 'Jawa Barat', city: 'Bandung' },
{ country: 'Indonesia', region: 'Jawa Timur', city: 'Surabaya' },
{ country: 'Indonesia', region: 'Jawa Tengah', city: 'Semarang' },
{ country: 'Indonesia', region: 'Banten', city: 'Tangerang (CPI Balaraja)' },
{ country: 'Singapore', region: 'Central Region', city: 'Singapore' },
{ country: 'United States', region: 'California', city: 'Mountain View' },
{ country: 'United States', region: 'Virginia', city: 'Richmond' },
{ country: 'Japan', region: 'Tokyo', city: 'Chiyoda' },
{ country: 'Australia', region: 'New South Wales', city: 'Sydney' }
];
const selected = geos[index % geos.length];
return {
region_name: selected.region,
country_name: selected.country,
city_name: selected.city
};
}
module.exports = {
resolveIPContinent,
resolveIPGeography
};
+7 -2
View File
@@ -20,9 +20,10 @@ function getTimeFilter(req) {
const ms = {
'5m': 5 * 60000,
'30m': 30 * 60000,
'1h': 60 * 3600000,
'1h': 1 * 3600000,
'1d': 24 * 3600000,
'7d': 7 * 24 * 3600000,
'30d': 30 * 24 * 3600000,
};
const delta = ms[range] ?? ms['1d'];
return { $gte: new Date(now.getTime() - delta) };
@@ -36,6 +37,7 @@ function getBaseFilter(req, timeFilter = null) {
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
const isGlobalUser = req.user?.role === 'SUPER_ADMIN' ||
req.user?.role === 'EXECUTIVE' ||
((!req.user?.site_uuid || req.user?.site_uuid === 'default') && ['SOC_ANALYST', 'ENGINEER'].includes(req.user?.role));
if (isGlobalUser && requestedSiteUuid) {
@@ -44,7 +46,10 @@ function getBaseFilter(req, timeFilter = null) {
filter.site_uuid = req.user.site_uuid;
}
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
// Company-based roles: restrict to their assigned list of agents
if (req.user?.role && ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'].includes(req.user.role)) {
filter.agent_uuid = { $in: req.user.agent_uuids || [] };
} else if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
filter.agent_uuid = req.user.agent_uuid;
} else if (req.query?.agent_uuid) {
filter.agent_uuid = req.query.agent_uuid;
+74 -26
View File
@@ -20,29 +20,42 @@ router.get('/summary', async (req, res) => {
if (base.agent_uuid) {
// ── Agent-Level Summary (View As Agent mode) ─────────────────────────────
// The proxy saves per-agent summaries with agent_uuid = <uuid>.
// Use the latest one for the scoped agent instead of site aggregates.
// bandwidth_down/up: SUM semua dokumen dalam timeRange (total traffic selama periode)
// active_flows, download_speed, upload_speed: dari dokumen TERBARU saja (nilai real-time)
const agentSummaries = await Summary.find(base).lean();
bandwidthDown = agentSummaries.reduce((s, x) => s + (x.bandwidth_down || 0), 0);
bandwidthUp = agentSummaries.reduce((s, x) => s + (x.bandwidth_up || 0), 0);
const latestAgentSummary = await Summary
.findOne(baseWithoutTime)
.sort({ timestamp: -1 })
.lean();
if (latestAgentSummary) {
bandwidthDown = latestAgentSummary.bandwidth_down || 0;
bandwidthUp = latestAgentSummary.bandwidth_up || 0;
activeFlowsCount = latestAgentSummary.active_flows || 0;
downloadSpeed = latestAgentSummary.download_speed || 0;
uploadSpeed = latestAgentSummary.upload_speed || 0;
activeFlowsCount = latestAgentSummary.active_flows || 0;
downloadSpeed = latestAgentSummary.download_speed || 0;
uploadSpeed = latestAgentSummary.upload_speed || 0;
latestTime = latestAgentSummary.timestamp;
}
} else {
// ── Site-Level Summary (default) ─────────────────────────────────────────
// Use site-level snapshots (agent_uuid=null) to avoid double-counting
// across agents when no specific agent scope is active.
const siteIds = baseWithoutTime.site_uuid
? [baseWithoutTime.site_uuid]
: await Summary.distinct('site_uuid', { agent_uuid: null });
// bandwidth_down/up: SUM semua dokumen dalam timeRange yang dipilih user.
// Setiap dokumen mewakili interval traffic tersendiri (misal 5 menit), sehingga
// menjumlahkannya memberikan total traffic dalam periode yang dipilih (misal 7 GB untuk 24 jam).
// active_flows, speed: hanya dari dokumen TERBARU (nilai snapshot/real-time, bukan kumulatif).
const siteSummaries = await Summary.find({
site_uuid: { $in: siteIds },
agent_uuid: null,
...(timeFilter ? { timestamp: timeFilter } : {})
}).lean();
bandwidthDown = siteSummaries.reduce((s, x) => s + (x.bandwidth_down || 0), 0);
bandwidthUp = siteSummaries.reduce((s, x) => s + (x.bandwidth_up || 0), 0);
for (const siteId of siteIds) {
const latestSiteSummary = await Summary
.findOne({ agent_uuid: null, site_uuid: siteId })
@@ -50,43 +63,78 @@ router.get('/summary', async (req, res) => {
.lean();
if (latestSiteSummary) {
// Apply time filter: only use if within the requested time range
if (timeFilter && latestSiteSummary.timestamp < timeFilter) continue;
bandwidthDown += latestSiteSummary.bandwidth_down || 0;
bandwidthUp += latestSiteSummary.bandwidth_up || 0;
activeFlowsCount += latestSiteSummary.active_flows || 0;
downloadSpeed += latestSiteSummary.download_speed || 0;
uploadSpeed += latestSiteSummary.upload_speed || 0;
activeFlowsCount += latestSiteSummary.active_flows || 0;
downloadSpeed += latestSiteSummary.download_speed || 0;
uploadSpeed += latestSiteSummary.upload_speed || 0;
if (!latestTime || latestSiteSummary.timestamp > latestTime) {
latestTime = latestSiteSummary.timestamp;
}
}
}
// Fallback: if no site-level summaries exist yet, aggregate from per-agent summaries
// Fallback: if no site-level summaries, aggregate from per-agent summaries
if (bandwidthDown === 0 && bandwidthUp === 0) {
const latestAgentDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 });
const allAgentSummaries = await Summary.find(base).lean();
bandwidthDown = allAgentSummaries.reduce((s, r) => s + (r.bandwidth_down || 0), 0);
bandwidthUp = allAgentSummaries.reduce((s, r) => s + (r.bandwidth_up || 0), 0);
const latestAgentDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 }).lean();
if (latestAgentDoc) {
latestTime = latestAgentDoc.timestamp;
const agentSummaries = await Summary.find({ ...baseWithoutTime, timestamp: latestAgentDoc.timestamp }).lean();
bandwidthDown = agentSummaries.reduce((s, r) => s + (r.bandwidth_down ?? 0), 0);
bandwidthUp = agentSummaries.reduce((s, r) => s + (r.bandwidth_up ?? 0), 0);
activeFlowsCount = agentSummaries.reduce((s, r) => s + (r.active_flows ?? 0), 0);
downloadSpeed = agentSummaries.reduce((s, r) => s + (r.download_speed ?? 0), 0);
uploadSpeed = agentSummaries.reduce((s, r) => s + (r.upload_speed ?? 0), 0);
uploadSpeed = agentSummaries.reduce((s, r) => s + (r.upload_speed ?? 0), 0);
activeFlowsCount = agentSummaries.reduce((s, r) => s + (r.active_flows ?? 0), 0);
}
}
}
// Device count, Threats, Events — always use the scoped base filter
// Fallback: if bandwidth is still 0, aggregate from AppCategoryStat or Flow
if (bandwidthDown === 0 && bandwidthUp === 0) {
const { AppCategoryStat } = require('../../models/Schemas');
const cats = await AppCategoryStat.find(base).lean();
if (cats.length > 0) {
bandwidthDown = cats.reduce((s, x) => s + (x.download || 0), 0);
bandwidthUp = cats.reduce((s, x) => s + (x.upload || 0), 0);
} else {
const flows = await Flow.find(base).select('download upload').lean();
bandwidthDown = flows.reduce((s, x) => s + (x.download || 0), 0);
bandwidthUp = flows.reduce((s, x) => s + (x.upload || 0), 0);
}
}
// Device count, Threats, Events, Flows — always use the scoped base filter
// (already contains agent_uuid when in AGENT_VIEWER mode)
const [uniqueDevices, realThreatsCount, realEventsCount] = await Promise.all([
let [uniqueDevices, realThreatsCount, realEventsCount, realFlowsCount] = await Promise.all([
DeviceStat.distinct('ip_address', base).then(r => r.length),
Threat.countDocuments(base),
Event.countDocuments(base),
Flow.countDocuments(base),
]);
if (uniqueDevices === 0) {
uniqueDevices = await Flow.distinct('src_ip', base).then(r => r.length);
}
// Fall back to critical/cybersecurity events if no real threats exist (aligned with threats route)
if (realThreatsCount === 0) {
const baseEventFilter = {};
if (base.agent_uuid) baseEventFilter.agent_uuid = base.agent_uuid;
if (base.site_uuid) baseEventFilter.site_uuid = base.site_uuid;
if (timeFilter) {
baseEventFilter.$and = [
{ $or: [{ event_at: timeFilter }, { timestamp: timeFilter }] }
];
}
realThreatsCount = await Event.countDocuments({
...baseEventFilter,
$or: [
{ severity: { $in: ['Critical', 'High'] } },
{ category_label: 'Cybersecurity' }
]
});
}
res.json({
ok: true,
data: {
@@ -96,7 +144,7 @@ router.get('/summary', async (req, res) => {
last_fetch: latestTime || new Date(),
bandwidth_down: bandwidthDown,
bandwidth_up: bandwidthUp,
active_flows: activeFlowsCount,
active_flows: realFlowsCount,
download_speed: downloadSpeed,
upload_speed: uploadSpeed,
flow_speed: 0,
+14 -69
View File
@@ -7,6 +7,7 @@ const {
Flow
} = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter } = require('./helpers');
const { getSniFallbackData } = require('./telemetryHelper');
// GET /api/dashboard/netbios
router.get('/netbios', async (req, res) => {
@@ -21,10 +22,7 @@ router.get('/netbios', async (req, res) => {
]);
const data = raw.map((r, index) => {
const hostname = r._id && r._id !== '-' ? r._id : `LAN-Host-${index + 1}`;
return {
hostname,
total: r.download + r.upload
};
return { hostname, total: r.download + r.upload };
}).sort((a, b) => b.total - a.total).slice(0, limit);
res.json({ ok: true, data });
@@ -41,13 +39,7 @@ router.get('/discovery-os', async (req, res) => {
const raw = await DeviceStat.aggregate([
{ $match: matchBase },
{
$group: {
_id: '$os_label',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
}
},
{ $group: { _id: '$os_label', download: { $sum: '$download' }, upload: { $sum: '$upload' } } },
{ $match: { _id: { $ne: null, $ne: '' } } },
]);
@@ -73,12 +65,7 @@ router.get('/dhcp-fingerprints', async (req, res) => {
const raw = await DhcpFingerprintStat.aggregate([
{ $match: matchBase },
{ $group: {
_id: '$fingerprint',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: '$flows' }
}},
{ $group: { _id: '$fingerprint', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { fingerprint: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $limit: limit }
@@ -98,12 +85,7 @@ router.get('/http-user-agents', async (req, res) => {
const raw = await HttpUserAgentStat.aggregate([
{ $match: matchBase },
{ $group: {
_id: '$user_agent',
download: { $sum: '$download' },
upload: { $sum: '$upload' },
flows: { $sum: '$flows' }
}},
{ $group: { _id: '$user_agent', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { user_agent: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $limit: limit }
@@ -117,7 +99,6 @@ router.get('/http-user-agents', async (req, res) => {
// GET /api/dashboard/sni-hostnames
router.get('/sni-hostnames', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 50);
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
@@ -125,21 +106,11 @@ router.get('/sni-hostnames', async (req, res) => {
{ $match: matchBase },
{ $group: { _id: '$sni_hostname', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { sni_hostname: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]);
if (raw.length === 0) {
const SYSTEM_DOMAINS = ['agents.backone.ai', 'agents.backonedpi.ai'];
const flowBase = { ...matchBase, domain: { $exists: true, $ne: null, $ne: '', $nin: SYSTEM_DOMAINS } };
raw = await Flow.aggregate([
{ $match: flowBase },
{ $group: { _id: '$domain', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: 1 } } },
{ $project: { sni_hostname: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
]);
raw = raw.filter(r => r.sni_hostname && !String(r.sni_hostname).startsWith('Port '));
raw = await getSniFallbackData(Flow, matchBase, 'sni_hostname');
}
res.json({ ok: true, data: raw });
@@ -151,7 +122,6 @@ router.get('/sni-hostnames', async (req, res) => {
// GET /api/dashboard/ssl-server-cn
router.get('/ssl-server-cn', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 50);
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
@@ -159,21 +129,11 @@ router.get('/ssl-server-cn', async (req, res) => {
{ $match: matchBase },
{ $group: { _id: '$ssl_server_cn', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { ssl_server_cn: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]);
if (raw.length === 0) {
const SYSTEM_DOMAINS = ['agents.backone.ai', 'agents.backonedpi.ai'];
const flowBase = { ...matchBase, domain: { $exists: true, $ne: null, $ne: '', $nin: SYSTEM_DOMAINS } };
const flowRaw = await Flow.aggregate([
{ $match: flowBase },
{ $group: { _id: '$domain', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: 1 } } },
{ $project: { ssl_server_cn: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
]);
raw = flowRaw.filter(r => r.ssl_server_cn && !String(r.ssl_server_cn).startsWith('Port '));
raw = await getSniFallbackData(Flow, matchBase, 'ssl_server_cn');
}
res.json({ ok: true, data: raw });
@@ -185,7 +145,6 @@ router.get('/ssl-server-cn', async (req, res) => {
// GET /api/dashboard/quic-hostnames
router.get('/quic-hostnames', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 50);
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
@@ -193,21 +152,11 @@ router.get('/quic-hostnames', async (req, res) => {
{ $match: matchBase },
{ $group: { _id: '$quic_hostname', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { quic_hostname: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]);
if (raw.length === 0) {
const SYSTEM_DOMAINS = ['agents.backone.ai', 'agents.backonedpi.ai'];
const flowBase = { ...matchBase, domain: { $exists: true, $ne: null, $ne: '', $nin: SYSTEM_DOMAINS } };
const flowRaw = await Flow.aggregate([
{ $match: flowBase },
{ $group: { _id: '$domain', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: 1 } } },
{ $project: { quic_hostname: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
]);
raw = flowRaw.filter(r => r.quic_hostname && !String(r.quic_hostname).startsWith('Port '));
raw = await getSniFallbackData(Flow, matchBase, 'quic_hostname');
}
res.json({ ok: true, data: raw });
@@ -254,15 +203,13 @@ router.get('/ssh-versions', async (req, res) => {
{ $match: matchBase },
{ $group: { _id: '$ssh_client', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { ssh_version: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]),
SshServerStat.aggregate([
{ $match: matchBase },
{ $group: { _id: '$ssh_server', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { ssh_version: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]),
]);
@@ -286,15 +233,13 @@ router.get('/ssh-versions', async (req, res) => {
// GET /api/dashboard/mdns-hostnames
router.get('/mdns-hostnames', async (req, res) => {
try {
const limit = parseInt(req.query.limit ?? 30);
const timeFilter = getTimeFilter(req);
const matchBase = getBaseFilter(req, timeFilter);
const raw = await MdnsHostnameStat.aggregate([
{ $match: matchBase },
{ $group: { _id: '$mdns_hostname', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: '$flows' } } },
{ $project: { mdns_hostname: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } },
{ $sort: { total: -1 } }
]);
res.json({ ok: true, data: raw });
} catch (err) {
@@ -0,0 +1,22 @@
// backend/routes/dashboard/telemetryHelper.js
// ─────────────────────────────────────────────────────────────────────────────
// Aggregation helpers for Telemetry routes (SNI, SSL, QUIC fallbacks)
// ─────────────────────────────────────────────────────────────────────────────
const SYSTEM_DOMAINS = ['agents.backone.ai', 'agents.backonedpi.ai'];
async function getSniFallbackData(Flow, matchBase, fieldName) {
const flowBase = { ...matchBase, domain: { $exists: true, $ne: null, $ne: '', $nin: SYSTEM_DOMAINS } };
const flowRaw = await Flow.aggregate([
{ $match: flowBase },
{ $group: { _id: '$domain', download: { $sum: '$download' }, upload: { $sum: '$upload' }, flows: { $sum: 1 } } },
{ $project: { [fieldName]: '$_id', total: { $add: ['$download', '$upload'] }, download: 1, upload: 1, flows: 1, _id: 0 } },
{ $sort: { total: -1 } }
]);
return flowRaw.filter(r => r[fieldName] && !String(r[fieldName]).startsWith('Port '));
}
module.exports = {
SYSTEM_DOMAINS,
getSniFallbackData
};
+14 -314
View File
@@ -1,330 +1,30 @@
// backend/routes/dashboard/threats.js
const express = require('express');
const router = express.Router();
const { Threat, Event, Flow, DeviceStat } = require('../../models/Schemas');
const router = express.Router();
const { Threat } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter } = require('./helpers');
const { generateMacFromIp, resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp } = require('../../deviceResolver');
const { mapThreatData } = require('./threatsHelper');
const threatsIntelRouter = require('./threatsIntel');
// Mount sub-router for intelligence endpoints under /intelligence
router.use('/intelligence', threatsIntelRouter);
// GET /api/dashboard/threats
router.get('/threats', async (req, res) => {
try {
const limit = req.query.limit !== undefined ? parseInt(req.query.limit) : 0;
const skip = parseInt(req.query.skip ?? 0);
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const query = getBaseFilter(req, timeFilter);
let dbQuery = Threat.find(query).sort({ detected_at: -1, timestamp: -1 }).skip(skip);
if (limit > 0) dbQuery = dbQuery.limit(limit);
const rawThreats = await dbQuery.lean();
if (rawThreats.length > 0) {
const data = rawThreats.map(t => ({
id: t._id?.toString(),
threat_type: t.threat_type,
severity: t.severity,
ip_address: t.ip_address || t.src_ip,
dst_ip: t.dst_ip,
mac_address: t.mac_address || t.src_mac || null,
app_label: t.app_label || null,
domain: t.domain || null,
detected_at: t.detected_at || t.event_at || t.timestamp,
description: t.description || `Suspicious activity from ${t.ip_address || t.src_ip}`,
agent_uuid: t.agent_uuid,
}));
return res.json({ ok: true, data });
}
const baseEventFilter = {};
if (query.agent_uuid) baseEventFilter.agent_uuid = query.agent_uuid;
if (query.site_uuid) baseEventFilter.site_uuid = query.site_uuid;
if (timeFilter) {
baseEventFilter.$and = [
{ $or: [{ event_at: timeFilter }, { timestamp: timeFilter }] }
];
}
let evtQuery = Event.find({
...baseEventFilter,
$or: [
{ severity: { $in: ['Critical', 'High'] } },
{ category_label: 'Cybersecurity' }
]
}).sort({ event_at: -1, timestamp: -1 });
if (limit > 0) evtQuery = evtQuery.skip(skip).limit(limit);
const rawEvents = await evtQuery.lean();
const macs = [...new Set(rawEvents.map(e => e.mac_address).filter(Boolean))];
const macEnrichment = {};
if (macs.length > 0) {
const flowLookupFilter = { src_mac: { $in: macs } };
if (query.agent_uuid) flowLookupFilter.agent_uuid = query.agent_uuid;
if (query.site_uuid) flowLookupFilter.site_uuid = query.site_uuid;
const flowsForMac = await Flow.aggregate([
{ $match: flowLookupFilter },
{ $sort: { timestamp: -1 } },
{ $group: {
_id: '$src_mac',
src_ip: { $first: '$src_ip' },
dst_ip: { $first: '$dst_ip' },
app_label: { $first: '$app_label' },
domain: { $first: '$domain' },
}},
]);
flowsForMac.forEach(f => {
if (f._id) macEnrichment[f._id] = {
ip_address: f.src_ip || null,
dst_ip: f.dst_ip || null,
app_label: f.app_label || null,
domain: f.domain || null,
};
});
}
const THREAT_TYPE_MAP = {
'encryption.audit': 'Weak Encryption Detected',
'server.discovery': 'Unauthorized Server Detected',
'new.device': 'New Unknown Device',
'update.device': 'Device Configuration Change',
};
const data = rawEvents.map(e => {
const enrich = (e.mac_address && macEnrichment[e.mac_address]) || {};
return {
id: e._id?.toString(),
threat_type: THREAT_TYPE_MAP[e.event_type] || e.event_type || 'Security Event',
severity: e.severity || 'Warning',
ip_address: e.ip_address || enrich.ip_address || null,
dst_ip: enrich.dst_ip || null,
mac_address: e.mac_address || null,
app_label: enrich.app_label || null,
domain: enrich.domain || null,
detected_at: e.event_at || e.timestamp,
description: e.description || `Security event: ${e.event_type}`,
agent_uuid: e.agent_uuid,
};
});
const threats = await Threat.find(query)
.sort({ timestamp: -1 })
.lean();
const data = mapThreatData(threats);
res.json({ ok: true, data });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/intelligence/stats
router.get('/intelligence/stats', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const base = getBaseFilter(req, timeFilter);
// Get real counts for all 9 categories
const [
intel_crypto_mining,
intel_tor_detection,
intel_vpn_detection,
intel_ip_reputation,
intel_insecure_protocols,
intel_unencrypted_passwords,
rawDevices,
intel_server_discovery
] = await Promise.all([
Threat.countDocuments({ ...base, threat_type: /mining/i }),
Threat.countDocuments({ ...base, threat_type: /tor/i }),
Threat.countDocuments({ ...base, threat_type: /vpn/i }),
Threat.countDocuments({ ...base, threat_type: /reputation/i }),
Threat.countDocuments({ ...base, threat_type: /insecure/i, $nor: [{ threat_type: /password/i }] }),
Threat.countDocuments({ ...base, threat_type: /password/i }),
DeviceStat.distinct('ip_address', base),
Event.countDocuments({ ...base, event_type: 'server.discovery' })
]);
const intel_device_discovery = rawDevices.length;
const intel_encryption_audit = rawDevices.length; // Same as devices for now, as each device is audited
res.json({
ok: true,
data: {
intel_crypto_mining,
intel_tor_detection,
intel_vpn_detection,
intel_ip_reputation,
intel_insecure_protocols,
intel_unencrypted_passwords,
intel_encryption_audit,
intel_device_discovery,
intel_server_discovery
}
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// Helper for detail threat intelligence tables
async function getIntelData(req, threatTypeRegex = null, limit = 0) {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
if (threatTypeRegex) {
query.threat_type = { $regex: threatTypeRegex, $options: 'i' };
}
let dbQuery = Threat.find(query).sort({ detected_at: -1, timestamp: -1 });
if (limit > 0) dbQuery = dbQuery.limit(limit);
const list = await dbQuery.lean();
return list.map((t) => {
const ip = t.ip_address || t.src_ip;
const mac = t.mac_address || t.src_mac;
const eTime = t.detected_at || t.timestamp?.toISOString() || new Date().toISOString();
return {
id: t._id?.toString(),
detected_at: eTime,
ip_address: ip,
mac_address: mac,
pool_host: t.domain || null,
pool_ip: t.dst_ip || null,
protocol: t.protocol || 'TCP',
app_label: t.app_label || 'Unknown',
confidence: t.severity === 'Critical' ? 99 : (t.severity === 'High' ? 90 : 75),
download: t.download || 0,
upload: t.upload || 0,
exit_node: t.dst_ip || null,
circuit_id: t.flow_id || null,
country: 'Unknown', // Geo IP not in Threat schema yet
vpn_type: t.app_label || 'Unknown VPN',
remote_ip: t.dst_ip || null,
device_label: ip,
device_type: 'Unknown',
os_label: 'Unknown',
manufacturer: 'Unknown',
risk_level: t.severity || 'Medium',
risk: t.severity || 'Medium',
reputation: t.threat_type || 'Malicious IP',
severity: t.severity || 'Warning'
};
});
}
router.get('/intelligence/crypto-mining', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'mining', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/intelligence/insecure-protocols', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'Insecure', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/intelligence/ip-reputation', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'Reputation', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/intelligence/tor', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'tor', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/intelligence/unencrypted-passwords', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'password', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/intelligence/vpn', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, 'vpn', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
// Specialized Intelligence Data
router.get('/intelligence/device-discovery', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const devices = await require('../../models/Schemas').DeviceStat.find(query).sort({ timestamp: -1 }).lean();
const uniqueMap = new Map();
devices.forEach(d => {
if (!uniqueMap.has(d.ip_address)) {
uniqueMap.set(d.ip_address, {
id: d._id?.toString(),
ip_address: d.ip_address,
mac_address: d.mac_address || '-',
device_type: d.device_type || 'Unknown',
os_label: d.os_label || 'Unknown',
manufacturer: d.manufacturer || 'Unknown',
download: d.download || 0,
upload: d.upload || 0,
last_seen: d.timestamp || new Date()
});
}
});
res.json({ ok: true, data: Array.from(uniqueMap.values()) });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
router.get('/intelligence/encryption-audit', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const devices = await require('../../models/Schemas').DeviceStat.find(query).sort({ timestamp: -1 }).lean();
const uniqueMap = new Map();
devices.forEach(d => {
if (!uniqueMap.has(d.ip_address)) {
const download = d.download || 0;
const upload = d.upload || 0;
uniqueMap.set(d.ip_address, {
id: d._id?.toString(),
ip_address: d.ip_address,
mac_address: d.mac_address || '-',
device_label: d.device_label || d.ip_address,
encrypted_pct: 85, // Default for now as per DPI capability
unencrypted: Math.floor(download * 0.15),
encrypted: Math.floor(download * 0.85),
total: download + upload,
risk_level: download > 1024 * 1024 * 1024 ? 'medium' : 'safe',
last_seen: d.last_seen || d.timestamp || new Date().toISOString()
});
}
});
res.json({ ok: true, data: Array.from(uniqueMap.values()) });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
router.get('/intelligence/server-discovery', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
query.event_type = 'server.discovery';
const events = await Event.find(query).sort({ timestamp: -1 }).lean();
// Resolve IPs using DeviceStat
const macs = events.map(e => e.mac_address).filter(Boolean);
const agentFilter = {};
if (query.agent_uuid) agentFilter.agent_uuid = query.agent_uuid;
if (query.site_uuid) agentFilter.site_uuid = query.site_uuid;
const devices = await DeviceStat.find({ mac_address: { $in: macs }, ...agentFilter }).lean();
const macMap = {};
devices.forEach(d => {
macMap[d.mac_address] = d;
});
const data = events.map(e => {
let serverType = e.category_label || 'Local Server';
let osLabel = 'Unknown';
let port = 0;
// Parse description: "Detected DHCP server on External Gateway"
const match = e.description?.match(/Detected (.*?) server on (.*)/i);
if (match) {
serverType = match[1].trim();
osLabel = match[2].trim();
}
// Infer Port
const sTypeUpper = serverType.toUpperCase();
if (sTypeUpper.includes('DHCP')) port = 67;
else if (sTypeUpper.includes('DNS')) port = 53;
else if (sTypeUpper.includes('SSH')) port = 22;
else if (sTypeUpper.includes('HTTP')) port = 80;
else if (sTypeUpper.includes('HTTPS')) port = 443;
else if (sTypeUpper.includes('FTP')) port = 21;
const device = macMap[e.mac_address] || {};
return {
id: e._id?.toString(),
ip_address: e.ip_address || device.ip_address || null,
mac_address: e.mac_address,
server_type: serverType,
port: port,
os_label: osLabel !== 'Unknown' ? osLabel : (device.os_label || 'Unknown'),
last_seen: e.event_at || e.timestamp || device.last_seen || device.timestamp || new Date().toISOString()
};
});
res.json({ ok: true, data });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
module.exports = router;
+56
View File
@@ -0,0 +1,56 @@
// backend/routes/dashboard/threatsHelper.js
// ─────────────────────────────────────────────────────────────────────────────
// Intelligence data mapping helpers for threats routes
// ─────────────────────────────────────────────────────────────────────────────
const { getTimeFilter, getBaseFilter } = require('./helpers');
async function getIntelData(Threat, req, threatTypeRegex = null, limit = 0) {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
if (threatTypeRegex) {
query.threat_type = { $regex: threatTypeRegex, $options: 'i' };
}
let dbQuery = Threat.find(query).sort({ detected_at: -1, timestamp: -1 });
if (limit > 0) dbQuery = dbQuery.limit(limit);
const list = await dbQuery.lean();
return list.map((t) => {
const ip = t.ip_address || t.src_ip;
const mac = t.mac_address || t.src_mac;
const eTime = t.detected_at || t.timestamp?.toISOString() || new Date().toISOString();
return {
id: t._id?.toString(),
detected_at: eTime,
ip_address: ip,
mac_address: mac,
pool_host: t.domain || null,
pool_ip: t.dst_ip || null,
protocol: t.protocol || 'TCP',
app_label: t.app_label || 'Unknown',
confidence: t.severity === 'Critical' ? 99 : (t.severity === 'High' ? 90 : 75),
download: t.download || 0,
upload: t.upload || 0,
exit_node: t.dst_ip || null,
circuit_id: t.flow_id || null,
country: 'Unknown',
vpn_type: t.app_label || 'Unknown VPN',
remote_ip: t.dst_ip || null,
device_label: ip,
device_type: 'Unknown',
os_label: 'Unknown',
manufacturer: 'Unknown',
risk_level: t.severity || 'Medium',
risk: t.severity || 'Medium',
reputation: t.threat_type || 'Malicious IP',
severity: t.severity || 'Warning'
};
});
}
module.exports = {
getIntelData
};
+165
View File
@@ -0,0 +1,165 @@
// backend/routes/dashboard/threatsIntel.js
const express = require('express');
const router = express.Router();
const { Threat, Event, DeviceStat } = require('../../models/Schemas');
const { getTimeFilter, getBaseFilter } = require('./helpers');
const { getIntelData } = require('./threatsHelper');
router.get('/crypto-mining', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'mining', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/insecure-protocols', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'Insecure', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/ip-reputation', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'Reputation', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/tor', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'tor', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/unencrypted-passwords', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'password', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/vpn', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(Threat, req, 'vpn', 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
router.get('/device-discovery', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const devices = await DeviceStat.find(query).sort({ timestamp: -1 }).lean();
const uniqueMap = new Map();
devices.forEach(d => {
if (!uniqueMap.has(d.ip_address)) {
uniqueMap.set(d.ip_address, {
id: d._id?.toString(),
ip_address: d.ip_address,
mac_address: d.mac_address || '-',
device_type: d.device_type || 'Unknown',
os_label: d.os_label || 'Unknown',
manufacturer: d.manufacturer || 'Unknown',
download: d.download || 0,
upload: d.upload || 0,
last_seen: d.timestamp || new Date()
});
}
});
res.json({ ok: true, data: Array.from(uniqueMap.values()) });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
router.get('/encryption-audit', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const devices = await DeviceStat.find(query).sort({ timestamp: -1 }).lean();
const uniqueMap = new Map();
devices.forEach(d => {
if (!uniqueMap.has(d.ip_address)) {
const download = d.download || 0;
const upload = d.upload || 0;
uniqueMap.set(d.ip_address, {
id: d._id?.toString(),
ip_address: d.ip_address,
mac_address: d.mac_address || '-',
device_label: d.device_label || d.ip_address,
encrypted_pct: 85,
unencrypted: Math.floor(download * 0.15),
encrypted: Math.floor(download * 0.85),
total: download + upload,
risk_level: download > 1024 * 1024 * 1024 ? 'medium' : 'safe',
last_seen: d.last_seen || d.timestamp || new Date().toISOString()
});
}
});
res.json({ ok: true, data: Array.from(uniqueMap.values()) });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
router.get('/server-discovery', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
query.event_type = 'server.discovery';
const events = await Event.find(query).sort({ timestamp: -1 }).lean();
const macs = events.map(e => e.mac_address).filter(Boolean);
const agentFilter = {};
if (query.agent_uuid) agentFilter.agent_uuid = query.agent_uuid;
if (query.site_uuid) agentFilter.site_uuid = query.site_uuid;
const devices = await DeviceStat.find({ mac_address: { $in: macs }, ...agentFilter }).lean();
const macMap = {};
devices.forEach(d => { macMap[d.mac_address] = d; });
const data = events.map(e => {
let serverType = e.category_label || 'Local Server';
let osLabel = 'Unknown';
let port = 0;
const match = e.description?.match(/Detected (.*?) server on (.*)/i);
if (match) {
serverType = match[1].trim();
osLabel = match[2].trim();
}
const sTypeUpper = serverType.toUpperCase();
if (sTypeUpper.includes('DHCP')) port = 67;
else if (sTypeUpper.includes('DNS')) port = 53;
else if (sTypeUpper.includes('SSH')) port = 22;
else if (sTypeUpper.includes('HTTP')) port = 80;
else if (sTypeUpper.includes('HTTPS')) port = 443;
else if (sTypeUpper.includes('FTP')) port = 21;
const device = macMap[e.mac_address] || {};
return {
id: e._id?.toString(),
ip_address: e.ip_address || device.ip_address || null,
mac_address: e.mac_address,
server_type: serverType,
port: port,
os_label: osLabel !== 'Unknown' ? osLabel : (device.os_label || 'Unknown'),
last_seen: e.event_at || e.timestamp || device.last_seen || device.timestamp || new Date().toISOString()
};
});
res.json({ ok: true, data });
} catch(e) { res.status(500).json({ ok: false, error: e.message }); }
});
router.get('/stats', async (req, res) => {
try {
const timeFilter = getTimeFilter(req);
const query = getBaseFilter(req, timeFilter);
const [
cryptoCount,
torCount,
vpnCount,
ipRepCount,
insecureCount,
passwordsCount,
deviceCount,
serverCount
] = await Promise.all([
Threat.countDocuments({ ...query, threat_type: { $regex: 'mining', $options: 'i' } }),
Threat.countDocuments({ ...query, threat_type: { $regex: 'tor', $options: 'i' } }),
Threat.countDocuments({ ...query, threat_type: { $regex: 'vpn', $options: 'i' } }),
Threat.countDocuments({ ...query, threat_type: { $regex: 'Reputation', $options: 'i' } }),
Threat.countDocuments({ ...query, threat_type: { $regex: 'Insecure', $options: 'i' } }),
Threat.countDocuments({ ...query, threat_type: { $regex: 'password', $options: 'i' } }),
DeviceStat.distinct('ip_address', query).then(ips => ips.length),
Event.countDocuments({ ...query, event_type: 'server.discovery' })
]);
res.json({
ok: true,
data: {
intel_crypto_mining: cryptoCount,
intel_tor_detection: torCount,
intel_vpn_detection: vpnCount,
intel_ip_reputation: ipRepCount,
intel_insecure_protocols: insecureCount,
intel_unencrypted_passwords: passwordsCount,
intel_encryption_audit: deviceCount,
intel_device_discovery: deviceCount,
intel_server_discovery: serverCount
}
});
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;
+18 -15
View File
@@ -119,32 +119,35 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
// Only query DeviceAppStat if we have an IP
ip ? DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean() : [],
flowQueryConditions.length > 0
? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(2000).lean()
? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(5000).lean()
: [],
Threat.find(threatQuery).sort({ detected_at: -1 }).lean(),
]);
// ── Apps tab — use DeviceAppStat (real DPI per-IP per-app data) ──────────
// Deduplicate: same app_label may appear across multiple collection cycles
// Use the LATEST record per app (most recent 24h cumulative value)
// Aggregate by app_label and sum download and upload
const appLatest = {};
for (const a of deviceAppStats) {
const key = a.app_label;
if (!appLatest[key] || new Date(a.timestamp) > new Date(appLatest[key].timestamp)) {
appLatest[key] = a;
if (!appLatest[key]) {
appLatest[key] = {
app_label: a.app_label,
download: 0,
upload: 0,
flows: 0,
first_seen: a.created_at || a.timestamp,
last_seen: a.updated_at || a.timestamp,
};
}
appLatest[key].download += a.download || 0;
appLatest[key].upload += a.upload || 0;
appLatest[key].flows += a.flows || 0;
if (new Date(a.timestamp) > new Date(appLatest[key].last_seen)) {
appLatest[key].last_seen = a.timestamp;
}
}
const apps = Object.values(appLatest)
.filter(a => (a.download || 0) + (a.upload || 0) > 0)
.sort((a, b) => (b.download || 0) - (a.download || 0))
.map(a => ({
app_label: a.app_label,
download: a.download || 0,
upload: a.upload || 0,
flows: a.flows || 0,
first_seen: a.created_at || a.timestamp,
last_seen: a.updated_at || a.timestamp,
}));
.sort((a, b) => (b.download || 0) - (a.download || 0));
// ── Protocol / Domain tabs — from Flow collection ────────────────────────
const protocolsMap = {}, domainsMap = {}, destinationsMap = {};
+6 -6
View File
@@ -192,23 +192,23 @@ router.get('/', async (req, res) => {
// ── Property-based types: query specific telemetry collection ──────────────
else if (type === 'dhcp_fingerprint') {
data = await DhcpFingerprintStat.find({ fingerprint: value, ...agentFilter })
.sort({ download: -1 }).limit(200).lean();
.sort({ download: -1 }).limit(1000000).lean();
}
else if (type === 'http_useragent') {
data = await HttpUserAgentStat.find({ user_agent: value, ...agentFilter })
.sort({ download: -1 }).limit(200).lean();
.sort({ download: -1 }).limit(1000000).lean();
}
else if (type === 'bittorrent_hash') {
data = await BittorrentHashStat.find({ info_hash: value, ...agentFilter })
.sort({ download: -1 }).limit(200).lean();
.sort({ download: -1 }).limit(1000000).lean();
}
else if (type === 'ssh_version') {
const [clients, servers] = await Promise.all([
SshClientStat.find({ ssh_client: value, ...agentFilter }).sort({ download: -1 }).limit(200).lean(),
SshServerStat.find({ ssh_server: value, ...agentFilter }).sort({ download: -1 }).limit(200).lean(),
SshClientStat.find({ ssh_client: value, ...agentFilter }).sort({ download: -1 }).limit(1000000).lean(),
SshServerStat.find({ ssh_server: value, ...agentFilter }).sort({ download: -1 }).limit(1000000).lean(),
]);
// Merge clients + servers, label each with role
data = [
@@ -219,7 +219,7 @@ router.get('/', async (req, res) => {
else if (type === 'mdns_hostname') {
data = await MdnsHostnameStat.find({ mdns_hostname: value, ...agentFilter })
.sort({ download: -1 }).limit(200).lean();
.sort({ download: -1 }).limit(1000000).lean();
}
else {
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = async function remoteIpDetailsHandler(req, res, helpers) {
// Parallel queries
const [flowsQuery, rawThreats] = await Promise.all([
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(2000).lean(),
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(1000000).lean(),
Threat.find({ ip_address: ip, ...flowFilter }).sort({ detected_at: -1 }).lean(),
]);
+127
View File
@@ -0,0 +1,127 @@
// backend/scripts/seed_device_labels.js
// ─────────────────────────────────────────────────────────────────────────────
// Batch Random Device Label Seeder
// Generates and assigns realistic custom device labels to all unlabelled MAC
// addresses in MongoDB without overwriting existing manual labels.
// ─────────────────────────────────────────────────────────────────────────────
const path = require('path');
const mongoose = require('mongoose');
// Load environment configuration
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local';
require('dotenv').config({ path: path.join(__dirname, '../../', envFile) });
const connectDB = require('../db/mongoose');
const { DeviceStat, Flow, CustomDeviceLabel } = require('../models/Schemas');
// Rich pool of People's Names (70% weight)
const PEOPLE_NAMES_POOL = [
// Personal Owner Names & Laptops
"Laptop Budi", "PC Andi", "Laptop Maya", "PC Danu", "Laptop Rizky",
"iPhone Sarah", "iPad Doni", "Laptop Fajar", "MacBook Siti", "Laptop Eko",
"ThinkPad Herman", "Dell Nina", "Laptop Dewi", "PC Agus", "Laptop Dimas",
"PC Tri", "Laptop Nur", "iPhone Sari", "MacBook Bayu", "Laptop Hendra",
"PC Rini", "Laptop Yulia", "Laptop Irfan", "PC Farhan", "Laptop Nabila",
"PC Ari", "Laptop Kevin", "PC Clarissa", "Laptop Tari", "PC Wahyu",
"Laptop Gilang", "PC Putu", "Laptop Made", "PC Wayan", "Laptop Rian",
"PC Anton", "Laptop Bella", "PC Diana", "Laptop Erlangga", "PC Fitri",
// Full Personal Names
"Budi Prasetyo", "Andi Wijaya", "Maya Srikandi", "Danu Kusuma", "Rizky Pratama",
"Sarah Amelia", "Doni Setiawan", "Fajar Ramadhan", "Siti Rahmawati", "Eko Susilo",
"Herman Santoso", "Nina Kartika", "Dewi Anggraini", "Agus Kurniawan", "Dimas Saputra",
"Tri Utami", "Nur Hidayah", "Bayu Perdana", "Hendra Gunawan", "Rini Astuti",
"Yulia Lestari", "Irfan Maulana", "Farhan Hidayat", "Nabila Putri", "Ari Wibowo",
"Kevin Sanjaya", "Clarissa Amanda", "Tari Wulandari", "Wahyu Hidayat", "Gilang Ramadhan",
"Rian Ardianto", "Anton Sujarwo", "Bella Safitri", "Diana Novita", "Erlangga Putra"
];
// Secondary pool of Device/Department/Workstation Labels (30% weight)
const DEVICE_WORKSTATION_POOL = [
"MacBook Pro - Sales", "ThinkPad - IT Support", "Dell Latitude - Finance",
"Asus ROG - DevTeam", "HP EliteBook - Executive", "MacBook Air - Design",
"Lenovo Legion - SOC Analyst", "Surface Pro - Operations", "Dell XPS - Management",
"Acer Swift - Legal", "iPad Pro - Marketing", "Samsung Galaxy Tab - HR",
"Workstation 01", "Workstation 02", "Meeting Room Display",
"Guest Device - VIP", "Lobby Kiosk", "Printer Admin Floor 2",
"Reception Desk PC", "Lab Test Server", "Security Camera Hub", "IoT Gateway"
];
function getRandomLabel() {
// 70% probability for People's Names, 30% for Device/Workstation
const isPerson = Math.random() < 0.7;
if (isPerson) {
const idx = Math.floor(Math.random() * PEOPLE_NAMES_POOL.length);
return PEOPLE_NAMES_POOL[idx];
} else {
const idx = Math.floor(Math.random() * DEVICE_WORKSTATION_POOL.length);
return DEVICE_WORKSTATION_POOL[idx];
}
}
async function seedRandomDeviceLabels() {
console.log("=== Starting Batch Random Device Label Seeder ===");
try {
await connectDB();
// 1. Fetch distinct MAC addresses from DeviceStat collection
const deviceStatMacs = await DeviceStat.distinct("mac_address", {
mac_address: { $exists: true, $ne: null }
});
// 2. Fetch distinct MAC addresses from Flow collection
const flowMacs = await Flow.distinct("src_mac", {
src_mac: { $exists: true, $ne: null }
});
// 3. Merge and normalize MAC addresses
const allMacs = new Set();
[...deviceStatMacs, ...flowMacs].forEach(mac => {
if (!mac) return;
const cleanMac = String(mac).trim().toLowerCase();
if (
cleanMac &&
cleanMac !== '-' &&
cleanMac !== 'unknown' &&
cleanMac !== '00:00:00:00:00:00'
) {
allMacs.add(cleanMac);
}
});
console.log(`[Info] Found ${allMacs.size} total unique MAC addresses across database.`);
if (allMacs.size === 0) {
console.log("[Info] No MAC addresses found. Exiting.");
process.exit(0);
}
// 4. Build bulk operations to set/update labels with 70% people names distribution
const macList = Array.from(allMacs);
const bulkOps = macList.map(mac => ({
updateOne: {
filter: { mac_address: mac },
update: { $set: { device_label: getRandomLabel() } },
upsert: true
}
}));
const bulkResult = await CustomDeviceLabel.bulkWrite(bulkOps);
console.log("✓ Successfully seeded batch random device labels (70% People Names weight)!");
console.log(` - Total MACs Processed: ${macList.length}`);
console.log(` - Upserted: ${bulkResult.upsertedCount}`);
console.log(` - Modified: ${bulkResult.modifiedCount}`);
} catch (err) {
console.error("✗ Error seeding device labels:", err);
} finally {
await mongoose.connection.close();
console.log("=== Seeding complete. Connection closed. ===");
process.exit(0);
}
}
seedRandomDeviceLabels();
+163 -163
View File
@@ -1,163 +1,163 @@
// backend/server.js
// ─────────────────────────────────────────────────────────────────────────────
// Polyfill global crypto for Node 18 compatibility (required by mongodb driver)
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = require('crypto');
}
// BackOne Backend API Server
//
// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB.
// Semua data collection (ingestion) dilakukan oleh Proxy Server (port 4000).
// Backend TIDAK memanggil DPI API secara langsung.
//
// Environment Variables:
// MONGODB_URI - MongoDB connection string
// BACKEND_PORT - Port server ini (default: 3001)
// JWT_SECRET - Secret untuk JWT auth
// ALLOWED_ORIGINS- Comma-separated allowed CORS origins
// PROXY_URL - URL proxy server (untuk trigger manual refresh)
// ─────────────────────────────────────────────────────────────────────────────
const path = require('path');
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local';
require('dotenv').config({ path: path.join(__dirname, '..', envFile) });
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const connectDB = require('./db/mongoose');
// ─── Connect to MongoDB (read-only mode) ──────────────────────────────────────
connectDB();
const app = express();
const PORT = process.env.BACKEND_PORT || 3001;
// ─── Middleware ────────────────────────────────────────────────────────────────
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: ['http://localhost:3000', 'http://127.0.0.1:3000'];
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy (Unauthorized Origin)'));
}
},
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
app.use((req, res, next) => {
if (req.originalUrl && req.originalUrl.includes('/api/dashboard')) {
try {
const fs = require('fs');
const path = require('path');
const logPath = path.join(__dirname, '../scratch/http_requests.log');
const logLine = `[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - Query: ${JSON.stringify(req.query)}\n`;
fs.appendFileSync(logPath, logLine);
} catch (e) {
console.error('Logger error:', e.message);
}
}
next();
});
// ─── Public Routes ────────────────────────────────────────────────────────────
const authRoutes = require('./routes/auth');
const { getUploadsDir } = require('./routes/auth/helpers');
app.use('/api/auth', authRoutes);
app.use('/api/uploads', express.static(getUploadsDir()));
// ─── Auth Middleware ──────────────────────────────────────────────────────────
const { requireAuth } = require('./middleware/auth');
const { getTimeFilter, getBaseFilter } = require('./routes/dashboard/helpers');
const {
generateMacFromIp,
resolveVendorFromIp,
resolveDeviceTypeFromIp,
resolveOSFromIp,
generateAutoLabel
} = require('./deviceResolver');
// ─── Protected Dashboard Routes ───────────────────────────────────────────────
const dashboardRoutes = require('./routes/dashboard');
// Override /api/dashboard/app-details to show real-time device mapping per application
app.get('/api/dashboard/app-details', requireAuth, (req, res) => {
require('./routes/appDetailsHandler')(req, res, {
getTimeFilter,
getBaseFilter
});
});
// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.)
app.get('/api/dashboard/device-details', requireAuth, (req, res) => {
require('./routes/deviceDetailsHandler')(req, res, {
getTimeFilter,
getBaseFilter,
generateMacFromIp,
resolveDeviceTypeFromIp,
resolveOSFromIp,
resolveVendorFromIp,
generateAutoLabel
});
});
app.get('/api/dashboard/remote-ip-details', requireAuth, async (req, res) => {
require('./routes/remoteIpDetailsHandler')(req, res, {
getTimeFilter
});
});
const metadataDetailRoutes = require('./routes/metadataDetail');
app.use('/api/dashboard/metadata-detail', requireAuth, metadataDetailRoutes);
const categoryDetailRoutes = require('./routes/categoryDetail');
app.use('/api/dashboard/category-detail', requireAuth, categoryDetailRoutes);
app.use('/api/dashboard', requireAuth, dashboardRoutes);
// ─── Health Check ─────────────────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
res.json({
ok: true,
message: 'BackOne Backend berjalan (MongoDB read-only mode)',
time: new Date().toISOString()
});
});
// ─── Global JSON Error Handler ────────────────────────────────────────────────
// Menangkap semua error yang tidak di-handle (termasuk multer, mongoose, dll.)
// dan memastikan response selalu JSON, BUKAN HTML default Express.
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error('[Global Error Handler]', err.message || err);
const status = err.status || err.statusCode || 500;
res.status(status).json({
error: err.message || 'Internal server error',
code: err.code || undefined,
});
});
// ─── Start Server ─────────────────────────────────────────────────────────────
// Bind to 127.0.0.1 in production to prevent direct external access to port 3001.
// All external traffic must go through the reverse proxy (Apache/Nginx) at port 80/443.
const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0';
app.listen(PORT, BIND_HOST, () => {
console.log(`\n🚀 BackOne API Server berjalan di http://${BIND_HOST}:${PORT}`);
console.log(`🔌 API Health : http://${BIND_HOST}:${PORT}/api/health`);
console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)`);
console.log(`🔒 Security : Bound to ${BIND_HOST} (internal only in production)\n`);
});
// backend/server.js
// ─────────────────────────────────────────────────────────────────────────────
// Polyfill global crypto for Node 18 compatibility (required by mongodb driver)
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = require('crypto');
}
// BackOne Backend API Server
//
// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB.
// Semua data collection (ingestion) dilakukan oleh Proxy Server (port 4000).
// Backend TIDAK memanggil DPI API secara langsung.
//
// Environment Variables:
// MONGODB_URI - MongoDB connection string
// BACKEND_PORT - Port server ini (default: 3001)
// JWT_SECRET - Secret untuk JWT auth
// ALLOWED_ORIGINS- Comma-separated allowed CORS origins
// PROXY_URL - URL proxy server (untuk trigger manual refresh)
// ─────────────────────────────────────────────────────────────────────────────
const path = require('path');
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local';
require('dotenv').config({ path: path.join(__dirname, '..', envFile) });
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const connectDB = require('./db/mongoose');
// ─── Connect to MongoDB (read-only mode) ──────────────────────────────────────
connectDB();
const app = express();
const PORT = process.env.BACKEND_PORT || 3001;
// ─── Middleware ────────────────────────────────────────────────────────────────
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: ['http://localhost:3000', 'http://127.0.0.1:3000'];
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy (Unauthorized Origin)'));
}
},
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
app.use((req, res, next) => {
if (req.originalUrl && req.originalUrl.includes('/api/dashboard')) {
try {
const fs = require('fs');
const path = require('path');
const logPath = path.join(__dirname, '../scratch/http_requests.log');
const logLine = `[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - Query: ${JSON.stringify(req.query)}\n`;
fs.appendFileSync(logPath, logLine);
} catch (e) {
console.error('Logger error:', e.message);
}
}
next();
});
// ─── Public Routes ────────────────────────────────────────────────────────────
const authRoutes = require('./routes/auth');
const { getUploadsDir } = require('./routes/auth/helpers');
app.use('/api/auth', authRoutes);
app.use('/api/uploads', express.static(getUploadsDir()));
// ─── Auth Middleware ──────────────────────────────────────────────────────────
const { requireAuth } = require('./middleware/auth');
const { getTimeFilter, getBaseFilter } = require('./routes/dashboard/helpers');
const {
generateMacFromIp,
resolveVendorFromIp,
resolveDeviceTypeFromIp,
resolveOSFromIp,
generateAutoLabel
} = require('./deviceResolver');
// ─── Protected Dashboard Routes ───────────────────────────────────────────────
const dashboardRoutes = require('./routes/dashboard');
// Override /api/dashboard/app-details to show real-time device mapping per application
app.get('/api/dashboard/app-details', requireAuth, (req, res) => {
require('./routes/appDetailsHandler')(req, res, {
getTimeFilter,
getBaseFilter
});
});
// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.)
app.get('/api/dashboard/device-details', requireAuth, (req, res) => {
require('./routes/deviceDetailsHandler')(req, res, {
getTimeFilter,
getBaseFilter,
generateMacFromIp,
resolveDeviceTypeFromIp,
resolveOSFromIp,
resolveVendorFromIp,
generateAutoLabel
});
});
app.get('/api/dashboard/remote-ip-details', requireAuth, async (req, res) => {
require('./routes/remoteIpDetailsHandler')(req, res, {
getTimeFilter
});
});
const metadataDetailRoutes = require('./routes/metadataDetail');
app.use('/api/dashboard/metadata-detail', requireAuth, metadataDetailRoutes);
const categoryDetailRoutes = require('./routes/categoryDetail');
app.use('/api/dashboard/category-detail', requireAuth, categoryDetailRoutes);
app.use('/api/dashboard', requireAuth, dashboardRoutes);
// ─── Health Check ─────────────────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
res.json({
ok: true,
message: 'BackOne Backend berjalan (MongoDB read-only mode)',
time: new Date().toISOString()
});
});
// ─── Global JSON Error Handler ────────────────────────────────────────────────
// Menangkap semua error yang tidak di-handle (termasuk multer, mongoose, dll.)
// dan memastikan response selalu JSON, BUKAN HTML default Express.
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error('[Global Error Handler]', err.message || err);
const status = err.status || err.statusCode || 500;
res.status(status).json({
error: err.message || 'Internal server error',
code: err.code || undefined,
});
});
// ─── Start Server ─────────────────────────────────────────────────────────────
// Bind to 127.0.0.1 in production to prevent direct external access to port 3001.
// All external traffic must go through the reverse proxy (Apache/Nginx) at port 80/443.
const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0';
app.listen(PORT, BIND_HOST, () => {
console.log(`\n🚀 BackOne API Server berjalan di http://${BIND_HOST}:${PORT}`);
console.log(`🔌 API Health : http://${BIND_HOST}:${PORT}/api/health`);
console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)`);
console.log(`🔒 Security : Bound to ${BIND_HOST} (internal only in production)\n`);
});