Files
Deep-Package-Inspection/backend/routes/dashboard/agents.js
T

303 lines
13 KiB
JavaScript

// backend/routes/dashboard/agents.js
// ─────────────────────────────────────────────────────────────────────────────
// Agent Management and Telemetry API Router
// ─────────────────────────────────────────────────────────────────────────────
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const { Summary } = require('../../models/Schemas');
const { getTimeFilter, isKnownSite } = require('./helpers');
// GET /api/dashboard/agents/uptime
router.get('/agents/uptime', async (req, res) => {
try {
const range = req.query.timeRange || '1d';
const cyclesMap = {
'5m': 1,
'30m': 6,
'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() - 30 * 24 * 3600000) };
}
const query = { timestamp: timeFilter };
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 && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) {
query.site_uuid = { $in: [requestedSiteUuid, 'global'] };
} else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) {
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) query.site_uuid = { $in: [...envSites, 'global'] };
} else if (!isGlobalUser) {
const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) {
query.site_uuid = { $in: [...envSites, req.user?.site_uuid, 'global'].filter(Boolean) };
} else if (req.user?.site_uuid) {
query.site_uuid = { $in: [req.user.site_uuid, 'global'] };
}
}
const stats = await Summary.aggregate([
{ $match: query },
{ $group: { _id: "$agent_uuid", count: { $sum: 1 } } }
]);
const uptimeMap = {};
stats.forEach(s => {
if (s._id) {
const pct = Math.min(100, Math.round((s.count / ideal) * 1000) / 10);
uptimeMap[s._id] = pct;
}
});
res.json({ ok: true, uptime: uptimeMap });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// GET /api/dashboard/agents
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';
if (!isAuthorized) {
return res.status(403).json({ ok: false, error: 'Forbidden: Admin access only' });
}
// Always filter out null/empty agent_uuid entries
const query = { agent_uuid: { $nin: [null, '', undefined] } };
const effectiveRole = req.user?._originalRole || req.user?.role;
const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE';
const requestedSiteUuid = req.headers['x-backone-site-uuid'];
if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) {
query.site_uuid = { $in: [requestedSiteUuid, 'global'] };
} else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) {
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) query.site_uuid = { $in: [...envSites, 'global'] };
} else if (effectiveRole === 'TENANT_ADMIN') {
const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) {
query.site_uuid = { $in: [...envSites, req.user.site_uuid, 'global'].filter(Boolean) };
} else {
query.site_uuid = { $in: [req.user.site_uuid, 'global'] };
}
}
const agents = await Summary.distinct('agent_uuid', query);
// 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.
// Values represent total MongoDB storage footprint per agent (across 7-day retention window).
router.get('/agents/storage', 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: '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 && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) {
siteUuid = requestedSiteUuid;
} else if (!isGlobalUser && req.user?.site_uuid) {
siteUuid = req.user.site_uuid;
}
const { agentSizesCache, lastCacheUpdate } = require('../../db/capacityTracker');
const allStorage = agentSizesCache();
const cachedAt = lastCacheUpdate();
let storage = allStorage;
if (siteUuid) {
const registryAgents = await mongoose.connection.db.collection('agent_registry')
.find({ site_uuid: { $in: [siteUuid, 'global'] } })
.toArray();
const siteAgentUuids = new Set(registryAgents.map(a => a.uuid));
const summaryAgents = await Summary.distinct('agent_uuid', { site_uuid: { $in: [siteUuid, 'global'] } });
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 });
}
});
// ─── 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'];
const effectiveRole = user?._originalRole || user?.role;
const isGlobalUser = effectiveRole === 'SUPER_ADMIN' || effectiveRole === 'EXECUTIVE';
if (isGlobalUser && requestedSiteUuid && requestedSiteUuid !== 'all' && isKnownSite(requestedSiteUuid)) {
filter.site_uuid = { $in: [requestedSiteUuid, 'global'] };
} else if (isGlobalUser && (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID)) {
const envSites = (process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID).split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) filter.site_uuid = { $in: [...envSites, 'global'] };
} else if (!isGlobalUser) {
const envSites = ((process.env.BACKONE_SITE_UUIDS || process.env.BACKONE_SITE_UUID) || '').split(',').map(s => s.trim()).filter(Boolean);
if (envSites.length > 0) {
filter.site_uuid = { $in: [...envSites, user?.site_uuid, 'global'].filter(Boolean) };
} else if (user?.site_uuid) {
filter.site_uuid = { $in: [user.site_uuid, 'global'] };
}
}
}
const agents = await db.collection('agent_registry')
.find(filter)
.project({ uuid: 1, label: 1, protected_label: 1, _id: 0 })
.sort({ uuid: 1 })
.toArray();
// Use protected_label if available to prevent proxy overwrite bug
const mappedAgents = agents.map(a => ({
uuid: a.uuid,
label: a.protected_label || a.label || a.uuid
}));
res.json({ ok: true, data: mappedAgents });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── GET /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
// Kembalikan konfigurasi subnet yang diizinkan untuk agent tertentu
router.get('/agents/:uuid/subnets', async (req, res) => {
try {
const db = mongoose.connection.db;
const doc = await db.collection('agent_registry').findOne({ uuid: req.params.uuid });
res.json({ ok: true, data: { allowed_subnets: doc?.allowed_subnets || [] } });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ─── PUT /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
// Simpan konfigurasi subnet yang diizinkan untuk agent tertentu
// Body: { allowed_subnets: ["192.168.1", "10.21"] }
function ipToLong(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}
function ipMatchesSubnets(ip, subnets) {
if (!subnets || subnets.length === 0) return true;
if (!ip) return false;
return subnets.some(subnet => {
if (subnet.includes('/')) {
try {
const [range, bitsStr] = subnet.split('/');
const bits = parseInt(bitsStr, 10);
if (isNaN(bits) || bits < 0 || bits > 32) return false;
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
return (ipToLong(ip) & mask) === (ipToLong(range) & mask);
} catch (e) { return false; }
} else { return ip === subnet; }
});
}
// PUT /api/dashboard/agents/:uuid/subnets
router.put('/agents/:uuid/subnets', async (req, res) => {
try {
const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN'];
if (!allowedRoles.includes(req.user?.role)) {
return res.status(403).json({ ok: false, error: 'Forbidden' });
}
const db = mongoose.connection.db;
const subnets = (req.body.allowed_subnets || []).map(s => s.trim()).filter(Boolean);
await db.collection('agent_registry').updateOne(
{ uuid: req.params.uuid },
{ $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } }
);
// Auto-cleanup background task
if (subnets.length > 0) {
setTimeout(async () => {
try {
const ips = await db.collection('devicestats').distinct('ip_address', { agent_uuid: req.params.uuid });
const invalidIps = ips.filter(ip => !ipMatchesSubnets(ip, subnets));
if (invalidIps.length > 0) {
await db.collection('devicestats').deleteMany({ agent_uuid: req.params.uuid, ip_address: { $in: invalidIps } });
await db.collection('flows').deleteMany({ agent_uuid: req.params.uuid, src_ip: { $in: invalidIps } });
}
} catch (e) { console.error('Auto-cleanup error:', e); }
}, 100);
}
res.json({ ok: true, data: { allowed_subnets: subnets } });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
module.exports = router;