9 Commits
9 changed files with 66 additions and 20 deletions

No files matched your search

+2 -2
View File
@@ -2,10 +2,10 @@ FROM oven/bun:1-alpine
WORKDIR /app WORKDIR /app
COPY backend/package*.json ./ COPY package*.json ./
RUN bun install --production RUN bun install --production
COPY backend/ . COPY . .
EXPOSE 3001 EXPOSE 3001
+36
View File
@@ -239,6 +239,27 @@ router.get('/agents/:uuid/subnets', async (req, res) => {
// ─── PUT /api/dashboard/agents/:uuid/subnets ───────────────────────────────── // ─── PUT /api/dashboard/agents/:uuid/subnets ─────────────────────────────────
// Simpan konfigurasi subnet yang diizinkan untuk agent tertentu // Simpan konfigurasi subnet yang diizinkan untuk agent tertentu
// Body: { allowed_subnets: ["192.168.1", "10.21"] } // 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) => { router.put('/agents/:uuid/subnets', async (req, res) => {
try { try {
const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN']; const allowedRoles = ['SUPER_ADMIN', 'TENANT_ADMIN', 'COMPANY_ADMIN'];
@@ -251,6 +272,21 @@ router.put('/agents/:uuid/subnets', async (req, res) => {
{ uuid: req.params.uuid }, { uuid: req.params.uuid },
{ $set: { allowed_subnets: subnets, subnets_updated_at: new Date() } } { $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 } }); res.json({ ok: true, data: { allowed_subnets: subnets } });
} catch (err) { } catch (err) {
res.status(500).json({ ok: false, error: err.message }); res.status(500).json({ ok: false, error: err.message });
+1 -1
View File
@@ -38,7 +38,7 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backone-inspect-backend-bun-0:3001} - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:3001}
- JWT_SECRET=${JWT_SECRET:-super-secret-backone-key} - JWT_SECRET=${JWT_SECRET:-super-secret-backone-key}
- MONGODB_URI=${MONGODB_URI} - MONGODB_URI=${MONGODB_URI}
- BACKONE_API_KEY=${NETIFY_API_KEY:-sk_db_source2} - BACKONE_API_KEY=${NETIFY_API_KEY:-sk_db_source2}
+5 -1
View File
@@ -13,6 +13,10 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
backoneFetch('/data/stats/top/tls_sni/download', { filter_interval: intervalMinutes, settings_limit: 50 }, agentUuid, siteUuid), backoneFetch('/data/stats/top/tls_sni/download', { filter_interval: intervalMinutes, settings_limit: 50 }, agentUuid, siteUuid),
]); ]);
if (!raw || !Array.isArray(raw)) return null; if (!raw || !Array.isArray(raw)) return null;
// Safety check: Filter flows strictly to the requested agent to prevent Org-level pollution
const filteredRaw = agentUuid ? raw.filter(r => r.agent_uuid === agentUuid) : raw;
const sniList = []; const sniList = [];
if (sniRaw && Array.isArray(sniRaw)) { if (sniRaw && Array.isArray(sniRaw)) {
for (const r of sniRaw) { for (const r of sniRaw) {
@@ -22,7 +26,7 @@ async function fetchFlows(limit = 500, agentUuid = null, siteUuid = null, interv
} }
} }
} }
return raw.map(r => { return filteredRaw.map(r => {
const port = r.remote_port ?? null; const port = r.remote_port ?? null;
const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null; const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null;
const appLabel = r.application?.label || portService; const appLabel = r.application?.label || portService;
+2 -1
View File
@@ -37,7 +37,8 @@ function fixDates(obj) {
async function backoneFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) { async function backoneFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) {
if (agentUuid) { if (agentUuid) {
const agentId = agentMap[agentUuid]; const agentId = agentMap[agentUuid];
if (agentId) { // Only use filter_agents if agentId is a number or numeric string
if (agentId && !isNaN(Number(agentId))) {
params.filter_agents = `[${agentId}]`; params.filter_agents = `[${agentId}]`;
} else { } else {
params.settings_agent = agentUuid; params.settings_agent = agentUuid;
+1 -1
View File
@@ -103,7 +103,7 @@ export function getAgentColumns({
className: "w-[10%] text-center", className: "w-[10%] text-center",
accessor: (row) => { accessor: (row) => {
const uuid = row.uuid || row.serial; const uuid = row.uuid || row.serial;
const uptime = uptimeMap[uuid] ?? 100; const uptime = uptimeMap[uuid] ?? 0;
let color = "text-emerald-400"; let color = "text-emerald-400";
if (uptime < 90) color = "text-red-400"; if (uptime < 90) color = "text-red-400";
else if (uptime < 98) color = "text-amber-400"; else if (uptime < 98) color = "text-amber-400";
+5 -6
View File
@@ -99,8 +99,8 @@ export default function AgentsPage() {
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:')); return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
}).length; }).length;
const offlineAgents = totalAgents - onlineAgents; const offlineAgents = totalAgents - onlineAgents;
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100); const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 0);
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100; const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 0;
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja // External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal // AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
@@ -175,11 +175,10 @@ export default function AgentsPage() {
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid)) .filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
.map(loc => { .map(loc => {
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid); const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
const dynamicLabel = resolveAgentLabel(loc.agent_uuid, managedUsers);
const agentLabel = agent const agentLabel = agent
? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid ? (agent.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid))
? resolveAgentLabel(loc.agent_uuid, managedUsers) : (loc.label || (dynamicLabel !== loc.agent_uuid ? dynamicLabel : loc.agent_uuid));
: (agent.label || loc.label || loc.agent_uuid))
: (loc.label || loc.agent_uuid);
const isOnline = !!(agent?.last_seen_at?.human === 'Online' || const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
agent?.last_seen_at?.human === 'Flows Detected' || agent?.last_seen_at?.human === 'Flows Detected' ||
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:'))); (agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
+6
View File
@@ -35,6 +35,12 @@ export async function updateAgent(agentId: string, label: string) {
{ $set: { label: label } } { $set: { label: label } }
); );
// Bismillah: Hapus semua data duplikat/hantu milik agent ini agar tidak ditimpa oleh proxy lawas/bug ganda
await db.collection('agent_registry').deleteMany({
$or: [{ uuid: agentId }, { serial: agentId }],
_id: { $ne: agent._id }
});
// Also try to update the summaries if possible, so historical displays match // Also try to update the summaries if possible, so historical displays match
await db.collection('summaries').updateMany( await db.collection('summaries').updateMany(
{ agent_uuid: agent.uuid }, { agent_uuid: agent.uuid },
+8 -8
View File
@@ -89,7 +89,7 @@ export async function fetchAgentsFromMongo(
} }
// 3. Optimize status check and last seen per agent using parallel indexed queries // 3. Optimize status check and last seen per agent using parallel indexed queries
const twentyFourHoursAgo = new Date(Date.now() - 24 * 3600000); const fifteenMinutesAgoQuery = new Date(Date.now() - 15 * 60000);
const agentResults = await Promise.all( const agentResults = await Promise.all(
agentSource.map(async (item: any, idx: number) => { agentSource.map(async (item: any, idx: number) => {
@@ -99,7 +99,7 @@ export async function fetchAgentsFromMongo(
// Execute status checks for active flow, latest flow, and latest summary in parallel // Execute status checks for active flow, latest flow, and latest summary in parallel
const [activeFlow, latestFlow, latestSummary] = await Promise.all([ const [activeFlow, latestFlow, latestSummary] = await Promise.all([
db.collection('flows').findOne( db.collection('flows').findOne(
{ agent_uuid: uuid, timestamp: { $gte: twentyFourHoursAgo } }, { agent_uuid: uuid, timestamp: { $gte: fifteenMinutesAgoQuery } },
{ projection: { _id: 1 } } { projection: { _id: 1 } }
), ),
db.collection('flows').findOne( db.collection('flows').findOne(
@@ -112,8 +112,6 @@ export async function fetchAgentsFromMongo(
), ),
]); ]);
const isOnline = !!activeFlow;
const registryLastSeenRaw = item.last_seen_at; const registryLastSeenRaw = item.last_seen_at;
let lastSeenDate: Date | null = null; let lastSeenDate: Date | null = null;
if (registryLastSeenRaw?.date) { if (registryLastSeenRaw?.date) {
@@ -137,8 +135,9 @@ export async function fetchAgentsFromMongo(
} }
} }
const isOnline = !!activeFlow;
let statusHuman = 'No Flows Detected'; let statusHuman = 'No Flows Detected';
if (isOnline) {
if (lastSeenDate) { if (lastSeenDate) {
const now = new Date(); const now = new Date();
const isToday = lastSeenDate.getDate() === now.getDate() && const isToday = lastSeenDate.getDate() === now.getDate() &&
@@ -146,11 +145,12 @@ export async function fetchAgentsFromMongo(
lastSeenDate.getFullYear() === now.getFullYear(); lastSeenDate.getFullYear() === now.getFullYear();
const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB'; const timeStr = lastSeenDate.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit' }) + ' WIB';
const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' }); const dateStr = lastSeenDate.toLocaleDateString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit' });
statusHuman = isToday ? `Last seen: ${timeStr}` : `Last seen: ${dateStr} ${timeStr}`; const seenStr = isToday ? timeStr : `${dateStr} ${timeStr}`;
} else {
statusHuman = isOnline ? `Last seen: ${seenStr}` : `Offline (Last seen: ${seenStr})`;
} else if (isOnline) {
statusHuman = 'Last seen: Just now'; statusHuman = 'Last seen: Just now';
} }
}
return { return {
id: idx + 1, id: idx + 1,