feat(source2): lock dark mode, sans-serif typography, and all recent updates
This commit is contained in:
1 parent
dd4c8f6876
commit
9f24b56e97
259 files changed
+15596
-8966
No files matched your search
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user