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

+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(),
]);