feat(site): implement dynamic site selection dropdown in sidebar and enable multi-site monitoring
This commit is contained in:
1 parent
be6bc14190
commit
4882108068
125 files changed
+5666
-4305
No files matched your search
@@ -29,7 +29,7 @@ module.exports = async function agentDetailsHandler(req, res, helpers) {
|
||||
Summary.findOne(baseQuery).sort({ timestamp: -1 }),
|
||||
DeviceStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(),
|
||||
Threat.find(baseQuery).sort({ detected_at: -1 }).lean(),
|
||||
Flow.find(baseQuery).sort({ timestamp: -1 }).lean(),
|
||||
Flow.find(baseQuery).sort({ timestamp: -1 }).limit(2000).lean(),
|
||||
AppStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(),
|
||||
Event.find(baseQuery).sort({ timestamp: -1 }).lean(),
|
||||
getCustomLabelsMap()
|
||||
|
||||
@@ -228,11 +228,8 @@ module.exports = async function appDetailsHandler(req, res, helpers) {
|
||||
// Enrich domain & protocol info from Flow if available
|
||||
const flows = await Flow.find({
|
||||
...baseFilter,
|
||||
$or: [
|
||||
{ app_label: label },
|
||||
{ domain: { $regex: label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' } }
|
||||
]
|
||||
}).sort({ timestamp: -1 }).lean();
|
||||
app_label: label
|
||||
}).sort({ timestamp: -1 }).limit(100).lean();
|
||||
|
||||
flows.forEach(f => {
|
||||
const ip = f.src_ip;
|
||||
|
||||
@@ -77,9 +77,17 @@ function requireAdmin(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function getUploadsDir() {
|
||||
if (fs.existsSync('/home/adminbackend/web/demoplace.my.id/public_html')) {
|
||||
return '/home/adminbackend/web/demoplace.my.id/public_html/api/uploads';
|
||||
} else {
|
||||
return path.join(__dirname, '..', '..', 'public', 'api', 'uploads');
|
||||
}
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const dir = path.join(__dirname, '..', '..', 'uploads');
|
||||
const dir = getUploadsDir();
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
@@ -96,5 +104,6 @@ module.exports = {
|
||||
setCookieToken,
|
||||
requireAuth,
|
||||
requireAdmin,
|
||||
upload
|
||||
upload,
|
||||
getUploadsDir
|
||||
};
|
||||
@@ -4,7 +4,7 @@ const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const User = require('../../models/User');
|
||||
const { requireAuth, makeToken, setCookieToken, upload } = require('./helpers');
|
||||
const { requireAuth, makeToken, setCookieToken, upload, getUploadsDir } = require('./helpers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -128,7 +128,7 @@ router.post('/remove-profile-picture', requireAuth, async (req, res) => {
|
||||
if (!user) return res.status(404).json({ error: 'User not found' });
|
||||
|
||||
if (user.profile_picture) {
|
||||
const filePath = path.join(__dirname, '..', '..', 'uploads', user.profile_picture);
|
||||
const filePath = path.join(getUploadsDir(), user.profile_picture);
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
|
||||
@@ -115,4 +115,42 @@ router.post('/admin/upload-agent-picture/:id', requireAdmin, blockAnalyst, uploa
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/admin/create-external-user — buat akun Eksternal (SOC Analyst, Engineer, dll)
|
||||
router.post('/admin/create-external-user', requireAdmin, blockAnalyst, upload.single('profile_picture'), async (req, res) => {
|
||||
try {
|
||||
const { username, password, account_name, role } = req.body;
|
||||
if (!username || !password || !role) {
|
||||
return res.status(400).json({ ok: false, error: 'Username, password, dan role wajib diisi' });
|
||||
}
|
||||
|
||||
// Validasi role (hanya boleh role tertentu, tidak boleh SUPER_ADMIN baru atau AGENT_VIEWER)
|
||||
const validRoles = ['SOC_ANALYST', 'ENGINEER', 'TENANT_ADMIN'];
|
||||
if (!validRoles.includes(role)) {
|
||||
return res.status(400).json({ ok: false, error: 'Role tidak valid untuk pembuatan akun eksternal' });
|
||||
}
|
||||
|
||||
const existing = await User.findOne({ username: username.trim() });
|
||||
if (existing) {
|
||||
return res.status(400).json({ ok: false, error: 'Username sudah digunakan' });
|
||||
}
|
||||
|
||||
const passwordHash = bcrypt.hashSync(password, 10);
|
||||
const siteUuid = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID || null;
|
||||
|
||||
const newUser = await User.create({
|
||||
username: username.trim(),
|
||||
password_hash: passwordHash,
|
||||
account_name: account_name?.trim() || null,
|
||||
role: role,
|
||||
site_uuid: siteUuid,
|
||||
profile_picture: req.file ? req.file.filename : null
|
||||
});
|
||||
|
||||
res.json({ ok: true, message: 'Akun eksternal berhasil dibuat', userId: newUser._id.toString() });
|
||||
} catch (err) {
|
||||
const msg = err.code === 11000 ? 'Username sudah digunakan' : err.message;
|
||||
res.status(400).json({ ok: false, error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,131 @@
|
||||
// backend/routes/categoryDetail.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Category Detail endpoint for the BackOne Network Intelligence page.
|
||||
// Returns the real MongoDB breakdown for a clicked category.
|
||||
// GET /api/dashboard/category-detail?category=<CategoryName>
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { DeviceAppStat, DeviceStat, LookupApp } = require('../models/Schemas');
|
||||
|
||||
// ─── Helper: build base filter from request user/time ──────────────────────────
|
||||
function buildBaseFilter(req) {
|
||||
const range = req.query.timeRange || 'all';
|
||||
const filter = {};
|
||||
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
filter.agent_uuid = req.user.agent_uuid;
|
||||
}
|
||||
if (range !== 'all') {
|
||||
const ms = { '5m': 300000, '30m': 1800000, '1h': 3600000, '1d': 86400000, '7d': 604800000 };
|
||||
const delta = ms[range];
|
||||
if (delta) filter.timestamp = { $gte: new Date(Date.now() - delta) };
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// ─── GET /api/dashboard/category-detail ───────────────────────────────────────
|
||||
router.get('/', async (req, res) => {
|
||||
const { category } = req.query;
|
||||
if (!category) return res.status(400).json({ ok: false, error: 'category is required' });
|
||||
|
||||
const base = buildBaseFilter(req);
|
||||
try {
|
||||
// Lookup all application labels that belong to this category
|
||||
const appsInCategory = await LookupApp.find({ 'application_category.label': category }).lean();
|
||||
const appLabels = appsInCategory.map(app => app.label);
|
||||
|
||||
if (appLabels.length === 0) {
|
||||
return res.json({ ok: true, category, apps: [], devices: [] });
|
||||
}
|
||||
|
||||
const filter = { ...base, app_label: { $in: appLabels } };
|
||||
|
||||
// 1. Get Top Apps for this category
|
||||
const topAppsData = await DeviceAppStat.aggregate([
|
||||
{ $match: filter },
|
||||
{ $group: {
|
||||
_id: '$app_label',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
flows: { $sum: '$flows' },
|
||||
agent_uuid: { $first: '$agent_uuid' },
|
||||
last_seen: { $max: '$timestamp' }
|
||||
}},
|
||||
{ $sort: { download: -1 } },
|
||||
{ $limit: 200 }
|
||||
]);
|
||||
|
||||
const apps = topAppsData.map(r => ({
|
||||
app_label: r._id,
|
||||
download: r.download,
|
||||
upload: r.upload,
|
||||
flows: r.flows,
|
||||
agent_uuid: r.agent_uuid,
|
||||
last_seen: r.last_seen
|
||||
}));
|
||||
|
||||
// 2. Get Top Devices for this category
|
||||
const topDevicesData = await DeviceAppStat.aggregate([
|
||||
{ $match: filter },
|
||||
{ $group: {
|
||||
_id: '$ip_address',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
flows: { $sum: '$flows' },
|
||||
agent_uuid: { $first: '$agent_uuid' },
|
||||
last_seen: { $max: '$timestamp' }
|
||||
}},
|
||||
{ $sort: { download: -1 } },
|
||||
{ $limit: 200 }
|
||||
]);
|
||||
|
||||
// Enrich devices with DeviceStat info (mac, os, manufacturer)
|
||||
const { generateMacFromIp, resolveVendorFromIp, resolveOSFromIp, generateAutoLabel } = require('../deviceResolver');
|
||||
const ips = topDevicesData.map(r => r._id).filter(Boolean);
|
||||
|
||||
const agentFilter = {};
|
||||
if (base.agent_uuid) agentFilter.agent_uuid = base.agent_uuid;
|
||||
if (base.site_uuid) agentFilter.site_uuid = base.site_uuid;
|
||||
|
||||
const devicesInfo = await DeviceStat.find({ ip_address: { $in: ips }, ...agentFilter }).lean();
|
||||
const deviceMap = {};
|
||||
for (const d of devicesInfo) deviceMap[d.ip_address] = d;
|
||||
|
||||
const devices = topDevicesData.map(r => {
|
||||
const ip = r._id;
|
||||
const d = deviceMap[ip];
|
||||
const mac = d?.mac_address && d.mac_address !== '-' ? d.mac_address : generateMacFromIp(ip);
|
||||
const manufacturer = d?.manufacturer && d.manufacturer !== '-' && d.manufacturer !== 'Unknown' ? d.manufacturer : resolveVendorFromIp(ip);
|
||||
const os = d?.os_label && d.os_label !== '-' && d.os_label !== 'Unknown' ? d.os_label : resolveOSFromIp(ip);
|
||||
const label = d?.device_label && d.device_label !== '-' && d.device_label !== ip ? d.device_label : generateAutoLabel(ip, mac, manufacturer, 'Workstation');
|
||||
|
||||
return {
|
||||
src_ip: ip,
|
||||
device_label: label,
|
||||
mac_address: mac,
|
||||
manufacturer: manufacturer,
|
||||
os_label: os,
|
||||
download: r.download,
|
||||
upload: r.upload,
|
||||
flows: r.flows,
|
||||
agent_uuid: r.agent_uuid,
|
||||
last_seen: r.last_seen
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
category,
|
||||
apps,
|
||||
devices
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[CategoryDetail] Error:`, error);
|
||||
res.status(500).json({ ok: false, error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { AppStat, ProtocolStat, AppCategoryStat } = require('../../models/Schemas');
|
||||
const { AppStat, ProtocolStat, AppCategoryStat, LookupApp } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter } = require('./helpers');
|
||||
|
||||
// GET /api/dashboard/apps
|
||||
@@ -102,16 +102,57 @@ router.get('/app-categories', async (req, res) => {
|
||||
// GET /api/dashboard/lookup/applications
|
||||
router.get('/lookup/applications', async (req, res) => {
|
||||
try {
|
||||
const q = String(req.query.q || '').trim();
|
||||
if (!q) return res.json({ ok: true, data: [] });
|
||||
const search = String(req.query.search || req.query.q || '').trim();
|
||||
const category = String(req.query.category || '').trim();
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const limit = Math.max(1, parseInt(req.query.limit) || 25);
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const baseFilter = getBaseFilter(req, null);
|
||||
const apps = await AppStat.distinct('app_label', {
|
||||
...baseFilter,
|
||||
app_label: { $regex: q, $options: 'i' }
|
||||
let filter = {};
|
||||
if (search) {
|
||||
filter.$or = [
|
||||
{ label: { $regex: search, $options: 'i' } },
|
||||
{ name: { $regex: search, $options: 'i' } },
|
||||
{ tag: { $regex: search, $options: 'i' } }
|
||||
];
|
||||
}
|
||||
|
||||
if (category) {
|
||||
filter['application_category.label'] = category;
|
||||
}
|
||||
|
||||
const [applications, total_records] = await Promise.all([
|
||||
LookupApp.find(filter).sort({ label: 1 }).skip(skip).limit(limit).lean(),
|
||||
LookupApp.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const total_pages = Math.ceil(total_records / limit) || 1;
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
applications,
|
||||
pagination: {
|
||||
total_records,
|
||||
total_pages,
|
||||
current_page: page,
|
||||
start: skip,
|
||||
length: applications.length,
|
||||
limit
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ ok: true, data: apps.map(name => ({ label: name, value: name })) });
|
||||
// GET /api/dashboard/lookup/categories
|
||||
router.get('/lookup/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await LookupApp.distinct('application_category.label');
|
||||
const validCategories = categories.filter(c => c).sort();
|
||||
res.json({ ok: true, data: validCategories });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
|
||||
@@ -12,16 +12,23 @@ router.get('/devices', async (req, res) => {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const query = getBaseFilter(req, timeFilter);
|
||||
|
||||
let dbQuery = DeviceStat.find(query).sort({ timestamp: -1, download: -1 }).skip(skip);
|
||||
if (limit > 0) dbQuery = dbQuery.limit(limit);
|
||||
const pipeline = [
|
||||
{ $match: query },
|
||||
{ $sort: { timestamp: -1 } },
|
||||
{ $group: { _id: "$ip_address", doc: { $first: "$$ROOT" } } },
|
||||
{ $replaceRoot: { newRoot: "$doc" } },
|
||||
{ $sort: { timestamp: -1, download: -1 } }
|
||||
];
|
||||
|
||||
if (skip > 0) pipeline.push({ $skip: skip });
|
||||
if (limit > 0) pipeline.push({ $limit: limit });
|
||||
|
||||
const [data, customLabelsMap] = await Promise.all([
|
||||
dbQuery,
|
||||
DeviceStat.aggregate(pipeline),
|
||||
getCustomLabelsMap()
|
||||
]);
|
||||
|
||||
const mapped = data.map(d => {
|
||||
const obj = d.toObject();
|
||||
const mapped = data.map(obj => {
|
||||
const ip = obj.ip_address;
|
||||
|
||||
const mac = obj.mac_address && obj.mac_address !== '-' ? obj.mac_address : generateMacFromIp(ip);
|
||||
@@ -125,7 +132,7 @@ router.get('/security-devices', async (req, res) => {
|
||||
const baseFilter = getBaseFilter(req, timeFilter);
|
||||
|
||||
const uniqueDevices = await DeviceStat.aggregate([
|
||||
{ $match: { site_uuid: baseFilter.site_uuid } },
|
||||
{ $match: baseFilter },
|
||||
{ $sort: { timestamp: -1 } },
|
||||
{ $group: {
|
||||
_id: '$ip_address',
|
||||
@@ -173,6 +180,7 @@ router.get('/security-devices', async (req, res) => {
|
||||
const label = baseLabel && baseLabel !== '-' && baseLabel !== 'Unknown' && baseLabel !== 'Generic Client'
|
||||
? baseLabel
|
||||
: generateAutoLabel(ip, mac, man, type);
|
||||
const lastSeen = obj.last_seen || obj.timestamp || new Date().toISOString();
|
||||
|
||||
const fStat = flowMap[ip] || { total: 0, encrypted: 0 };
|
||||
const encrypted = fStat.encrypted;
|
||||
@@ -197,7 +205,8 @@ router.get('/security-devices', async (req, res) => {
|
||||
unencrypted,
|
||||
encrypted_pct,
|
||||
risk_level,
|
||||
has_insecure: unencrypted > encrypted * 2
|
||||
has_insecure: unencrypted > encrypted * 2,
|
||||
timestamp: lastSeen
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Event } = require('../../models/Schemas');
|
||||
const { Event, DeviceStat, Flow } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter } = require('./helpers');
|
||||
|
||||
// GET /api/dashboard/events
|
||||
router.get('/events', async (req, res) => {
|
||||
try {
|
||||
console.log('[/events] Request received. Query:', req.query);
|
||||
const limit = parseInt(req.query.limit ?? 0);
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const base = getBaseFilter(req, timeFilter);
|
||||
console.log('[/events] Event base filter:', base);
|
||||
|
||||
let query = Event.find(base).sort({ timestamp: -1 });
|
||||
if (limit > 0) {
|
||||
@@ -17,12 +19,43 @@ router.get('/events', async (req, res) => {
|
||||
|
||||
const events = await query.lean();
|
||||
|
||||
// Collect all MAC addresses for events missing IP addresses
|
||||
const missingIpMacs = [...new Set(events.filter(e => !e.ip_address && e.mac_address).map(e => e.mac_address))];
|
||||
|
||||
// Lookup DeviceStat for these MACs
|
||||
let macToIpMap = {};
|
||||
if (missingIpMacs.length > 0) {
|
||||
const baseFilterNull = getBaseFilter(req, null);
|
||||
console.log('[/events] getBaseFilter(req, null) returned:', baseFilterNull);
|
||||
|
||||
const filterForDevices = {
|
||||
mac_address: { $in: missingIpMacs },
|
||||
...baseFilterNull
|
||||
};
|
||||
console.log('[/events] DEBUG filterForDevices:', filterForDevices);
|
||||
const devices = await DeviceStat.find(filterForDevices).lean();
|
||||
for (const d of devices) {
|
||||
macToIpMap[d.mac_address] = d.ip_address;
|
||||
}
|
||||
|
||||
// Fallback: Query Flow collection for remaining unresolved MACs
|
||||
const unresolvedMacs = missingIpMacs.filter(mac => !macToIpMap[mac]);
|
||||
if (unresolvedMacs.length > 0) {
|
||||
for (const mac of unresolvedMacs) {
|
||||
const flow = await Flow.findOne({ src_mac: mac, ...baseFilterNull }).sort({ timestamp: -1 }).lean();
|
||||
if (flow && flow.src_ip) {
|
||||
macToIpMap[mac] = flow.src_ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = events.map(e => ({
|
||||
id: e._id?.toString() || e.event_id,
|
||||
event_type: e.event_type,
|
||||
severity: e.severity,
|
||||
message: e.description || 'System event triggered',
|
||||
source_ip: e.ip_address || null,
|
||||
source_ip: e.ip_address || macToIpMap[e.mac_address] || null,
|
||||
mac_address: e.mac_address || null,
|
||||
timestamp: e.timestamp,
|
||||
}));
|
||||
|
||||
@@ -217,7 +217,7 @@ router.get('/ip-versions', async (req, res) => {
|
||||
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 }).lean();
|
||||
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);
|
||||
|
||||
@@ -134,24 +134,32 @@ router.get('/dns', async (req, res) => {
|
||||
const limit = req.query.limit !== undefined ? parseInt(req.query.limit) : 0;
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const matchBase = getBaseFilter(req, timeFilter);
|
||||
const { SniHostnameStat } = require('../../models/SchemasTelemetry');
|
||||
|
||||
const pipeline = [
|
||||
{ $match: { ...matchBase, domain: { $ne: null } } },
|
||||
{ $match: { ...matchBase, sni_hostname: { $ne: null } } },
|
||||
{ $group: {
|
||||
_id: '$domain',
|
||||
_id: '$sni_hostname',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
count: { $sum: 1 },
|
||||
app_label: { $last: '$app_label' },
|
||||
count: { $sum: '$flows' }
|
||||
}},
|
||||
{ $project: {
|
||||
domain: '$_id',
|
||||
query_count: '$count',
|
||||
download: 1,
|
||||
upload: 1,
|
||||
app_label: { $literal: null },
|
||||
category: { $literal: null },
|
||||
_id: 0
|
||||
}},
|
||||
{ $project: { domain: '$_id', query_count: '$count', download: 1, upload: 1, app_label: 1, category: { $literal: null }, _id: 0 } },
|
||||
{ $sort: { query_count: -1 } },
|
||||
];
|
||||
if (limit > 0) {
|
||||
pipeline.push({ $limit: limit });
|
||||
}
|
||||
|
||||
const data = await Flow.aggregate(pipeline);
|
||||
const data = await SniHostnameStat.aggregate(pipeline);
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
|
||||
@@ -18,7 +18,18 @@ function getTimeFilter(req) {
|
||||
function getBaseFilter(req, timeFilter = null) {
|
||||
const filter = {};
|
||||
if (timeFilter) filter.timestamp = timeFilter;
|
||||
if (req.user?.site_uuid) filter.site_uuid = req.user.site_uuid;
|
||||
|
||||
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
|
||||
console.log('[DEBUG] getBaseFilter headers:', Object.keys(req.headers), 'x-backone-site-uuid:', requestedSiteUuid, 'role:', req.user?.role);
|
||||
|
||||
const hasSwitcherRole = ['SUPER_ADMIN', 'SOC_ANALYST', 'ENGINEER'].includes(req.user?.role);
|
||||
|
||||
if (hasSwitcherRole && requestedSiteUuid) {
|
||||
filter.site_uuid = requestedSiteUuid;
|
||||
} else if (req.user?.site_uuid) {
|
||||
filter.site_uuid = req.user.site_uuid;
|
||||
}
|
||||
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
filter.agent_uuid = req.user.agent_uuid;
|
||||
} else if (req.query?.agent_uuid) {
|
||||
|
||||
@@ -24,6 +24,7 @@ router.get('/ssl-subject-alt-names', async (req, res) => {
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
flows: { $sum: '$flows' },
|
||||
timestamp: { $max: '$timestamp' },
|
||||
}},
|
||||
{ $project: {
|
||||
alt_name: '$_id',
|
||||
@@ -31,6 +32,7 @@ router.get('/ssl-subject-alt-names', async (req, res) => {
|
||||
upload: 1,
|
||||
flows: 1,
|
||||
total: { $add: ['$download', '$upload'] },
|
||||
timestamp: 1,
|
||||
_id: 0
|
||||
}},
|
||||
{ $sort: { total: -1 } },
|
||||
@@ -46,6 +48,7 @@ router.get('/ssl-subject-alt-names', async (req, res) => {
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
flows: { $sum: '$flows' },
|
||||
timestamp: { $max: '$timestamp' },
|
||||
}},
|
||||
{ $project: {
|
||||
alt_name: '$_id',
|
||||
@@ -53,6 +56,7 @@ router.get('/ssl-subject-alt-names', async (req, res) => {
|
||||
upload: 1,
|
||||
flows: 1,
|
||||
total: { $add: ['$download', '$upload'] },
|
||||
timestamp: 1,
|
||||
_id: 0
|
||||
}},
|
||||
{ $sort: { total: -1 } },
|
||||
|
||||
@@ -29,7 +29,7 @@ router.get('/summary', async (req, res) => {
|
||||
activeFlowsCount = summaries.reduce((s, r) => s + (r.active_flows ?? 0), 0);
|
||||
}
|
||||
|
||||
const [fallbackDevices, fallbackFlows, fallbackFlowBandwidth, realThreatsCount, realEventsCount] = await Promise.all([
|
||||
const [fallbackDevices, fallbackFlows, fallbackFlowBandwidth, realThreatsCount, realEventsCount, fallbackThreatsCount] = await Promise.all([
|
||||
DeviceStat.distinct('ip_address', base).then(r => r.length),
|
||||
Flow.countDocuments(base),
|
||||
Flow.aggregate([
|
||||
@@ -37,7 +37,14 @@ router.get('/summary', async (req, res) => {
|
||||
{ $group: { _id: null, down: { $sum: '$download' }, up: { $sum: '$upload' } } }
|
||||
]),
|
||||
Threat.countDocuments(base),
|
||||
Event.countDocuments(base)
|
||||
Event.countDocuments(base),
|
||||
Event.countDocuments({
|
||||
...base,
|
||||
$or: [
|
||||
{ severity: { $in: ['Critical', 'High'] } },
|
||||
{ category_label: 'Cybersecurity' }
|
||||
]
|
||||
})
|
||||
]);
|
||||
|
||||
const flowDown = fallbackFlowBandwidth[0]?.down || 0;
|
||||
@@ -45,7 +52,7 @@ router.get('/summary', async (req, res) => {
|
||||
|
||||
let finalDown = bandwidthDown > 0 ? bandwidthDown : flowDown;
|
||||
let finalUp = bandwidthUp > 0 ? bandwidthUp : flowUp;
|
||||
let finalDevices = totalDevicesCount > 0 ? totalDevicesCount : fallbackDevices;
|
||||
let finalDevices = fallbackDevices;
|
||||
let finalActiveFlows = activeFlowsCount > 0 ? activeFlowsCount : fallbackFlows;
|
||||
|
||||
const range = req.query.timeRange || '1d';
|
||||
@@ -67,7 +74,7 @@ router.get('/summary', async (req, res) => {
|
||||
ok: true,
|
||||
data: {
|
||||
total_devices: finalDevices,
|
||||
total_threats: realThreatsCount,
|
||||
total_threats: realThreatsCount > 0 ? realThreatsCount : fallbackThreatsCount,
|
||||
total_events: realEventsCount,
|
||||
last_fetch: latestTime || new Date(),
|
||||
bandwidth_down: finalDown,
|
||||
|
||||
@@ -126,7 +126,7 @@ router.get('/sni-hostnames', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
|
||||
if (raw.length === 0) {
|
||||
@@ -137,7 +137,7 @@ router.get('/sni-hostnames', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
raw = raw.filter(r => r.sni_hostname && !String(r.sni_hostname).startsWith('Port '));
|
||||
}
|
||||
@@ -160,7 +160,7 @@ router.get('/ssl-server-cn', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
|
||||
if (raw.length === 0) {
|
||||
@@ -171,7 +171,7 @@ router.get('/ssl-server-cn', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
raw = flowRaw.filter(r => r.ssl_server_cn && !String(r.ssl_server_cn).startsWith('Port '));
|
||||
}
|
||||
@@ -194,7 +194,7 @@ router.get('/quic-hostnames', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
|
||||
if (raw.length === 0) {
|
||||
@@ -205,7 +205,7 @@ router.get('/quic-hostnames', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
raw = flowRaw.filter(r => r.quic_hostname && !String(r.quic_hostname).startsWith('Port '));
|
||||
}
|
||||
@@ -255,14 +255,14 @@ router.get('/ssh-versions', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]),
|
||||
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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -294,7 +294,7 @@ router.get('/mdns-hostnames', async (req, res) => {
|
||||
{ $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 } },
|
||||
{ $limit: limit },
|
||||
|
||||
]);
|
||||
res.json({ ok: true, data: raw });
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Threat, Event, Flow } = require('../../models/Schemas');
|
||||
const { Threat, Event, Flow, DeviceStat } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter } = require('./helpers');
|
||||
const { generateMacFromIp, resolveDeviceTypeFromIp, resolveOSFromIp, resolveVendorFromIp } = require('../../deviceResolver');
|
||||
|
||||
@@ -118,25 +118,56 @@ router.get('/intelligence/stats', async (req, res) => {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const base = getBaseFilter(req, timeFilter);
|
||||
|
||||
const list = await Threat.find(base).lean();
|
||||
const total = list.length;
|
||||
const high = list.filter(t => (t.severity || 'medium').toLowerCase() === 'high' || (t.severity || 'medium').toLowerCase() === 'critical').length;
|
||||
const medium = list.filter(t => (t.severity || 'medium').toLowerCase() === 'medium' || (t.severity || 'medium').toLowerCase() === 'warning').length;
|
||||
const low = list.filter(t => (t.severity || 'medium').toLowerCase() === 'low' || (t.severity || 'medium').toLowerCase() === 'info').length;
|
||||
// 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' })
|
||||
]);
|
||||
|
||||
res.json({ ok: true, data: { total, high, medium, low } });
|
||||
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, threatTypeFilter = null, limit = 0) {
|
||||
async function getIntelData(req, threatTypeRegex = null, limit = 0) {
|
||||
const timeFilter = getTimeFilter(req);
|
||||
const query = getBaseFilter(req, timeFilter);
|
||||
|
||||
if (threatTypeFilter) {
|
||||
query.threat_type = { $regex: threatTypeFilter, $options: 'i' };
|
||||
if (threatTypeRegex) {
|
||||
query.threat_type = { $regex: threatTypeRegex, $options: 'i' };
|
||||
}
|
||||
|
||||
let dbQuery = Threat.find(query).sort({ detected_at: -1, timestamp: -1 });
|
||||
@@ -144,60 +175,153 @@ async function getIntelData(req, threatTypeFilter = null, limit = 0) {
|
||||
|
||||
const list = await dbQuery.lean();
|
||||
|
||||
return list.map((t, index) => {
|
||||
const ip = t.ip_address || t.src_ip || '10.6.10.44';
|
||||
const mac = t.mac_address || t.src_mac || generateMacFromIp(ip);
|
||||
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 || 'stratum.antpool.com',
|
||||
pool_ip: t.dst_ip || '172.217.194.100',
|
||||
pool_host: t.domain || null,
|
||||
pool_ip: t.dst_ip || null,
|
||||
protocol: t.protocol || 'TCP',
|
||||
app_label: t.app_label || 'Stratum Protocol',
|
||||
confidence: 95.5,
|
||||
download: t.download || 12450,
|
||||
upload: t.upload || 8450,
|
||||
exit_node: t.dst_ip || '185.220.101.5',
|
||||
circuit_id: 'circ_' + Math.abs(index * 1337),
|
||||
country: 'Germany',
|
||||
vpn_type: t.app_label?.includes('WireGuard') ? 'WireGuard' : 'OpenVPN',
|
||||
remote_ip: t.dst_ip || '198.51.100.44',
|
||||
device_label: t.ip_address || ip,
|
||||
device_type: resolveDeviceTypeFromIp(ip),
|
||||
os_label: resolveOSFromIp(ip),
|
||||
manufacturer: resolveVendorFromIp(ip),
|
||||
is_new: 1,
|
||||
encrypted_pct: 85.0,
|
||||
unencrypted: 150000,
|
||||
encrypted: 850000,
|
||||
total: 1000000,
|
||||
risk_level: 'Low',
|
||||
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',
|
||||
source: 'DPI Scanner',
|
||||
reputation: t.threat_type || 'Malicious IP',
|
||||
score: 8.5,
|
||||
local_ip: ip,
|
||||
blacklisted: 1,
|
||||
server_type: 'Database Server',
|
||||
hostname: t.domain || 'db-01.local',
|
||||
port: t.dst_port || 3306,
|
||||
username: 'admin_backone',
|
||||
severity: t.severity || 'Critical'
|
||||
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/device-discovery', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, null, 0) }); } catch(e){ res.status(500).json({ok:false,error:e.message}); } });
|
||||
router.get('/intelligence/encryption-audit', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, null, 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/server-discovery', async (req, res) => { try { res.json({ ok: true, data: await getIntelData(req, null, 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, 'Insecure Plaintext 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|VPN', 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 devices = await DeviceStat.find({ mac_address: { $in: macs } }).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;
|
||||
@@ -3,6 +3,26 @@ const router = express.Router();
|
||||
const { TlsVersionStat, TlsCipherStat, TlsSecurityStat } = require('../../models/Schemas');
|
||||
const { getTimeFilter, getBaseFilter } = require('./helpers');
|
||||
|
||||
function analyzeCipherSuite(cipher) {
|
||||
if (!cipher || cipher === '-' || cipher === 'Unknown') return { status: 'Unknown', description: 'Cipher suite information not available.' };
|
||||
|
||||
const c = cipher.toUpperCase();
|
||||
|
||||
if (c.includes('NULL') || c.includes('RC4') || c.includes('DES') || c.includes('MD5') || c.includes('EXP') || c.includes('ANON')) {
|
||||
return { status: 'Vulnerable', description: 'Uses obsolete and highly insecure cryptographic algorithms. Must be disabled immediately.' };
|
||||
}
|
||||
|
||||
if (c.includes('CBC') || c.includes('SHA1') || c.startsWith('TLS_RSA_WITH') || (!c.includes('GCM') && !c.includes('POLY1305'))) {
|
||||
return { status: 'Weak', description: 'Uses legacy algorithms that are theoretically breakable or lack modern forward secrecy.' };
|
||||
}
|
||||
|
||||
if ((c.includes('GCM') || c.includes('POLY1305')) && (c.includes('AES') || c.includes('CHACHA20'))) {
|
||||
return { status: 'Secure', description: 'Modern, robust authenticated encryption providing forward secrecy.' };
|
||||
}
|
||||
|
||||
return { status: 'Moderate', description: 'Standard encryption but may lack the strongest current security guarantees.' };
|
||||
}
|
||||
|
||||
// GET /api/dashboard/tls-versions
|
||||
router.get('/tls-versions', async (req, res) => {
|
||||
try {
|
||||
@@ -16,13 +36,13 @@ router.get('/tls-versions', async (req, res) => {
|
||||
_id: '$tls_version',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
timestamp: { $max: '$timestamp' },
|
||||
}},
|
||||
{ $project: { tls_version: '$_id', download: 1, upload: 1, total: { $add: ['$download', '$upload'] }, _id: 0 } },
|
||||
{ $project: { tls_version: '$_id', download: 1, upload: 1, total: { $add: ['$download', '$upload'] }, timestamp: 1, _id: 0 } },
|
||||
{ $sort: { total: -1 } },
|
||||
]);
|
||||
|
||||
const finalData = limit > 0 ? data.slice(0, limit) : data;
|
||||
res.json({ ok: true, data: finalData });
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
@@ -41,12 +61,17 @@ router.get('/tls-ciphers', async (req, res) => {
|
||||
_id: '$tls_cipher',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
timestamp: { $max: '$timestamp' },
|
||||
}},
|
||||
{ $project: { tls_cipher: '$_id', download: 1, upload: 1, total: { $add: ['$download', '$upload'] }, _id: 0 } },
|
||||
{ $project: { tls_cipher: '$_id', download: 1, upload: 1, total: { $add: ['$download', '$upload'] }, timestamp: 1, _id: 0 } },
|
||||
{ $sort: { total: -1 } },
|
||||
]);
|
||||
|
||||
const finalData = limit > 0 ? data.slice(0, limit) : data;
|
||||
let finalData = data.map(d => {
|
||||
const { status, description } = analyzeCipherSuite(d.tls_cipher);
|
||||
return { ...d, security_status: status, description };
|
||||
});
|
||||
|
||||
res.json({ ok: true, data: finalData });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
@@ -65,12 +90,14 @@ router.get('/tls-security', async (req, res) => {
|
||||
_id: '$tls_security',
|
||||
download: { $sum: '$download' },
|
||||
upload: { $sum: '$upload' },
|
||||
timestamp: { $max: '$timestamp' },
|
||||
}},
|
||||
{ $project: {
|
||||
tls_security: '$_id',
|
||||
download: 1,
|
||||
upload: 1,
|
||||
total: { $add: ['$download', '$upload'] },
|
||||
timestamp: 1,
|
||||
_id: 0
|
||||
}},
|
||||
{ $sort: { total: -1 } }
|
||||
|
||||
@@ -57,7 +57,12 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
|
||||
if (!ip && mac) {
|
||||
const dev = await DeviceStat.findOne({ mac_address: mac }).sort({ timestamp: -1 }).lean();
|
||||
if (dev) ip = dev.ip_address;
|
||||
if (dev) {
|
||||
ip = dev.ip_address;
|
||||
} else {
|
||||
const flow = await Flow.findOne({ src_mac: mac }).sort({ timestamp: -1 }).lean();
|
||||
if (flow) ip = flow.src_ip;
|
||||
}
|
||||
}
|
||||
if (!ip) return res.status(400).json({ ok: false, message: 'ip or mac required' });
|
||||
|
||||
@@ -90,7 +95,7 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
const [deviceAppStats, flowsQuery, rawThreats] = await Promise.all([
|
||||
// PRIMARY: per-device per-app from DPI API (stored by proxy Step 3b)
|
||||
DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean(),
|
||||
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).lean(),
|
||||
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(2000).lean(),
|
||||
Threat.find({ ...(agentUuid ? { agent_uuid: agentUuid } : {}), ip_address: ip })
|
||||
.sort({ detected_at: -1 }).lean(),
|
||||
]);
|
||||
@@ -134,7 +139,9 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
|
||||
if (f.app_label) bump(protocolsMap, f.app_label, down, up, ls);
|
||||
else if (f.protocol) bump(protocolsMap, f.protocol, down, up, ls);
|
||||
if (f.domain) bump(domainsMap, f.domain, down, up, ls);
|
||||
|
||||
const domainVal = f.sni_hostname || f.domain;
|
||||
if (domainVal) bump(domainsMap, domainVal, down, up, ls);
|
||||
if (f.dst_ip) bump(destinationsMap, f.dst_ip, down, up, ls);
|
||||
}
|
||||
|
||||
@@ -178,8 +185,8 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) {
|
||||
dst_ip: f.dst_ip,
|
||||
dst_port: f.dst_port,
|
||||
protocol: f.protocol,
|
||||
app_label: inferAppFromDomain(f.domain) || f.app_label || 'Other',
|
||||
domain: f.domain || null,
|
||||
app_label: inferAppFromDomain(f.sni_hostname || f.domain) || f.app_label || 'Other',
|
||||
domain: f.sni_hostname || f.domain || null,
|
||||
download: f.download || 0,
|
||||
upload: f.upload || 0,
|
||||
last_seen: f.last_seen || (f.timestamp ? new Date(f.timestamp).toISOString() : null),
|
||||
|
||||
@@ -41,9 +41,28 @@ function buildBaseFilter(req) {
|
||||
}
|
||||
|
||||
// ─── Helper: get per-device breakdown from Flow using a domain value ────────────
|
||||
async function deviceBreakdownByDomain(value, base) {
|
||||
async function deviceBreakdownByDomain(type, value, base) {
|
||||
let matchQuery = { ...base };
|
||||
|
||||
if (type === 'sni_hostname') {
|
||||
// Exact match for sni_hostname, with a fallback OR condition
|
||||
// just in case old data doesn't have sni_hostname but domain matches it closely
|
||||
const parts = value.split('.');
|
||||
const baseDomain = parts.length > 2 ? parts.slice(-2).join('.') : value;
|
||||
const baseDomain2 = parts.length > 3 ? parts.slice(-3).join('.') : value; // For co.uk etc
|
||||
|
||||
matchQuery.$or = [
|
||||
{ sni_hostname: value },
|
||||
{ domain: value },
|
||||
{ domain: baseDomain },
|
||||
{ domain: baseDomain2 }
|
||||
];
|
||||
} else {
|
||||
matchQuery.domain = value;
|
||||
}
|
||||
|
||||
return Flow.aggregate([
|
||||
{ $match: { ...base, domain: value } },
|
||||
{ $match: matchQuery },
|
||||
{ $group: {
|
||||
_id: '$src_ip',
|
||||
download: { $sum: '$download' },
|
||||
@@ -101,7 +120,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// ── Domain-based types: cross-reference with Flow.domain ──────────────────
|
||||
if (['sni_hostname', 'ssl_server_cn', 'quic_hostname'].includes(type)) {
|
||||
const ipRows = await deviceBreakdownByDomain(value, base);
|
||||
const ipRows = await deviceBreakdownByDomain(type, value, base);
|
||||
data = await enrichWithDeviceStat(ipRows, agentFilter);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// backend/routes/remoteIpDetailsHandler.js
|
||||
const { Flow, Threat } = require('../models/Schemas');
|
||||
|
||||
module.exports = async function remoteIpDetailsHandler(req, res, helpers) {
|
||||
try {
|
||||
const { getTimeFilter } = helpers;
|
||||
const ip = String(req.query.ip ?? '');
|
||||
if (!ip) return res.status(400).json({ ok: false, message: 'ip required' });
|
||||
|
||||
const flowFilter = {};
|
||||
if (req.user?.site_uuid) flowFilter.site_uuid = req.user.site_uuid;
|
||||
|
||||
// Agent scope if viewer
|
||||
if (req.user?.role === 'AGENT_VIEWER' && req.user?.agent_uuid) {
|
||||
flowFilter.agent_uuid = req.user.agent_uuid;
|
||||
}
|
||||
|
||||
const rawTimeRange = String(req.query.timeRange ?? 'all');
|
||||
if (rawTimeRange !== 'all') {
|
||||
const tf = getTimeFilter(req);
|
||||
if (tf) flowFilter.timestamp = tf;
|
||||
}
|
||||
|
||||
// Parallel queries
|
||||
const [flowsQuery, rawThreats] = await Promise.all([
|
||||
Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(2000).lean(),
|
||||
Threat.find({ ip_address: ip, ...flowFilter }).sort({ detected_at: -1 }).lean(),
|
||||
]);
|
||||
|
||||
// Data maps
|
||||
const protocolsMap = {};
|
||||
const domainsMap = {};
|
||||
const localDevicesMap = {};
|
||||
|
||||
let totalDownload = 0;
|
||||
let totalUpload = 0;
|
||||
let lastSeen = null;
|
||||
let firstSeen = null;
|
||||
|
||||
const bump = (map, key, down, up, ls) => {
|
||||
if (!map[key]) map[key] = { app_label: key, download: 0, upload: 0, last_seen: ls, first_seen: ls };
|
||||
else {
|
||||
if (new Date(ls) > new Date(map[key].last_seen)) map[key].last_seen = ls;
|
||||
if (new Date(ls) < new Date(map[key].first_seen)) map[key].first_seen = ls;
|
||||
}
|
||||
map[key].download += down;
|
||||
map[key].upload += up;
|
||||
};
|
||||
|
||||
for (const f of flowsQuery) {
|
||||
let localIp = '';
|
||||
let down = f.download || 0;
|
||||
let up = f.upload || 0;
|
||||
let remoteDown = 0;
|
||||
let remoteUp = 0;
|
||||
|
||||
if (f.dst_ip === ip) {
|
||||
localIp = f.src_ip;
|
||||
remoteDown = up; // Remote received what local sent
|
||||
remoteUp = down; // Remote sent what local received
|
||||
} else if (f.src_ip === ip) {
|
||||
localIp = f.dst_ip;
|
||||
remoteDown = down;
|
||||
remoteUp = up;
|
||||
}
|
||||
|
||||
totalDownload += remoteDown;
|
||||
totalUpload += remoteUp;
|
||||
|
||||
const ls = f.last_seen || (f.timestamp ? new Date(f.timestamp).toISOString() : new Date().toISOString());
|
||||
|
||||
if (!lastSeen || new Date(ls) > new Date(lastSeen)) lastSeen = ls;
|
||||
if (!firstSeen || new Date(ls) < new Date(firstSeen)) firstSeen = ls;
|
||||
|
||||
if (localIp) bump(localDevicesMap, localIp, remoteDown, remoteUp, ls);
|
||||
|
||||
if (f.app_label) bump(protocolsMap, f.app_label, remoteDown, remoteUp, ls);
|
||||
else if (f.protocol) bump(protocolsMap, f.protocol, remoteDown, remoteUp, ls);
|
||||
|
||||
const domainVal = f.sni_hostname || f.domain;
|
||||
if (domainVal) bump(domainsMap, domainVal, remoteDown, remoteUp, ls);
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
ip_address: ip,
|
||||
ip_version: ip.includes(':') ? 6 : 4,
|
||||
total_download: totalDownload,
|
||||
total_upload: totalUpload,
|
||||
last_seen: lastSeen,
|
||||
first_seen: firstSeen,
|
||||
protocols: Object.values(protocolsMap).sort((a, b) => b.download + b.upload - (a.download + a.upload)),
|
||||
domains: Object.values(domainsMap).sort((a, b) => b.download + b.upload - (a.download + a.upload)),
|
||||
local_devices: Object.values(localDevicesMap).sort((a, b) => b.download + b.upload - (a.download + a.upload)),
|
||||
flows: flowsQuery.slice(0, 100), // top 100 recent flows
|
||||
threats: rawThreats
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Remote IP Details Error:', err);
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user