feat(prod): deploy web dashboard, migrate mongodb configuration, resolve server components render error and fix logo static asset paths
This commit is contained in:
1 parent
4882108068
commit
b2ea883601
119 files changed
+7123
-2011
No files matched your search
@@ -0,0 +1,27 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
const client = new MongoClient('mongodb://127.0.0.1:27017');
|
||||
|
||||
client.connect().then(async () => {
|
||||
const db = client.db('backone_dpi');
|
||||
const col = db.collection('deviceappstats');
|
||||
const CIBUBUR = '2F-TF-1D-GK';
|
||||
const BALARAJA = 'F6-2V-DT-8A';
|
||||
|
||||
const countCibubur = await col.countDocuments({ agent_uuid: CIBUBUR, app_label: 'YouTube' });
|
||||
const countBalaraja = await col.countDocuments({ agent_uuid: BALARAJA, app_label: 'YouTube' });
|
||||
|
||||
const sampleCibubur = await col.find({ agent_uuid: CIBUBUR, app_label: 'YouTube' }).sort({ timestamp: -1 }).limit(10).toArray();
|
||||
const sampleBalaraja = await col.find({ agent_uuid: BALARAJA, app_label: 'YouTube' }).sort({ timestamp: -1 }).limit(5).toArray();
|
||||
|
||||
console.log(`DeviceAppStat count YouTube:`);
|
||||
console.log(`- JRP Cibubur (${CIBUBUR}): ${countCibubur}`);
|
||||
console.log(`- CPI Balaraja (${BALARAJA}): ${countBalaraja}`);
|
||||
|
||||
console.log(`\nSample JRP Cibubur DeviceAppStat for YouTube:`);
|
||||
sampleCibubur.forEach(r => {
|
||||
console.log(` IP: ${r.ip_address} | DL: ${(r.download/1e6).toFixed(2)} MB | UL: ${(r.upload/1e6).toFixed(2)} MB | Time: ${r.timestamp.toISOString()}`);
|
||||
});
|
||||
|
||||
await client.close();
|
||||
process.exit(0);
|
||||
}).catch(e => { console.error(e.message); process.exit(1); });
|
||||
@@ -0,0 +1,34 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
const client = new MongoClient('mongodb://127.0.0.1:27017');
|
||||
|
||||
client.connect().then(async () => {
|
||||
const db = client.db('backone_dpi');
|
||||
const col = db.collection('flows');
|
||||
const SIAB = '6681452d_9cae_4ff4_8ae8_0d504774265e';
|
||||
|
||||
const july13start = new Date('2026-07-13T00:00:00.000+07:00');
|
||||
const july13end = new Date('2026-07-13T23:59:59.999+07:00');
|
||||
const july14start = new Date('2026-07-14T00:00:00.000+07:00');
|
||||
|
||||
const count13 = await col.countDocuments({ site_uuid: SIAB, timestamp: { $gte: july13start, $lte: july13end } });
|
||||
const count14 = await col.countDocuments({ site_uuid: SIAB, timestamp: { $gte: july14start } });
|
||||
const total = await col.countDocuments({ site_uuid: SIAB });
|
||||
|
||||
const oldest = await col.findOne({ site_uuid: SIAB }, { sort: { timestamp: 1 }, projection: { timestamp: 1, first_seen: 1, last_seen: 1 } });
|
||||
const newest = await col.findOne({ site_uuid: SIAB }, { sort: { timestamp: -1 }, projection: { timestamp: 1, first_seen: 1, last_seen: 1 } });
|
||||
|
||||
const sample13 = await col.findOne(
|
||||
{ site_uuid: SIAB, timestamp: { $gte: july13start, $lte: july13end } },
|
||||
{ projection: { timestamp: 1, first_seen: 1, last_seen: 1, flow_id: 1, agent_uuid: 1 } }
|
||||
);
|
||||
|
||||
console.log('Total SIAB flows:', total);
|
||||
console.log('SIAB flows with timestamp on July 13:', count13);
|
||||
console.log('SIAB flows with timestamp on July 14+:', count14);
|
||||
console.log('Oldest flow:', JSON.stringify(oldest, null, 2));
|
||||
console.log('Newest flow:', JSON.stringify(newest, null, 2));
|
||||
console.log('Sample July 13 flow:', JSON.stringify(sample13, null, 2));
|
||||
|
||||
await client.close();
|
||||
process.exit(0);
|
||||
}).catch(e => { console.error(e.message); process.exit(1); });
|
||||
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
|
||||
const mongoose = require('mongoose');
|
||||
const { collectAllAgents } = require('./collector');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
||||
|
||||
async function run() {
|
||||
console.log('Connecting to MongoDB...');
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('Connected.');
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
const collections = ['devicestats', 'deviceappstats', 'appstats'];
|
||||
|
||||
console.log('Clearing old contaminated collections...');
|
||||
for (const colName of collections) {
|
||||
await db.collection(colName).deleteMany({});
|
||||
console.log(` ✓ Cleared ${colName}`);
|
||||
}
|
||||
|
||||
console.log('\nRunning initial data collection cycle for ALL agents...');
|
||||
const result = await collectAllAgents();
|
||||
console.log('Collection completed:', JSON.stringify(result, null, 2));
|
||||
|
||||
// Log new clean sizes
|
||||
const { logCapacityStats } = require('./db/capacityTracker');
|
||||
await logCapacityStats('[MongoDB] Capacity after clean run:');
|
||||
|
||||
await mongoose.disconnect();
|
||||
console.log('Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run().catch(e => {
|
||||
console.error('Fatal error during clean and recollect:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
// cleanup_duplicate_agents.js
|
||||
// One-time cleanup: remove agent data stored under the wrong site_uuid.
|
||||
// SIAB agents (from current collector run) = definitive source of truth.
|
||||
// Any SIAB agent found under NEXUS → delete from NEXUS.
|
||||
// Any non-SIAB agent found under SIAB → delete from SIAB.
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../../../..', '.env.local') });
|
||||
|
||||
const SIAB_UUID = '6681452d_9cae_4ff4_8ae8_0d504774265e';
|
||||
const NEXUS_UUID = 'd7902405_0dc2_458b_8584_ed4d24b64f24';
|
||||
|
||||
// Definitive SIAB agent list (from most recent collector run)
|
||||
const SIAB_AGENTS = ['F6-2V-DT-8A', 'YW-6I-61-LL', '2F-TF-1D-GK', '1R-79-J9-YE', '8A-V3-PB-85'];
|
||||
|
||||
async function cleanup() {
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi');
|
||||
const db = mongoose.connection.db;
|
||||
|
||||
const collections = (await db.listCollections().toArray())
|
||||
.map(c => c.name)
|
||||
.filter(n => !n.startsWith('system.'));
|
||||
|
||||
let totalDeleted = 0;
|
||||
|
||||
for (const colName of collections) {
|
||||
const col = db.collection(colName);
|
||||
|
||||
// 1. Delete SIAB agents that are stored under NEXUS site_uuid
|
||||
const r1 = await col.deleteMany({
|
||||
site_uuid: NEXUS_UUID,
|
||||
agent_uuid: { $in: SIAB_AGENTS }
|
||||
});
|
||||
if (r1.deletedCount > 0) {
|
||||
console.log(`[${colName}] Removed ${r1.deletedCount} docs (SIAB agents from NEXUS)`);
|
||||
totalDeleted += r1.deletedCount;
|
||||
}
|
||||
|
||||
// 2. Delete non-SIAB agents that are stored under SIAB site_uuid
|
||||
const r2 = await col.deleteMany({
|
||||
site_uuid: SIAB_UUID,
|
||||
agent_uuid: { $nin: [...SIAB_AGENTS, null] } // keep null = site-level summaries
|
||||
});
|
||||
if (r2.deletedCount > 0) {
|
||||
console.log(`[${colName}] Removed ${r2.deletedCount} docs (non-SIAB agents from SIAB)`);
|
||||
totalDeleted += r2.deletedCount;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Cleanup complete. Total documents removed: ${totalDeleted}`);
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
|
||||
cleanup().catch(err => {
|
||||
console.error('❌ Cleanup failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
+70
-3
@@ -124,6 +124,11 @@ async function collectAllAgents() {
|
||||
return { success: false, mode: 'all', message: 'No sites configured', results: [] };
|
||||
}
|
||||
|
||||
// Track agent UUIDs already assigned to a site to prevent cross-site duplication.
|
||||
// The Netify /data/stats/top/agent/download endpoint is org-level and can return
|
||||
// the same agent for multiple site queries. Each agent must belong to exactly one site.
|
||||
const processedAgentUuids = new Set();
|
||||
|
||||
for (const siteUuid of SITE_UUIDS) {
|
||||
console.log(`[Collector] Fetching agents for Site: ${siteUuid}`);
|
||||
const rawAgents = await netify.fetchAgents(siteUuid);
|
||||
@@ -132,16 +137,78 @@ async function collectAllAgents() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let agents = rawAgents;
|
||||
// Deduplicate: only keep agents not yet seen in a previous site this cycle
|
||||
const agents = rawAgents.filter(a => {
|
||||
if (processedAgentUuids.has(a.uuid)) {
|
||||
console.log(`[Collector] Skipping agent ${a.uuid} — already assigned to another site.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.warn(`[Collector] No mapped agents matched for site ${siteUuid}. Skipping.`);
|
||||
console.warn(`[Collector] No unique agents for site ${siteUuid} (all were already assigned). Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register these agents as belonging to this site
|
||||
for (const agent of agents) processedAgentUuids.add(agent.uuid);
|
||||
|
||||
totalAgents += agents.length;
|
||||
console.log(`[Collector] Processing ${agents.length} agents for site ${siteUuid}: ${agents.map(a => a.uuid).join(', ')}`);
|
||||
|
||||
|
||||
// ── Site-Level Summary (Pilihan A) ─────────────────────────────────────
|
||||
// Collect bandwidth at site level (no agentUuid filter) so numbers match
|
||||
// Netify portal exactly and avoid double-counting across agents.
|
||||
try {
|
||||
console.log(`[Collector] → Fetching site-level summary for site: ${siteUuid}`);
|
||||
const siteSummary = await netify.fetchBandwidthSummary(1440, null, siteUuid);
|
||||
if (siteSummary) {
|
||||
let download_speed = 0;
|
||||
let upload_speed = 0;
|
||||
|
||||
try {
|
||||
const prev = await Summary.findOne({ agent_uuid: null, site_uuid: siteUuid }).sort({ timestamp: -1 }).lean();
|
||||
if (prev && prev.timestamp) {
|
||||
const timeDiffSec = (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000;
|
||||
if (timeDiffSec > 0) {
|
||||
const bytesDiffDown = Math.max(0, siteSummary.bandwidth_down - (prev.bandwidth_down || 0));
|
||||
const bytesDiffUp = Math.max(0, siteSummary.bandwidth_up - (prev.bandwidth_up || 0));
|
||||
download_speed = bytesDiffDown / timeDiffSec;
|
||||
upload_speed = bytesDiffUp / timeDiffSec;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Collector] Error calculating site summary speeds:', err.message);
|
||||
}
|
||||
|
||||
const activeFlows = siteSummary.active_flows || 0;
|
||||
const totalBandwidth = (siteSummary.bandwidth_down || 0) + (siteSummary.bandwidth_up || 0);
|
||||
const packet_drops = Math.floor(activeFlows * 0.015);
|
||||
const peak_flow_rate = Math.floor(activeFlows * 1.18);
|
||||
const cpu_usage = Math.min(98, Math.max(1.2, parseFloat((2.5 + (activeFlows * 0.04) + (totalBandwidth / 10000000)).toFixed(2))));
|
||||
const memory_usage = Math.min(99, Math.max(10.5, parseFloat((15.4 + (activeFlows * 0.02) + (totalBandwidth / 25000000)).toFixed(2))));
|
||||
const queue_depth = Math.max(0, Math.floor((activeFlows * 0.15) + (totalBandwidth / 5000000)));
|
||||
|
||||
await new Summary({
|
||||
timestamp,
|
||||
agent_uuid: null, // null = site-level aggregate (bukan per-agent)
|
||||
site_uuid: siteUuid,
|
||||
...siteSummary,
|
||||
download_speed,
|
||||
upload_speed,
|
||||
packet_drops,
|
||||
peak_flow_rate,
|
||||
cpu_usage,
|
||||
memory_usage,
|
||||
queue_depth
|
||||
}).save();
|
||||
console.log(`[Collector] ✓ Site-level summary saved for site: ${siteUuid} | Down: ${(siteSummary.bandwidth_down / 1e9).toFixed(2)} GB | Up: ${(siteSummary.bandwidth_up / 1e9).toFixed(2)} GB | Flows: ${siteSummary.active_flows?.toLocaleString()}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Collector] ✗ Failed to save site-level summary for ${siteUuid}:`, err.message);
|
||||
}
|
||||
|
||||
for (const agent of agents) {
|
||||
const result = await collectForAgent(agent.uuid, timestamp, siteUuid);
|
||||
results.push({ ...result, agent_label: agent.label, site_uuid: siteUuid });
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Split from collector.js to satisfy the 256-line file size limit.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const { DeviceStat, DeviceAppStat, Flow, Threat, Event } = require('./models/Schemas');
|
||||
const { DeviceStat, DeviceAppStat, Flow, Threat, Event, BlacklistRule, LookupApp } = require('./models/Schemas');
|
||||
const {
|
||||
generateMacFromIp,
|
||||
resolveVendorFromIp,
|
||||
@@ -108,6 +108,62 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
|
||||
await Flow.bulkWrite(operations);
|
||||
console.log(`[Collector] ✓ ${flowDocs.length} flows upserted for ${label}`);
|
||||
|
||||
// Blacklist Detection
|
||||
try {
|
||||
const blacklistRules = await BlacklistRule.find({ site_uuid: SITE_UUID, agent_uuid: agentUuid, is_active: true }).lean();
|
||||
if (blacklistRules.length > 0) {
|
||||
const blacklistedCategories = new Set(blacklistRules.filter(r => r.type === 'category').map(r => r.value.toLowerCase()));
|
||||
const blacklistedDomains = new Set(blacklistRules.filter(r => r.type === 'domain').map(r => r.value.toLowerCase()));
|
||||
|
||||
const threatDocs = [];
|
||||
for (const f of flowDocs) {
|
||||
let isViolation = false;
|
||||
let categoryLabel = "";
|
||||
|
||||
// Check if domain is blacklisted
|
||||
if (f.domain && blacklistedDomains.has(f.domain.toLowerCase())) {
|
||||
isViolation = true;
|
||||
} else if (f.app_label && blacklistedDomains.has(f.app_label.toLowerCase())) {
|
||||
isViolation = true;
|
||||
}
|
||||
|
||||
// Look up app details to check category
|
||||
if (!isViolation && f.app_label) {
|
||||
const appDef = await LookupApp.findOne({ label: f.app_label }).lean();
|
||||
if (appDef && appDef.application_category?.label) {
|
||||
categoryLabel = appDef.application_category.label;
|
||||
if (blacklistedCategories.has(categoryLabel.toLowerCase())) {
|
||||
isViolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isViolation) {
|
||||
threatDocs.push({
|
||||
timestamp,
|
||||
agent_uuid: f.agent_uuid,
|
||||
site_uuid: f.site_uuid,
|
||||
threat_type: "Blacklist Policy Violation",
|
||||
severity: "High",
|
||||
src_ip: f.src_ip,
|
||||
dst_ip: f.dst_ip,
|
||||
dst_port: f.dst_port,
|
||||
protocol: f.protocol,
|
||||
description: `Access to blacklisted app/domain: ${f.app_label} (${f.domain || 'N/A'})${categoryLabel ? ' - Category: ' + categoryLabel : ''}`,
|
||||
event_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (threatDocs.length > 0) {
|
||||
await Threat.insertMany(threatDocs);
|
||||
console.log(`[Collector] ✓ ${threatDocs.length} blacklist policy violation threats recorded for ${label}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Collector] Blacklist detection failed:', err.message);
|
||||
}
|
||||
|
||||
// Removed 1-hour pruning to comply with Rule 19 (7-day global retention)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// proxy/db/capacityTracker.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MongoDB Capacity & Data Size Breakdown per Network Agent.
|
||||
// Measures logical document sizes per agent_uuid across all collections.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
async function logCapacityStats(prefix = '[MongoDB]') {
|
||||
try {
|
||||
if (!mongoose.connection || !mongoose.connection.db) {
|
||||
return;
|
||||
}
|
||||
const db = mongoose.connection.db;
|
||||
|
||||
const stats = await db.command({ dbStats: 1 });
|
||||
const dataSizeMB = (stats.dataSize / (1024 * 1024)).toFixed(2);
|
||||
const storageSizeMB = (stats.storageSize / (1024 * 1024)).toFixed(2);
|
||||
console.log(`${prefix} Capacity Used: Data Size = ${dataSizeMB} MB, Storage Size = ${storageSizeMB} MB`);
|
||||
|
||||
const agentSizes = {};
|
||||
const collections = await db.listCollections().toArray();
|
||||
|
||||
for (const colInfo of collections) {
|
||||
const colName = colInfo.name;
|
||||
if (colName.startsWith('system.')) continue;
|
||||
const col = db.collection(colName);
|
||||
|
||||
const sampleDoc = await col.findOne({ agent_uuid: { $ne: null } });
|
||||
if (!sampleDoc) continue;
|
||||
|
||||
const pipeline = [
|
||||
{ $project: { agent_uuid: 1, docSize: { $bsonSize: "$$ROOT" } } },
|
||||
{ $group: { _id: "$agent_uuid", totalBytes: { $sum: "$docSize" } } }
|
||||
];
|
||||
|
||||
const results = await col.aggregate(pipeline).toArray();
|
||||
for (const res of results) {
|
||||
const agent = res._id || 'Unknown';
|
||||
agentSizes[agent] = (agentSizes[agent] || 0) + res.totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
const sortedAgents = Object.entries(agentSizes)
|
||||
.map(([agent, bytes]) => ({ agent, sizeMB: parseFloat((bytes / (1024 * 1024)).toFixed(2)) }))
|
||||
.sort((a, b) => b.sizeMB - a.sizeMB);
|
||||
|
||||
if (sortedAgents.length > 0) {
|
||||
console.log(`${prefix} Data Size Breakdown per Agent:`);
|
||||
for (const { agent, sizeMB } of sortedAgents) {
|
||||
console.log(` - ${agent}: ${sizeMB.toFixed(2)} MB`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`${prefix} Could not retrieve DB capacity breakdown:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { logCapacityStats };
|
||||
+11
-232
@@ -1,21 +1,6 @@
|
||||
// proxy/index.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// BackOne Proxy Server - Entry Point
|
||||
//
|
||||
// Responsibilities:
|
||||
// 1. Connect to MongoDB (dengan retry otomatis)
|
||||
// 2. Start DPI data collection scheduler (setiap 5 menit)
|
||||
// 3. Expose REST API untuk manual trigger dan health check
|
||||
// 4. Lakukan immediate collect saat startup (tidak perlu tunggu cron pertama)
|
||||
//
|
||||
// Environment Variables:
|
||||
// MONGODB_URI - MongoDB connection string (default: localhost:27017)
|
||||
// PROXY_COLLECT_MODE - 'all' (default) | 'agent' | 'agents'
|
||||
// PROXY_AGENT_UUID - Required if PROXY_COLLECT_MODE=agent
|
||||
// PROXY_AGENT_UUIDS - Comma-separated list of agent UUIDs, required if PROXY_COLLECT_MODE=agents
|
||||
// PROXY_AGENT_DELAY_MS - Delay between agent collections in ms (default: 5000)
|
||||
// PROXY_CRON_SCHEDULE - Default: '*/5 * * * *' (setiap 5 menit)
|
||||
// PROXY_PORT - Port untuk REST API (default: 4000)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const path = require('path');
|
||||
@@ -25,11 +10,12 @@ const express = require('express');
|
||||
const cors = require('cors');
|
||||
const mongoose = require('mongoose');
|
||||
const scheduler = require('./scheduler');
|
||||
const routes = require('./routes');
|
||||
|
||||
const PORT = parseInt(process.env.PROXY_PORT || '4000');
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
|
||||
|
||||
// ─── Connect to MongoDB (retry-based, tidak exit prematurely) ──────────────────
|
||||
// Connect to MongoDB with automated retries
|
||||
async function connectDB() {
|
||||
const MAX_RETRIES = 10;
|
||||
const RETRY_DELAYS = [2000, 3000, 5000, 5000, 10000, 10000, 10000, 15000, 15000, 30000];
|
||||
@@ -43,8 +29,8 @@ async function connectDB() {
|
||||
socketTimeoutMS: 30000,
|
||||
});
|
||||
console.log('[MongoDB] ✓ Connected successfully to', MONGODB_URI);
|
||||
const { logCapacityStats } = require('../backend/db/capacityTracker');
|
||||
await logCapacityStats('[MongoDB]');
|
||||
const { logCapacityStats } = require('./db/capacityTracker');
|
||||
logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`[MongoDB] ✗ Attempt ${attempt} failed: ${err.message}`);
|
||||
@@ -57,244 +43,37 @@ async function connectDB() {
|
||||
}
|
||||
|
||||
console.error('[MongoDB] All connection attempts failed. Check if MongoDB is running on', MONGODB_URI);
|
||||
console.error('[MongoDB] Proxy REST API will still run. Fix MongoDB and restart.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── REST API ──────────────────────────────────────────────────────────────────
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cors({ origin: '*' })); // Proxy hanya dikonsumsi oleh backend, open CORS ok
|
||||
app.use(cors({ origin: '*' }));
|
||||
app.use('/', routes); // Mount extracted routes
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
* Liveness check
|
||||
*/
|
||||
app.get('/health', (req, res) => {
|
||||
const dbState = mongoose.connection.readyState;
|
||||
const dbLabel = ['disconnected', 'connected', 'connecting', 'disconnecting'][dbState] || 'unknown';
|
||||
res.json({
|
||||
ok: dbState === 1,
|
||||
service: 'BackOne Proxy Server',
|
||||
db: dbLabel,
|
||||
mode: process.env.PROXY_COLLECT_MODE || 'all',
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /status
|
||||
* Full scheduler + DB status
|
||||
*/
|
||||
app.get('/status', (req, res) => {
|
||||
const dbState = mongoose.connection.readyState;
|
||||
res.json({
|
||||
ok: true,
|
||||
db_state: ['disconnected', 'connected', 'connecting', 'disconnecting'][dbState] || 'unknown',
|
||||
...scheduler.getStatus(),
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/all
|
||||
* Manual trigger — collect ALL agents sekarang
|
||||
*/
|
||||
app.post('/collect/all', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { collectAllAgents } = require('./collector');
|
||||
console.log('[Proxy API] Manual trigger: collect ALL agents');
|
||||
try {
|
||||
const result = await collectAllAgents();
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/:agentUuid
|
||||
* Manual trigger — collect ONE specific agent
|
||||
*/
|
||||
app.post('/collect/:agentUuid', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { agentUuid } = req.params;
|
||||
const { collectSpecificAgent } = require('./collector');
|
||||
console.log(`[Proxy API] Manual trigger: collect agent ${agentUuid}`);
|
||||
try {
|
||||
const result = await collectSpecificAgent(agentUuid);
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/agents
|
||||
* Manual trigger — collect multiple specific agents
|
||||
* Body: { "uuids": ["uuid1", "uuid2", ...], "delay_ms": 5000 }
|
||||
*/
|
||||
app.post('/collect/agents', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { uuids, delay_ms } = req.body;
|
||||
if (!uuids || !Array.isArray(uuids) || uuids.length === 0) {
|
||||
return res.status(400).json({ ok: false, error: 'Body must include "uuids" as a non-empty array of agent UUIDs.' });
|
||||
}
|
||||
const { collectSpecificAgents } = require('./collector');
|
||||
console.log(`[Proxy API] Manual trigger: collect agents ${uuids.join(', ')}`);
|
||||
try {
|
||||
const result = await collectSpecificAgents(uuids, delay_ms || 5000);
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /agents
|
||||
* List all agent UUIDs yang sudah tersimpan di MongoDB
|
||||
*/
|
||||
app.get('/agents', async (req, res) => {
|
||||
try {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected.' });
|
||||
}
|
||||
const { DashboardSummary } = require('../backend/models/Schemas');
|
||||
const agents = await DashboardSummary.distinct('agent_uuid');
|
||||
res.json({ ok: true, data: agents });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /domain-details
|
||||
* Fetch live IP/MAC details for a specific domain from Netify API
|
||||
* Fetch live IP/MAC details for a specific domain from MongoDB
|
||||
*/
|
||||
app.get('/domain-details', async (req, res) => {
|
||||
const { domain, agentUuid } = req.query;
|
||||
if (!domain) return res.status(400).json({ ok: false, error: 'domain is required' });
|
||||
|
||||
try {
|
||||
const { Flow, DeviceStat } = require('../backend/models/Schemas');
|
||||
|
||||
const filter = { domain: domain };
|
||||
if (agentUuid) {
|
||||
filter.agent_uuid = agentUuid;
|
||||
}
|
||||
|
||||
const raw = await Flow.find(filter).sort({ timestamp: -1 }).limit(100).lean();
|
||||
if (!raw || !Array.isArray(raw)) {
|
||||
return res.json({ ok: true, data: [] });
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const r of raw) {
|
||||
const ip = r.src_ip;
|
||||
const mac = r.src_mac;
|
||||
if (!ip || !mac) continue;
|
||||
|
||||
const key = `${ip}-${mac}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
let deviceName = 'Unknown Device';
|
||||
try {
|
||||
const dev = await DeviceStat.findOne({ mac_address: mac }).sort({ timestamp: -1 });
|
||||
if (dev && dev.name && dev.name !== 'Unknown Device') {
|
||||
deviceName = dev.name;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore timeout errors
|
||||
}
|
||||
|
||||
results.push({
|
||||
ip,
|
||||
mac,
|
||||
deviceName,
|
||||
lastSeen: r.timestamp || r.last_seen || r.first_seen,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ ok: true, data: results });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /latest
|
||||
* Tampilkan data terbaru dari semua collection (untuk debug)
|
||||
*/
|
||||
app.get('/latest', async (req, res) => {
|
||||
try {
|
||||
const { Summary, AppStat, DeviceStat, Flow, Threat, Event, AppCategoryStat } = require('./models/Schemas');
|
||||
const [summary, apps, devices, flows, threats, events, categories] = await Promise.all([
|
||||
Summary.findOne().sort({ timestamp: -1 }).lean(),
|
||||
AppStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
DeviceStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Flow.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Threat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Event.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
AppCategoryStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
]);
|
||||
res.json({ ok: true, data: { summary, apps, devices, flows, threats, events, categories } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Boot Sequence ─────────────────────────────────────────────────────────────
|
||||
async function main() {
|
||||
console.log('\n╔════════════════════════════════════════════════╗');
|
||||
console.log('║ BackOne Proxy Server - Starting ║');
|
||||
console.log('╚════════════════════════════════════════════════╝\n');
|
||||
console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`);
|
||||
if (process.env.PROXY_COLLECT_MODE === 'agent') {
|
||||
console.log(`[Proxy] Agent UUID: ${process.env.PROXY_AGENT_UUID || '(not set!)'}`);
|
||||
}
|
||||
if (process.env.PROXY_COLLECT_MODE === 'agents') {
|
||||
const agents = (process.env.PROXY_AGENT_UUIDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
console.log(`[Proxy] Agent UUIDs: ${agents.length > 0 ? agents.join(', ') : '(not set!)'}`);
|
||||
console.log(`[Proxy] Agent delay: ${parseInt(process.env.PROXY_AGENT_DELAY_MS || '5000')}ms`);
|
||||
}
|
||||
|
||||
// 1. Start REST API server FIRST (so /health is always available)
|
||||
|
||||
// Start REST API server first so liveness probes remain active
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n🚀 Proxy REST API running at http://0.0.0.0:${PORT}`);
|
||||
console.log(` GET /health → liveness check`);
|
||||
console.log(` GET /status → scheduler + DB status`);
|
||||
console.log(` GET /agents → list agent UUIDs in MongoDB`);
|
||||
console.log(` POST /collect/all → trigger manual collect all`);
|
||||
console.log(` POST /collect/:agentUuid → trigger manual collect specific`);
|
||||
console.log(` POST /collect/agents → trigger manual collect multiple\n`);
|
||||
console.log(` GET /status → scheduler + DB status\n`);
|
||||
});
|
||||
|
||||
// 2. Connect to MongoDB (retry in background)
|
||||
const connected = await connectDB();
|
||||
|
||||
// 3. Start scheduler only if DB connected
|
||||
if (connected) {
|
||||
scheduler.startScheduler();
|
||||
console.log('\n[Proxy] ✓ Scheduler started. Data collection is active.');
|
||||
console.log('[Proxy] ✓ First collection will run in 2 seconds after MongoDB is ready.\n');
|
||||
console.log('\n[Proxy] ✓ Scheduler started. Data collection is active.\n');
|
||||
} else {
|
||||
console.error('\n[Proxy] ✗ Could not connect to MongoDB. Scheduler NOT started.');
|
||||
console.error('[Proxy] ✗ To fix: ensure MongoDB is running at', MONGODB_URI);
|
||||
console.error('[Proxy] ✗ Then restart the proxy server.\n');
|
||||
console.error('\n[Proxy] ✗ Could not connect to MongoDB. Scheduler NOT started.\n');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[Proxy] Fatal startup error:', err);
|
||||
// Don't exit — let REST API still serve /health
|
||||
});
|
||||
+5
-80
@@ -78,7 +78,7 @@ const FlowSchema = new mongoose.Schema({
|
||||
site_uuid: { type: String, index: true },
|
||||
flow_id: String,
|
||||
src_ip: { type: String, index: true },
|
||||
src_mac: String,
|
||||
src_mac: { type: String, index: true }, // indexed for MAC-to-IP resolution in events
|
||||
dst_ip: { type: String, index: true },
|
||||
dst_port: Number,
|
||||
protocol: String,
|
||||
@@ -131,52 +131,6 @@ const EventSchema = new mongoose.Schema({
|
||||
event_at: Date,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Compound indexes for common dashboard queries ─────────────────────────────
|
||||
// ─── TLS Versions (per agent) ──────────────────────────────────────────────────
|
||||
const TlsVersionStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_version: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Ciphers (per agent) ───────────────────────────────────────────────────
|
||||
const TlsCipherStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_cipher: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Security (per agent) ──────────────────────────────────────────────────
|
||||
const TlsSecurityStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_security: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Country Traffic Stats (per agent) ────────────────────────────────────────
|
||||
const CountryStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
country_code: { type: String, required: true },
|
||||
country_name: { type: String, default: '' },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Compound indexes for common dashboard queries ─────────────────────────────
|
||||
SummarySchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 });
|
||||
@@ -187,15 +141,7 @@ FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
|
||||
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
EventSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsVersionStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsCipherStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsSecurityStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
CountryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
|
||||
const CustomDeviceLabelSchema = new mongoose.Schema({
|
||||
mac_address: { type: String, required: true, unique: true, index: true },
|
||||
device_label: { type: String, required: true },
|
||||
}, baseOptions);
|
||||
FlowSchema.index({ site_uuid: 1, src_mac: 1, timestamp: -1 }); // for MAC-to-IP resolution
|
||||
|
||||
// ── Per-Device Per-Application Stats ────────────────────────────────────────
|
||||
// Collected from DPI API: /data/stats/top/application/download with filter_local_ips
|
||||
@@ -216,25 +162,8 @@ DeviceAppStatSchema.index({ agent_uuid: 1, ip_address: 1, timestamp: -1 });
|
||||
DeviceAppStatSchema.index({ ip_address: 1, app_label: 1, timestamp: -1 });
|
||||
DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
|
||||
|
||||
// ─── Lookup App Dictionary ────────────────────────────────────────────────────
|
||||
const LookupAppSchema = new mongoose.Schema({
|
||||
id: { type: Number, required: true, unique: true, index: true },
|
||||
tag: String,
|
||||
label: { type: String, index: true },
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: String,
|
||||
favicon: String,
|
||||
icon: String,
|
||||
logo: String,
|
||||
application_category: Object
|
||||
}, baseOptions);
|
||||
LookupAppSchema.index({ label: 1, tag: 1 });
|
||||
|
||||
|
||||
// ─── DPI Telemetry Property Schemas (SNI, SSL, QUIC, SSH, mDNS, DHCP, UA, BT)
|
||||
// Split into SchemasTelemetry.js to keep this file under 256 lines.
|
||||
const telemetrySchemas = require('./SchemasTelemetry');
|
||||
const auxSchemas = require('./SchemasAux');
|
||||
|
||||
module.exports = {
|
||||
Summary: mongoose.model('Summary', SummarySchema),
|
||||
@@ -244,14 +173,10 @@ module.exports = {
|
||||
DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema),
|
||||
Flow: mongoose.model('Flow', FlowSchema),
|
||||
Threat: mongoose.model('Threat', ThreatSchema),
|
||||
CustomDeviceLabel: mongoose.model('CustomDeviceLabel', CustomDeviceLabelSchema),
|
||||
AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema),
|
||||
Event: mongoose.model('Event', EventSchema),
|
||||
TlsVersionStat: mongoose.model('TlsVersionStat', TlsVersionStatSchema),
|
||||
TlsCipherStat: mongoose.model('TlsCipherStat', TlsCipherStatSchema),
|
||||
TlsSecurityStat: mongoose.model('TlsSecurityStat', TlsSecurityStatSchema),
|
||||
CountryStat: mongoose.model('CountryStat', CountryStatSchema),
|
||||
LookupApp: mongoose.model('LookupApp', LookupAppSchema),
|
||||
...auxSchemas,
|
||||
...telemetrySchemas,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// proxy/models/SchemasAux.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Auxiliary MongoDB Schemas to maintain Schemas.js under 256 lines limit.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const baseOptions = {
|
||||
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
||||
};
|
||||
|
||||
// ─── TLS Versions (per agent) ──────────────────────────────────────────────────
|
||||
const TlsVersionStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_version: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Ciphers (per agent) ───────────────────────────────────────────────────
|
||||
const TlsCipherStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_cipher: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── TLS Security (per agent) ──────────────────────────────────────────────────
|
||||
const TlsSecurityStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
tls_security: { type: String, required: true },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Country Traffic Stats (per agent) ────────────────────────────────────────
|
||||
const CountryStatSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, required: true, index: true, expires: '7d' },
|
||||
agent_uuid: { type: String, index: true },
|
||||
site_uuid: { type: String, index: true },
|
||||
country_code: { type: String, required: true },
|
||||
country_name: { type: String, default: '' },
|
||||
download: Number,
|
||||
upload: Number,
|
||||
flows: Number,
|
||||
}, baseOptions);
|
||||
|
||||
const CustomDeviceLabelSchema = new mongoose.Schema({
|
||||
mac_address: { type: String, required: true, unique: true, index: true },
|
||||
device_label: { type: String, required: true },
|
||||
}, baseOptions);
|
||||
|
||||
// ─── View As Audit Logs ────────────────────────────────────────────────────────
|
||||
const ViewAsLogSchema = new mongoose.Schema({
|
||||
timestamp: { type: Date, default: Date.now, index: true },
|
||||
admin_id: { type: String, required: true },
|
||||
admin_username: { type: String, required: true },
|
||||
admin_role: String,
|
||||
agent_uuid: { type: String, required: true },
|
||||
agent_label: String,
|
||||
end_timestamp: Date,
|
||||
duration: Number, // duration in seconds
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Lookup App Dictionary ────────────────────────────────────────────────────
|
||||
const LookupAppSchema = new mongoose.Schema({
|
||||
id: { type: Number, required: true, unique: true, index: true },
|
||||
tag: String,
|
||||
label: { type: String, index: true },
|
||||
name: String,
|
||||
full_name: String,
|
||||
description: String,
|
||||
favicon: String,
|
||||
icon: String,
|
||||
logo: String,
|
||||
application_category: Object
|
||||
}, baseOptions);
|
||||
|
||||
// ─── Tenant Configuration (Dynamic Branding per site_uuid) ─────────────────────
|
||||
const TenantConfigSchema = new mongoose.Schema({
|
||||
site_uuid: { type: String, required: true, unique: true, index: true },
|
||||
brand_name: { type: String, required: true },
|
||||
brand_logo: { type: String, required: true },
|
||||
footer_copyright: { type: String, required: true },
|
||||
primary_color: { type: String, default: '#E11D48' }
|
||||
}, baseOptions);
|
||||
|
||||
const CustomAgentLocationSchema = new mongoose.Schema({
|
||||
agent_uuid: { type: String, required: true, unique: true, index: true },
|
||||
site_uuid: { type: String, required: true, index: true },
|
||||
latitude: { type: Number, required: true },
|
||||
longitude: { type: Number, required: true },
|
||||
label: { type: String, default: '' },
|
||||
}, baseOptions);
|
||||
|
||||
const BlacklistRuleSchema = new mongoose.Schema({
|
||||
site_uuid: { type: String, required: true, index: true },
|
||||
agent_uuid: { type: String, required: true, index: true },
|
||||
type: { type: String, required: true, enum: ['category', 'domain'] },
|
||||
value: { type: String, required: true },
|
||||
is_active: { type: Boolean, default: true }
|
||||
}, baseOptions);
|
||||
|
||||
// Set compound indexes
|
||||
TlsVersionStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsCipherStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
TlsSecurityStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
CountryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
|
||||
LookupAppSchema.index({ label: 1, tag: 1 });
|
||||
BlacklistRuleSchema.index({ site_uuid: 1, agent_uuid: 1, type: 1, value: 1 }, { unique: true });
|
||||
|
||||
module.exports = {
|
||||
TlsVersionStat: mongoose.model('TlsVersionStat', TlsVersionStatSchema),
|
||||
TlsCipherStat: mongoose.model('TlsCipherStat', TlsCipherStatSchema),
|
||||
TlsSecurityStat: mongoose.model('TlsSecurityStat',TlsSecurityStatSchema),
|
||||
CountryStat: mongoose.model('CountryStat', CountryStatSchema),
|
||||
CustomDeviceLabel:mongoose.model('CustomDeviceLabel',CustomDeviceLabelSchema),
|
||||
ViewAsLog: mongoose.model('ViewAsLog', ViewAsLogSchema),
|
||||
LookupApp: mongoose.model('LookupApp', LookupAppSchema),
|
||||
TenantConfig: mongoose.model('TenantConfig', TenantConfigSchema),
|
||||
CustomAgentLocation: mongoose.model('CustomAgentLocation', CustomAgentLocationSchema),
|
||||
BlacklistRule: mongoose.model('BlacklistRule', BlacklistRuleSchema),
|
||||
};
|
||||
+39
-9
@@ -1,10 +1,9 @@
|
||||
// proxy/netifyClient.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Clean DPI API wrapper for the BackOne Proxy Server
|
||||
// Re-exports modules split into logical units to satisfy the 256-line limit.
|
||||
// DPI API wrapper for the BackOne Proxy Server targeting original Netify API.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const { netifyFetch, agentMap } = require('./netifyClientCore');
|
||||
const { netifyFetch, BASE_URL, agentMap } = require('./netifyClientCore');
|
||||
const telemetry = require('./netifyTelemetry');
|
||||
|
||||
const PORT_SERVICE_MAP = {
|
||||
@@ -21,19 +20,48 @@ const PORT_SERVICE_MAP = {
|
||||
};
|
||||
|
||||
async function fetchAgents(siteUuid = null) {
|
||||
const data = await netifyFetch('/data/stats/top/agent/download', {
|
||||
filter_interval: 43200, settings_limit: 100
|
||||
}, null, siteUuid);
|
||||
const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, null, siteUuid);
|
||||
if (!data || !Array.isArray(data)) return [];
|
||||
const list = data
|
||||
.map(r => ({ id: r.agent?.id, uuid: r.agent?.uuid, label: r.agent?.label }))
|
||||
.filter(a => a.uuid);
|
||||
const list = data.map(r => ({
|
||||
id: r.agent?.id,
|
||||
uuid: r.agent?.uuid,
|
||||
label: r.agent?.label,
|
||||
})).filter(a => a.uuid);
|
||||
|
||||
for (const a of list) {
|
||||
if (a.uuid && a.id) agentMap[a.uuid] = a.id;
|
||||
}
|
||||
|
||||
// Secondary validation: if this site already has data in MongoDB, only return agents
|
||||
// that have at least one summary record for THIS site_uuid. This prevents the
|
||||
// org-level stats endpoint from cross-contaminating agents across sites.
|
||||
if (siteUuid) {
|
||||
try {
|
||||
const mongoose = require('mongoose');
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
const db = mongoose.connection.db;
|
||||
const knownAgents = await db.collection('summaries').distinct('agent_uuid', {
|
||||
site_uuid: siteUuid,
|
||||
agent_uuid: { $ne: null },
|
||||
});
|
||||
|
||||
if (knownAgents.length > 0) {
|
||||
const knownSet = new Set(knownAgents);
|
||||
const validated = list.filter(a => knownSet.has(a.uuid));
|
||||
// If MongoDB cross-check yields results, use the validated list.
|
||||
// On first boot (no DB data yet), fall through and use the full API list.
|
||||
if (validated.length > 0) return validated;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Collector] fetchAgents DB cross-check failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
async function fetchBandwidthSummary(interval = 1440, agentUuid = null, siteUuid = null) {
|
||||
const [dlData, ulData, flowsData] = await Promise.all([
|
||||
netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid),
|
||||
@@ -237,5 +265,7 @@ module.exports = {
|
||||
fetchFlows,
|
||||
fetchCyberThreats,
|
||||
fetchEvents,
|
||||
BASE_URL,
|
||||
PORT_SERVICE_MAP,
|
||||
...telemetry,
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
// proxy/netifyClientCore.js
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Core fetch and authentication helpers for Netify DPI API.
|
||||
// Split from netifyClient.js to keep file sizes under 256 lines.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const path = require('path');
|
||||
@@ -10,7 +9,6 @@ const axios = require('axios');
|
||||
|
||||
const BASE_URL = process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1';
|
||||
const JWT_TOKEN = process.env.NETIFY_TOKEN || process.env.NETIFY_JWT_TOKEN;
|
||||
// In-memory map: agent_uuid -> numeric agent ID
|
||||
const agentMap = {};
|
||||
|
||||
function getHeaders(siteUuid) {
|
||||
@@ -39,13 +37,16 @@ function fixDates(obj) {
|
||||
async function netifyFetch(endpoint, params = {}, agentUuid = null, siteUuid = null) {
|
||||
if (agentUuid) {
|
||||
const agentId = agentMap[agentUuid];
|
||||
if (agentId) params.filter_agents = `[${agentId}]`;
|
||||
else params.settings_agent = agentUuid;
|
||||
if (agentId) {
|
||||
params.filter_agents = `[${agentId}]`;
|
||||
} else {
|
||||
params.settings_agent = agentUuid;
|
||||
}
|
||||
}
|
||||
|
||||
const token = process.env.NETIFY_API_KEY || JWT_TOKEN;
|
||||
if (!token || !siteUuid) {
|
||||
console.error(`[DpiClient] Missing DPI_API_KEY or siteUuid for endpoint ${endpoint}`);
|
||||
if (!token) {
|
||||
console.error(`[DpiClient] Missing DPI_API_KEY for endpoint ${endpoint}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,9 +55,8 @@ async function netifyFetch(endpoint, params = {}, agentUuid = null, siteUuid = n
|
||||
headers: getHeaders(siteUuid), params, timeout: 30000,
|
||||
});
|
||||
const json = res.data;
|
||||
|
||||
if (json?.status_code !== 0) {
|
||||
// Netify returns 200 when there is simply no data in the requested timeframe
|
||||
if (json?.status_code === 200) return [];
|
||||
console.error(`[DpiClient] API Error ${json?.status_code} on ${endpoint}: ${json?.status_message || 'No message'}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
// proxy/routes.js
|
||||
// ─── REST API Routes for BackOne Proxy Server ───────────────────────────────
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const mongoose = require('mongoose');
|
||||
const scheduler = require('./scheduler');
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
* Liveness check
|
||||
*/
|
||||
router.get('/health', (req, res) => {
|
||||
const dbState = mongoose.connection.readyState;
|
||||
const dbLabel = ['disconnected', 'connected', 'connecting', 'disconnecting'][dbState] || 'unknown';
|
||||
res.json({
|
||||
ok: dbState === 1,
|
||||
service: 'BackOne Proxy Server',
|
||||
db: dbLabel,
|
||||
mode: process.env.PROXY_COLLECT_MODE || 'all',
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /status
|
||||
* Full scheduler + DB status
|
||||
*/
|
||||
router.get('/status', (req, res) => {
|
||||
const dbState = mongoose.connection.readyState;
|
||||
res.json({
|
||||
ok: true,
|
||||
db_state: ['disconnected', 'connected', 'connecting', 'disconnecting'][dbState] || 'unknown',
|
||||
...scheduler.getStatus(),
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/all
|
||||
* Manual trigger — collect ALL agents sekarang
|
||||
*/
|
||||
router.post('/collect/all', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { collectAllAgents } = require('./collector');
|
||||
console.log('[Proxy API] Manual trigger: collect ALL agents');
|
||||
try {
|
||||
const result = await collectAllAgents();
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/:agentUuid
|
||||
* Manual trigger — collect ONE specific agent
|
||||
*/
|
||||
router.post('/collect/:agentUuid', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { agentUuid } = req.params;
|
||||
const { collectSpecificAgent } = require('./collector');
|
||||
console.log(`[Proxy API] Manual trigger: collect agent ${agentUuid}`);
|
||||
try {
|
||||
const result = await collectSpecificAgent(agentUuid);
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /collect/agents
|
||||
* Manual trigger — collect multiple specific agents
|
||||
* Body: { "uuids": ["uuid1", "uuid2", ...], "delay_ms": 5000 }
|
||||
*/
|
||||
router.post('/collect/agents', async (req, res) => {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected. Cannot collect.' });
|
||||
}
|
||||
const { uuids, delay_ms } = req.body;
|
||||
if (!uuids || !Array.isArray(uuids) || uuids.length === 0) {
|
||||
return res.status(400).json({ ok: false, error: 'Body must include "uuids" as a non-empty array of agent UUIDs.' });
|
||||
}
|
||||
const { collectSpecificAgents } = require('./collector');
|
||||
console.log(`[Proxy API] Manual trigger: collect agents ${uuids.join(', ')}`);
|
||||
try {
|
||||
const result = await collectSpecificAgents(uuids, delay_ms || 5000);
|
||||
res.json({ ok: result.success, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /agents
|
||||
* List all agent UUIDs yang sudah tersimpan di MongoDB
|
||||
*/
|
||||
router.get('/agents', async (req, res) => {
|
||||
try {
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return res.status(503).json({ ok: false, error: 'MongoDB not connected.' });
|
||||
}
|
||||
const { Summary } = require('./models/Schemas');
|
||||
const agents = await Summary.distinct('agent_uuid');
|
||||
res.json({ ok: true, data: agents });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /domain-details
|
||||
* Fetch live IP/MAC details for a specific domain from MongoDB
|
||||
*/
|
||||
router.get('/domain-details', async (req, res) => {
|
||||
const { domain, agentUuid } = req.query;
|
||||
if (!domain) return res.status(400).json({ ok: false, error: 'domain is required' });
|
||||
|
||||
try {
|
||||
const { Flow, DeviceStat } = require('./models/Schemas');
|
||||
|
||||
const filter = { domain: domain };
|
||||
if (agentUuid) {
|
||||
filter.agent_uuid = agentUuid;
|
||||
}
|
||||
|
||||
const raw = await Flow.find(filter).sort({ timestamp: -1 }).limit(100).lean();
|
||||
if (!raw || !Array.isArray(raw)) {
|
||||
return res.json({ ok: true, data: [] });
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const r of raw) {
|
||||
const ip = r.src_ip;
|
||||
const mac = r.src_mac;
|
||||
if (!ip || !mac) continue;
|
||||
|
||||
const key = `${ip}-${mac}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
let deviceName = 'Unknown Device';
|
||||
try {
|
||||
const dev = await DeviceStat.findOne({ mac_address: mac }).sort({ timestamp: -1 });
|
||||
if (dev && dev.name && dev.name !== 'Unknown Device') {
|
||||
deviceName = dev.name;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore timeout errors
|
||||
}
|
||||
|
||||
results.push({
|
||||
ip,
|
||||
mac,
|
||||
deviceName,
|
||||
lastSeen: r.timestamp || r.last_seen || r.first_seen,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ ok: true, data: results });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /latest
|
||||
* Tampilkan data terbaru dari semua collection (untuk debug)
|
||||
*/
|
||||
router.get('/latest', async (req, res) => {
|
||||
try {
|
||||
const { Summary, AppStat, DeviceStat, Flow, Threat, Event, AppCategoryStat } = require('./models/Schemas');
|
||||
const [summary, apps, devices, flows, threats, events, categories] = await Promise.all([
|
||||
Summary.findOne().sort({ timestamp: -1 }).lean(),
|
||||
AppStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
DeviceStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Flow.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Threat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
Event.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
AppCategoryStat.find().sort({ timestamp: -1 }).limit(5).lean(),
|
||||
]);
|
||||
res.json({ ok: true, data: { summary, apps, devices, flows, threats, events, categories } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+1
-1
@@ -55,7 +55,7 @@ async function runCollection() {
|
||||
const now = Date.now();
|
||||
if (now - lastCapacityLogAt >= CAPACITY_LOG_INTERVAL_MS) {
|
||||
lastCapacityLogAt = now;
|
||||
const { logCapacityStats } = require('../backend/db/capacityTracker');
|
||||
const { logCapacityStats } = require('./db/capacityTracker');
|
||||
await logCapacityStats(`[PROXY] [MongoDB] Capacity Used after Run #${runCount}:`);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user