diff --git a/.gitignore b/.gitignore index 041ff12..3993065 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,19 @@ CLAUDE.md docs/ plans/ .env* + +# Local uploads +backend/public/api/uploads/ + +# Sensitive helper scripts (contain hardcoded SSH/API credentials - local use only) +compare-netify-vs-dashboard.js +ssh-read-source1-proxy.js +check-frontend-uri-now.js +verify-final.js +check-frontend-uri.js +ssh-check-logs.js +ssh-*.js + +# Sensitive documentation (contains production API keys / credentials) +BUKTI-AKSES-MONGODB.txt +DOKUMENTASI-PROXY-NETIFY.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/DOKUMENTASI-FILTER-PER-SITE.md b/DOKUMENTASI-FILTER-PER-SITE.md new file mode 100644 index 0000000..79150ac --- /dev/null +++ b/DOKUMENTASI-FILTER-PER-SITE.md @@ -0,0 +1,252 @@ +# DETAIL TEKNIS: Cara Proxy Memfilter Data per Site (SIAB vs Office) + +Dokumen ini menjelaskan **secara kode** bagaimana data dipisahkan per site. +Ada **3 lapis filter** yang bekerja dari Netify API sampai ke tampilan dashboard. + +--- + +## LAPIS 1 — Saat Minta Data ke Netify API +### File: `proxy/netifyClientCore.js` + +``` +NETIFY_SITE_UUIDS = "6681452d_....(SIAB), 1959bb55_....(Office)" + | + proxy loop satu per satu: + ┌─────────────────────────┐ + │ for SIAB UUID: │ + │ kirim request ke │ + │ Netify dengan header │ + │ x-net-site: SIAB-UUID│ + └─────────────────────────┘ + ┌─────────────────────────┐ + │ for Office UUID: │ + │ kirim request ke │ + │ Netify dengan header │ + │ x-net-site: OFFICE-UUID│ + └─────────────────────────┘ +``` + +**KODE ASLI — cara header dikirim:** +```javascript +// proxy/netifyClientCore.js baris 14-18 +function getHeaders(siteUuid) { + const headers = { + 'x-api-key': process.env.NETIFY_API_KEY, + 'Accept': 'application/json' + }; + + if (siteUuid) headers['x-net-site'] = siteUuid; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // Ini yang memfilter data di sisi Netify! + // Netify API hanya kembalikan data untuk site ini saja. + + return headers; +} + +async function netifyFetch(endpoint, params = {}, agentUuid, siteUuid) { + const res = await axios.get(`${BASE_URL}${endpoint}`, { + headers: getHeaders(siteUuid), // <--- siteUuid dikirim ke Netify + params, + timeout: 30000, + }); +} +``` + +**Artinya:** Netify API sendiri yang memfilter. Kalau kita kirim header +`x-net-site: SIAB-UUID`, Netify HANYA kembalikan data milik SIAB. +Kita tidak perlu filter manual — Netify sudah filter dari sumbernya. + +--- + +## LAPIS 2 — Saat Simpan ke MongoDB +### File: `proxy/collector.js` (loop utama) + +Setelah data dari Netify masuk, setiap dokumen diberi **stempel `site_uuid`** +sebelum disimpan ke MongoDB. + +**KODE ASLI — loop per site di collector.js:** +```javascript +// proxy/collector.js baris 123-222 + +// SITE_UUIDS diambil dari env: +// NETIFY_SITE_UUIDS="6681452d_..., 1959bb55_..." +const SITE_UUIDS = SITE_UUIDS_STR.split(','); // ["SIAB-UUID", "OFFICE-UUID"] + +for (const siteUuid of SITE_UUIDS) { +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// Loop: pertama SIAB, lalu Office (satu per satu) + + console.log(`Fetching agents for Site: ${siteUuid}`); + const agents = await netify.fetchAgents(siteUuid); + // ^^^^^^^^^ + // fetchAgents pakai siteUuid → Netify hanya beri agent milik site ini + + // --- PENTING: Anti-duplikat antar site --- + // Kadang Netify bisa kembalikan agent yang sama untuk 2 site. + // Di sini kita cegah agar 1 agent hanya masuk 1 site. + const agents = rawAgents.filter(a => { + if (processedAgentUuids.has(a.uuid)) { + console.log(`Skipping ${a.uuid} — already assigned to another site.`); + return false; // lewati agent yang sudah diproses site lain + } + return true; + }); + for (const agent of agents) processedAgentUuids.add(agent.uuid); + + // Simpan agent ke MongoDB dengan site_uuid + await AgentRegistry.findOneAndUpdate( + { uuid: agent.uuid }, + { $set: { + uuid: agent.uuid, + site_uuid: siteUuid, // <--- stempel site di sini! + ... + }}, + { upsert: true } + ); + + // Kumpulkan data untuk setiap agent di site ini + for (const agent of agents) { + await collectForAgent(agent.uuid, timestamp, siteUuid); + // ^^^^^^^^^ + // siteUuid terus dibawa ke setiap fungsi collect + } +} +``` + +**KODE ASLI — cara flows disimpan dengan site_uuid:** +```javascript +// proxy/collectorHelperDpi2.js baris 88-98 + +const flowDocs = flows.map(f => ({ + timestamp, + agent_uuid: agentUuid, // siapa agent-nya + site_uuid: SITE_UUID, // <--- data ini milik site mana! (SIAB atau Office) + flow_id: f.flow_id, + src_ip: f.src_ip, + dst_ip: f.dst_ip, + download: f.download, + upload: f.upload, + // ... +})); + +// Upsert ke MongoDB (tidak duplikat berdasarkan flow_id + agent_uuid) +await Flow.bulkWrite(flowDocs.map(f => ({ + updateOne: { + filter: { flow_id: f.flow_id, agent_uuid: f.agent_uuid }, + update: { $set: f }, + upsert: true, + } +}))); +``` + +**Hasilnya di MongoDB — data terpisah per site:** +``` +Collection: flows +┌────────────────────┬────────────────────────────────────────────────────┬────────┬──────────┐ +│ flow_id │ site_uuid │ src_ip │ download │ +├────────────────────┼────────────────────────────────────────────────────┼────────┼──────────┤ +│ flow-001 │ 6681452d_9cae_4ff4_8ae8_0d504774265e (SIAB) │ 10.0.x │ 1234 │ +│ flow-002 │ 6681452d_9cae_4ff4_8ae8_0d504774265e (SIAB) │ 10.0.x │ 5678 │ +│ flow-003 │ 1959bb55_045b_47c7_bbdd_f33b7db197b9 (Office) │ 192.168.x │ 9012 │ +│ flow-004 │ 1959bb55_045b_47c7_bbdd_f33b7db197b9 (Office) │ 192.168.x │ 3456 │ +└────────────────────┴────────────────────────────────────────────────────┴────────┴──────────┘ + ^^^^^^^^^^ Field ini yang memisahkan data ^^^^^^^^^^ +``` + +**Semua collection lain juga sama:** +- `devices` → tiap dokumen ada `site_uuid` +- `threats` → tiap dokumen ada `site_uuid` +- `events` → tiap dokumen ada `site_uuid` +- `summaries` → tiap dokumen ada `site_uuid` +- `telemetry` → tiap dokumen ada `site_uuid` + +--- + +## LAPIS 3 — Saat Dashboard Baca dari MongoDB +### File: `backend/routes/dashboard/flows.js` (contoh) + +Ketika user login sebagai admin SIAB dan buka halaman Flows, +backend hanya query dokumen dengan `site_uuid` yang sesuai: + +```javascript +// backend/routes/dashboard/flows.js (contoh query) +const userSiteUuid = req.user.site_uuid; +// → "6681452d_9cae_4ff4_8ae8_0d504774265e" (SIAB) + +const flows = await Flow.find({ + site_uuid: userSiteUuid, // <--- hanya ambil data site ini! + // ...filter waktu, pagination, dsb +}).limit(50); +``` + +Admin Office login → `site_uuid = 1959bb55_...` → hanya lihat data Office. +Admin SIAB login → `site_uuid = 6681452d_...` → hanya lihat data SIAB. +Super Admin → bisa pilih site mana yang ingin dilihat. + +--- + +## RINGKASAN — Alur Lengkap Filter Data + +``` +Netify API + | + |-- Lapis 1: Header x-net-site dikirim ke Netify + | Netify hanya kirim data milik site tersebut + | + v +Proxy Server (setiap 5 menit) + | + |-- Lapis 2: Setiap dokumen diberi stempel site_uuid + | - SIAB data → { site_uuid: "6681452d_..." } + | - Office data → { site_uuid: "1959bb55_..." } + | - Anti-duplikat: 1 agent hanya masuk 1 site + | + v +MongoDB (semua data tercampur tapi ter-tag per site) + | + |-- Lapis 3: Backend query MongoDB dengan filter site_uuid + | - Admin SIAB login → WHERE site_uuid = SIAB-UUID + | - Admin Office login → WHERE site_uuid = OFFICE-UUID + | + v +Web Dashboard (tampil hanya data site yang sesuai) +``` + +--- + +## Skenario Konkret + +**Skenario:** Network agent "F6-2V-DT-8A" ada di SIAB. Network agent "23-TE-6L-I2" ada di Office. + +### Langkah 1 — Proxy request ke Netify +``` +[Iter 1] siteUuid = "6681452d..." (SIAB) + → GET /data/flows + Header: x-net-site: 6681452d... + → Netify kembalikan: flows dari F6-2V-DT-8A (agent SIAB) + → Simpan ke MongoDB: { site_uuid: "6681452d...", agent_uuid: "F6-2V-DT-8A", flow_id: ... } + +[Iter 2] siteUuid = "1959bb55..." (Office) + → GET /data/flows + Header: x-net-site: 1959bb55... + → Netify kembalikan: flows dari 23-TE-6L-I2 (agent Office) + → Simpan ke MongoDB: { site_uuid: "1959bb55...", agent_uuid: "23-TE-6L-I2", flow_id: ... } +``` + +### Langkah 2 — Dashboard tampilkan +``` +User siab login: + req.user.site_uuid = "6681452d..." + DB query: Flow.find({ site_uuid: "6681452d..." }) + Hasil: hanya flow dari F6-2V-DT-8A ✓ + +User office login: + req.user.site_uuid = "1959bb55..." + DB query: Flow.find({ site_uuid: "1959bb55..." }) + Hasil: hanya flow dari 23-TE-6L-I2 ✓ +``` + +**Data tidak pernah tercampur** karena ada 3 lapis isolasi ini. + +--- +*Dokumentasi teknis Source 2 — 29 Juli 2026* diff --git a/README.md b/README.md index 2b50f81..559c70b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Produk BackOne oleh **PT. Data Bisnis Solusi** — Dashboard monitoring jaringan ## 📡 Proxy — 2 Mode Pengambilan Data -Proxy server (port 4000) mendukung 3 mode yang dikontrol via environment variable: +Proxy server (port 4000) mendukung 2 mode yang dikontrol via environment variable: ### Mode 1: Semua Network Agent (Admin BackOne) @@ -62,26 +62,14 @@ PROXY_AGENT_UUID=2F-TF-1D-GK # UUID Network Agent CPI Balaraja Proxy hanya mengambil data dari **satu Network Agent spesifik** (berdasarkan UUID). Data agent lain tidak pernah masuk ke database. Cocok untuk deployment di sisi client (Pihak A, B, C) agar mereka hanya punya data milik mereka sendiri. -### Mode 3: Beberapa Agent Spesifik (Multi-Agent) - -```env -# .env.local -PROXY_COLLECT_MODE=agents -PROXY_AGENT_UUIDS=UUID-AGENT-A,UUID-AGENT-B,UUID-AGENT-C # comma-separated list -PROXY_AGENT_DELAY_MS=5000 # delay antar agent (default: 5000ms) -``` - -Proxy mengambil data dari **beberapa Network Agent spesifik** (berdasarkan daftar UUID yang dipisahkan koma). Data agent di luar daftar tidak pernah masuk ke database. Delay antar agent dapat diatur dengan `PROXY_AGENT_DELAY_MS` untuk menghindari rate-limit. - ### Contoh Multi-Tenant Deployment -| Deployment | PROXY_COLLECT_MODE | PROXY_AGENT_UUID / PROXY_AGENT_UUIDS | Data yang disimpan | +| Deployment | PROXY_COLLECT_MODE | PROXY_AGENT_UUID | Data yang disimpan | |---|---|---|---| | Kantor BackOne (Admin) | `all` | _(kosong)_ | Semua agent | | Pihak A | `agent` | `UUID-AGENT-A` | Hanya data Pihak A | | Pihak B | `agent` | `UUID-AGENT-B` | Hanya data Pihak B | | Pihak C | `agent` | `UUID-AGENT-C` | Hanya data Pihak C | -| Multi-Client | `agents` | `UUID-A,UUID-B` | Data Pihak A dan B | --- @@ -94,7 +82,6 @@ Proxy mengambil data dari **beberapa Network Agent spesifik** (berdasarkan dafta | GET | `/agents` | List semua agent UUID yang ada di MongoDB | | POST | `/collect/all` | Trigger manual — kumpulkan semua agent | | POST | `/collect/:uuid` | Trigger manual — kumpulkan agent spesifik | -| POST | `/collect/agents` | Trigger manual — kumpulkan multiple agents (body: `{"uuids": [...], "delay_ms": 5000}`) | --- diff --git a/SKILLS.md b/SKILLS.md index 25e2810..5d975a9 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -26,9 +26,7 @@ contract, not just a task description. **Responsibilities** - Implement API/data-layer logic. -- Wire the mock-vs-live routing required by the Demo/Live switch (`AGENTS.md` §5) and - the Cloud/Local endpoint switch (`AGENTS.md` §6) — both must resolve through the - same contract so swapping either setting never changes calling code. +- Ensure all endpoints aggregate real-time data from MongoDB and Netify, avoiding any mock or simulated responses. - Keep business logic out of route handlers; route handlers stay thin. **When invoked**: any task touching data, APIs, or service integration. @@ -39,8 +37,7 @@ QA the list of new/changed endpoints and their expected error modes. ## 3. Frontend Engineer **Responsibilities** -- Implement UI for the task, including the Demo/Live and Cloud/Local switcher - controls where relevant. +- Implement UI for the task. - Consume the Backend's contract rather than reaching around it. - Keep components small and composable, respecting the 256-LOC rule. diff --git a/backend/Dockerfile.bun b/backend/Dockerfile.bun new file mode 100644 index 0000000..2a5595a --- /dev/null +++ b/backend/Dockerfile.bun @@ -0,0 +1,15 @@ +FROM oven/bun:1-alpine + +WORKDIR /app + +COPY backend/package*.json ./ +RUN bun install --production + +COPY backend/ . + +EXPOSE 3001 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \ + CMD bun -e "require('http').get('http://localhost:3001/api/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))" + +CMD ["bun", "run", "server.js"] diff --git a/backend/check_devices.js b/backend/check_devices.js new file mode 100644 index 0000000..e72ab28 --- /dev/null +++ b/backend/check_devices.js @@ -0,0 +1,3 @@ +const db = require('better-sqlite3')('backend/netify_data.db'); +console.log('Devices:', db.prepare("SELECT * FROM devices WHERE ip_address = '192.168.9.2'").all()); +console.log('Discovery:', db.prepare("SELECT * FROM intel_device_discovery WHERE ip_address = '192.168.9.2'").all()); diff --git a/backend/check_events.js b/backend/check_events.js new file mode 100644 index 0000000..d8ef55e --- /dev/null +++ b/backend/check_events.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose'); +require('dotenv').config({path: '../.env.local'}); +mongoose.connect(process.env.MONGODB_URI).then(async () => { + const db = mongoose.connection; + const highEvents = await db.collection('events').find({ + $or: [ + {severity: {$in: ['Critical', 'High']}}, + {category_label: 'Cybersecurity'} + ] + }).toArray(); + + if (highEvents.length > 0) { + console.log("High Events timestamps:"); + highEvents.forEach(e => { + console.log("- event_at:", e.event_at, " | timestamp:", e.timestamp); + }); + } else { + console.log("No high events found in array"); + } + + process.exit(0); +}).catch(e => console.error(e)); diff --git a/backend/check_overview.js b/backend/check_overview.js new file mode 100644 index 0000000..4bb0d7c --- /dev/null +++ b/backend/check_overview.js @@ -0,0 +1,44 @@ +const mongoose = require('mongoose'); + +mongoose.connect('mongodb://backone_user:SusuKudaLiar@103.80.237.29:27017/backone_dpi?authSource=backone_dpi') + .then(async () => { + const db = mongoose.connection.useDb('backone_dpi'); + const yesterday = new Date(Date.now() - 24 * 3600 * 1000); + const SIAB = '6681452d_9cae_4ff4_8ae8_0d504774265e'; + + const catCount = await db.db.collection('appcategorystats').countDocuments({ site_uuid: SIAB, timestamp: { $gte: yesterday } }); + const catSum = await db.db.collection('appcategorystats').aggregate([ + { $match: { site_uuid: SIAB, timestamp: { $gte: yesterday } } }, + { $group: { _id: null, dl: { $sum: '$download' }, ul: { $sum: '$upload' } } } + ]).toArray(); + + const sumCount = await db.db.collection('summaries').countDocuments({ site_uuid: SIAB, timestamp: { $gte: yesterday } }); + const sumSum = await db.db.collection('summaries').aggregate([ + { $match: { site_uuid: SIAB, timestamp: { $gte: yesterday } } }, + { $group: { _id: null, dl: { $sum: '$bandwidth_down' }, ul: { $sum: '$bandwidth_up' } } } + ]).toArray(); + + const flowCount = await db.db.collection('flows').countDocuments({ site_uuid: SIAB, timestamp: { $gte: yesterday } }); + const flowSum = await db.db.collection('flows').aggregate([ + { $match: { site_uuid: SIAB, timestamp: { $gte: yesterday } } }, + { $group: { _id: null, dl: { $sum: '$download' }, ul: { $sum: '$upload' } } } + ]).toArray(); + + // Check latest timestamp in each collection for SIAB + const latestCat = await db.db.collection('appcategorystats').findOne({ site_uuid: SIAB }, { sort: { timestamp: -1 } }); + const latestFlow = await db.db.collection('flows').findOne({ site_uuid: SIAB }, { sort: { timestamp: -1 } }); + const latestSum = await db.db.collection('summaries').findOne({ site_uuid: SIAB }, { sort: { timestamp: -1 } }); + + console.log('=== SIAB Site Data Check (Last 24h) ==='); + console.log('AppCatStats (24h):', catCount, 'docs | Sum:', JSON.stringify(catSum[0])); + console.log('Summaries (24h) :', sumCount, 'docs | Sum:', JSON.stringify(sumSum[0])); + console.log('Flows (24h) :', flowCount, 'docs | Sum:', JSON.stringify(flowSum[0])); + console.log(''); + console.log('=== Latest Timestamps ==='); + console.log('Latest AppCat :', latestCat?.timestamp); + console.log('Latest Flow :', latestFlow?.timestamp); + console.log('Latest Summary :', latestSum?.timestamp); + + mongoose.disconnect(); + }) + .catch(e => { console.error('Error:', e.message); process.exit(1); }); diff --git a/backend/check_server.js b/backend/check_server.js new file mode 100644 index 0000000..0684919 --- /dev/null +++ b/backend/check_server.js @@ -0,0 +1,31 @@ +const { Client } = require('ssh2'); +const conn = new Client(); + +conn.on('ready', () => { + const cmd = [ + 'export PM2=/home/adminbackend/.npm-global/bin/pm2', + '$PM2 list', + 'echo "=== MEMORY ==="', + 'free -m', + 'echo "=== DISK ==="', + 'df -h /', + 'echo "=== FRONTEND LOGS ==="', + '$PM2 logs backone-frontend --lines 20 --nostream 2>&1', + 'echo "=== BACKEND LOGS ==="', + '$PM2 logs backone-backend --lines 10 --nostream 2>&1', + ].join(' && '); + + conn.exec(cmd, (err, stream) => { + if (err) { console.error(err); conn.end(); return; } + stream.on('data', d => process.stdout.write(d.toString())); + stream.stderr.on('data', d => process.stderr.write(d.toString())); + stream.on('close', () => conn.end()); + }); +}).connect({ + host: '103.185.47.52', + port: 2222, + username: 'adminbackend', + password: 'htEo7x6LsBQiEHHH', +}); + +conn.on('error', e => console.error('SSH Error:', e.message)); diff --git a/backend/check_threats.js b/backend/check_threats.js new file mode 100644 index 0000000..e26573d --- /dev/null +++ b/backend/check_threats.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +require('dotenv').config({path: '../.env.local'}); +mongoose.connect(process.env.MONGODB_URI).then(async () => { + const db = mongoose.connection; + const threats = await db.collection('threats').countDocuments(); + const events = await db.collection('events').countDocuments(); + const highEvents = await db.collection('events').countDocuments({ + $or: [ + {severity: {$in: ['Critical', 'High']}}, + {category_label: 'Cybersecurity'} + ] + }); + console.log({threats, events, highEvents}); + process.exit(0); +}).catch(e => console.error(e)); diff --git a/backend/check_types.js b/backend/check_types.js new file mode 100644 index 0000000..fec586b --- /dev/null +++ b/backend/check_types.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); +require('dotenv').config({path: '../.env.local'}); +mongoose.connect(process.env.MONGODB_URI).then(async () => { + const db = mongoose.connection; + const threats = await db.collection('threats').aggregate([{ $group: { _id: '$threat_type', count: { $sum: 1 } } }]).toArray(); + console.log('Threat types:', threats); + const events = await db.collection('events').aggregate([{ $group: { _id: '$event_type', count: { $sum: 1 } } }]).toArray(); + console.log('Event types:', events); + process.exit(0); +}); diff --git a/backend/database.js b/backend/database.js new file mode 100644 index 0000000..dcdc472 --- /dev/null +++ b/backend/database.js @@ -0,0 +1,2146 @@ +// backend/database.js +const Database = require('better-sqlite3'); +const path = require('path'); +const bcrypt = require('bcryptjs'); + +const DB_PATH = path.join(__dirname, 'netify_data.db'); +let db; + +// Agent UUID mappings to MAC addresses and numeric interface IDs +const AGENT_MAC_MAP = { + '2F-TF-1D-GK': ['60:be:b4:1f:05:96'], + '8A-V3-PB-85': [ + 'bc:45:5b:ca:d5:be', 'de:ed:cc:57:58:34', 'aa:b2:5e:30:51:30', + '76:32:c3:dd:b2:bb', '5c:ba:ef:d5:80:a1', '5a:e8:2f:f4:99:3d', + '8e:91:0f:6e:24:63', '12:46:2e:63:2c:b7', '92:df:61:3e:ff:5e', + '14:ea:63:96:40:78', '44:e5:17:b9:0d:07', '78:93:c3:08:41:ea', + '0e:15:c3:8e:29:a8', '58:a0:23:ae:f2:72', 'f2:69:9d:a1:5e:11', + '60:be:b4:2a:39:b0', 'ae:5d:99:33:67:2c' + ], + 'F6-2V-DT-8A': [ + '2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'f4:6d:3f:ef:01:a0', + '60:be:b4:29:d3:36', '60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', + '60:be:b4:29:d3:32', '04:f4:1c:ce:c2:e6' + ], + '1R-79-J9-YE': [ + '70:85:6c:6d:f7:17', '70:85:6c:81:50:d4', 'a2:cc:8e:7d:39:51' + ], +}; + +const AGENT_NUMERIC_IDS = { + '2F-TF-1D-GK': ['4897042839'], + '8A-V3-PB-85': ['4895530456'], + 'F6-2V-DT-8A': ['4894730147'], + '1R-79-J9-YE': ['4895853843'], +}; + +function getAgentTrafficRatio(agentUuid) { + const d = getDB(); + const macs = AGENT_MAC_MAP[agentUuid]; + if (!macs || macs.length === 0) return 0; + + const latest = d.prepare("SELECT MAX(fetched_at) as t FROM mac_bandwidth").get(); + if (!latest?.t) return 0; + + const siteTotal = d.prepare("SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ?").get(latest.t)?.val || 1; + + const placeholders = macs.map(() => '?').join(','); + const agentTotal = d.prepare(`SELECT SUM(total) as val FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${placeholders})`).get(latest.t, ...macs)?.val || 0; + + return siteTotal > 0 ? (agentTotal / siteTotal) : 0; +} + +function getDB() { + if (!db) { + db = new Database(DB_PATH); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + initSchema(); + } + return db; +} + +function initSchema() { + const d = getDB(); + + // ─── AUTHENTICATION ───────────────────────────────────────────────────────── + d.exec(`CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'AGENT_VIEWER', + site_uuid TEXT DEFAULT NULL, + agent_uuid TEXT DEFAULT NULL, + account_name TEXT DEFAULT NULL, + profile_picture TEXT DEFAULT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + )`); + + // Alter users table to add agent_uuid if it was created on an older schema + try { + d.exec("ALTER TABLE users ADD COLUMN agent_uuid TEXT DEFAULT NULL"); + } catch (e) { + // Column already exists, safe to ignore + } + + // Alter bandwidth_timeline table to add new speed columns dynamically if they do not exist + try { + d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN download_speed INTEGER DEFAULT 0"); + } catch (_) {} + try { + d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN upload_speed INTEGER DEFAULT 0"); + } catch (_) {} + try { + d.exec("ALTER TABLE bandwidth_timeline ADD COLUMN flow_speed INTEGER DEFAULT 0"); + } catch (_) {} + + try { + d.exec("ALTER TABLE users ADD COLUMN account_name TEXT DEFAULT NULL"); + d.exec("ALTER TABLE users ADD COLUMN profile_picture TEXT DEFAULT NULL"); + } catch (e) { + // Columns already exist, safe to ignore + } + + const adminExists = d.prepare("SELECT count(*) as count FROM users WHERE username = 'admin'").get(); + if (adminExists.count === 0) { + const hash = bcrypt.hashSync('admin', 10); + d.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)").run('admin', hash, 'SUPER_ADMIN'); + console.log('[DB] Created default admin user (admin / admin)'); + } + + d.exec(`CREATE TABLE IF NOT EXISTS tls_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + tls_version TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_ciphers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + tls_cipher TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_security ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + tls_security TEXT, + color TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS netbios_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // ─── DPI Fields 12-21 ────────────────────────────────────────────────────── + + d.exec(`CREATE TABLE IF NOT EXISTS dhcp_fingerprints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + fingerprint TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS http_user_agents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + user_agent TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS sni_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + sni_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssl_server_cn ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + ssl_server_cn TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS quic_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + quic_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bittorrent_hashes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + info_hash TEXT, + label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssh_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + ssh_version TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mdns_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + mdns_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // ─── Intelligence API 22-30 ──────────────────────────────────────────────── + + d.exec(`CREATE TABLE IF NOT EXISTS intel_crypto_mining ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + pool_host TEXT, pool_ip TEXT, + protocol TEXT, app_label TEXT, + confidence REAL, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_device_discovery ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + device_label TEXT, device_type TEXT, + os_label TEXT, manufacturer TEXT, + is_new INTEGER DEFAULT 1 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_encryption_audit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + device_label TEXT, + encrypted_pct REAL, + unencrypted INTEGER DEFAULT 0, + encrypted INTEGER DEFAULT 0, + total INTEGER DEFAULT 0, + risk_level TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_insecure_protocols ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + protocol TEXT, + ip_address TEXT, mac_address TEXT, + dst_ip TEXT, dst_port INTEGER, + app_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + risk TEXT, + source TEXT DEFAULT 'api' + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_ip_reputation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, local_ip TEXT, + mac_address TEXT, reputation TEXT, + score REAL, country TEXT, + app_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + blacklisted INTEGER DEFAULT 1 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_server_discovery ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + server_type TEXT, hostname TEXT, + port INTEGER, protocol TEXT, + os_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_tor_detection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + exit_node TEXT, circuit_id TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + country TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_unencrypted_passwords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + dst_ip TEXT, dst_port INTEGER, + protocol TEXT, username TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + severity TEXT DEFAULT 'Critical' + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_vpn_detection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + vpn_type TEXT, remote_ip TEXT, + protocol TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + country TEXT, confidence REAL + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS discovery_os ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + os_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // Tabel baru fitur 1-11 + d.exec(`CREATE TABLE IF NOT EXISTS app_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + category_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS continents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + continent_name TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS regions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + region_name TEXT, region_code TEXT, + country_name TEXT, country_code TEXT, + download INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS cities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + city_name TEXT, region_name TEXT, + country_name TEXT, country_code TEXT, + download INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS vlans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + vlan_id INTEGER, vlan_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS interfaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + iface_id INTEGER, iface_name TEXT, + iface_role TEXT, agent_id TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + flow_type_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_origins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + flow_origin_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ip_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + ip_version_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS remote_ips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + remote_ip TEXT, ip_version INTEGER, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mac_bandwidth ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + mac_address TEXT, manufacturer TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_apps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + app_id INTEGER, app_label TEXT, app_tag TEXT, category TEXT, + favicon TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0, flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + mac_address TEXT, ip_address TEXT, device_label TEXT, + device_type TEXT, os_label TEXT, manufacturer TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, last_seen TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + flow_id TEXT, src_ip TEXT, src_mac TEXT, + dst_ip TEXT, dst_port INTEGER, protocol TEXT, + app_label TEXT, domain TEXT, + bytes_download INTEGER DEFAULT 0, bytes_upload INTEGER DEFAULT 0, + first_seen TEXT, last_seen TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS threats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + threat_id TEXT, threat_type TEXT, severity TEXT, + mac_address TEXT, ip_address TEXT, dst_ip TEXT, + app_label TEXT, domain TEXT, description TEXT, detected_at TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_protocols ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + protocol_id INTEGER, protocol_label TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_countries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + country_code TEXT, country_name TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS dns_queries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + domain TEXT, query_count INTEGER DEFAULT 0, + app_label TEXT, category TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + event_id TEXT, event_type TEXT, severity TEXT, + mac_address TEXT, ip_address TEXT, + description TEXT, event_at TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_timeline ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_uuid TEXT, + fetched_at TEXT NOT NULL, + total_download INTEGER DEFAULT 0, total_upload INTEGER DEFAULT 0, + total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0, + download_speed INTEGER DEFAULT 0, upload_speed INTEGER DEFAULT 0, + flow_speed INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS geoip_cache ( + ip_address TEXT PRIMARY KEY, + isp TEXT, + country TEXT, + city TEXT, + as_org TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + )`); + + // Ensure agent_uuid exists in all core data tables + const tables = [ + 'bandwidth_apps', 'devices', 'flows', 'threats', 'bandwidth_protocols', 'bandwidth_countries', + 'dns_queries', 'events', 'bandwidth_timeline', + 'app_categories', 'continents', 'regions', 'cities', + 'vlans', 'interfaces', 'flow_types', 'flow_origins', + 'ip_versions', 'remote_ips', 'mac_bandwidth', + 'tls_versions', 'tls_ciphers', 'tls_security', 'netbios_hostnames', + 'dhcp_fingerprints', 'http_user_agents', 'sni_hostnames', 'ssl_server_cn', + 'quic_hostnames', 'bittorrent_hashes', 'ssh_versions', 'mdns_hostnames', + 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', + 'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery', + 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', + 'discovery_os' + ]; + for (const table of tables) { + try { + d.exec(`ALTER TABLE ${table} ADD COLUMN agent_uuid TEXT DEFAULT NULL`); + } catch (_) {} + } + + console.log('[DB] Schema siap.'); +} + +// ─── INSERT FUNCTIONS ───────────────────────────────────────────────────────── + +function insertBandwidthApps(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_apps + (agent_uuid, site_uuid, fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.app_id ?? null, + r.app_label ?? 'Unknown', + r.app_tag ?? null, + r.category ?? null, + r.favicon ?? null, + r.download ?? 0, + r.upload ?? 0, + r.total ?? (r.download ?? 0) + (r.upload ?? 0), + r.flows ?? 0 + ); + } + })(rows); +} + +function insertDevices(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO devices + (agent_uuid, site_uuid, fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.mac_address ?? null, + r.ip_address ?? null, + r.device_label ?? r.ip_address ?? 'Unknown', + r.device_type ?? null, + r.os_label ?? null, + r.manufacturer ?? null, + r.download ?? 0, + r.upload ?? 0, + r.last_seen ?? fetchedAt + ); + } + })(rows); +} + +function insertFlows(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO flows + (agent_uuid, site_uuid, fetched_at, flow_id, src_ip, src_mac, dst_ip, dst_port, protocol, app_label, domain, bytes_download, bytes_upload, first_seen, last_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.flow_id ?? null, + r.src_ip ?? null, + r.src_mac ?? null, + r.dst_ip ?? null, + r.dst_port ?? null, + r.protocol ?? null, + r.app_label ?? null, + r.domain ?? null, + r.download ?? 0, + r.upload ?? 0, + r.first_seen ?? fetchedAt, + r.last_seen ?? fetchedAt + ); + } + })(rows); +} + +function insertThreats(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO threats + (agent_uuid, site_uuid, fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.threat_id ?? null, + r.threat_type ?? null, + r.severity ?? null, + r.mac_address ?? null, + r.ip_address ?? null, + r.dst_ip ?? null, + r.app_label ?? null, + r.domain ?? null, + r.description ?? null, + r.detected_at ?? fetchedAt + ); + } + })(rows); +} + +function insertProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_protocols + (agent_uuid, site_uuid, fetched_at, protocol_id, protocol_label, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.protocol_id ?? null, + r.protocol_label ?? 'Unknown', + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 + ); + } + })(rows); +} + +function insertCountries(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_countries + (agent_uuid, site_uuid, fetched_at, country_code, country_name, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.country_code ?? null, + r.country_name ?? 'Unknown', + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 + ); + } + })(rows); +} + +function insertDNS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO dns_queries + (agent_uuid, site_uuid, fetched_at, domain, query_count, app_label, category) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.domain ?? null, + r.query_count ?? 0, + r.app_label ?? null, + r.category ?? null + ); + } + })(rows); +} + +function insertEvents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO events + (agent_uuid, site_uuid, fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + agentUuid, siteUuid, fetchedAt, + r.event_id ?? null, + r.event_type ?? null, + r.severity ?? null, + r.mac_address ?? null, + r.ip_address ?? null, + r.description ?? null, + r.event_at ?? fetchedAt + ); + } + })(rows); +} + +function insertBandwidthTimeline(summary, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + d.prepare(` + INSERT INTO bandwidth_timeline + (agent_uuid, site_uuid, fetched_at, total_download, total_upload, total_flows, active_devices, download_speed, upload_speed, flow_speed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + agentUuid, + siteUuid, + fetchedAt, + summary.download ?? 0, + summary.upload ?? 0, + summary.flows ?? 0, + summary.devices ?? 0, + summary.download_speed ?? 0, + summary.upload_speed ?? 0, + summary.flow_speed ?? 0 + ); +} + +// ─── QUERY FUNCTIONS ────────────────────────────────────────────────────────── + +function getLatestBandwidthApps(limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const appsLatest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!appsLatest?.t) return []; + + const rows = agentUuid + ? d.prepare(`SELECT * FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: appsLatest.t, limit }) + : d.prepare(`SELECT * FROM bandwidth_apps WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: appsLatest.t, limit }); + + return correlateAppLabels(rows); +} + +function resolveDeviceMetadata(ip, mac, dbLabel, dbManufacturer, dbType) { + let label = dbLabel || ip; + let manufacturer = dbManufacturer || 'Unknown'; + let type = dbType || 'Generic Client'; + let os = 'Unknown'; + + if (manufacturer.includes('Routerboard') || manufacturer.includes('MikroTik')) { + manufacturer = 'MikroTik'; + type = 'Router/Network'; + os = 'RouterOS'; + } else if (manufacturer.includes('Fortinet')) { + manufacturer = 'Fortinet'; + type = 'Firewall/Network'; + os = 'FortiOS'; + } else if (manufacturer.includes('WatchGuard')) { + manufacturer = 'WatchGuard'; + type = 'Firewall/Network'; + os = 'Fireware'; + } else if (manufacturer.includes('Juniper')) { + manufacturer = 'Juniper'; + type = 'Switch/Network'; + os = 'Junos'; + } else if (manufacturer.includes('Apple')) { + manufacturer = 'Apple'; + type = 'Smart Device'; + os = 'iOS/macOS'; + } else if (manufacturer.includes('Samsung')) { + manufacturer = 'Samsung'; + type = 'Smart TV'; + os = 'Tizen OS'; + } else if (manufacturer.includes('LCFC') || manufacturer.includes('Lenovo')) { + manufacturer = 'Lenovo'; + type = 'Workstation'; + os = 'Windows/Linux'; + } else if (manufacturer.includes('Huawei')) { + manufacturer = 'Huawei'; + type = 'Mobile'; + os = 'Android'; + } else if (manufacturer.includes('Dahua')) { + manufacturer = 'Dahua'; + type = 'IP Camera'; + os = 'Embedded OS'; + } + + const labelLower = label.toLowerCase(); + if (labelLower.includes('windows') || labelLower.includes('microsoft')) { + os = 'Windows'; + type = 'Workstation'; + manufacturer = manufacturer === 'Unknown' ? 'Microsoft' : manufacturer; + } else if (labelLower.includes('apple') || labelLower.includes('iphone') || labelLower.includes('ipad') || labelLower.includes('mac')) { + os = labelLower.includes('mac') ? 'macOS' : 'Apple iOS'; + type = labelLower.includes('mac') ? 'Workstation' : 'Mobile'; + manufacturer = 'Apple'; + } else if (labelLower.includes('android') || labelLower.includes('oppo') || labelLower.includes('samsung phone')) { + os = 'Android'; + type = 'Mobile'; + if (labelLower.includes('oppo')) manufacturer = 'Oppo'; + if (labelLower.includes('samsung')) manufacturer = 'Samsung'; + } else if (labelLower.includes('samsung tv') || labelLower.includes('tizen')) { + os = 'Tizen OS'; + type = 'Smart TV'; + manufacturer = 'Samsung'; + } else if (labelLower.includes('agent device')) { + os = 'Linux'; + type = 'Security Agent'; + manufacturer = 'S-Bluetech'; + } + + const isRouterIP = ['10.6.50.25', '10.6.12.242', '192.168.9.1', '10.6.11.208', '10.6.10.4'].includes(ip); + if (type === 'Router/Network' && !isRouterIP) { + type = 'LAN Client'; + manufacturer = 'Unknown'; + os = 'Windows/Linux'; + } + + return { label, manufacturer, type, os }; +} + +function deviceMatchesAgent(ip, mac, agentUuid) { + if (!agentUuid) return true; + const macs = AGENT_MAC_MAP[agentUuid]; + if (!macs) return false; + + // 1. Direct MAC check + if (macs.includes(mac)) { + // If it is the routed gateway MAC, only allow if the IP belongs to the agent's subnet + if (mac === '04:f4:1c:ce:c2:e6') { + return ip && ip.startsWith('10.6.'); + } + return true; + } + + // 2. Subnet checks for client IPs + if (ip) { + if (agentUuid === '8A-V3-PB-85') { + return ip.startsWith('10.250.0.'); + } + if (agentUuid === 'F6-2V-DT-8A') { + return (ip.startsWith('10.250.') && !ip.startsWith('10.250.0.')) || + ip.startsWith('192.168.') || + ip.startsWith('10.121.') || + ip.startsWith('10.6.'); + } + if (agentUuid === '2F-TF-1D-GK') { + return ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.'); + } + if (agentUuid === '1R-79-J9-YE') { + return ip.startsWith('192.168.201.'); + } + } + + return false; +} + +function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null, search = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const latest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!latest?.t) return []; + + const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); + const intelMap = new Map(intelList.map(i => [i.ip_address, i])); + + // Get active flow bandwidth in latest flows snapshot for this agent + const latestFlowFetch = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + let flowsBandwidth = []; + if (latestFlowFetch?.t) { + flowsBandwidth = agentUuid + ? d.prepare(` + SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload + FROM flows + WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at + GROUP BY src_ip + `).all({ siteUuid, agentUuid, fetched_at: latestFlowFetch.t }) + : d.prepare(` + SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload + FROM flows + WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at + GROUP BY src_ip + `).all({ siteUuid, fetched_at: latestFlowFetch.t }); + } + const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f])); + + // Build historical MAC lookup from ALL flows (not just latest snapshot) + // This catches devices that appeared before with a MAC address + const historicalMacs = agentUuid + ? d.prepare(` + SELECT src_ip, src_mac + FROM flows + WHERE ${siteClause} AND agent_uuid = @agentUuid AND src_mac IS NOT NULL + GROUP BY src_ip + ORDER BY COUNT(*) DESC + `).all({ siteUuid, agentUuid }) + : d.prepare(` + SELECT src_ip, src_mac + FROM flows + WHERE ${siteClause} AND agent_uuid IS NULL AND src_mac IS NOT NULL + GROUP BY src_ip + ORDER BY COUNT(*) DESC + `).all({ siteUuid }); + const historicalMacMap = new Map(historicalMacs.map(f => [f.src_ip, f.src_mac])); + + // Get all devices in latest snapshot from devices table + const devices = agentUuid + ? d.prepare(`SELECT * FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at`).all({ siteUuid, agentUuid, fetched_at: latest.t }) + : d.prepare(`SELECT * FROM devices WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).all({ siteUuid, fetched_at: latest.t }); + + const resolved = []; + const seenIps = new Set(); + + function processDevice(ip, mac, dbDev, flowInfo) { + const intelInfo = intelMap.get(ip); + const dbLabel = dbDev ? dbDev.device_label : (intelInfo ? intelInfo.device_label : null); + const dbMan = dbDev ? dbDev.manufacturer : (intelInfo ? intelInfo.manufacturer : null); + const dbType = intelInfo ? intelInfo.device_type : null; + + // Enrich MAC — fallback chain: + // 1. devices.mac_address (most accurate, from API) + // 2. intel_device_discovery.mac_address (from device discovery intel) + // 3. flows.src_mac latest snapshot + // 4. flows.src_mac historical (all time) + const resolvedMac = mac + || (intelInfo && intelInfo.mac_address ? intelInfo.mac_address : null) + || (flowInfo && flowInfo.src_mac ? flowInfo.src_mac : null) + || historicalMacMap.get(ip) + || null; + + const meta = resolveDeviceMetadata(ip, resolvedMac, dbLabel, dbMan, dbType); + const isRouted = resolvedMac === '04:f4:1c:ce:c2:e6' && ip !== '10.6.50.25' && ip !== '10.6.12.242'; + + const download = flowInfo ? flowInfo.flow_download : (dbDev ? dbDev.download : 0); + const upload = flowInfo ? flowInfo.flow_upload : (dbDev ? dbDev.upload : 0); + + return { + id: dbDev ? dbDev.id : null, + site_uuid: dbDev ? dbDev.site_uuid : siteUuid, + fetched_at: latest.t, + mac_address: resolvedMac, + ip_address: ip, + device_label: meta.label, + device_type: meta.type, + os_label: meta.os, + manufacturer: meta.manufacturer, + download: download || 0, + upload: upload || 0, + total: (download || 0) + (upload || 0), + is_gateway_routed: isRouted ? 1 : 0 + }; + } + + // 1. Process all devices in the devices table + for (const dev of devices) { + if (!dev.ip_address) continue; + if (seenIps.has(dev.ip_address)) continue; + seenIps.add(dev.ip_address); + const flowInfo = flowMap.get(dev.ip_address); + resolved.push(processDevice(dev.ip_address, dev.mac_address, dev, flowInfo)); + } + + // 2. Process any active flow IPs that are NOT present in the devices table + for (const flow of flowsBandwidth) { + if (!flow.src_ip || seenIps.has(flow.src_ip)) continue; + seenIps.add(flow.src_ip); + resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow)); + } + + resolved.sort((a, b) => b.download - a.download); + + // We no longer filter out devices with 0 traffic for agents. + // This allows the Devices page to show offline/idle devices properly. + let filtered = resolved; + + return filtered.slice(0, limit); +} + +function correlateFlows(flows) { + if (!Array.isArray(flows) || flows.length === 0) return flows; + + const d = getDB(); + + // 1. Build cache maps for local IPs and public IPs in history + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + // Matches map for standard ports + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + + return flows.map(f => { + let appLabel = f.app_label; + let domain = f.domain; + + const isPortLabel = !appLabel || appLabel.startsWith("Port ") || appLabel.toLowerCase().includes("port"); + + if (isPortLabel) { + const dstIp = f.dst_ip; + const dstPort = String(f.dst_port); + const proto = f.protocol || "TCP"; + + // Case A: Intranet IP + const isIntranet = dstIp && ( + dstIp.startsWith("10.") || + dstIp.startsWith("192.168.") || + dstIp.startsWith("172.16.") || + dstIp.startsWith("172.17.") || + dstIp.startsWith("172.18.") || + dstIp.startsWith("172.19.") || + dstIp.startsWith("172.20.") || + dstIp.startsWith("172.21.") || + dstIp.startsWith("172.22.") || + dstIp.startsWith("172.23.") || + dstIp.startsWith("172.24.") || + dstIp.startsWith("172.25.") || + dstIp.startsWith("172.26.") || + dstIp.startsWith("172.27.") || + dstIp.startsWith("172.28.") || + dstIp.startsWith("172.29.") || + dstIp.startsWith("172.30.") || + dstIp.startsWith("172.31.") + ); + + if (isIntranet) { + let friendlyName = devMap.get(dstIp); + if (!friendlyName) { + if (dstIp.startsWith("10.250.0.")) { + friendlyName = "IFG Client"; + } else if (dstIp.startsWith("10.6.") || (dstIp.startsWith("10.250.") && !dstIp.startsWith("10.250.0.")) || dstIp.startsWith("192.168.") || dstIp.startsWith("10.121.")) { + friendlyName = "CPI Client"; + } else if ( + dstIp.startsWith("10.0.") || dstIp.startsWith("10.1.") || dstIp.startsWith("10.26.") || + dstIp.startsWith("10.43.") || dstIp.startsWith("10.35.") || dstIp.startsWith("10.21.") || + dstIp.startsWith("10.7.") || dstIp.startsWith("10.182.") || dstIp.startsWith("10.109.") || + dstIp.startsWith("10.181.") || dstIp.startsWith("10.75.") || dstIp.startsWith("10.202.") || + dstIp.startsWith("10.93.") + ) { + friendlyName = "JRP Client"; + } else { + friendlyName = "Intranet Client"; + } + } + appLabel = `${friendlyName} (${dstIp})`; + domain = `Port ${dstPort} (${proto})`; + } else { + // Case B: Public IP + const cached = publicIpMap.get(dstIp); + if (cached) { + appLabel = cached.domain || cached.app_label || appLabel; + domain = `Port ${dstPort} (${proto})`; + } else { + const stdName = matches[dstPort]; + if (stdName) { + appLabel = stdName; + domain = `Port ${dstPort} (${proto})`; + } else { + appLabel = `Public IP: ${dstIp}`; + domain = `Port ${dstPort} (${proto})`; + } + } + } + } + + return { + ...f, + app_label: appLabel, + domain: domain + }; + }); +} + +function correlateAppLabels(apps) { + if (!Array.isArray(apps) || apps.length === 0) return apps; + + const d = getDB(); + + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + function getFriendlyIpName(ip) { + if (devMap.has(ip)) return devMap.get(ip); + if (publicIpMap.has(ip)) { + const pub = publicIpMap.get(ip); + return pub.domain || pub.app_label; + } + if (ip.startsWith('10.6.')) return 'IFG Client'; + if (ip.startsWith('10.250.') || ip.startsWith('192.168.') || ip.startsWith('10.121.')) return 'CPI Client'; + if ( + ip.startsWith('10.0.') || ip.startsWith('10.1.') || ip.startsWith('10.26.') || + ip.startsWith('10.43.') || ip.startsWith('10.35.') || ip.startsWith('10.21.') || + ip.startsWith('10.7.') || ip.startsWith('10.182.') || ip.startsWith('10.109.') || + ip.startsWith('10.181.') || ip.startsWith('10.75.') || ip.startsWith('10.202.') || + ip.startsWith('10.93.') + ) return 'JRP Client'; + return 'Intranet Client'; + } + + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + + return apps.map(app => { + let label = app.app_label; + if (!label) return app; + + const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port"); + + if (isPortLabel) { + const portStr = label.replace("Port ", "").trim(); + + const flow = d.prepare(` + SELECT dst_ip, protocol, dst_port + FROM flows + WHERE app_label = ? OR domain = ? OR dst_port = ? + GROUP BY dst_ip, protocol, dst_port + ORDER BY COUNT(*) DESC + LIMIT 1 + `).get(label, label, portStr); + + if (flow && flow.dst_ip) { + const friendlyName = getFriendlyIpName(flow.dst_ip); + label = `${friendlyName} (Port ${portStr})`; + } else { + const stdName = matches[portStr]; + if (stdName) { + label = `${stdName} (Port ${portStr})`; + } + } + } + + return { + ...app, + app_label: label + }; + }); +} + +function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const latest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!latest?.t) return []; + + const rows = agentUuid + ? d.prepare(`SELECT * FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: latest.t, limit }) + : d.prepare(`SELECT * FROM flows WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY bytes_download DESC LIMIT @limit`).all({ siteUuid, fetched_at: latest.t, limit }); + + const mapped = rows.map(r => ({ + ...r, + download: r.bytes_download ?? 0, + upload: r.bytes_upload ?? 0 + })); + + return correlateFlows(mapped); +} + +function getLatestThreats(limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + return agentUuid + ? d.prepare(`SELECT * FROM threats WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit }) + : d.prepare(`SELECT * FROM threats WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit }); +} + +function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const protoLatest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!protoLatest?.t) return []; + + return agentUuid + ? d.prepare(`SELECT * FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: protoLatest.t, limit }) + : d.prepare(`SELECT * FROM bandwidth_protocols WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: protoLatest.t, limit }); +} + +function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const countryLatest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!countryLatest?.t) return []; + + return agentUuid + ? d.prepare(`SELECT * FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: countryLatest.t, limit }) + : d.prepare(`SELECT * FROM bandwidth_countries WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit`).all({ siteUuid, fetched_at: countryLatest.t, limit }); +} + +function getLatestDNS(limit = 20, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const dnsLatest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!dnsLatest?.t) return []; + + return agentUuid + ? d.prepare(`SELECT * FROM dns_queries WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: dnsLatest.t, limit }) + : d.prepare(`SELECT * FROM dns_queries WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY query_count DESC LIMIT @limit`).all({ siteUuid, fetched_at: dnsLatest.t, limit }); +} + +function getLatestEvents(limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + return agentUuid + ? d.prepare(`SELECT * FROM events WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit }) + : d.prepare(`SELECT * FROM events WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit }); +} + +function getBandwidthTimeline(points = 60, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + const rows = agentUuid + ? d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, agentUuid, limit: points }) + : d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ siteUuid, limit: points }); + return rows.reverse(); +} + +function getStats(siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const latestDevFetch = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + const totalDevices = latestDevFetch?.t + ? (agentUuid + ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at AND download > 0`).get({ siteUuid, agentUuid, fetched_at: latestDevFetch.t })?.n ?? 0) + : (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).get({ siteUuid, fetched_at: latestDevFetch.t })?.n ?? 0)) + : 0; + + const latestFlowFetch = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + const activeFlows = latestFlowFetch?.t + ? (agentUuid + ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at`).get({ siteUuid, agentUuid, fetched_at: latestFlowFetch.t })?.n ?? 0) + : (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at`).get({ siteUuid, fetched_at: latestFlowFetch.t })?.n ?? 0)) + : 0; + + const totalThreats = agentUuid + ? (d.prepare(`SELECT COUNT(*) as n FROM threats WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })?.n ?? 0) + : (d.prepare(`SELECT COUNT(*) as n FROM threats WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid })?.n ?? 0); + + const totalEvents = agentUuid + ? (d.prepare(`SELECT COUNT(*) as n FROM events WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid })?.n ?? 0) + : (d.prepare(`SELECT COUNT(*) as n FROM events WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid })?.n ?? 0); + + const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_timeline`).get()?.t ?? null; + const latestBw = agentUuid + ? d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT * FROM bandwidth_timeline WHERE ${siteClause} AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT 1`).get({ siteUuid }); + + return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; +} + +// ─── INSERT FITUR BARU 1-11 ─────────────────────────────────────────────────── +function insertAppCategories(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO app_categories + (agent_uuid, site_uuid, fetched_at, category_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.category_label, r.download, r.upload, r.total); + })(rows); +} + +function insertContinents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO continents + (agent_uuid, site_uuid, fetched_at, continent_name, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.continent_name, r.download, r.upload, r.total); + })(rows); +} + +function insertRegions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO regions + (agent_uuid, site_uuid, fetched_at, region_name, region_code, country_name, country_code, download) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); + })(rows); +} + +function insertCities(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO cities + (agent_uuid, site_uuid, fetched_at, city_name, region_name, country_name, country_code, download) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); + })(rows); +} + +function insertVLANs(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO vlans + (agent_uuid, site_uuid, fetched_at, vlan_id, vlan_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); + })(rows); +} + +function insertInterfaces(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO interfaces + (agent_uuid, site_uuid, fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); + })(rows); +} + +function insertFlowTypes(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_types + (agent_uuid, site_uuid, fetched_at, flow_type_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.flow_type_label, r.download, r.upload, r.total); + })(rows); +} + +function insertFlowOrigins(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_origins + (agent_uuid, site_uuid, fetched_at, flow_origin_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); + })(rows); +} + +function insertIPVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ip_versions + (agent_uuid, site_uuid, fetched_at, ip_version_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.ip_version_label, r.download, r.upload, r.total); + })(rows); +} + +function insertRemoteIPs(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO remote_ips + (agent_uuid, site_uuid, fetched_at, remote_ip, ip_version, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); + })(rows); +} + +function insertMACBandwidth(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mac_bandwidth + (agent_uuid, site_uuid, fetched_at, mac_address, manufacturer, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); + })(rows); +} + +// ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── +function getLatest(table, orderBy = 'download', limit = 50, siteUuid = null, agentUuid = null) { + const d = getDB(); + const siteClause = siteUuid ? 'site_uuid = @siteUuid' : '1=1'; + + const latest = agentUuid + ? d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${siteClause} AND agent_uuid = @agentUuid`).get({ siteUuid, agentUuid }) + : d.prepare(`SELECT MAX(fetched_at) as t FROM ${table} WHERE ${siteClause} AND agent_uuid IS NULL`).get({ siteUuid }); + + if (!latest?.t) return []; + + const rows = agentUuid + ? d.prepare(`SELECT * FROM ${table} WHERE ${siteClause} AND agent_uuid = @agentUuid AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ siteUuid, agentUuid, fetched_at: latest.t, limit }) + : d.prepare(`SELECT * FROM ${table} WHERE ${siteClause} AND agent_uuid IS NULL AND fetched_at = @fetched_at ORDER BY ${orderBy} DESC LIMIT @limit`).all({ siteUuid, fetched_at: latest.t, limit }); + + return rows; +} + +// DPI Fields +function insertTLSVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_versions + (agent_uuid, site_uuid, fetched_at, tls_version, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.tls_version, r.download, r.upload, r.total); + })(rows); +} + +function insertTLSCiphers(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_ciphers + (agent_uuid, site_uuid, fetched_at, tls_cipher, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.tls_cipher, r.download, r.upload, r.total); + })(rows); +} + +function insertTLSSecurity(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_security + (agent_uuid, site_uuid, fetched_at, tls_security, color, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); + })(rows); +} + +function insertNetBIOSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO netbios_hostnames + (agent_uuid, site_uuid, fetched_at, hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertDiscoveryOS(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO discovery_os + (agent_uuid, site_uuid, fetched_at, os_label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.os_label, r.download, r.upload, r.total); + })(rows); +} + +// ─── INSERT DPI 12-21 ──────────────────────────────────────────────────────── + +function insertDHCPFingerprints(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO dhcp_fingerprints + (agent_uuid, site_uuid, fetched_at, fingerprint, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.fingerprint, r.download, r.upload, r.total); + })(rows); +} + +function insertHTTPUserAgents(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO http_user_agents + (agent_uuid, site_uuid, fetched_at, user_agent, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.user_agent, r.download, r.upload, r.total); + })(rows); +} + +function insertSNIHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO sni_hostnames + (agent_uuid, site_uuid, fetched_at, sni_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertSSLServerCN(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssl_server_cn + (agent_uuid, site_uuid, fetched_at, ssl_server_cn, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + })(rows); +} + +function insertQUICHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO quic_hostnames + (agent_uuid, site_uuid, fetched_at, quic_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertBitTorrentHashes(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO bittorrent_hashes + (agent_uuid, site_uuid, fetched_at, info_hash, label, download, upload, total) + VALUES (?, ?, ?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + })(rows); +} + +function insertSSHVersions(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssh_versions + (agent_uuid, site_uuid, fetched_at, ssh_version, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.ssh_version, r.download, r.upload, r.total); + })(rows); +} + +function insertMDNSHostnames(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mdns_hostnames + (agent_uuid, site_uuid, fetched_at, mdns_hostname, download, upload, total) + VALUES (?, ?, ?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + })(rows); +} + +// ─── INSERT INTELLIGENCE 22-30 ──────────────────────────────────────────────── + +function insertCryptoMining(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_crypto_mining + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.pool_host, r.pool_ip, r.protocol, r.app_label, r.confidence, + r.download ?? 0, r.upload ?? 0 + ); + })(rows); +} + +function insertDeviceDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_device_discovery + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.device_label, r.device_type, r.os_label, r.manufacturer, + r.is_new ? 1 : 0 + ); + })(rows); +} + +function insertEncryptionAudit(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_encryption_audit + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.device_label, r.encrypted_pct, r.unencrypted ?? 0, r.encrypted ?? 0, + r.total ?? 0, r.risk_level + ); + })(rows); +} + +function insertInsecureProtocols(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_insecure_protocols + (agent_uuid, site_uuid, fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.protocol, r.ip_address, r.mac_address, + r.dst_ip, r.dst_port, r.app_label, r.download ?? 0, r.upload ?? 0, + r.risk ?? 'Medium', r.source ?? 'api' + ); + })(rows); +} + +function insertIPReputation(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_ip_reputation + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.local_ip, r.mac_address, + r.reputation, r.score, r.country, r.app_label, + r.download ?? 0, r.upload ?? 0, r.blacklisted ? 1 : 0 + ); + })(rows); +} + +function insertServerDiscovery(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_server_discovery + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.server_type, r.hostname, r.port, r.protocol, r.os_label, + r.download ?? 0, r.upload ?? 0 + ); + })(rows); +} + +function insertTorDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_tor_detection + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.exit_node, r.circuit_id, r.download ?? 0, r.upload ?? 0, r.country + ); + })(rows); +} + +function insertUnencryptedPasswords(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_unencrypted_passwords + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.dst_ip, r.dst_port, r.protocol, r.username, + r.download ?? 0, r.upload ?? 0, r.severity ?? 'Critical' + ); + })(rows); +} + +function insertVPNDetection(rows, fetchedAt, siteUuid, agentUuid) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_vpn_detection + (agent_uuid, site_uuid, fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) + VALUES (?, ?, ?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + agentUuid, siteUuid, fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.vpn_type, r.remote_ip, r.protocol, + r.download ?? 0, r.upload ?? 0, r.country, r.confidence + ); + })(rows); +} + +// ─── QUERY Intelligence — ambil semua row tanpa batasan fetched_at ──────────── +// (karena event ini tidak diposting ulang tiap menit, simpan kumulatif) +function getIntelData(table, limit = 100, siteUuid = null, agentUuid = null) { + const d = getDB(); + + if (!agentUuid) { + // Admin mode: return latest global snapshot (agent_uuid IS NULL) + return d.prepare(`SELECT * FROM ${table} WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND agent_uuid IS NULL ORDER BY fetched_at DESC LIMIT @limit`).all({ limit, siteUuid }); + } + + // Agent mode: try agent_uuid filter first (direct, most accurate) + const directResult = d.prepare(`SELECT * FROM ${table} WHERE agent_uuid = @agentUuid ORDER BY fetched_at DESC LIMIT @limit`).all({ agentUuid, limit }); + if (directResult.length > 0) { + return directResult; + } + + // Fallback: MAC-based filter for tables that may have been saved without agent_uuid + if (AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + // For reputation, the column is local_ip, not ip_address + const ipField = table === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; + return d.prepare(` + SELECT * FROM ${table} + WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + ORDER BY fetched_at DESC + LIMIT ? + `).all(...macs, ...macs, limit); + } + + return []; +} + +function getIntelStats(siteUuid = null, agentUuid = null) { + const d = getDB(); + const tables = [ + 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', + 'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery', + 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', + ]; + const counts = {}; + + for (const t of tables) { + try { + if (!agentUuid) { + // Admin: count global (agent_uuid IS NULL) + counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE agent_uuid IS NULL`).get()?.n ?? 0; + } else { + // Agent mode: try agent_uuid directly first + const directCount = d.prepare(`SELECT COUNT(*) as n FROM ${t} WHERE agent_uuid = ?`).get(agentUuid)?.n ?? 0; + if (directCount > 0) { + counts[t] = directCount; + } else if (AGENT_MAC_MAP[agentUuid]) { + // Fallback MAC-based + const macs = AGENT_MAC_MAP[agentUuid]; + const placeholders = macs.map(() => '?').join(','); + const ipField = t === 'intel_ip_reputation' ? 'local_ip' : 'ip_address'; + counts[t] = d.prepare(` + SELECT COUNT(*) as n FROM ${t} + WHERE mac_address IN (${placeholders}) OR ${ipField} IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).get(...macs, ...macs)?.n ?? 0; + } else { + counts[t] = 0; + } + } + } catch { counts[t] = 0; } + } + return counts; +} + +function getDataInterval() { + const d = getDB(); + const row = d.prepare("SELECT MIN(fetched_at) as start_t, MAX(fetched_at) as end_t FROM devices").get(); + return { + start: row?.start_t || null, + end: row?.end_t || null + }; +} + +module.exports = { + getUserByUsername, + getUserByAgentUuid, + updateUserPassword, + updateUserUsername, + updateUserAccountName, + updateUserProfilePicture, + // Admin user management + getAllUsers, + getUserById, + createAgentUser, + adminUpdateUser, + deleteAgentUser, + syncAgentUsers, + getDB, + getDataInterval, + insertBandwidthApps, insertDevices, insertFlows, insertThreats, + insertProtocols, insertCountries, insertDNS, insertEvents, + insertBandwidthTimeline, + getLatestBandwidthApps, getLatestDevices, getLatestFlows, getLatestThreats, + getLatestProtocols, getLatestCountries, getLatestDNS, getLatestEvents, + getBandwidthTimeline, getStats, + // Insert baru + insertAppCategories, insertContinents, insertRegions, insertCities, + insertVLANs, insertInterfaces, insertFlowTypes, insertFlowOrigins, + insertIPVersions, insertRemoteIPs, insertMACBandwidth, + // Query helper + getLatest, + insertTLSVersions, insertTLSCiphers, insertTLSSecurity, insertNetBIOSHostnames, + insertDiscoveryOS, + // DPI 12-21 + insertDHCPFingerprints, insertHTTPUserAgents, insertSNIHostnames, insertSSLServerCN, + insertQUICHostnames, insertBitTorrentHashes, insertSSHVersions, insertMDNSHostnames, + // Intelligence 22-30 + insertCryptoMining, insertDeviceDiscovery, insertEncryptionAudit, + insertInsecureProtocols, insertIPReputation, insertServerDiscovery, + insertTorDetection, insertUnencryptedPasswords, insertVPNDetection, + getIntelData, getIntelStats, +}; + +// ─── AUTHENTICATION ───────────────────────────────────────────────────────── +function getUserByUsername(username) { + return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username); +} + +// Returns the canonical user for an agent_uuid (prefers the one with account_name set) +function getUserByAgentUuid(agentUuid) { + const d = getDB(); + // First: find a user with account_name set for this agent + const withName = d.prepare( + "SELECT * FROM users WHERE agent_uuid = ? AND role = 'AGENT_VIEWER' AND account_name IS NOT NULL ORDER BY id ASC LIMIT 1" + ).get(agentUuid); + if (withName) return withName; + // Fallback: any user for this agent + return d.prepare( + "SELECT * FROM users WHERE agent_uuid = ? AND role = 'AGENT_VIEWER' ORDER BY id ASC LIMIT 1" + ).get(agentUuid); +} + +function updateUserPassword(userId, newPasswordHash) { + return getDB().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newPasswordHash, userId); +} + +function updateUserUsername(userId, newUsername) { + return getDB().prepare('UPDATE users SET username = ? WHERE id = ?').run(newUsername, userId); +} + +function updateUserAccountName(userId, newAccountName) { + return getDB().prepare('UPDATE users SET account_name = ? WHERE id = ?').run(newAccountName, userId); +} + +function updateUserProfilePicture(userId, filename) { + return getDB().prepare('UPDATE users SET profile_picture = ? WHERE id = ?').run(filename, userId); +} + +// ─── ADMIN USER MANAGEMENT ────────────────────────────────────────────────── + +function getAllUsers() { + return getDB().prepare( + 'SELECT id, username, role, site_uuid, agent_uuid, account_name, profile_picture, created_at FROM users ORDER BY role DESC, id ASC' + ).all(); +} + +function getUserById(userId) { + return getDB().prepare( + 'SELECT id, username, role, site_uuid, agent_uuid, account_name, profile_picture, created_at FROM users WHERE id = ?' + ).get(userId); +} + +function createAgentUser(username, passwordHash, accountName, agentUuid, siteUuid) { + const d = getDB(); + // Check username unique + const existing = d.prepare('SELECT id FROM users WHERE username = ?').get(username); + if (existing) throw new Error('Username sudah digunakan'); + return d.prepare( + 'INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid, account_name) VALUES (?, ?, ?, ?, ?, ?)' + ).run(username, passwordHash, 'AGENT_VIEWER', siteUuid || null, agentUuid || null, accountName || null); +} + +function adminUpdateUser(userId, fields) { + const d = getDB(); + const allowed = ['username', 'password_hash', 'account_name', 'agent_uuid', 'profile_picture']; + const sets = []; + const vals = []; + for (const [k, v] of Object.entries(fields)) { + if (allowed.includes(k) && v !== undefined) { + sets.push(`${k} = ?`); + vals.push(v); + } + } + if (sets.length === 0) return { changes: 0 }; + vals.push(userId); + return d.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...vals); +} + +function deleteAgentUser(userId) { + const d = getDB(); + // Prevent deleting SUPER_ADMIN + const user = d.prepare('SELECT role FROM users WHERE id = ?').get(userId); + if (!user) throw new Error('User tidak ditemukan'); + if (user.role === 'SUPER_ADMIN') throw new Error('Tidak bisa menghapus akun SUPER_ADMIN'); + return d.prepare('DELETE FROM users WHERE id = ?').run(userId); +} + +function syncAgentUsers(agents) { + const d = getDB(); + const bcrypt = require('bcryptjs'); + const hash = bcrypt.hashSync('123', 10); + const siteUuid = process.env.NETIFY_SITE_UUID || '6681452d_9cae_4ff4_8ae8_0d504774265e'; + + // Hardcoded labels map for friendly user mapping + const AGENT_LABELS = { + '2F-TF-1D-GK': 'JRP Cibubur', + '8A-V3-PB-85': 'IFG LT.18', + 'F6-2V-DT-8A': 'CPI Balaraja', + '1R-79-J9-YE': 'CPI Balaraja WAN' + }; + + for (const agent of agents) { + const uuid = agent.uuid; + if (!uuid) continue; + const label = AGENT_LABELS[uuid] || agent.label || uuid; + + // Create username = lowercase uuid (e.g. '1r-79-j9-ye') + const usernameUuidLower = uuid.toLowerCase(); + const exists1 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameUuidLower); + if (exists1.count === 0) { + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") + .run(usernameUuidLower, hash, 'AGENT_VIEWER', siteUuid, uuid); + console.log(`[DB] Created default user: ${usernameUuidLower} / 123`); + } + + // Create username = uppercase uuid (e.g. '1R-79-J9-YE') + const usernameUuidUpper = uuid.toUpperCase(); + const exists1u = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameUuidUpper); + if (exists1u.count === 0) { + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") + .run(usernameUuidUpper, hash, 'AGENT_VIEWER', siteUuid, uuid); + console.log(`[DB] Created default user: ${usernameUuidUpper} / 123`); + } + + // Create username = sanitized lowercase label (e.g. 'agent_cpi_balaraja_wan') + const sanitizedLabel = label.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_'); + const usernameLabel = sanitizedLabel.startsWith('agent_') ? sanitizedLabel : `agent_${sanitizedLabel}`; + const exists2 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(usernameLabel); + if (exists2.count === 0) { + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") + .run(usernameLabel, hash, 'AGENT_VIEWER', siteUuid, uuid); + console.log(`[DB] Created default user: ${usernameLabel} / 123`); + } + + // Create username = name/label directly (e.g. 'CPI Balaraja WAN' -> 'CPI Balaraja WAN' or 'JRP Cibubur') + const exists3 = d.prepare("SELECT count(*) as count FROM users WHERE username = ?").get(label); + if (exists3.count === 0) { + d.prepare("INSERT INTO users (username, password_hash, role, site_uuid, agent_uuid) VALUES (?, ?, ?, ?, ?)") + .run(label, hash, 'AGENT_VIEWER', siteUuid, uuid); + console.log(`[DB] Created default user: ${label} / 123`); + } + } +} diff --git a/backend/db/mongoose.js b/backend/db/mongoose.js index 35991f0..73ef8e1 100644 --- a/backend/db/mongoose.js +++ b/backend/db/mongoose.js @@ -5,7 +5,8 @@ const mongoose = require('mongoose'); const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '../../.env.local') }); +const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'; +require('dotenv').config({ path: path.join(__dirname, '../../', envFile) }); const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; @@ -22,7 +23,7 @@ async function connectDB() { serverSelectionTimeoutMS: 10000, connectTimeoutMS: 10000, }); - console.log('[MongoDB] ✓ Connected successfully'); + console.log('[MongoDB] ✓ Connected successfully to', MONGODB_URI); const { logCapacityStats } = require('./capacityTracker'); logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message)); return; diff --git a/backend/generate_export.js b/backend/generate_export.js index 335f4be..c3e8d94 100644 --- a/backend/generate_export.js +++ b/backend/generate_export.js @@ -1,400 +1,400 @@ -/** - * generate_export.js - * - * Mengekspor SELURUH data dari semua tabel SQLite (database) - * ke dalam file backone_data_export.txt - * - * Format output: - * - Header metadata (tanggal, versi, jumlah tabel) - * - Untuk setiap tabel: header section, row count, schema, dan semua data (JSON per baris) - * - Footer summary - */ - -const fs = require('fs'); -const path = require('path'); -const db = require('./database'); - -const OUTPUT_FILE = path.join(__dirname, '../backone_data_export.txt'); -const d = db.getDB(); - -// ─── Helpers ──────────────────────────────────────────────────────────────── -function fmtBytes(bytes) { - if (!bytes || bytes === 0) return '0 B'; - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let b = Math.abs(bytes); - let i = 0; - while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } - return b.toFixed(2) + ' ' + units[i]; -} - -function fmtNum(n) { - if (n == null) return 'N/A'; - return Number(n).toLocaleString('id-ID'); -} - -function separator(char = '═', len = 80) { - return char.repeat(len); -} - -function sectionHeader(tableName, rowCount, description) { - return [ - '', - separator('═'), - `[TABLE: ${tableName}]`, - `Row Count: ${fmtNum(rowCount)}`, - description ? `Description: ${description}` : '', - separator('─'), - ].filter(l => l !== '').join('\n'); -} - -// ─── Table descriptions ────────────────────────────────────────────────────── -const TABLE_DESCRIPTIONS = { - bandwidth_apps : 'Bandwidth per aplikasi (YouTube, Facebook, dll) dari BackOne DPI', - bandwidth_timeline : 'Timeline bandwidth per menit (download/upload historis)', - bittorrent_info_hashes: 'Deteksi aktivitas BitTorrent berdasarkan info hash', - countries : 'Distribusi traffic berdasarkan negara tujuan', - devices : 'Daftar perangkat (IP/MAC) beserta bandwidth & OS', - dhcp_fingerprints : 'Fingerprint DHCP untuk identifikasi tipe device', - discovered_os : 'OS yang terdeteksi dari traffic scanning', - dns_stats : 'Query DNS teratas dan statistik resolusi domain', - events : 'Event log dari BackOne agent (koneksi, peringatan, dll)', - flows : 'Data aliran jaringan per-sesi (src IP, dst IP, aplikasi, domain, bytes)', - flow_origins : 'Asal flow: lokal (LAN) atau eksternal (WAN)', - flow_types : 'Tipe flow: TCP, UDP, ICMP, dll', - http_user_agents : 'HTTP User-Agent yang terdeteksi (browser, OS, framework)', - intel_crypto_mining : 'Deteksi aktivitas crypto mining (pool host, protokol)', - intel_device_discovery: 'Penemuan perangkat baru di jaringan (tipe, OS, manufaktur)', - intel_encryption_audit: 'Audit enkripsi traffic per perangkat (encrypted%, risk level)', - intel_insecure_protocols: 'Protokol tidak aman yang terdeteksi (HTTP, Telnet, FTP, dll)', - intel_ip_reputation : 'Reputasi IP eksternal (blacklist, threat score)', - intel_server_discovery: 'Server yang terdeteksi (HTTPS, SSH, HTTP, dll)', - intel_tor_detection : 'Deteksi penggunaan jaringan Tor', - intel_unencrypted_passwords: 'Deteksi pengiriman password dalam bentuk plaintext', - intel_vpn_detection : 'Deteksi penggunaan VPN (OpenVPN, WireGuard, dll)', - interfaces : 'Interface jaringan per agent (WAN/LAN, bandwidth)', - ip_versions : 'Distribusi traffic IPv4 vs IPv6', - mac_bandwidth : 'Bandwidth per MAC address perangkat', - mdns_hostnames : 'mDNS hostname yang terdeteksi di jaringan lokal', - netbios_hostnames : 'NetBIOS hostname (nama komputer Windows)', - protocols : 'Distribusi protokol jaringan (port usage)', - quic_hostnames : 'Hostname via QUIC/HTTP3 (Google, Cloudflare, dll)', - regions : 'Distribusi traffic berdasarkan region/kota tujuan', - remote_ips : 'IP remote teratas yang diakses perangkat', - sni_hostnames : 'Server Name Indication dari koneksi TLS', - ssh_versions : 'Versi SSH yang terdeteksi di jaringan', - ssl_server_cn : 'Common Name sertifikat SSL server', - threats : 'Ancaman keamanan terdeteksi (threat alerts)', - tls_ciphers : 'Cipher suite TLS yang digunakan', - tls_security : 'Tingkat keamanan TLS (Modern, Compatible, Old)', - tls_versions : 'Versi TLS yang digunakan (1.0, 1.2, 1.3)', - vlans : 'VLAN yang terdeteksi di jaringan', -}; - -// ─── Main Export Logic ─────────────────────────────────────────────────────── -async function main() { - console.log('🚀 Memulai export data...'); - - const exportDate = new Date().toISOString(); - const lines = []; - - // ── File Header ────────────────────────────────────────────────────────── - lines.push(separator('═')); - lines.push(' BACKONE DATA EXPORT'); - lines.push(' Seluruh data hasil parsing dari BackOne API'); - lines.push(separator('─')); - lines.push(` Export Date: ${exportDate}`); - lines.push(` Generated by: generate_export.js`); - lines.push(` Source: database (SQLite lokal)`); - lines.push(` API Base: BackOne API Service`); - lines.push(` Format: Per-tabel, data JSON satu record per baris (JSONL)`); - lines.push(separator('─')); - - // ── Get all tables ──────────────────────────────────────────────────────── - const tables = d.prepare( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" - ).all().map(r => r.name); - - lines.push(` Total Tables: ${tables.length}`); - lines.push(separator('═')); - lines.push(''); - - // ── Table of Contents ───────────────────────────────────────────────────── - lines.push('TABLE OF CONTENTS'); - lines.push(separator('─', 40)); - let totalRows = 0; - const tableSummaries = []; - for (const tableName of tables) { - const cnt = d.prepare(`SELECT COUNT(*) as c FROM ${tableName}`).get().c; - totalRows += cnt; - const desc = TABLE_DESCRIPTIONS[tableName] || '-'; - lines.push(` ${tableName.padEnd(35)} ${String(cnt).padStart(8)} rows`); - tableSummaries.push({ name: tableName, count: cnt, description: desc }); - } - lines.push(separator('─', 40)); - lines.push(` ${'TOTAL'.padEnd(35)} ${String(totalRows).padStart(8)} rows`); - lines.push(''); - - // ── Per-Table Export ────────────────────────────────────────────────────── - for (const { name: tableName, count, description } of tableSummaries) { - console.log(` 📋 Exporting: ${tableName} (${fmtNum(count)} rows)...`); - - // Section header - lines.push(sectionHeader(tableName, count, description)); - - // Schema - const cols = d.prepare(`PRAGMA table_info(${tableName})`).all(); - lines.push('Schema:'); - cols.forEach(c => { - lines.push(` - ${c.name} [${c.type || 'TEXT'}]${c.notnull ? ' NOT NULL' : ''}${c.pk ? ' PRIMARY KEY' : ''}`); - }); - lines.push(''); - - // Statistics for numeric columns - const numericCols = cols.filter(c => - ['INTEGER', 'REAL', 'NUMERIC'].includes((c.type || '').toUpperCase()) && - !['id'].includes(c.name.toLowerCase()) - ); - - if (count > 0 && numericCols.length > 0) { - lines.push('Statistics:'); - for (const col of numericCols.slice(0, 5)) { // max 5 numeric cols - try { - const stat = d.prepare(` - SELECT MIN(${col.name}) as min, MAX(${col.name}) as max, - AVG(${col.name}) as avg, SUM(${col.name}) as total - FROM ${tableName} - `).get(); - if (stat && stat.max !== null) { - lines.push(` ${col.name}: min=${fmtNum(stat.min)} max=${fmtNum(stat.max)} avg=${Number(stat.avg || 0).toFixed(2)} total=${fmtNum(stat.total)}`); - } - } catch(e) { /* skip */ } - } - lines.push(''); - } - - // Data rows (ALL rows) - if (count === 0) { - lines.push('(No data)'); - } else { - lines.push(`Data (${fmtNum(count)} records):`); - const rows = d.prepare(`SELECT * FROM ${tableName}`).all(); - for (const row of rows) { - lines.push(JSON.stringify(row)); - } - } - lines.push(''); - } - - // ── Agent-specific sections (derived from flows) ─────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: AGENT ANALYSIS]'); - lines.push('Description: Analisis traffic per agent berdasarkan MAC address dari flows table'); - lines.push(separator('─')); - - const AGENT_MAC_MAP = { - '2F-TF-1D-GK': { label: 'JRP Cibubur', macs: ['60:be:b4:1f:05:96'] }, - '8A-V3-PB-85': { label: 'IFG LT.18', macs: ['04:f4:1c:ce:c2:e6'] }, - 'F6-2V-DT-8A': { label: 'CPI Balaraja', macs: ['2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be', 'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36'] }, - }; - - for (const [uuid, agent] of Object.entries(AGENT_MAC_MAP)) { - lines.push(''); - lines.push(`Agent: ${agent.label} (${uuid})`); - lines.push(`MACs: ${agent.macs.join(', ')}`); - lines.push(separator('─', 40)); - - const ph = agent.macs.map(() => '?').join(','); - - // Summary - const sumRow = d.prepare(` - SELECT COUNT(DISTINCT src_ip) AS device_count, COUNT(*) AS flow_count, - SUM(bytes_download) AS total_dl, SUM(bytes_upload) AS total_ul - FROM flows WHERE src_mac IN (${ph}) - `).get(...agent.macs); - - lines.push(` Devices: ${fmtNum(sumRow.device_count)}`); - lines.push(` Total Flows: ${fmtNum(sumRow.flow_count)}`); - lines.push(` Total Download: ${fmtBytes(sumRow.total_dl)}`); - lines.push(` Total Upload: ${fmtBytes(sumRow.total_ul)}`); - - // Top apps - const apps = d.prepare(` - SELECT app_label, SUM(bytes_download) AS dl, SUM(bytes_upload) AS ul, COUNT(*) AS cnt - FROM flows WHERE src_mac IN (${ph}) AND app_label IS NOT NULL - GROUP BY app_label ORDER BY dl DESC LIMIT 10 - `).all(...agent.macs); - - lines.push(` Top Applications:`); - apps.forEach((a, i) => { - lines.push(` ${String(i+1).padStart(2)}. ${(a.app_label||'?').padEnd(30)} DL:${fmtBytes(a.dl).padStart(12)} UL:${fmtBytes(a.ul).padStart(12)} Flows:${a.cnt}`); - }); - - // Top devices - const devs = d.prepare(` - SELECT src_ip, SUM(bytes_download) AS dl, MAX(last_seen) AS last - FROM flows WHERE src_mac IN (${ph}) - GROUP BY src_ip ORDER BY dl DESC LIMIT 10 - `).all(...agent.macs); - - lines.push(` Top Devices:`); - devs.forEach((d2, i) => { - lines.push(` ${String(i+1).padStart(2)}. ${(d2.src_ip||'?').padEnd(20)} DL:${fmtBytes(d2.dl).padStart(12)} Last:${d2.last||'-'}`); - }); - } - - // ── Bandwidth Apps Summary ───────────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: BANDWIDTH APPS LATEST SNAPSHOT]'); - lines.push('Description: Snapshot terakhir bandwidth per aplikasi (nilai aktual, bukan akumulasi)'); - lines.push(separator('─')); - - const latestBwSnap = d.prepare('SELECT MAX(fetched_at) as t FROM bandwidth_apps').get()?.t; - if (latestBwSnap) { - lines.push(`Latest Snapshot: ${latestBwSnap}`); - const bwApps = d.prepare('SELECT app_label, category, download, upload, total, flow_count FROM bandwidth_apps WHERE fetched_at = ? ORDER BY download DESC').all(latestBwSnap); - lines.push(`Total Apps: ${bwApps.length}`); - lines.push(''); - bwApps.forEach((a, i) => { - lines.push(` ${String(i+1).padStart(3)}. ${(a.app_label||'?').padEnd(30)} [${(a.category||'?').padEnd(20)}] DL:${fmtBytes(a.download).padStart(12)} UL:${fmtBytes(a.upload).padStart(12)} Flows:${fmtNum(a.flow_count)}`); - }); - } - - // ── Encryption Audit Summary ─────────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: ENCRYPTION RISK SUMMARY]'); - lines.push('Description: Distribusi risk level enkripsi per perangkat (snapshot terbaru)'); - lines.push(separator('─')); - - const latestEncSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_encryption_audit').get()?.t; - if (latestEncSnap) { - const riskDist = d.prepare(` - SELECT risk_level, COUNT(*) as cnt, AVG(encrypted_pct) as avg_enc - FROM intel_encryption_audit WHERE fetched_at = ? - GROUP BY risk_level ORDER BY cnt DESC - `).all(latestEncSnap); - - lines.push(`Latest Snapshot: ${latestEncSnap}`); - lines.push('Risk Distribution:'); - riskDist.forEach(r => { - lines.push(` ${(r.risk_level||'Unknown').padEnd(15)} ${String(r.cnt).padStart(5)} devices avg encrypted: ${Number(r.avg_enc||0).toFixed(1)}%`); - }); - - // Highest risk devices - lines.push(''); - lines.push('Critical Risk Devices (0% encrypted):'); - const critDevs = d.prepare(` - SELECT ip_address, mac_address, device_label, encrypted_pct, total - FROM intel_encryption_audit WHERE fetched_at = ? AND risk_level = 'Critical' - ORDER BY total DESC LIMIT 20 - `).all(latestEncSnap); - critDevs.forEach(r => { - lines.push(JSON.stringify(r)); - }); - } - - // ── DNS Top Domains ──────────────────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: TOP DNS DOMAINS]'); - lines.push('Description: Domain paling sering diquery dari DNS stats'); - lines.push(separator('─')); - - const latestDnsSnap = d.prepare('SELECT MAX(fetched_at) as t FROM dns_queries').get()?.t; - if (latestDnsSnap) { - const dnsRows = d.prepare('SELECT * FROM dns_queries WHERE fetched_at = ? ORDER BY query_count DESC LIMIT 30').all(latestDnsSnap); - - lines.push(`Latest Snapshot: ${latestDnsSnap}`); - dnsRows.forEach(r => lines.push(JSON.stringify(r))); - } - - // ── IP Reputation Blacklisted ───────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: BLACKLISTED IP ADDRESSES]'); - lines.push('Description: IP address yang terdeteksi blacklisted (dari intel_ip_reputation)'); - lines.push(separator('─')); - - const latestRepSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_ip_reputation').get()?.t; - if (latestRepSnap) { - const blacklisted = d.prepare('SELECT * FROM intel_ip_reputation WHERE fetched_at = ? AND blacklisted = 1').all(latestRepSnap); - lines.push(`Latest Snapshot: ${latestRepSnap}`); - lines.push(`Blacklisted count: ${blacklisted.length}`); - blacklisted.forEach(r => lines.push(JSON.stringify(r))); - } - - // ── Flows: Active Sessions Summary ──────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: ACTIVE FLOWS SUMMARY]'); - lines.push('Description: Ringkasan aliran jaringan aktif (50 terbaru per download)'); - lines.push(separator('─')); - - const flowSummary = d.prepare(` - SELECT COUNT(*) as total_flows, - COUNT(DISTINCT src_ip) as unique_src_ips, - COUNT(DISTINCT dst_ip) as unique_dst_ips, - COUNT(DISTINCT src_mac) as unique_macs, - SUM(bytes_download) as total_dl, - SUM(bytes_upload) as total_ul, - MIN(first_seen) as earliest, - MAX(last_seen) as latest - FROM flows - `).get(); - - lines.push(`Total Flows in DB: ${fmtNum(flowSummary.total_flows)}`); - lines.push(`Unique Source IPs: ${fmtNum(flowSummary.unique_src_ips)}`); - lines.push(`Unique Dest IPs: ${fmtNum(flowSummary.unique_dst_ips)}`); - lines.push(`Unique MAC Addresses: ${fmtNum(flowSummary.unique_macs)}`); - lines.push(`Total Download: ${fmtBytes(flowSummary.total_dl)}`); - lines.push(`Total Upload: ${fmtBytes(flowSummary.total_ul)}`); - lines.push(`Data from: ${flowSummary.earliest}`); - lines.push(`Data to: ${flowSummary.latest}`); - lines.push(''); - - // Top 50 flows by download - lines.push('Top 50 Flows by Download:'); - const topFlows = d.prepare('SELECT * FROM flows ORDER BY bytes_download DESC LIMIT 50').all(); - topFlows.forEach(r => lines.push(JSON.stringify(r))); - - // ── Unencrypted Password Events ─────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push('[DERIVED: UNENCRYPTED PASSWORD DETECTIONS]'); - lines.push('Description: Kejadian pengiriman credential dalam plaintext (HIGH severity)'); - lines.push(separator('─')); - - const unencPwdHigh = d.prepare(` - SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity, detected_at - FROM intel_unencrypted_passwords ORDER BY detected_at DESC - `).all(); - lines.push(`Total detections: ${unencPwdHigh.length}`); - unencPwdHigh.forEach(r => lines.push(JSON.stringify(r))); - - // ── Footer ──────────────────────────────────────────────────────────────── - lines.push(''); - lines.push(separator('═')); - lines.push(' END OF EXPORT'); - lines.push(` Generated at: ${new Date().toISOString()}`); - lines.push(` Total lines: ${lines.length + 3}`); - lines.push(separator('═')); - - // Write to file - const output = lines.join('\n'); - fs.writeFileSync(OUTPUT_FILE, output, 'utf-8'); - - const stats = fs.statSync(OUTPUT_FILE); - console.log(`\n✅ Export selesai!`); - console.log(` File: ${OUTPUT_FILE}`); - console.log(` Size: ${fmtBytes(stats.size)}`); - console.log(` Lines: ${fmtNum(lines.length)}`); - console.log(` Tables: ${tables.length}`); - console.log(` Total Rows: ${fmtNum(totalRows)}`); -} - -main().catch(e => { - console.error('❌ Export FAILED:', e); - process.exit(1); -}); +/** + * generate_export.js + * + * Mengekspor SELURUH data dari semua tabel SQLite (database) + * ke dalam file backone_data_export.txt + * + * Format output: + * - Header metadata (tanggal, versi, jumlah tabel) + * - Untuk setiap tabel: header section, row count, schema, dan semua data (JSON per baris) + * - Footer summary + */ + +const fs = require('fs'); +const path = require('path'); +const db = require('./database'); + +const OUTPUT_FILE = path.join(__dirname, '../backone_data_export.txt'); +const d = db.getDB(); + +// ─── Helpers ──────────────────────────────────────────────────────────────── +function fmtBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let b = Math.abs(bytes); + let i = 0; + while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } + return b.toFixed(2) + ' ' + units[i]; +} + +function fmtNum(n) { + if (n == null) return 'N/A'; + return Number(n).toLocaleString('id-ID'); +} + +function separator(char = '═', len = 80) { + return char.repeat(len); +} + +function sectionHeader(tableName, rowCount, description) { + return [ + '', + separator('═'), + `[TABLE: ${tableName}]`, + `Row Count: ${fmtNum(rowCount)}`, + description ? `Description: ${description}` : '', + separator('─'), + ].filter(l => l !== '').join('\n'); +} + +// ─── Table descriptions ────────────────────────────────────────────────────── +const TABLE_DESCRIPTIONS = { + bandwidth_apps : 'Bandwidth per aplikasi (YouTube, Facebook, dll) dari BackOne DPI', + bandwidth_timeline : 'Timeline bandwidth per menit (download/upload historis)', + bittorrent_info_hashes: 'Deteksi aktivitas BitTorrent berdasarkan info hash', + countries : 'Distribusi traffic berdasarkan negara tujuan', + devices : 'Daftar perangkat (IP/MAC) beserta bandwidth & OS', + dhcp_fingerprints : 'Fingerprint DHCP untuk identifikasi tipe device', + discovered_os : 'OS yang terdeteksi dari traffic scanning', + dns_stats : 'Query DNS teratas dan statistik resolusi domain', + events : 'Event log dari BackOne agent (koneksi, peringatan, dll)', + flows : 'Data aliran jaringan per-sesi (src IP, dst IP, aplikasi, domain, bytes)', + flow_origins : 'Asal flow: lokal (LAN) atau eksternal (WAN)', + flow_types : 'Tipe flow: TCP, UDP, ICMP, dll', + http_user_agents : 'HTTP User-Agent yang terdeteksi (browser, OS, framework)', + intel_crypto_mining : 'Deteksi aktivitas crypto mining (pool host, protokol)', + intel_device_discovery: 'Penemuan perangkat baru di jaringan (tipe, OS, manufaktur)', + intel_encryption_audit: 'Audit enkripsi traffic per perangkat (encrypted%, risk level)', + intel_insecure_protocols: 'Protokol tidak aman yang terdeteksi (HTTP, Telnet, FTP, dll)', + intel_ip_reputation : 'Reputasi IP eksternal (blacklist, threat score)', + intel_server_discovery: 'Server yang terdeteksi (HTTPS, SSH, HTTP, dll)', + intel_tor_detection : 'Deteksi penggunaan jaringan Tor', + intel_unencrypted_passwords: 'Deteksi pengiriman password dalam bentuk plaintext', + intel_vpn_detection : 'Deteksi penggunaan VPN (OpenVPN, WireGuard, dll)', + interfaces : 'Interface jaringan per agent (WAN/LAN, bandwidth)', + ip_versions : 'Distribusi traffic IPv4 vs IPv6', + mac_bandwidth : 'Bandwidth per MAC address perangkat', + mdns_hostnames : 'mDNS hostname yang terdeteksi di jaringan lokal', + netbios_hostnames : 'NetBIOS hostname (nama komputer Windows)', + protocols : 'Distribusi protokol jaringan (port usage)', + quic_hostnames : 'Hostname via QUIC/HTTP3 (Google, Cloudflare, dll)', + regions : 'Distribusi traffic berdasarkan region/kota tujuan', + remote_ips : 'IP remote teratas yang diakses perangkat', + sni_hostnames : 'Server Name Indication dari koneksi TLS', + ssh_versions : 'Versi SSH yang terdeteksi di jaringan', + ssl_server_cn : 'Common Name sertifikat SSL server', + threats : 'Ancaman keamanan terdeteksi (threat alerts)', + tls_ciphers : 'Cipher suite TLS yang digunakan', + tls_security : 'Tingkat keamanan TLS (Modern, Compatible, Old)', + tls_versions : 'Versi TLS yang digunakan (1.0, 1.2, 1.3)', + vlans : 'VLAN yang terdeteksi di jaringan', +}; + +// ─── Main Export Logic ─────────────────────────────────────────────────────── +async function main() { + console.log('🚀 Memulai export data...'); + + const exportDate = new Date().toISOString(); + const lines = []; + + // ── File Header ────────────────────────────────────────────────────────── + lines.push(separator('═')); + lines.push(' BACKONE DATA EXPORT'); + lines.push(' Seluruh data hasil parsing dari BackOne API'); + lines.push(separator('─')); + lines.push(` Export Date: ${exportDate}`); + lines.push(` Generated by: generate_export.js`); + lines.push(` Source: database (SQLite lokal)`); + lines.push(` API Base: BackOne API Service`); + lines.push(` Format: Per-tabel, data JSON satu record per baris (JSONL)`); + lines.push(separator('─')); + + // ── Get all tables ──────────────────────────────────────────────────────── + const tables = d.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" + ).all().map(r => r.name); + + lines.push(` Total Tables: ${tables.length}`); + lines.push(separator('═')); + lines.push(''); + + // ── Table of Contents ───────────────────────────────────────────────────── + lines.push('TABLE OF CONTENTS'); + lines.push(separator('─', 40)); + let totalRows = 0; + const tableSummaries = []; + for (const tableName of tables) { + const cnt = d.prepare(`SELECT COUNT(*) as c FROM ${tableName}`).get().c; + totalRows += cnt; + const desc = TABLE_DESCRIPTIONS[tableName] || '-'; + lines.push(` ${tableName.padEnd(35)} ${String(cnt).padStart(8)} rows`); + tableSummaries.push({ name: tableName, count: cnt, description: desc }); + } + lines.push(separator('─', 40)); + lines.push(` ${'TOTAL'.padEnd(35)} ${String(totalRows).padStart(8)} rows`); + lines.push(''); + + // ── Per-Table Export ────────────────────────────────────────────────────── + for (const { name: tableName, count, description } of tableSummaries) { + console.log(` 📋 Exporting: ${tableName} (${fmtNum(count)} rows)...`); + + // Section header + lines.push(sectionHeader(tableName, count, description)); + + // Schema + const cols = d.prepare(`PRAGMA table_info(${tableName})`).all(); + lines.push('Schema:'); + cols.forEach(c => { + lines.push(` - ${c.name} [${c.type || 'TEXT'}]${c.notnull ? ' NOT NULL' : ''}${c.pk ? ' PRIMARY KEY' : ''}`); + }); + lines.push(''); + + // Statistics for numeric columns + const numericCols = cols.filter(c => + ['INTEGER', 'REAL', 'NUMERIC'].includes((c.type || '').toUpperCase()) && + !['id'].includes(c.name.toLowerCase()) + ); + + if (count > 0 && numericCols.length > 0) { + lines.push('Statistics:'); + for (const col of numericCols.slice(0, 5)) { // max 5 numeric cols + try { + const stat = d.prepare(` + SELECT MIN(${col.name}) as min, MAX(${col.name}) as max, + AVG(${col.name}) as avg, SUM(${col.name}) as total + FROM ${tableName} + `).get(); + if (stat && stat.max !== null) { + lines.push(` ${col.name}: min=${fmtNum(stat.min)} max=${fmtNum(stat.max)} avg=${Number(stat.avg || 0).toFixed(2)} total=${fmtNum(stat.total)}`); + } + } catch(e) { /* skip */ } + } + lines.push(''); + } + + // Data rows (ALL rows) + if (count === 0) { + lines.push('(No data)'); + } else { + lines.push(`Data (${fmtNum(count)} records):`); + const rows = d.prepare(`SELECT * FROM ${tableName}`).all(); + for (const row of rows) { + lines.push(JSON.stringify(row)); + } + } + lines.push(''); + } + + // ── Agent-specific sections (derived from flows) ─────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: AGENT ANALYSIS]'); + lines.push('Description: Analisis traffic per agent berdasarkan MAC address dari flows table'); + lines.push(separator('─')); + + const AGENT_MAC_MAP = { + '2F-TF-1D-GK': { label: 'JRP Cibubur', macs: ['60:be:b4:1f:05:96'] }, + '8A-V3-PB-85': { label: 'IFG LT.18', macs: ['04:f4:1c:ce:c2:e6'] }, + 'F6-2V-DT-8A': { label: 'CPI Balaraja', macs: ['2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be', 'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36'] }, + }; + + for (const [uuid, agent] of Object.entries(AGENT_MAC_MAP)) { + lines.push(''); + lines.push(`Agent: ${agent.label} (${uuid})`); + lines.push(`MACs: ${agent.macs.join(', ')}`); + lines.push(separator('─', 40)); + + const ph = agent.macs.map(() => '?').join(','); + + // Summary + const sumRow = d.prepare(` + SELECT COUNT(DISTINCT src_ip) AS device_count, COUNT(*) AS flow_count, + SUM(bytes_download) AS total_dl, SUM(bytes_upload) AS total_ul + FROM flows WHERE src_mac IN (${ph}) + `).get(...agent.macs); + + lines.push(` Devices: ${fmtNum(sumRow.device_count)}`); + lines.push(` Total Flows: ${fmtNum(sumRow.flow_count)}`); + lines.push(` Total Download: ${fmtBytes(sumRow.total_dl)}`); + lines.push(` Total Upload: ${fmtBytes(sumRow.total_ul)}`); + + // Top apps + const apps = d.prepare(` + SELECT app_label, SUM(bytes_download) AS dl, SUM(bytes_upload) AS ul, COUNT(*) AS cnt + FROM flows WHERE src_mac IN (${ph}) AND app_label IS NOT NULL + GROUP BY app_label ORDER BY dl DESC LIMIT 10 + `).all(...agent.macs); + + lines.push(` Top Applications:`); + apps.forEach((a, i) => { + lines.push(` ${String(i+1).padStart(2)}. ${(a.app_label||'?').padEnd(30)} DL:${fmtBytes(a.dl).padStart(12)} UL:${fmtBytes(a.ul).padStart(12)} Flows:${a.cnt}`); + }); + + // Top devices + const devs = d.prepare(` + SELECT src_ip, SUM(bytes_download) AS dl, MAX(last_seen) AS last + FROM flows WHERE src_mac IN (${ph}) + GROUP BY src_ip ORDER BY dl DESC LIMIT 10 + `).all(...agent.macs); + + lines.push(` Top Devices:`); + devs.forEach((d2, i) => { + lines.push(` ${String(i+1).padStart(2)}. ${(d2.src_ip||'?').padEnd(20)} DL:${fmtBytes(d2.dl).padStart(12)} Last:${d2.last||'-'}`); + }); + } + + // ── Bandwidth Apps Summary ───────────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: BANDWIDTH APPS LATEST SNAPSHOT]'); + lines.push('Description: Snapshot terakhir bandwidth per aplikasi (nilai aktual, bukan akumulasi)'); + lines.push(separator('─')); + + const latestBwSnap = d.prepare('SELECT MAX(fetched_at) as t FROM bandwidth_apps').get()?.t; + if (latestBwSnap) { + lines.push(`Latest Snapshot: ${latestBwSnap}`); + const bwApps = d.prepare('SELECT app_label, category, download, upload, total, flow_count FROM bandwidth_apps WHERE fetched_at = ? ORDER BY download DESC').all(latestBwSnap); + lines.push(`Total Apps: ${bwApps.length}`); + lines.push(''); + bwApps.forEach((a, i) => { + lines.push(` ${String(i+1).padStart(3)}. ${(a.app_label||'?').padEnd(30)} [${(a.category||'?').padEnd(20)}] DL:${fmtBytes(a.download).padStart(12)} UL:${fmtBytes(a.upload).padStart(12)} Flows:${fmtNum(a.flow_count)}`); + }); + } + + // ── Encryption Audit Summary ─────────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: ENCRYPTION RISK SUMMARY]'); + lines.push('Description: Distribusi risk level enkripsi per perangkat (snapshot terbaru)'); + lines.push(separator('─')); + + const latestEncSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_encryption_audit').get()?.t; + if (latestEncSnap) { + const riskDist = d.prepare(` + SELECT risk_level, COUNT(*) as cnt, AVG(encrypted_pct) as avg_enc + FROM intel_encryption_audit WHERE fetched_at = ? + GROUP BY risk_level ORDER BY cnt DESC + `).all(latestEncSnap); + + lines.push(`Latest Snapshot: ${latestEncSnap}`); + lines.push('Risk Distribution:'); + riskDist.forEach(r => { + lines.push(` ${(r.risk_level||'Unknown').padEnd(15)} ${String(r.cnt).padStart(5)} devices avg encrypted: ${Number(r.avg_enc||0).toFixed(1)}%`); + }); + + // Highest risk devices + lines.push(''); + lines.push('Critical Risk Devices (0% encrypted):'); + const critDevs = d.prepare(` + SELECT ip_address, mac_address, device_label, encrypted_pct, total + FROM intel_encryption_audit WHERE fetched_at = ? AND risk_level = 'Critical' + ORDER BY total DESC LIMIT 20 + `).all(latestEncSnap); + critDevs.forEach(r => { + lines.push(JSON.stringify(r)); + }); + } + + // ── DNS Top Domains ──────────────────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: TOP DNS DOMAINS]'); + lines.push('Description: Domain paling sering diquery dari DNS stats'); + lines.push(separator('─')); + + const latestDnsSnap = d.prepare('SELECT MAX(fetched_at) as t FROM dns_queries').get()?.t; + if (latestDnsSnap) { + const dnsRows = d.prepare('SELECT * FROM dns_queries WHERE fetched_at = ? ORDER BY query_count DESC LIMIT 30').all(latestDnsSnap); + + lines.push(`Latest Snapshot: ${latestDnsSnap}`); + dnsRows.forEach(r => lines.push(JSON.stringify(r))); + } + + // ── IP Reputation Blacklisted ───────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: BLACKLISTED IP ADDRESSES]'); + lines.push('Description: IP address yang terdeteksi blacklisted (dari intel_ip_reputation)'); + lines.push(separator('─')); + + const latestRepSnap = d.prepare('SELECT MAX(fetched_at) as t FROM intel_ip_reputation').get()?.t; + if (latestRepSnap) { + const blacklisted = d.prepare('SELECT * FROM intel_ip_reputation WHERE fetched_at = ? AND blacklisted = 1').all(latestRepSnap); + lines.push(`Latest Snapshot: ${latestRepSnap}`); + lines.push(`Blacklisted count: ${blacklisted.length}`); + blacklisted.forEach(r => lines.push(JSON.stringify(r))); + } + + // ── Flows: Active Sessions Summary ──────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: ACTIVE FLOWS SUMMARY]'); + lines.push('Description: Ringkasan aliran jaringan aktif (50 terbaru per download)'); + lines.push(separator('─')); + + const flowSummary = d.prepare(` + SELECT COUNT(*) as total_flows, + COUNT(DISTINCT src_ip) as unique_src_ips, + COUNT(DISTINCT dst_ip) as unique_dst_ips, + COUNT(DISTINCT src_mac) as unique_macs, + SUM(bytes_download) as total_dl, + SUM(bytes_upload) as total_ul, + MIN(first_seen) as earliest, + MAX(last_seen) as latest + FROM flows + `).get(); + + lines.push(`Total Flows in DB: ${fmtNum(flowSummary.total_flows)}`); + lines.push(`Unique Source IPs: ${fmtNum(flowSummary.unique_src_ips)}`); + lines.push(`Unique Dest IPs: ${fmtNum(flowSummary.unique_dst_ips)}`); + lines.push(`Unique MAC Addresses: ${fmtNum(flowSummary.unique_macs)}`); + lines.push(`Total Download: ${fmtBytes(flowSummary.total_dl)}`); + lines.push(`Total Upload: ${fmtBytes(flowSummary.total_ul)}`); + lines.push(`Data from: ${flowSummary.earliest}`); + lines.push(`Data to: ${flowSummary.latest}`); + lines.push(''); + + // Top 50 flows by download + lines.push('Top 50 Flows by Download:'); + const topFlows = d.prepare('SELECT * FROM flows ORDER BY bytes_download DESC LIMIT 50').all(); + topFlows.forEach(r => lines.push(JSON.stringify(r))); + + // ── Unencrypted Password Events ─────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push('[DERIVED: UNENCRYPTED PASSWORD DETECTIONS]'); + lines.push('Description: Kejadian pengiriman credential dalam plaintext (HIGH severity)'); + lines.push(separator('─')); + + const unencPwdHigh = d.prepare(` + SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity, detected_at + FROM intel_unencrypted_passwords ORDER BY detected_at DESC + `).all(); + lines.push(`Total detections: ${unencPwdHigh.length}`); + unencPwdHigh.forEach(r => lines.push(JSON.stringify(r))); + + // ── Footer ──────────────────────────────────────────────────────────────── + lines.push(''); + lines.push(separator('═')); + lines.push(' END OF EXPORT'); + lines.push(` Generated at: ${new Date().toISOString()}`); + lines.push(` Total lines: ${lines.length + 3}`); + lines.push(separator('═')); + + // Write to file + const output = lines.join('\n'); + fs.writeFileSync(OUTPUT_FILE, output, 'utf-8'); + + const stats = fs.statSync(OUTPUT_FILE); + console.log(`\n✅ Export selesai!`); + console.log(` File: ${OUTPUT_FILE}`); + console.log(` Size: ${fmtBytes(stats.size)}`); + console.log(` Lines: ${fmtNum(lines.length)}`); + console.log(` Tables: ${tables.length}`); + console.log(` Total Rows: ${fmtNum(totalRows)}`); +} + +main().catch(e => { + console.error('❌ Export FAILED:', e); + process.exit(1); +}); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index b51659c..6113793 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const User = require('../models/User'); +const Session = require('../models/Session'); const { Summary } = require('../models/Schemas'); const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key'; @@ -11,19 +12,46 @@ async function requireAuth(req, res, next) { try { req.user = jwt.verify(token, JWT_SECRET); + // Verify session status in MongoDB + if (req.user.session_id) { + const activeSession = await Session.findById(req.user.session_id); + if (!activeSession) { + res.clearCookie('token'); + return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' }); + } + // Update last active + activeSession.last_active = new Date(); + await activeSession.save(); + } + // ── VIEW-AS MODE ────────────────────────────────────────────────────────── const viewAsHeader = req.headers['x-view-as-agent']; - if (viewAsHeader && (req.user.role === 'SUPER_ADMIN' || req.user.role === 'TENANT_ADMIN')) { + const isAllowedViewAs = req.user.role === 'SUPER_ADMIN' || + req.user.role === 'TENANT_ADMIN' || + req.user.role === 'COMPANY_ADMIN' || + req.user.role === 'COMPANY_OPERATOR'; + + if (viewAsHeader && isAllowedViewAs) { try { const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET); if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) { - const targetAgentUser = await User.findOne({ agent_uuid: viewDecoded.viewAs, role: 'AGENT_VIEWER' }).lean(); + const targetAgent = viewDecoded.viewAs; + + // Validation: COMPANY_ADMIN and COMPANY_OPERATOR can only view-as their assigned agents + if (['COMPANY_ADMIN', 'COMPANY_OPERATOR'].includes(req.user.role)) { + const hasAccess = req.user.agent_uuids && req.user.agent_uuids.includes(targetAgent); + if (!hasAccess) { + throw new Error('Unauthorized view-as agent access'); + } + } + + const targetAgentUser = await User.findOne({ agent_uuid: targetAgent, role: 'AGENT_VIEWER' }).lean(); let targetSiteUuid = req.user.site_uuid; if (targetAgentUser && targetAgentUser.site_uuid) { targetSiteUuid = targetAgentUser.site_uuid; } else { - const summaryDoc = await Summary.findOne({ agent_uuid: viewDecoded.viewAs }).lean(); + const summaryDoc = await Summary.findOne({ agent_uuid: targetAgent }).lean(); if (summaryDoc && summaryDoc.site_uuid) { targetSiteUuid = summaryDoc.site_uuid; } @@ -32,7 +60,7 @@ async function requireAuth(req, res, next) { req.user = { ...req.user, role: 'AGENT_VIEWER', - agent_uuid: viewDecoded.viewAs, + agent_uuid: targetAgent, agent_label: viewDecoded.viewAsLabel, site_uuid: targetSiteUuid, _viewAsMode: true, @@ -50,12 +78,25 @@ async function requireAuth(req, res, next) { } } -function requireAdmin(req, res, next) { +async function requireAdmin(req, res, next) { const token = req.cookies?.token; if (!token) return res.status(401).json({ error: 'Not authenticated' }); try { const decoded = jwt.verify(token, JWT_SECRET); - if (decoded.role !== 'SUPER_ADMIN' && decoded.role !== 'TENANT_ADMIN' && decoded.role !== 'SOC_ANALYST') { + + // Verify session status in MongoDB + if (decoded.session_id) { + const activeSession = await Session.findById(decoded.session_id); + if (!activeSession) { + res.clearCookie('token'); + return res.status(401).json({ error: 'Sesi login telah dinonaktifkan atau kedaluwarsa.' }); + } + activeSession.last_active = new Date(); + await activeSession.save(); + } + + const validAdminRoles = ['SUPER_ADMIN', 'COMPANY_ADMIN', 'COMPANY_OPERATOR', 'TENANT_ADMIN', 'SOC_ANALYST']; + if (!validAdminRoles.includes(decoded.role)) { return res.status(403).json({ error: 'Forbidden' }); } req.adminUser = decoded; diff --git a/backend/models/Schemas.js b/backend/models/Schemas.js index 1beafbf..8302b90 100644 --- a/backend/models/Schemas.js +++ b/backend/models/Schemas.js @@ -1,184 +1,187 @@ -// backend/models/Schemas.js -// ───────────────────────────────────────────────────────────────────────────── -// MongoDB Schemas untuk BackOne Backend (READ-ONLY) -// -// PENTING: Schema ini harus sinkron dengan proxy/models/Schemas.js -// Proxy yang MENULIS data, backend yang MEMBACA data. -// -// Setiap dokumen di-tag dengan: -// agent_uuid → identifikasi Network Agent spesifik (isolasi per tenant) -// site_uuid → identifikasi site DPI (BackOne) -// timestamp → waktu data dikumpulkan -// ───────────────────────────────────────────────────────────────────────────── - -const mongoose = require('mongoose'); - -const baseOptions = { - timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } -}; - -// ─── Bandwidth Summary (per agent, per collection cycle) ─────────────────────── -const SummarySchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, // null = global/all agents - site_uuid: { type: String, index: true }, - bandwidth_down: Number, - bandwidth_up: Number, - active_flows: Number, - download_speed: Number, - upload_speed: Number, - total_devices: Number, - total_threats: Number, - packet_drops: Number, - peak_flow_rate: Number, - cpu_usage: Number, - memory_usage: Number, - queue_depth: Number, -}, baseOptions); - -// ─── Top Applications (per agent) ───────────────────────────────────────────── -const AppStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - app_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── Protocol Statistics (per agent) ────────────────────────────────────────── -const ProtocolStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - protocol_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── Discovered Devices (per agent, includes IP + MAC + device info) ─────────── -const DeviceStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - ip_address: { type: String, required: true, index: true }, - mac_address: { type: String, index: true }, - device_label: String, - device_type: String, - os_label: String, - manufacturer: String, - download: Number, - upload: Number, - flows: Number, - last_seen: String, -}, baseOptions); - -// ─── Network Flows (per agent) ───────────────────────────────────────────────── -const FlowSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - flow_id: String, - src_ip: { type: String, index: true }, - src_mac: String, - dst_ip: { type: String, index: true }, - dst_port: Number, - protocol: String, - app_label: String, - domain: { type: String, index: true }, - download: Number, - upload: Number, - first_seen: String, - last_seen: String, -}, baseOptions); - -// ─── Cyber Threats (per agent) ───────────────────────────────────────────────── -const ThreatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - threat_type: String, - severity: String, - src_ip: String, - dst_ip: String, - dst_port: Number, - protocol: String, - description: String, - event_at: String, -}, baseOptions); - -// ─── App Categories (per agent) ─────────────────────────────────────────────── -const AppCategoryStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - category_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── System Events (per agent) ───────────────────────────────────────────────── -const EventSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - event_id: Number, - event_type: String, - severity: String, - description: String, - category_label: String, - ip_address: String, - mac_address: String, - event_at: Date, -}, baseOptions); - -// ─── Compound Indexes for common dashboard queries ───────────────────────────── -SummarySchema.index({ agent_uuid: 1, timestamp: -1 }); -AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 }); -DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); -FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); -FlowSchema.index({ agent_uuid: 1, flow_id: 1 }); -FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 }); -FlowSchema.index({ agent_uuid: 1, domain: 1, timestamp: -1 }); -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 }); - -// ── Per-Device Per-Application Stats (synced from proxy) ───────────────── -const DeviceAppStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - ip_address: { type: String, required: true, index: true }, - app_label: { type: String, required: true }, - app_id: Number, - download: { type: Number, default: 0 }, - upload: { type: Number, default: 0 }, - flows: { type: Number, default: 0 }, - last_seen: String, -}, baseOptions); -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 }); - -const telemetrySchemas = require('./SchemasTelemetry'); -const auxSchemas = require('./SchemasAux'); - -module.exports = { - Summary: mongoose.model('Summary', SummarySchema), - AppStat: mongoose.model('AppStat', AppStatSchema), - ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema), - DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), - DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema), - Flow: mongoose.model('Flow', FlowSchema), - Threat: mongoose.model('Threat', ThreatSchema), - AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), - Event: mongoose.model('Event', EventSchema), - ...auxSchemas, - ...telemetrySchemas -}; - +// backend/models/Schemas.js +// ───────────────────────────────────────────────────────────────────────────── +// MongoDB Schemas untuk BackOne Backend (READ-ONLY) +// +// PENTING: Schema ini harus sinkron dengan proxy/models/Schemas.js +// Proxy yang MENULIS data, backend yang MEMBACA data. +// +// Setiap dokumen di-tag dengan: +// agent_uuid → identifikasi Network Agent spesifik (isolasi per tenant) +// site_uuid → identifikasi site DPI (BackOne) +// timestamp → waktu data dikumpulkan +// ───────────────────────────────────────────────────────────────────────────── + +const mongoose = require('mongoose'); + +const baseOptions = { + timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } +}; + +// ─── Bandwidth Summary (per agent, per collection cycle) ─────────────────────── +const SummarySchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, // null = global/all agents + site_uuid: { type: String, index: true }, + bandwidth_down: Number, + bandwidth_up: Number, + active_flows: Number, + download_speed: Number, + upload_speed: Number, + total_devices: Number, + total_threats: Number, + packet_drops: Number, + peak_flow_rate: Number, + cpu_usage: Number, + memory_usage: Number, + queue_depth: Number, +}, baseOptions); + +// ─── Top Applications (per agent) ───────────────────────────────────────────── +const AppStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + app_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── Protocol Statistics (per agent) ────────────────────────────────────────── +const ProtocolStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + protocol_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── Discovered Devices (per agent, includes IP + MAC + device info) ─────────── +const DeviceStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + ip_address: { type: String, required: true, index: true }, + mac_address: { type: String, index: true }, + device_label: String, + device_type: String, + os_label: String, + manufacturer: String, + download: Number, + upload: Number, + flows: Number, + last_seen: String, +}, baseOptions); + +// ─── Network Flows (per agent) ───────────────────────────────────────────────── +const FlowSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + flow_id: String, + src_ip: { type: String, index: true }, + src_mac: { type: String, index: true }, + dst_ip: { type: String, index: true }, + dst_port: Number, + protocol: String, + app_label: String, + domain: { type: String, index: true }, + download: Number, + upload: Number, + first_seen: String, + last_seen: String, +}, baseOptions); + +// ─── Cyber Threats (per agent) ───────────────────────────────────────────────── +const ThreatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + threat_type: String, + severity: String, + src_ip: String, + dst_ip: String, + dst_port: Number, + protocol: String, + description: String, + event_at: String, + flow_id: { type: String, index: true }, +}, baseOptions); + +// ─── App Categories (per agent) ─────────────────────────────────────────────── +const AppCategoryStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + category_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── System Events (per agent) ───────────────────────────────────────────────── +const EventSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + event_id: Number, + event_type: String, + severity: String, + description: String, + category_label: String, + ip_address: String, + mac_address: String, + event_at: Date, + flow_id: { type: String, index: true }, +}, baseOptions); + +// ─── Compound Indexes for common dashboard queries ───────────────────────────── +SummarySchema.index({ agent_uuid: 1, timestamp: -1 }); +AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 }); +DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); +FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); +FlowSchema.index({ agent_uuid: 1, flow_id: 1 }); +FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 }); +FlowSchema.index({ agent_uuid: 1, domain: 1, timestamp: -1 }); +FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 }); +FlowSchema.index({ site_uuid: 1, src_mac: 1, timestamp: -1 }); +ThreatSchema.index({ agent_uuid: 1, timestamp: -1 }); +AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 }); +EventSchema.index({ agent_uuid: 1, timestamp: -1 }); + +// ── Per-Device Per-Application Stats (synced from proxy) ───────────────── +const DeviceAppStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + ip_address: { type: String, required: true, index: true }, + app_label: { type: String, required: true }, + app_id: Number, + download: { type: Number, default: 0 }, + upload: { type: Number, default: 0 }, + flows: { type: Number, default: 0 }, + last_seen: String, +}, baseOptions); +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 }); + +const telemetrySchemas = require('./SchemasTelemetry'); +const auxSchemas = require('./SchemasAux'); + +module.exports = { + Summary: mongoose.model('Summary', SummarySchema), + AppStat: mongoose.model('AppStat', AppStatSchema), + ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema), + DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), + DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema), + Flow: mongoose.model('Flow', FlowSchema), + Threat: mongoose.model('Threat', ThreatSchema), + AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), + Event: mongoose.model('Event', EventSchema), + ...auxSchemas, + ...telemetrySchemas +}; + diff --git a/backend/models/Session.js b/backend/models/Session.js new file mode 100644 index 0000000..29c8de8 --- /dev/null +++ b/backend/models/Session.js @@ -0,0 +1,22 @@ +// backend/models/Session.js +// ───────────────────────────────────────────────────────────────────────────── +// MongoDB User Session Schema for remote revocation capability +// ───────────────────────────────────────────────────────────────────────────── + +const mongoose = require('mongoose'); + +const SessionSchema = new mongoose.Schema({ + user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + ip_address: { type: String, default: 'Unknown' }, + user_agent: { type: String, default: 'Unknown' }, + session_token: { type: String, required: true, unique: true }, // JWT JTI or unique token hash + last_active: { type: Date, default: Date.now }, + expires_at: { type: Date, required: true }, // MongoDB TTL Index specified below via SessionSchema.index +}, { + timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } +}); + +// TTL index to automatically remove expired sessions from MongoDB +SessionSchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 }); + +module.exports = mongoose.model('Session', SessionSchema); diff --git a/backend/models/User.js b/backend/models/User.js index 5fbf12a..96310d7 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -14,11 +14,15 @@ const UserSchema = new mongoose.Schema({ password_hash: { type: String, required: true }, account_name: { type: String, default: null }, profile_picture: { type: String, default: null }, - role: { type: String, enum: ['SUPER_ADMIN', 'TENANT_ADMIN', 'SOC_ANALYST', 'ENGINEER', 'AGENT_VIEWER'], default: 'AGENT_VIEWER' }, + role: { type: String, enum: ['SUPER_ADMIN', 'EXECUTIVE', 'TENANT_ADMIN', 'SOC_ANALYST', 'ENGINEER', 'AGENT_VIEWER', 'COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER'], default: 'AGENT_VIEWER' }, site_uuid: { type: String, default: null, index: true }, agent_uuid: { type: String, default: null }, + company_name: { type: String, default: null, index: true }, + agent_uuids: { type: [String], default: [] }, created_by: { type: String, default: null, index: true }, is_active: { type: Boolean, default: true }, + login_attempts: { type: Number, default: 0 }, + lockout_until: { type: Date, default: null }, }, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } }); diff --git a/backend/netify.js b/backend/netify.js new file mode 100644 index 0000000..a4ab2a6 --- /dev/null +++ b/backend/netify.js @@ -0,0 +1,2718 @@ +// backend/netify.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const axios = require('axios'); + +const BASE_URL = process.env.BACKONE_INFORMATICS_BASE_URL || process.env.NETIFY_INFORMATICS_BASE_URL || 'https://informatics.netify.ai/api/v1'; +const JWT_TOKEN = process.env.BACKONE_JWT_TOKEN || process.env.NETIFY_TOKEN || process.env.NETIFY_JWT_TOKEN; +const SITE_UUID = process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID; +const agentMap = {}; + +function headersSite(useApiKey = false) { + const token = process.env.BACKONE_API_KEY || process.env.NETIFY_API_KEY || JWT_TOKEN; + const headers = { 'x-api-key': token, 'Accept': 'application/json' }; + if (SITE_UUID) { + headers['x-net-site'] = SITE_UUID; + } + return headers; +} + + + +function fixDates(obj) { + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) fixDates(obj[i]); + } else if (obj !== null && typeof obj === 'object') { + for (const key in obj) { + if ((key === 'first_seen_at' || key === 'last_seen_at' || key.endsWith('_at')) && obj[key] && typeof obj[key].date === 'string') { + let d = obj[key].date; + if (d.includes(' ') && !d.endsWith('Z')) { + obj[key].date = d.replace(' ', 'T') + 'Z'; + } else if (!d.endsWith('Z') && !d.includes('+') && d.includes('T')) { + obj[key].date = d + 'Z'; + } + } else if (typeof obj[key] === 'object') { + fixDates(obj[key]); + } + } + } +} + +async function netifyFetch(path, params = {}, useApiKey = false, agentUuid = null) { + if (agentUuid) { + const agentId = agentMap[agentUuid]; + if (agentId) { + params.filter_agents = `[${agentId}]`; + } else { + params.settings_agent = agentUuid; + } + } + if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) { + console.error('[Netify] Failed due to invalid config', {hasToken: !!JWT_TOKEN, SITE_UUID}); + return null; + } + try { + const res = await axios.get(`${BASE_URL}${path}`, { + headers: headersSite(useApiKey), params, timeout: 30000, + }); + const json = res.data; + if (json?.status_code !== 0) { + console.error(`[Netify] API Error ${json?.status_code} pada ${path}: ${json?.status_message}`); + return null; + } + if (json && json.data) fixDates(json.data); + return json?.data ?? null; + } catch (err) { + const s = err.response?.status; + const msg = JSON.stringify(err.response?.data ?? err.message); + console.error(`[Netify] ${s ?? 'ERR'} ${path}: ${msg}`); + return null; + } +} + +// ─── TOP APPS: download + upload digabung ───────────────────────────────────── +async function fetchTopApps(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + + // Buat map upload berdasarkan app_id + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const id = r.application?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + + return dlData.map(r => ({ + app_id : r.application?.id ?? null, + app_label : r.application?.label ?? 'Unknown', + app_tag : r.application?.tag ?? null, + category : r.application?.category?.label ?? null, + favicon : r.application?.favicon ?? null, + download : r.download ?? 0, + upload : ulMap[r.application?.id] ?? 0, + total : (r.download ?? 0) + (ulMap[r.application?.id] ?? 0), + flows : r.flows ?? 0, + })); +} + +// ─── TOP DEVICES: download + upload digabung ────────────────────────────────── +async function fetchTopDevices(interval = 1440, limit = 50, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + + // Map upload berdasarkan IP address + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + ulMap[ip] = r.upload ?? 0; + } + } + + return dlData.map(r => { + const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); + return { + ip_address : ip, + mac_address : null, + device_label : ip, + device_type : null, + os_label : null, + manufacturer : null, + download : r.download ?? 0, + upload : ulMap[ip] ?? 0, + last_seen : null, + }; + }); +} + +// ─── PORT-TO-SERVICE MAPPING — untuk enrichment flows ──────────────────────── +const PORT_SERVICE_MAP = { + 80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt', + 53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS', + 25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP', + 22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC', + 21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control', + 3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB', + 1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T', + 1723: 'PPTP', 1701: 'L2TP', + 67: 'DHCP', 68: 'DHCP Client', 123: 'NTP', + 161: 'SNMP', 162: 'SNMP Trap', 514: 'Syslog', + 6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent', + 9993: 'ZeroTier VPN', 3478: 'STUN/TURN', 5004: 'RTP Media', + 5060: 'SIP', 5061: 'SIP TLS', + 8883: 'MQTT TLS', 1883: 'MQTT', + 179: 'BGP', 520: 'RIP', +}; + +// ─── FLOWS: endpoint /data/flows dengan enrichment app/domain ───────────────── +// NOTE: API flows tidak punya field app/domain — enrichment dilakukan via: +// 1. PORT_SERVICE_MAP (reliable, berdasarkan dst port) +// 2. tls_sni field (satu-satunya SNI field valid, tapi nilai sering kosong) +async function fetchFlows(limit = 500, agentUuid = null) { + // Hanya fetch flows + tls_sni (satu-satunya SNI field yang valid = bukan 422) + const [raw, tslSniRaw] = await Promise.all([ + netifyFetch('/data/flows', { settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), + ]); + + if (!raw || !Array.isArray(raw)) return null; + + // Build TLS SNI lookup — filter yang tidak kosong + const sniList = []; + if (tslSniRaw && Array.isArray(tslSniRaw)) { + for (const r of tslSniRaw) { + const sni = r.tls_sni; + if (sni && sni.trim() !== '') { + sniList.push(sni.replace(/^\*\./, '').trim()); + } + } + } + + return raw.map(r => { + const port = r.remote_port ?? null; + // Primary enrichment: port → service name + const portService = port ? (PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null; + + let domain = null; + let appLabel = portService; + + // Secondary enrichment: for HTTPS flows, use SNI list if available + if ((port === 443 || port === 8443) && sniList.length > 0) { + domain = sniList[0]; + } + + return { + flow_id : String(r.flow_id ?? ''), + src_ip : r.local_ip?.address ?? null, + src_mac : r.local_mac ?? r.mac?.address ?? null, + dst_ip : r.remote_ip?.address ?? null, + dst_port : port, + protocol : r.ip_protocol?.label ?? null, + app_label : appLabel, + domain : domain, + download : r.download ?? 0, + upload : r.upload ?? 0, + first_seen : r.first_seen_at?.date ?? null, + last_seen : r.last_seen_at?.date ?? null, + }; + }); +} + + +// ─── PROTOCOLS ──────────────────────────────────────────────────────────────── +async function fetchTopProtocols(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const id = r.protocol?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + + return dlData.map(r => ({ + protocol_id : r.protocol?.id ?? null, + protocol_label : r.protocol?.label ?? 'Unknown', + download : r.download ?? 0, + upload : ulMap[r.protocol?.id] ?? 0, + flows : r.flows ?? 0, + })); +} + +// ─── COUNTRIES ──────────────────────────────────────────────────────────────── +async function fetchTopCountries(interval = 1440, limit = 15, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const code = r.country?.code; + if (code) ulMap[code] = r.upload ?? 0; + } + } + + return dlData.map(r => ({ + country_code : r.country?.code ?? null, + country_name : r.country?.label ?? 'Unknown', + download : r.download ?? 0, + upload : ulMap[r.country?.code] ?? 0, + flows : r.flows ?? 0, + })); +} + +// ─── DNS/HOSTNAME — gunakan tls_sni karena hostname endpoint timeout ────────── +async function fetchTopDomains(interval = 1440, limit = 50, agentUuid = null) { + // NOTE: /data/stats/top/hostname/download selalu timeout + // Gunakan tls_sni yang confirmed bekerja di API ini + const raw = await netifyFetch('/data/stats/top/tls_sni/download', { + filter_interval: interval, settings_limit: limit, + }, false, agentUuid); + if (!raw) return null; + + return raw + .filter(r => r.tls_sni && r.tls_sni.trim() !== '') + .map(r => ({ + domain : r.tls_sni.replace(/^\*\./, '').trim(), + app_label : null, + category : 'HTTPS/TLS', + download : r.download ?? 0, + query_count : r.download ?? 0, + })); +} + + +// ─── BANDWIDTH SUMMARY untuk timeline ───────────────────────────────────────── +async function fetchBandwidthSummary(interval = 30, agentUuid = null) { + const rawSummary = await netifyFetch('/data/stats/summary', { filter_interval: interval }, false, agentUuid); + if (rawSummary) { + return { + download: rawSummary.download ?? 0, + upload: rawSummary.upload ?? 0, + flows: rawSummary.flow_hourly_count ?? 0, + download_speed: rawSummary.download_speed ?? 0, + upload_speed: rawSummary.upload_speed ?? 0, + flow_speed: rawSummary.flow_speed ?? 0, + devices: 0, + }; + } + + // Fallback to old way (aggregate top 100 application stats) + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + ]); + + const download = (dlData ?? []).reduce((s, r) => s + (r.download ?? 0), 0); + const upload = (ulData ?? []).reduce((s, r) => s + (r.upload ?? 0), 0); + const flows = (dlData ?? []).reduce((s, r) => s + (r.flows ?? 0), 0); + + return { + download, + upload, + flows, + download_speed: 0, + upload_speed: 0, + flow_speed: 0, + devices: dlData?.length ?? 0 + }; +} + +// ─── THREATS — gunakan flows dengan filter anomali sebagai fallback ──────────── +async function fetchCyberThreats(limit = 50, agentUuid = null) { + // Events/threats belum ada di v1 — return array kosong agar tidak error + // Akan diisi saat endpoint ditemukan + return []; +} + +// ─── EVENTS ─────────────────────────────────────────────────────────────────── +async function fetchEvents(limit = 50, agentUuid = null) { + const raw = await netifyFetch('/event/events', { settings_limit: limit }, false, agentUuid); + if (!raw || !Array.isArray(raw)) return []; + + return raw.map(r => { + let msg = r.label || ''; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + msg = descObj.default || r.label || ''; + if (descObj.tags) { + for (const k in descObj.tags) { + const val = Array.isArray(descObj.tags[k]) ? descObj.tags[k][0] : descObj.tags[k]; + msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val); + } + } + } catch (e) { + msg = r.description; + } + } + + let sevLabel = 'Info'; + if (r.severity >= 30) sevLabel = 'Critical'; + else if (r.severity >= 20) sevLabel = 'High'; + else if (r.severity >= 10) sevLabel = 'Warning'; + + let srcIp = null; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null; + } catch {} + } + + return { + event_id: r.id || null, + event_type: r.basename || 'unknown', + severity: sevLabel, + mac_address: r.additional?.device?.mac?.address || null, + ip_address: srcIp, + description: msg, + event_at: r.created_at?.date || new Date().toISOString() + }; + }); +} + +async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null) { + // Ambil lebih banyak bandwidth data (limit x2) supaya coverage IP lebih luas + const [intelRaw, dlData, ulData, flowsRaw] = await Promise.all([ + netifyFetch('/intelligence/discovery/devices', { settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + ]); + + if (!intelRaw || !Array.isArray(intelRaw)) return []; + + // Map bandwidth per IP + const dlMap = {}; + const ulMap = {}; + if (dlData) { + for (const r of dlData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + if (ip) dlMap[ip] = r.download ?? 0; + } + } + if (ulData) { + for (const r of ulData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + if (ip) ulMap[ip] = r.upload ?? 0; + } + } + + // Enrich bandwidth from flows — aggregate per local_ip as fallback + // Bagi device yang IP-nya tidak ada di top stats (traffic kecil) + const dlFlowMap = {}; + const ulFlowMap = {}; + const macFlowMap = {}; + const lastSeenFlowMap = {}; + + if (flowsRaw && Array.isArray(flowsRaw)) { + for (const f of flowsRaw) { + const ip = f.local_ip?.address; + if (!ip) continue; + + if (!dlMap[ip]) { + dlFlowMap[ip] = (dlFlowMap[ip] ?? 0) + (f.download ?? 0); + } + if (!ulMap[ip]) { + ulFlowMap[ip] = (ulFlowMap[ip] ?? 0) + (f.upload ?? 0); + } + + // Correlate MAC Address from flow + if (!macFlowMap[ip]) { + const flowMac = f.local_mac ?? f.mac?.address; + if (flowMac && flowMac !== '00:00:00:00:00:00') { + macFlowMap[ip] = flowMac; + } + } + + // Correlate Timestamp from flow + const flowTime = f.last_seen_at?.date || f.created_at?.date; + if (flowTime) { + if (!lastSeenFlowMap[ip] || new Date(flowTime) > new Date(lastSeenFlowMap[ip])) { + lastSeenFlowMap[ip] = flowTime; + } + } + } + } + + const resolvedList = intelRaw.map(r => { + const ip = r.ip?.address ?? null; + let mac = r.mac_address ?? r.discovery_mac?.address ?? null; + if (!mac && ip && macFlowMap[ip]) mac = macFlowMap[ip]; + + const oui = mac ? mac.substring(0, 8).toUpperCase() : null; + const mfr = r.mac_vendor !== 'Unknown' && r.mac_vendor !== 'Local' ? r.mac_vendor : (OUI_MAP[oui] ?? r.mac_vendor ?? null); + + // Get device_type — prefer discovery_type if meaningful, else device_type + const rawType = r.discovery_type?.label; + const deviceType = (rawType && rawType !== 'Unknown' && rawType !== 'Unclassified') + ? rawType + : (r.device_type?.label && r.device_type.label !== 'Unclassified' ? r.device_type.label : rawType ?? null); + + // Get OS — prefer discovery_os if meaningful, else device_os + const rawOs = r.discovery_os?.label; + const osLabel = (rawOs && rawOs !== 'Unknown' && rawOs !== 'Unclassified') + ? rawOs + : (r.device_os?.label && r.device_os.label !== 'Unclassified' ? r.device_os.label : rawOs ?? null); + + // Bandwidth: prioritize top stats, fallback to flows aggregation + const dl = ip ? (dlMap[ip] ?? dlFlowMap[ip] ?? 0) : 0; + const ul = ip ? (ulMap[ip] ?? ulFlowMap[ip] ?? 0) : 0; + + let last_seen = r.last_seen_at?.date || r.discovery_mac?.last_seen_at?.date || null; + if (!last_seen && ip && lastSeenFlowMap[ip]) last_seen = lastSeenFlowMap[ip]; + + return { + ip_address : ip, + mac_address : mac, + device_label : r.device?.label || r.discovery_mac?.discovery_hardware || ip || 'Unknown', + device_type : deviceType, + os_label : osLabel, + manufacturer : mfr, + download : dl, + upload : ul, + last_seen : last_seen, + }; + }); + + const seenIps = new Set(resolvedList.map(d => d.ip_address).filter(Boolean)); + const allActiveIps = new Set([ + ...Object.keys(dlMap), + ...Object.keys(ulMap) + ]); + + for (const ip of allActiveIps) { + if (!seenIps.has(ip)) { + seenIps.add(ip); + const dl = dlMap[ip] ?? dlFlowMap[ip] ?? 0; + const ul = ulMap[ip] ?? ulFlowMap[ip] ?? 0; + resolvedList.push({ + ip_address : ip, + mac_address : macFlowMap[ip] || null, + device_label : ip, + device_type : 'LAN Client', + os_label : 'Windows/Linux', + manufacturer : 'Unknown', + download : dl, + upload : ul, + last_seen : lastSeenFlowMap[ip] || new Date().toISOString() + }); + } + } + + return resolvedList.sort((a, b) => (b.download + b.upload) - (a.download + a.upload)); +} + +// OUI lookup — manufacturer dari 3 oktet pertama MAC +const OUI_MAP = { + '60:BE:B4' : 'Ruckus Networks', + '74:6F:88' : 'Ruckus Networks', + '04:F4:1C' : 'MikroTik', + 'F4:6D:3F' : 'TP-Link', + 'B8:27:EB' : 'Raspberry Pi', + 'DC:A6:32' : 'Raspberry Pi', + 'E4:5F:01' : 'Raspberry Pi', + '00:50:56' : 'VMware', + '08:00:27' : 'VirtualBox', + 'AC:17:02' : 'ASUSTek', + 'B4:2E:99' : 'ASUSTek', + '18:31:BF' : 'ASUSTek', + '74:D0:2B' : 'Cisco', + 'F8:72:EA' : 'Cisco', + '00:1A:A0' : 'Cisco', + 'FC:FB:FB' : 'Cisco Meraki', + '88:15:44' : 'Cisco Meraki', + '00:18:0A' : 'Ubiquiti', + '04:18:D6' : 'Ubiquiti', + '24:A4:3C' : 'Ubiquiti', + '78:8A:20' : 'Ubiquiti', + 'DC:9F:DB' : 'Ubiquiti', + '80:2A:A8' : 'Ubiquiti', + 'FC:EC:DA' : 'Ubiquiti', + '00:27:22' : 'Ubiquiti', + 'B4:FB:E4' : 'Samsung', + '8C:79:F0' : 'Samsung', + 'F0:25:B7' : 'Samsung', + '78:BD:BC' : 'Samsung', + '3C:28:6D' : 'Apple', + 'A4:C3:F0' : 'Apple', + 'F8:FF:C2' : 'Apple', + '98:01:A7' : 'Apple', + '3C:22:FB' : 'Apple', + 'F0:DB:F8' : 'Huawei', + '00:E0:FC' : 'Huawei', + '54:89:98' : 'Huawei', + '28:31:52' : 'Xiaomi', + '64:09:80' : 'Xiaomi', + 'AC:C1:EE' : 'Xiaomi', + '50:64:2B' : 'Xiaomi', + '00:0C:29' : 'VMware', + '00:15:5D' : 'Microsoft Hyper-V', + '52:54:00' : 'QEMU/KVM', +}; + +// ─── TAMBAHAN FITUR 1-11 ────────────────────────────────────────────────────── + +// 1. Top Application Category +async function fetchTopAppCategories(interval = 1440, limit = 15, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.application_category?.label ?? r.application_category; + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.application_category?.label ?? String(r.application_category ?? 'Unknown'); + return { + category_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 2. Top Continent +async function fetchTopContinents(interval = 1440, limit = 10, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/continent/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/continent/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.continent?.label ?? String(r.continent ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.continent?.label ?? String(r.continent ?? 'Unknown'); + return { + continent_name : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 3. Top Region +async function fetchTopRegions(interval = 1440, limit = 20, agentUuid = null) { + const raw = await netifyFetch('/data/stats/top/region/download', { + filter_interval: interval, settings_limit: limit, + }, false, agentUuid); + if (!raw) return null; + return raw.map(r => ({ + region_name : r.region?.region_name?.trim() || '(Unknown Region)', + region_code : r.region?.region_code?.trim() || null, + country_name : r.region?.country_name ?? null, + country_code : r.region?.country_code ?? null, + download : r.download ?? 0, + })); +} + +// 4. Top City +async function fetchTopCities(interval = 1440, limit = 20, agentUuid = null) { + const raw = await netifyFetch('/data/stats/top/city/download', { + filter_interval: interval, settings_limit: limit, + }, false, agentUuid); + if (!raw) return null; + return raw.map(r => ({ + city_name : r.city?.city?.trim() || '(Unknown City)', + region_name : r.city?.region_name?.trim() || null, + country_name : r.city?.country_name ?? null, + country_code : r.city?.country_code ?? null, + download : r.download ?? 0, + })); +} + +// 5. Top VLAN +async function fetchTopVLANs(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/vlan/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/vlan/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.vlan?.id ?? 0; + ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => ({ + vlan_id : r.vlan?.id ?? 0, + vlan_label : r.vlan?.label ?? `VLAN ${r.vlan?.id ?? 0}`, + download : r.download ?? 0, + upload : ulMap[r.vlan?.id ?? 0] ?? 0, + total : (r.download ?? 0) + (ulMap[r.vlan?.id ?? 0] ?? 0), + })); +} + +// 6. Top Interface +async function fetchTopInterfaces(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/interface/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/interface/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.interface?.id; + if (key !== undefined) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => ({ + iface_id : r.interface?.id ?? null, + iface_name : r.interface?.name ?? 'Unknown', + iface_role : r.interface?.role ?? null, + agent_id : r.interface?.agent_id ?? null, + download : r.download ?? 0, + upload : ulMap[r.interface?.id] ?? 0, + total : (r.download ?? 0) + (ulMap[r.interface?.id] ?? 0), + })); +} + +// 7. Top Flow Type +async function fetchTopFlowTypes(interval = 1440, limit = 10, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/flow_type/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/flow_type/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.flow_type?.label ?? String(r.flow_type ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.flow_type?.label ?? String(r.flow_type ?? 'Unknown'); + return { + flow_type_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 8. Top Flow Origin +async function fetchTopFlowOrigins(interval = 1440, limit = 10, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/flow_origin/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.flow_origin?.label ?? String(r.flow_origin ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.flow_origin?.label ?? String(r.flow_origin ?? 'Unknown'); + return { + flow_origin_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 9. Top IP Version +async function fetchTopIPVersions(interval = 1440, limit = 5, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/ip_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/ip_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.ip_version?.label ?? String(r.ip_version ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.ip_version?.label ?? String(r.ip_version ?? 'Unknown'); + return { + ip_version_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 10. Top Remote IP +async function fetchTopRemoteIPs(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/remote_ip/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.remote_ip?.address ?? String(r.remote_ip ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const addr = r.remote_ip?.address ?? String(r.remote_ip ?? 'Unknown'); + return { + remote_ip : addr, + ip_version : r.remote_ip?.version ?? null, + download : r.download ?? 0, + upload : ulMap[addr] ?? 0, + total : (r.download ?? 0) + (ulMap[addr] ?? 0), + }; + }); +} + +// 11. Top Local MAC + Discovery OS +async function fetchTopLocalMACs(interval = 1440, limit = 50, agentUuid = null) { + const [dlData, ulData, osData] = await Promise.all([ + netifyFetch('/data/stats/top/local_mac/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/local_mac/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: 20 }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + if (r.local_mac) ulMap[r.local_mac] = r.upload ?? 0; + } + // OS summary untuk info panel + const osList = (osData ?? []).map(r => ({ + os_label : r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown'), + download : r.download ?? 0, + })); + return dlData.map(r => { + const mac = r.local_mac ?? 'Unknown'; + const oui = mac.substring(0, 8).toUpperCase(); + return { + mac_address : mac, + manufacturer : OUI_MAP[oui] ?? null, + download : r.download ?? 0, + upload : ulMap[mac] ?? 0, + total : (r.download ?? 0) + (ulMap[mac] ?? 0), + _os_summary : osList, // disertakan di item pertama saja + }; + }); +} + +// ─── DISCOVERY OS (standalone) ─────────────────────────────────────────────── +async function fetchTopDiscoveryOS(interval = 1440, limit = 20, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/discovery_os/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.discovery_os?.label ?? String(r.discovery_os ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown'); + return { + os_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// ─── DPI FIELDS ─────────────────────────────────────────────────────────────── + +// TLS Version +async function fetchTLSVersions(interval = 1440, limit = 10, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_version?.label ?? String(r.tls_version ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_version?.label ?? String(r.tls_version ?? 'Unknown'); + return { + tls_version : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// TLS Cipher +async function fetchTLSCiphers(interval = 1440, limit = 15, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_cipher?.label ?? String(r.tls_cipher ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_cipher?.label ?? String(r.tls_cipher ?? 'Unknown'); + return { + tls_cipher : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// TLS Security Level +async function fetchTLSSecurity(interval = 1440, limit = 10, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_security?.label ?? String(r.tls_security ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_security?.label ?? String(r.tls_security ?? 'Unknown'); + // Assign warna berdasarkan label untuk UI + const color = label === 'Recommended' ? 'green' + : label === 'Secure' ? 'blue' + : label === 'Weak' ? 'orange' + : label === 'Insecure' ? 'red' + : 'gray'; + return { + tls_security : label, + color : color, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// NetBIOS Hostname (nama PC Windows) +async function fetchNetBIOSHostnames(interval = 1440, limit = 30, agentUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/netbios_hostname/download', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch('/data/stats/top/netbios_hostname/upload', { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const name = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? 'Unknown'); + return { + hostname : name, + download : r.download ?? 0, + upload : ulMap[name] ?? 0, + total : (r.download ?? 0) + (ulMap[name] ?? 0), + }; + }); +} + +async function fetchLookupApplications(page = 1, limit = 50, search = '', agentUuid = null) { + if (!JWT_TOKEN || !SITE_UUID || JWT_TOKEN === 'YOUR_JWT_TOKEN' || SITE_UUID === 'YOUR_SITE_UUID' || JWT_TOKEN.startsWith('change_me') || JWT_TOKEN.startsWith('YOUR_')) { + return { + applications: [ + { id: 1, label: 'YouTube', tag: 'video', category: { label: 'Streaming' } }, + { id: 2, label: 'Netflix', tag: 'video', category: { label: 'Streaming' } }, + { id: 3, label: 'Web Browsing', tag: 'web', category: { label: 'General' } }, + { id: 4, label: 'Google Services', tag: 'google', category: { label: 'Tech' } }, + { id: 5, label: 'WhatsApp', tag: 'im', category: { label: 'Chat' } } + ], + pagination: { total_records: 5, current_page: 1, total_pages: 1 } + }; + } + const params = { + settings_page: page, + settings_limit: limit, + }; + if (search) { + params.filter_application = search; + } + try { + const res = await axios.get(`${BASE_URL}/lookup/applications`, { + headers: headersSite(true), params, timeout: 30000, + }); + const json = res.data; + if (json?.status_code !== 0) { + console.error(`[Netify] API Error ${json?.status_code}: ${json?.status_message}`); + return { applications: [], pagination: {} }; + } + return { + applications: json.data || [], + pagination: json.data_info || {} + }; + } catch (err) { + console.error('[Netify] Lookup error:', err.message); + return { applications: [], pagination: {} }; + } +} + +// ─── DETAIL FETCHERS FOR INTERACTIVE MODALS (DB-BASED — API ignores filter params) ───────── +// +// NOTE: Netify Informatics API does NOT filter by filter_agent / filter_local_ip — +// all filter params are silently ignored and global data is returned. +// All detail queries therefore use the local SQLite DB (flows, devices tables). +// +// Agent ↔ src_mac mapping (determined empirically from flows data): +// 2F-TF-1D-GK (JRP Cibubur) → src_mac '60:be:b4:1f:05:96' (IPs 10.1.x.x, 10.0.x.x) +// 8A-V3-PB-85 (IFG LT.18) → src_mac '04:f4:1c:ce:c2:e6' (IPs 10.6.x.x, 192.168.9.x) +// F6-2V-DT-8A (CPI Balaraja) → src_mac '2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', etc (IPs 10.250.x.x) + +const AGENT_LABELS = { + '2F-TF-1D-GK': 'JRP Cibubur', + '8A-V3-PB-85': 'IFG LT.18', + 'F6-2V-DT-8A': 'CPI Balaraja', + '1R-79-J9-YE': 'CPI Balaraja WAN', +}; + +// Maps each agent UUID to its gateway/interface MAC address(es) in flows +const AGENT_MAC_MAP = { + '2F-TF-1D-GK': ['60:be:b4:1f:05:96'], + '8A-V3-PB-85': ['04:f4:1c:ce:c2:e6'], + 'F6-2V-DT-8A': ['2c:7b:a0:d8:86:91', '16:11:ac:73:34:1d', 'bc:45:5b:ca:d5:be', + 'de:ed:cc:57:58:34', 'f4:6d:3f:ef:01:a0', '60:be:b4:29:d3:36', + '60:be:b4:29:d3:33', '60:be:b4:26:4c:d6', '60:be:b4:29:d3:32'], + '1R-79-J9-YE': ['70:85:6c:6d:f7:17', '70:85:6c:81:50:d4', 'a2:cc:8e:7d:39:51', + 'f2:69:9d:a1:5e:11', '8e:91:0f:6e:24:63'], +}; + +// Build SQL IN clause placeholders +function inClause(arr) { + return arr.map(() => '?').join(','); +} + +// Fetch all data for a specific agent (by UUID) — from local DB flows +async function fetchAgentDetails(agentUuid) { + const db = require('./database'); + const d = db.getDB(); + + const label = AGENT_LABELS[agentUuid] || agentUuid; + const macs = AGENT_MAC_MAP[agentUuid]; + + const emptyResult = { agent_uuid: agentUuid, agent_label: label, summary: null, devices: [], flows: [], top_apps: [], security: { encryption_audit: [], insecure_protocols: [], unencrypted_passwords: [], ip_reputation: [], tor_detections: [], vpn_detections: [] }, events: [], mac_bandwidth: [], server_discovery: [] }; + + if (!macs || macs.length === 0) return emptyResult; + + const ph = inClause(macs); + + // ── 1. Top apps by this agent (from flows) ───────────────────────────────── + const appRows = db.getLatestBandwidthApps(15, null, agentUuid); + + const top_apps = appRows.map(r => ({ + app_id : null, + app_label : r.app_label, + category : null, + favicon : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + })); + + // ── 2. Distinct devices for this agent (using aligned subnet and MAC mapping) ─ + let resolvedDevices = db.getLatestDevices(100, null, agentUuid); + + // FALLBACK: If agent-specific API failed to return devices, extract from global using MACs + if (resolvedDevices.length === 0 && macs.length > 0) { + const latestDevGlobal = d.prepare(`SELECT MAX(fetched_at) as t FROM devices WHERE agent_uuid IS NULL`).get()?.t; + if (latestDevGlobal) { + resolvedDevices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ? AND agent_uuid IS NULL AND mac_address IN (${ph})`).all(latestDevGlobal, ...macs); + } + } + + const agentIPs = resolvedDevices.map(d => d.ip_address).filter(Boolean); + const phIPs = agentIPs.length > 0 ? agentIPs.map(() => '?').join(',') : null; + + const latestEncAudit = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; + const riskMap = {}; + if (latestEncAudit) { + const riskRows = d.prepare(`SELECT ip_address, encrypted_pct, risk_level FROM intel_encryption_audit WHERE fetched_at = ?`).all(latestEncAudit); + for (const r of riskRows) { + if (r.ip_address) riskMap[r.ip_address] = { encrypted_pct: r.encrypted_pct, risk_level: r.risk_level }; + } + } + + const insecureIPs = new Set( + phIPs ? d.prepare(`SELECT DISTINCT ip_address FROM intel_insecure_protocols WHERE ip_address IN (${phIPs})`).all(...agentIPs).map(r => r.ip_address) : [] + ); + + const devices = resolvedDevices.map(r => ({ + ip_address : r.ip_address, + mac_address : r.mac_address, + device_label : r.device_label || r.ip_address || 'Unknown', + device_type : r.device_type || null, + os_label : r.os_label || null, + manufacturer : r.manufacturer || null, + last_seen : r.fetched_at || null, + download : r.download ?? 0, + upload : r.upload ?? 0, + encrypted_pct: riskMap[r.ip_address]?.encrypted_pct ?? null, + risk_level : riskMap[r.ip_address]?.risk_level ?? null, + has_insecure : insecureIPs.has(r.ip_address), + })); + + // ── 3. Recent flows for this agent (using aligned flows list) ─────────────── + let flows = db.getLatestFlows(100, null, agentUuid); + + // FALLBACK: Extrapolate flows from global using MACs/IPs if agent-specific fails + if (flows.length === 0 && (macs.length > 0 || agentIPs.length > 0)) { + const latestFlowGlobal = d.prepare(`SELECT MAX(fetched_at) as t FROM flows WHERE agent_uuid IS NULL`).get()?.t; + if (latestFlowGlobal) { + if (phIPs) { + flows = d.prepare(`SELECT * FROM flows WHERE fetched_at = ? AND agent_uuid IS NULL AND (local_mac IN (${ph}) OR local_ip IN (${phIPs})) ORDER BY download DESC LIMIT 100`).all(latestFlowGlobal, ...macs, ...agentIPs); + } else { + flows = d.prepare(`SELECT * FROM flows WHERE fetched_at = ? AND agent_uuid IS NULL AND local_mac IN (${ph}) ORDER BY download DESC LIMIT 100`).all(latestFlowGlobal, ...macs); + } + } + } + + // ── 4. Summary stats (aligned with dashboard stats query) ─────────────────── + const stats = db.getStats(null, agentUuid); + + // ACCURATE BANDWIDTH CALCULATION FROM MAC BANDWIDTH (Overrides broken Netify Summary endpoint) + let totalDown = 0; + let totalUp = 0; + const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t; + if (latestMacSnap && macs.length > 0) { + const macRows = d.prepare(`SELECT download, upload FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${ph})`).all(latestMacSnap, ...macs); + for (const r of macRows) { + totalDown += (r.download || 0); + totalUp += (r.upload || 0); + } + } + + const summary = { + total_devices : stats.totalDevices > 0 ? stats.totalDevices : devices.length, + active_flows : stats.activeFlows > 0 ? stats.activeFlows : flows.length, + bandwidth_down : totalDown > 0 ? totalDown : (stats.latestBw?.total_download ?? 0), + bandwidth_up : totalUp > 0 ? totalUp : (stats.latestBw?.total_upload ?? 0), + }; + + // ── 5. Security Intel filtered by agent IPs & MACs ───────────────────────── + const encryptionRows = (latestEncAudit && phIPs) + ? d.prepare(`SELECT ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level, detected_at FROM intel_encryption_audit WHERE fetched_at = ? AND ip_address IN (${phIPs}) ORDER BY CASE risk_level WHEN 'Vulnerable' THEN 1 WHEN 'Moderate' THEN 2 ELSE 3 END`).all(latestEncAudit, ...agentIPs) + : []; + + const insecureProtoRows = phIPs + ? d.prepare(`SELECT ip_address, mac_address, protocol, risk, app_label, dst_ip, dst_port, download, upload, detected_at FROM intel_insecure_protocols WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs) + : []; + + let unencPwdRows = d.prepare(`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 50`).all(...macs); + if (unencPwdRows.length === 0 && phIPs) { + unencPwdRows = d.prepare(`SELECT ip_address, mac_address, dst_ip, dst_port, protocol, username, severity, download, upload, detected_at FROM intel_unencrypted_passwords WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs); + } + + const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t; + const ipReputRows = (latestRepSnap && phIPs) + ? d.prepare(`SELECT ip_address, local_ip, mac_address, reputation, score, country, app_label, blacklisted, download, upload, detected_at FROM intel_ip_reputation WHERE fetched_at = ? AND (local_ip IN (${phIPs}) OR ip_address IN (${phIPs})) ORDER BY score DESC LIMIT 50`).all(latestRepSnap, ...agentIPs, ...agentIPs) + : []; + + let torRows = d.prepare(`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 20`).all(...macs); + if (torRows.length === 0 && phIPs) { + torRows = d.prepare(`SELECT ip_address, mac_address, exit_node, circuit_id, country, download, upload, detected_at FROM intel_tor_detection WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 20`).all(...agentIPs); + } + + let vpnRows = d.prepare(`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE mac_address IN (${ph}) ORDER BY detected_at DESC LIMIT 20`).all(...macs); + if (vpnRows.length === 0 && phIPs) { + vpnRows = d.prepare(`SELECT ip_address, mac_address, vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at FROM intel_vpn_detection WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 20`).all(...agentIPs); + } + + const serverDiscRows = phIPs + ? d.prepare(`SELECT ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload, detected_at FROM intel_server_discovery WHERE ip_address IN (${phIPs}) ORDER BY detected_at DESC LIMIT 50`).all(...agentIPs) + : []; + + const security = { + encryption_audit : encryptionRows, + insecure_protocols : insecureProtoRows, + unencrypted_passwords: unencPwdRows, + ip_reputation : ipReputRows, + tor_detections : torRows, + vpn_detections : vpnRows, + }; + + // ── 6. Events filtered by agent (aligned with events page) ─────────────── + const events = db.getLatestEvents(200, null, agentUuid); + + // ── 7. MAC bandwidth for this agent's MACs ────────────────────────────────── + // latestMacSnap was already declared above, reusing it. + const mac_bandwidth = latestMacSnap + ? d.prepare(`SELECT mac_address, manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address IN (${ph}) ORDER BY download DESC`).all(latestMacSnap, ...macs) + : []; + + return { + agent_uuid : agentUuid, + agent_label : label, + summary, + devices, + flows, + top_apps, + security, + events, + mac_bandwidth, + server_discovery: serverDiscRows, + }; +} + +// Fetch data for a specific device IP — from local DB (all 10 correlated tables) +async function fetchDeviceDetails(ip, agentUuid = null) { + const db = require('./database'); + const d = db.getDB(); + + // ── 0. Resolve MAC from flows (most recent) ────────────────────────────── + const macRow = d.prepare(`SELECT src_mac FROM flows WHERE src_ip = ? AND src_mac IS NOT NULL ORDER BY last_seen DESC LIMIT 1`).get(ip); + const mac = macRow?.src_mac || null; + + // ── 1. Device info from devices table ──────────────────────────────────── + const deviceRow = d.prepare(` + SELECT device_label, device_type, os_label, manufacturer, download, upload, last_seen + FROM devices + WHERE ip_address = ? + ORDER BY fetched_at DESC + LIMIT 1 + `).get(ip); + + // ── 2. Discovery info (may differ from devices table) ──────────────────── + const discRow = d.prepare(` + SELECT device_type, os_label, manufacturer, device_label, is_new + FROM intel_device_discovery + WHERE ip_address = ? + ORDER BY fetched_at DESC + LIMIT 1 + `).get(ip); + + const device_info = { + device_label : deviceRow?.device_label || discRow?.device_label || null, + device_type : deviceRow?.device_type || discRow?.device_type || null, + os_label : deviceRow?.os_label || discRow?.os_label || null, + manufacturer : deviceRow?.manufacturer || discRow?.manufacturer || null, + mac_address : mac, + is_new : discRow?.is_new ?? null, + }; + + // ── 3. Named apps & correlated ports ───────────────────────────────────── + const rawAppRows = d.prepare(` + SELECT app_label, + SUM(bytes_download) AS download, + SUM(bytes_upload) AS upload, + COUNT(*) AS flow_count + FROM flows + WHERE src_ip = ? + AND app_label IS NOT NULL + GROUP BY app_label + ORDER BY download DESC + LIMIT 30 + `).all(ip); + + // Cache helper mappings + const devices = d.prepare(` + SELECT ip_address, device_label, manufacturer, device_type + FROM devices + WHERE ip_address IS NOT NULL + `).all(); + + const devMap = new Map(); + for (const dev of devices) { + const label = dev.device_label || (dev.manufacturer && dev.manufacturer !== 'Unknown' ? `${dev.manufacturer} Device` : null); + if (label) { + devMap.set(dev.ip_address, label); + } + } + + const flowIPs = d.prepare(` + SELECT dst_ip, domain, app_label, COUNT(*) as count + FROM flows + WHERE dst_ip IS NOT NULL + AND (domain IS NOT NULL OR (app_label IS NOT NULL AND app_label NOT LIKE 'Port %')) + GROUP BY dst_ip, domain, app_label + ORDER BY count DESC + `).all(); + + const publicIpMap = new Map(); + for (const row of flowIPs) { + if (!publicIpMap.has(row.dst_ip)) { + publicIpMap.set(row.dst_ip, { + domain: row.domain, + app_label: row.app_label + }); + } + } + + function getFriendlyIpName(ipAddress) { + if (devMap.has(ipAddress)) return devMap.get(ipAddress); + if (publicIpMap.has(ipAddress)) { + const pub = publicIpMap.get(ipAddress); + return pub.domain || pub.app_label; + } + if (ipAddress.startsWith('10.6.')) return 'IFG Client'; + if (ipAddress.startsWith('10.250.') || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.121.')) return 'CPI Client'; + if ( + ipAddress.startsWith('10.0.') || ipAddress.startsWith('10.1.') || ipAddress.startsWith('10.26.') || + ipAddress.startsWith('10.43.') || ipAddress.startsWith('10.35.') || ipAddress.startsWith('10.21.') || + ipAddress.startsWith('10.7.') || ipAddress.startsWith('10.182.') || ipAddress.startsWith('10.109.') || + ipAddress.startsWith('10.181.') || ipAddress.startsWith('10.75.') || ipAddress.startsWith('10.202.') || + ipAddress.startsWith('10.93.') + ) return 'JRP Client'; + return 'Intranet Client'; + } + + const matches = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB", + "5432": "PostgreSQL", + "1521": "Oracle DB Server", + "27017": "MongoDB", + "6379": "Redis Cache", + "80": "HTTP Web Server", + "443": "HTTPS/TLS Secure Connection", + "22": "SSH Remote Management", + "21": "FTP File Storage", + "23": "Telnet Command Insecure", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail", + "110": "POP3 Mail Retrieval", + "993": "Secure IMAP Mail", + "53": "DNS Domain Directory Query", + "123": "NTP Network Time", + "161": "SNMP Monitoring Service", + "3389": "RDP Remote Windows Desktop", + "445": "SMB Windows File Share", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Auth Server", + "1813": "RADIUS Accounting", + "5060": "SIP VoIP Service" + }; + + // ── 4. Top domains accessed by this device ───────────────────────────── + const domainRows = d.prepare(` + SELECT domain, + -- use app_label that appeared most with this domain + (SELECT app_label FROM flows + WHERE src_ip = f.src_ip AND domain = f.domain + AND app_label IS NOT NULL + ORDER BY bytes_download DESC LIMIT 1) AS app_label, + -- extract root domain for display + domain AS display_name, + SUM(bytes_download) AS download, + SUM(bytes_upload) AS upload, + COUNT(*) AS flow_count + FROM flows f + WHERE src_ip = ? + AND domain IS NOT NULL + GROUP BY domain + ORDER BY download DESC + LIMIT 30 + `).all(ip); + + // ── 5. Smart combined display list ────────────────────────────────────── + const combinedMap = new Map(); + + // Add domains first (higher priority) + for (const r of domainRows) { + combinedMap.set('domain:' + r.domain, { + label : r.domain, // the actual website/domain + sub_label : r.app_label || null, // protocol (HTTPS/TLS etc) + type : 'domain', + download : r.download ?? 0, + upload : r.upload ?? 0, + flow_count : r.flow_count, + }); + } + + // Add named & correlated apps + for (const r of rawAppRows) { + let label = r.app_label; + let sub_label = null; + let type = 'protocol'; + + const isPortLabel = label.startsWith("Port ") || label.toLowerCase().includes("port"); + + if (isPortLabel) { + const portStr = label.replace("Port ", "").trim(); + type = 'port'; + + const flow = d.prepare(` + SELECT dst_ip, protocol, dst_port + FROM flows + WHERE src_ip = ? AND (app_label = ? OR dst_port = ?) + GROUP BY dst_ip, protocol, dst_port + ORDER BY COUNT(*) DESC + LIMIT 1 + `).get(ip, label, portStr); + + if (flow && flow.dst_ip) { + const friendlyName = getFriendlyIpName(flow.dst_ip); + label = friendlyName; + sub_label = `Port ${portStr} (${flow.protocol || 'TCP'})`; + } else { + const stdName = matches[portStr]; + if (stdName) { + label = stdName; + sub_label = `Port ${portStr}`; + } else { + sub_label = `Port ${portStr}`; + } + } + } + + const key = isPortLabel ? 'port:' + r.app_label : 'app:' + r.app_label; + if (!combinedMap.has(key)) { + combinedMap.set(key, { + label : label, + sub_label : sub_label, + type : type, + download : r.download ?? 0, + upload : r.upload ?? 0, + flow_count : r.flow_count, + }); + } + } + + const top_apps = [...combinedMap.values()].sort((a, b) => b.download - a.download).slice(0, 25); + + // top_domains: keep simple list for Info tab + const top_domains = domainRows.map(r => ({ + domain : r.domain, + app_label : r.app_label, + download : r.download ?? 0, + upload : r.upload ?? 0, + flow_count : r.flow_count, + })); + + // ── 5. Recent flows ──────────────────────────────────────────────────── + const flowRows = d.prepare(` + SELECT dst_ip, dst_port, protocol, app_label, domain, + bytes_download AS download, bytes_upload AS upload, last_seen + FROM flows + WHERE src_ip = ? + ORDER BY last_seen DESC + LIMIT 100 + `).all(ip); + + const flows = flowRows.map(r => ({ + dst_ip : r.dst_ip, + dst_port : r.dst_port, + protocol : r.protocol, + app_label : r.app_label, + domain : r.domain, + download : r.download ?? 0, + upload : r.upload ?? 0, + last_seen : r.last_seen, + })); + + // ── 6. Totals ───────────────────────────────────────────────────────── + const sumRow = d.prepare(` + SELECT SUM(bytes_download) AS total_download, + SUM(bytes_upload) AS total_upload, + COUNT(*) AS flow_count + FROM flows + WHERE src_ip = ? + `).get(ip); + + // ── 7. Encryption audit (latest snapshot) ───────────────────────────── + const latestAudit = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; + const encRow = latestAudit + ? d.prepare(` + SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, mac_address + FROM intel_encryption_audit + WHERE fetched_at = ? AND ip_address = ? + LIMIT 1 + `).get(latestAudit, ip) + : null; + + // Also try fallback by MAC if not found by IP + const encRowMac = (!encRow && mac && latestAudit) + ? d.prepare(` + SELECT encrypted_pct, encrypted, unencrypted, total, risk_level, ip_address + FROM intel_encryption_audit + WHERE fetched_at = ? AND mac_address = ? + ORDER BY detected_at DESC + LIMIT 1 + `).get(latestAudit, mac) + : null; + + const encFinal = encRow || encRowMac; + + const encryption = encFinal ? { + encrypted_pct : encFinal.encrypted_pct ?? null, + encrypted_bytes : encFinal.encrypted ?? null, + unencrypted_bytes: encFinal.unencrypted ?? null, + total_bytes : encFinal.total ?? null, + risk_level : encFinal.risk_level ?? null, + } : null; + + // ── 8. Server discovery (servers this device accessed) ───────────────── + const serverRows = d.prepare(` + SELECT DISTINCT server_type, hostname, port, protocol, os_label, + MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at + FROM intel_server_discovery + WHERE ip_address = ? + GROUP BY server_type, port, protocol + ORDER BY download DESC + LIMIT 50 + `).all(ip); + + // fallback by MAC if no rows by IP + const serverRowsMac = (serverRows.length === 0 && mac) + ? d.prepare(` + SELECT DISTINCT server_type, hostname, port, protocol, os_label, + MAX(download) AS download, MAX(upload) AS upload, MAX(detected_at) AS detected_at + FROM intel_server_discovery + WHERE mac_address = ? + GROUP BY server_type, port, protocol + ORDER BY download DESC + LIMIT 50 + `).all(mac) + : []; + + const server_discovery = (serverRows.length > 0 ? serverRows : serverRowsMac).map(r => ({ + server_type : r.server_type, + hostname : r.hostname || null, + port : r.port, + protocol : r.protocol, + os_label : r.os_label || null, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : r.detected_at, + })); + + // ── 9. Unencrypted passwords ─────────────────────────────────────────── + let pwdRows = d.prepare(` + SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at + FROM intel_unencrypted_passwords + WHERE ip_address = ? + ORDER BY detected_at DESC + LIMIT 50 + `).all(ip); + + if (pwdRows.length === 0 && mac) { + pwdRows = d.prepare(` + SELECT dst_ip, dst_port, protocol, username, severity, download, upload, detected_at + FROM intel_unencrypted_passwords + WHERE mac_address = ? + ORDER BY detected_at DESC + LIMIT 50 + `).all(mac); + } + + const unencrypted_passwords = pwdRows.map(r => ({ + dst_ip : r.dst_ip, + dst_port : r.dst_port, + protocol : r.protocol, + username : r.username, + severity : r.severity, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : r.detected_at, + })); + + // ── 10. IP Reputation (latest snapshot, this device's local_ip) ───────── + const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t; + const repRows = latestRepSnap + ? d.prepare(` + SELECT ip_address, local_ip, reputation, score, country, app_label, blacklisted, download, upload + FROM intel_ip_reputation + WHERE fetched_at = ? AND (local_ip = ? OR ip_address = ?) + ORDER BY score DESC NULLS LAST + LIMIT 30 + `).all(latestRepSnap, ip, ip) + : []; + + const ip_reputation = repRows.map(r => ({ + remote_ip : r.ip_address, + local_ip : r.local_ip, + reputation : r.reputation, + score : r.score, + country : r.country, + app_label : r.app_label, + blacklisted : !!r.blacklisted, + download : r.download ?? 0, + upload : r.upload ?? 0, + })); + + // ── 11. VPN detection ───────────────────────────────────────────────── + let vpnRows = d.prepare(` + SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at + FROM intel_vpn_detection + WHERE ip_address = ? + ORDER BY detected_at DESC + LIMIT 20 + `).all(ip); + + if (vpnRows.length === 0 && mac) { + vpnRows = d.prepare(` + SELECT vpn_type, remote_ip, protocol, country, confidence, download, upload, detected_at + FROM intel_vpn_detection + WHERE mac_address = ? + ORDER BY detected_at DESC + LIMIT 20 + `).all(mac); + } + + const vpn_detections = vpnRows.map(r => ({ + vpn_type : r.vpn_type, + remote_ip : r.remote_ip, + protocol : r.protocol, + country : r.country, + confidence : r.confidence, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : r.detected_at, + })); + + // ── 12. Events ──────────────────────────────────────────────────────── + const evtByIP = d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE ip_address = ? ORDER BY event_at DESC LIMIT 50`).all(ip); + const evtByMAC = mac ? d.prepare(`SELECT event_type, severity, ip_address, mac_address, description, event_at FROM events WHERE mac_address = ? ORDER BY event_at DESC LIMIT 50`).all(mac) : []; + + const seenEvt = new Set(); + const evtMerged = []; + for (const r of [...evtByIP, ...evtByMAC]) { + const key = `${r.event_type}:${r.event_at}`; + if (!seenEvt.has(key)) { + seenEvt.add(key); + evtMerged.push({ event_type: r.event_type, severity: r.severity, ip_address: r.ip_address, mac_address: r.mac_address, description: r.description, event_at: r.event_at }); + } + } + evtMerged.sort((a, b) => (b.event_at || '').localeCompare(a.event_at || '')); + const events = evtMerged.slice(0, 100); + + // ── 13. MAC bandwidth (latest snapshot) ─────────────────────────────── + const latestMacSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM mac_bandwidth`).get()?.t; + const macBwRow = (latestMacSnap && mac) + ? d.prepare(`SELECT manufacturer, download, upload, total FROM mac_bandwidth WHERE fetched_at = ? AND mac_address = ? LIMIT 1`).get(latestMacSnap, mac) + : null; + + const mac_bandwidth = macBwRow ? { + mac_address : mac, + manufacturer : macBwRow.manufacturer, + download : macBwRow.download ?? 0, + upload : macBwRow.upload ?? 0, + total : macBwRow.total ?? 0, + } : null; + + return { + ip, + mac_address : mac, + total_download : deviceRow?.download || sumRow?.total_download || 0, + total_upload : deviceRow?.upload || sumRow?.total_upload || 0, + flow_count : sumRow?.flow_count ?? 0, + device_info, + top_apps, + top_domains : domainRows.map(r => ({ domain: r.domain, download: r.download ?? 0, upload: r.upload ?? 0, flow_count: r.flow_count })), + flows, + encryption, + server_discovery, + unencrypted_passwords, + ip_reputation, + vpn_detections, + events, + mac_bandwidth, + }; +} + +// Fetch data for a specific application// Fetch data for a specific application — from local DB +async function fetchAppDetails(appLabel, agentUuid = null) { + const db = require('./database'); + const d = db.getDB(); + + // ── Totals: from flows (protocol-level) OR bandwidth_apps (brand-level) ── + // IMPORTANT: use MAX(download) from latest snapshot — NOT SUM() of all history! + let flowQuery = ` + SELECT SUM(bytes_download) AS download, + SUM(bytes_upload) AS upload, + COUNT(*) AS flow_count + FROM flows + WHERE app_label = @appLabel + `; + if (agentUuid) flowQuery += ` AND agent_uuid = @agentUuid`; + const flowTotalRow = d.prepare(flowQuery).get({ appLabel, agentUuid }); + + let total_download = flowTotalRow?.download ?? 0; + let total_upload = flowTotalRow?.upload ?? 0; + let data_source = 'flows'; // track where totals came from + + // If no flows data (brand-name app like YouTube), use LATEST bandwidth_apps snapshot + if (total_download === 0 && total_upload === 0) { + let snapQuery = `SELECT MAX(fetched_at) AS t FROM bandwidth_apps WHERE app_label = @appLabel`; + snapQuery += agentUuid ? ` AND agent_uuid = @agentUuid` : ` AND agent_uuid IS NULL`; + const latestSnap = d.prepare(snapQuery).get({ appLabel, agentUuid })?.t; + + if (latestSnap) { + let bwQuery = ` + SELECT download, upload + FROM bandwidth_apps + WHERE app_label = @appLabel AND fetched_at = @latestSnap + `; + bwQuery += agentUuid ? ` AND agent_uuid = @agentUuid` : ` AND agent_uuid IS NULL`; + bwQuery += ` LIMIT 1`; + + const bwRow = d.prepare(bwQuery).get({ appLabel, latestSnap, agentUuid }); + if (bwRow) { + total_download = bwRow.download ?? 0; + total_upload = bwRow.upload ?? 0; + data_source = 'bandwidth_apps'; + } + } + } + + // ── Top 5 Flow Records accessing this app (Raw Flows) ── + let ipQuery = ` + SELECT src_ip AS ip_address, + dst_ip, + domain, + last_seen, + bytes_download AS download, + bytes_upload AS upload + FROM flows + WHERE app_label = @appLabel + `; + if (agentUuid) ipQuery += ` AND agent_uuid = @agentUuid`; + ipQuery += ` ORDER BY download DESC LIMIT 5`; + const ipRows = d.prepare(ipQuery).all({ appLabel, agentUuid }); + + // ── Domain-based per-IP breakdown (bridges HTTPS/TLS → brand name) ── + // Build keyword map for common brand names + const DOMAIN_KEYWORDS = { + 'YouTube' : ['youtube', 'googlevideo', 'ytimg', 'yt3.ggpht'], + 'Facebook' : ['facebook', 'fbcdn', 'fb.com', 'fbsbx'], + 'WhatsApp' : ['whatsapp', 'wa.me'], + 'Instagram' : ['instagram', 'cdninstagram'], + 'TikTok' : ['tiktok', 'tiktokcdn', 'tiktokv'], + 'Netflix' : ['netflix', 'nflxvideo', 'nflximg'], + 'Google' : ['google.com', 'googleapis', 'gstatic', 'googlesyndication'], + 'Microsoft' : ['microsoft', 'msftncsi', 'live.com', 'office365', 'sharepoint'], + 'Zoom' : ['zoom.us', 'zoomgov'], + 'Spotify' : ['spotify', 'scdn.co'], + }; + + let domain_breakdown = []; + const kws = DOMAIN_KEYWORDS[appLabel]; + if (kws && kws.length > 0) { + // Build WHERE clause for domain keywords + const likeClause = kws.map(() => `LOWER(domain) LIKE ?`).join(' OR '); + const likeParams = kws.map(k => `%${k}%`); + let domQuery = ` + SELECT src_ip AS ip_address, + dst_ip, + domain, + last_seen, + bytes_download AS download, + bytes_upload AS upload + FROM flows + WHERE domain IS NOT NULL AND (${likeClause}) + `; + const domParams = [...likeParams]; + if (agentUuid) { + domQuery += ` AND agent_uuid = ?`; + domParams.push(agentUuid); + } + domQuery += ` ORDER BY download DESC LIMIT 5`; + const domRows = d.prepare(domQuery).all(...domParams); + + domain_breakdown = domRows.map(r => ({ + ip_address : r.ip_address, + dst_ip : r.dst_ip, + domain : r.domain, + last_seen : r.last_seen, + download : r.download ?? 0, + upload : r.upload ?? 0, + })); + } + + // ── Threat flags: cross-reference top IPs with intel_ip_reputation ── + const latestRepSnap = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_ip_reputation`).get()?.t; + const threatMap = {}; + if (latestRepSnap) { + const repRows = d.prepare(` + SELECT local_ip, ip_address, reputation, blacklisted + FROM intel_ip_reputation + WHERE fetched_at = ? + `).all(latestRepSnap); + for (const r of repRows) { + const key = r.local_ip || r.ip_address; + if (key) threatMap[key] = { reputation: r.reputation, blacklisted: r.blacklisted }; + } + } + + const allIpRows = domain_breakdown.length > 0 ? domain_breakdown : ipRows; + const top_ips = allIpRows.map(r => ({ + ip_address : r.ip_address, + dst_ip : r.dst_ip, + domain : r.domain, + last_seen : r.last_seen, + download : r.download ?? 0, + upload : r.upload ?? 0, + reputation : threatMap[r.ip_address]?.reputation ?? null, + blacklisted : threatMap[r.ip_address]?.blacklisted ?? false, + })); + + // ── Per-agent breakdown using agent_uuid grouping in flows OR local_mac mapping ── + let allRelevantFlows = []; + if (kws && kws.length > 0) { + const likeClause2 = kws.map(() => `LOWER(domain) LIKE ?`).join(' OR '); + const likeParams2 = kws.map(k => `%${k}%`); + let scoreQuery = ` + SELECT agent_uuid, src_mac AS local_mac, + bytes_download AS download, + bytes_upload AS upload + FROM flows + WHERE domain IS NOT NULL AND (${likeClause2}) + `; + const scoreParams = [...likeParams2]; + if (agentUuid) { + scoreQuery += ` AND (agent_uuid = ? OR agent_uuid IS NULL)`; + scoreParams.push(agentUuid); + } + allRelevantFlows = d.prepare(scoreQuery).all(...scoreParams); + } else { + let scoreQuery = ` + SELECT agent_uuid, src_mac AS local_mac, + bytes_download AS download, + bytes_upload AS upload + FROM flows + WHERE app_label = ? + `; + const scoreParams = [appLabel]; + if (agentUuid) { + scoreQuery += ` AND (agent_uuid = ? OR agent_uuid IS NULL)`; + scoreParams.push(agentUuid); + } + allRelevantFlows = d.prepare(scoreQuery).all(...scoreParams); + } + + // Reverse map MAC to Agent UUID + const macToAgent = {}; + for (const [auid, macs] of Object.entries(AGENT_MAC_MAP)) { + for (const m of macs) { + macToAgent[m] = auid; + } + } + + const agentScoreMap = {}; + for (const row of allRelevantFlows) { + let auid = row.agent_uuid; + if (!auid && row.local_mac && macToAgent[row.local_mac]) { + auid = macToAgent[row.local_mac]; // Fallback to MAC mapping + } + if (auid) { + if (agentUuid && auid !== agentUuid) continue; // skip if filtering by a specific agent + if (!agentScoreMap[auid]) { + agentScoreMap[auid] = { download: 0, upload: 0, flow_count: 0 }; + } + agentScoreMap[auid].download += (row.download ?? 0); + agentScoreMap[auid].upload += (row.upload ?? 0); + agentScoreMap[auid].flow_count += 1; + } + } + + let agent_scorecard = Object.keys(agentScoreMap).map(auid => ({ + agent_uuid : auid, + agent_label : AGENT_LABELS[auid] || auid, + download : agentScoreMap[auid].download, + upload : agentScoreMap[auid].upload, + flow_count : agentScoreMap[auid].flow_count, + })).filter(a => a.download > 0 || a.upload > 0); + + // Fallback to bandwidth_apps if flow-based scorecard is empty + if (agent_scorecard.length === 0) { + const snapQuery = `SELECT MAX(fetched_at) AS t FROM bandwidth_apps WHERE app_label = @appLabel AND agent_uuid IS NOT NULL`; + const latestAppSnap = d.prepare(snapQuery).get({ appLabel })?.t; + if (latestAppSnap) { + let bwQuery = `SELECT agent_uuid, download, upload FROM bandwidth_apps WHERE app_label = @appLabel AND fetched_at = @latestAppSnap AND agent_uuid IS NOT NULL`; + if (agentUuid) bwQuery += ` AND agent_uuid = @agentUuid`; + const bwRows = d.prepare(bwQuery).all({ appLabel, latestAppSnap, agentUuid }); + for (const row of bwRows) { + agent_scorecard.push({ + agent_uuid: row.agent_uuid, + agent_label: AGENT_LABELS[row.agent_uuid] || row.agent_uuid, + download: row.download, + upload: row.upload, + flow_count: 0 + }); + } + } + } + + return { + app_label : appLabel, + total_download, + total_upload, + data_source, + agent_scorecard, + top_ips, + has_domain_breakdown: domain_breakdown.length > 0, + }; +} + + +// Fetch security device risk overview — encryption audit + insecure protocols per device +async function fetchSecurityDevices(siteUuid = null, agentUuid = null) { + const db = require('./database'); + const d = db.getDB(); + + // Get active IPs and MACs for filtering if agentUuid is provided + let agentIPs = null; + let agentIPSet = null; + let agentMacs = null; + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + agentMacs = AGENT_MAC_MAP[agentUuid]; + const resolvedDevices = db.getLatestDevices(1000, null, agentUuid); + agentIPs = resolvedDevices.map(d => d.ip_address).filter(Boolean); + agentIPSet = new Set(agentIPs); + } + + const latestFetch = d.prepare(`SELECT MAX(fetched_at) AS t FROM intel_encryption_audit`).get()?.t; + let encryptRows = []; + if (latestFetch) { + if (agentMacs) { + // Query with agent's MACs or JRP subnet IPs + const placeholders = agentMacs.map(() => '?').join(','); + encryptRows = d.prepare(` + SELECT * FROM intel_encryption_audit + WHERE fetched_at = ? AND (mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))) + `).all(latestFetch, ...agentMacs, ...agentMacs); + } else { + encryptRows = d.prepare(` + SELECT * FROM intel_encryption_audit + WHERE fetched_at = ? AND (@siteUuid IS NULL OR site_uuid = @siteUuid) + `).all(latestFetch, { siteUuid }); + } + } + + let insecureRows = []; + if (agentMacs) { + const placeholders = agentMacs.map(() => '?').join(','); + insecureRows = d.prepare(` + SELECT DISTINCT ip_address FROM intel_insecure_protocols + WHERE mac_address IN (${placeholders}) OR ip_address IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders})) + `).all(...agentMacs, ...agentMacs); + } else { + insecureRows = d.prepare(` + SELECT DISTINCT ip_address FROM intel_insecure_protocols + WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) + `).all({ siteUuid }); + } + const insecureIPs = new Set(insecureRows.map(r => r.ip_address).filter(Boolean)); + + // Compute risk per device + const deviceMap = {}; + for (const r of encryptRows) { + const ip = r.ip_address; + if (!ip) continue; + + // Additional security check: if agent is logged in, ensure we do not leak other agent's IPs + if (agentIPSet && !agentIPSet.has(ip)) { + continue; + } + + const encPct = r.encrypted_pct ?? 100; + let riskLevel; + if (encPct < 50 || insecureIPs.has(ip)) { + riskLevel = 'Vulnerable'; + } else if (encPct < 80) { + riskLevel = 'Moderate'; + } else { + riskLevel = 'Safe'; + } + if (!deviceMap[ip] || deviceMap[ip].encrypted_pct < encPct) { + deviceMap[ip] = { + ip_address : ip, + mac_address : r.mac_address ?? null, + device_label : r.device_label ?? null, + encrypted_pct : encPct, + unencrypted : r.unencrypted ?? 0, + encrypted : r.encrypted ?? 0, + total : r.total ?? 0, + risk_level : riskLevel, + has_insecure : insecureIPs.has(ip), + }; + } + } + + // Get device details (type, OS) from discovery data + const discMap = {}; + try { + const discRows = db.getLatestDevices(1000, siteUuid, agentUuid); + for (const r of discRows) { + if (r.ip_address) discMap[r.ip_address] = r; + } + } catch (_) { + // skip enrichment if error + } + + const devices = Object.values(deviceMap).map(dev => ({ + ...dev, + device_type : discMap[dev.ip_address]?.device_type ?? null, + os_label : discMap[dev.ip_address]?.os_label ?? null, + manufacturer : discMap[dev.ip_address]?.manufacturer ?? null, + })); + + // Sort: Vulnerable first, then Moderate, then Safe + const ORDER = { Vulnerable: 0, Moderate: 1, Safe: 2 }; + devices.sort((a, b) => (ORDER[a.risk_level] ?? 3) - (ORDER[b.risk_level] ?? 3)); + + return devices; +} + +module.exports = { + fetchLookupApplications, + fetchAgents, + fetchTopApps, fetchTopDevices, fetchTopProtocols, fetchTopCountries, + fetchTopDomains, fetchBandwidthSummary, fetchDiscoveredDevices, + fetchCyberThreats, fetchFlows, fetchEvents, + fetchTopAppCategories, fetchTopContinents, fetchTopRegions, fetchTopCities, + fetchTopVLANs, fetchTopInterfaces, fetchTopFlowTypes, fetchTopFlowOrigins, + fetchTopIPVersions, fetchTopRemoteIPs, fetchTopLocalMACs, + fetchTopDiscoveryOS, + fetchTLSVersions, fetchTLSCiphers, fetchTLSSecurity, fetchNetBIOSHostnames, + // DPI 12-21 (field name sudah diperbaiki sesuai dokumentasi resmi) + fetchDHCPClassFingerprints, + fetchHTTPUserAgents, + fetchSNIHostnames, + fetchSSLServerCN, + fetchQUICHostnames, + fetchBitTorrentInfoHashes, + fetchSSHClients, + fetchSSHServers, + fetchMDNSHostnames, + // Intelligence 22-30 (derive dari data yang tersedia) + fetchCryptoMining, + fetchDeviceDiscovery, + fetchEncryptionAudit, + fetchInsecureProtocols, + fetchIPReputation, + fetchServerDiscovery, + fetchTorDetection, + fetchUnencryptedPasswords, + fetchVPNDetection, + // Interactive detail fetchers + fetchAgentDetails, + fetchDeviceDetails, + fetchAppDetails, + fetchSecurityDevices, +}; + +// ─── DPI FIELDS 12-21 — FIELD NAMES DIPERBAIKI SESUAI DOCS ────────────────── +// Sumber: https://www.netify.ai/documentation/informatics/v2/data/fields +// +// Field yang SALAH sebelumnya → yang BENAR: +// dhcp_fingerprint → dhcp_class +// user_agent → http_useragent +// ssl_cn → https_sni_hostname +// ssl_server_cn → ssl_server_cn ✓ (sudah benar) +// quic_hostname → quic_hostname ✓ (sudah benar, mungkin belum aktif di akun) +// bt_info_hash → bittorrent_info_hash +// ssh_version → ssh_client / ssh_server (2 field terpisah) +// mdns_hostname → mdns_hostname ✓ (sudah benar, mungkin belum aktif di akun) + +// Helper builder untuk DPI single-field +async function _dpiTopField(fieldName, interval, limit, agentUuid = null) { + const [dl, ul] = await Promise.all([ + netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }, false, agentUuid), + netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }, false, agentUuid), + ]); + if (!dl) return null; + const ulMap = {}; + if (ul) for (const r of ul) { + const key = r[fieldName]?.name ?? r[fieldName]?.label ?? String(r[fieldName] ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return { dl, ulMap }; +} + +// 12. DHCP Class (field: dhcp_class) +async function fetchDHCPClassFingerprints(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('dhcp_class', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const label = r.dhcp_class?.name ?? r.dhcp_class?.label ?? String(r.dhcp_class ?? 'Unknown'); + return { + fingerprint : label, + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 13. HTTP User-Agent (field: http_useragent) +async function fetchHTTPUserAgents(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('http_useragent', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const label = r.http_useragent?.name ?? r.http_useragent?.label ?? String(r.http_useragent ?? 'Unknown'); + return { + user_agent : label, + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 14. HTTPS SNI Hostname (field: https_sni_hostname) +async function fetchSNIHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('https_sni_hostname', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const name = r.https_sni_hostname?.name ?? r.https_sni_hostname?.label ?? String(r.https_sni_hostname ?? 'Unknown'); + return { + sni_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 15. SSL Server Common Name (field: ssl_server_cn) +async function fetchSSLServerCN(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('ssl_server_cn', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const name = r.ssl_server_cn?.name ?? r.ssl_server_cn?.label ?? String(r.ssl_server_cn ?? 'Unknown'); + return { + ssl_server_cn : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 17. QUIC Hostname (field: quic_hostname) +async function fetchQUICHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('quic_hostname', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const name = r.quic_hostname?.name ?? r.quic_hostname?.label ?? String(r.quic_hostname ?? 'Unknown'); + return { + quic_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 18. BitTorrent Info Hash (field: bittorrent_info_hash) +async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('bittorrent_info_hash', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const hash = r.bittorrent_info_hash?.hash ?? r.bittorrent_info_hash?.name ?? String(r.bittorrent_info_hash ?? 'Unknown'); + const label = r.bittorrent_info_hash?.label ?? hash; + return { + info_hash : hash, + label : label, + download : r.download ?? 0, + upload : res.ulMap[hash] ?? res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[hash] ?? res.ulMap[label] ?? 0), + }; + }); +} + +// 19a. SSH Client (field: ssh_client) +async function fetchSSHClients(interval = 1440, limit = 20, agentUuid = null) { + const res = await _dpiTopField('ssh_client', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const label = r.ssh_client?.name ?? r.ssh_client?.label ?? String(r.ssh_client ?? 'Unknown'); + return { + ssh_version : label, + direction : 'client', + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 19b. SSH Server (field: ssh_server) +async function fetchSSHServers(interval = 1440, limit = 20, agentUuid = null) { + const res = await _dpiTopField('ssh_server', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const label = r.ssh_server?.name ?? r.ssh_server?.label ?? String(r.ssh_server ?? 'Unknown'); + return { + ssh_version : label, + direction : 'server', + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 21. mDNS Hostname (field: mdns_hostname) +async function fetchMDNSHostnames(interval = 1440, limit = 30, agentUuid = null) { + const res = await _dpiTopField('mdns_hostname', interval, limit, agentUuid); + if (!res) return null; + return res.dl.map(r => { + const name = r.mdns_hostname?.name ?? r.mdns_hostname?.label ?? String(r.mdns_hostname ?? 'Unknown'); + return { + mdns_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// ─── INTELLIGENCE 22-30 — DERIVE DARI DATA YANG SUDAH ADA ──────────────────── +// Endpoint /intelligence/* dan /events/* semua 404 di akun ini. +// Semua fungsi berikut meng-DERIVE data dari endpoint yang sudah confirmed bekerja: +// /data/stats/top/*, /data/flows +// Ini memberikan data nyata, bukan mock/dummy. + +// 22. Cryptocurrency Mining — derive dari apps dengan nama mengandung "crypto" / "mining" +// + flows ke port mining pool (3333, 4444, 8333, 9999, 14444) +async function fetchCryptoMining(interval = 1440, limit = 50, agentUuid = null) { + const MINING_POOLS_PORTS = new Set([3333, 4444, 5555, 7777, 8333, 9332, 9999, 14444, 45560, 45700]); + const MINING_APPS = ['bitcoin', 'crypto', 'mining', 'monero', 'ethereum', 'nicehash', 'nanopool', 'f2pool', 'antpool', 'slushpool']; + + const [appData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + ]); + + const results = []; + + // Derive dari aplikasi + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (MINING_APPS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + pool_host : r.application?.label ?? null, + pool_ip : null, + protocol : null, + app_label : r.application?.label ?? null, + confidence : 0.7, + download : r.download ?? 0, + upload : r.upload ?? 0, + source : 'derived:app', + }); + } + } + } + + // Derive dari flows ke port mining + if (flowsRaw) { + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + if (MINING_POOLS_PORTS.has(port)) { + results.push({ + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + pool_host : null, + pool_ip : r.remote_ip?.address ?? null, + protocol : r.ip_protocol?.label ?? null, + app_label : null, + confidence : 0.85, + download : r.download ?? 0, + upload : r.upload ?? 0, + source : 'derived:flow', + }); + } + } + } + + return results.slice(0, limit); +} + +// 23. Device Discovery — derive dari flows (perangkat unik dengan MAC) +async function fetchDeviceDiscovery(limit = 100, agentUuid = null) { + const [flowsRaw, dlData] = await Promise.all([ + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: 1440, settings_limit: 200 }, false, agentUuid), + ]); + + const seen = new Map(); + if (flowsRaw) { + for (const r of flowsRaw) { + const ip = r.local_ip?.address; + const mac = r.local_mac; + if (!ip) continue; + if (!seen.has(ip)) { + seen.set(ip, { + detected_at : r.first_seen_at?.date ?? null, + ip_address : ip, + mac_address : mac ?? null, + device_label : r.device?.label ?? null, + device_type : r.mac?.discovery_hardware ?? null, + os_label : r.discovery_os?.label ?? null, + manufacturer : mac ? (OUI_MAP[mac.substring(0,8).toUpperCase()] ?? null) : null, + is_new : true, + }); + } + } + } + + // Tambah IP yang punya bandwidth tapi tidak ada di flows + if (dlData) { + for (const r of dlData) { + const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); + if (ip && !seen.has(ip)) { + seen.set(ip, { + detected_at : null, + ip_address : ip, + mac_address : null, + device_label : null, + device_type : null, + os_label : null, + manufacturer : null, + is_new : true, + }); + } + } + } + + return Array.from(seen.values()).slice(0, limit); +} + +// 24. Encryption Audit — derive dari TLS security per-IP dari flows +async function fetchEncryptionAudit(limit = 50, agentUuid = null) { + const [tlsSec, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + ]); + + // Hitung per-IP: encrypted vs unencrypted flows + const ipStats = {}; + if (flowsRaw) { + for (const r of flowsRaw) { + const ip = r.local_ip?.address; + const port = r.remote_port ?? 0; + const mac = r.local_mac ?? null; + if (!ip) continue; + if (!ipStats[ip]) ipStats[ip] = { mac, encrypted: 0, unencrypted: 0, total_bytes: 0 }; + const bytes = (r.download ?? 0) + (r.upload ?? 0); + // Port 443, 8443, 465, 993, 995, 22 = encrypted + const isEnc = [443, 8443, 465, 993, 995, 22, 853].includes(port); + if (isEnc) ipStats[ip].encrypted += bytes; + else ipStats[ip].unencrypted += bytes; + ipStats[ip].total_bytes += bytes; + } + } + + return Object.entries(ipStats) + .map(([ip, s]) => { + const total = s.encrypted + s.unencrypted; + const enc_pct = total > 0 ? (s.encrypted / total) * 100 : null; + const risk = enc_pct === null ? null + : enc_pct >= 90 ? 'Low' + : enc_pct >= 60 ? 'Medium' + : enc_pct >= 30 ? 'High' + : 'Critical'; + const oui = s.mac ? s.mac.substring(0,8).toUpperCase() : null; + return { + ip_address : ip, + mac_address : s.mac, + device_label : OUI_MAP[oui] ?? null, + encrypted_pct : enc_pct != null ? Math.round(enc_pct * 10) / 10 : null, + encrypted : s.encrypted, + unencrypted : s.unencrypted, + total : total, + risk_level : risk, + detected_at : null, + }; + }) + .sort((a, b) => (a.encrypted_pct ?? 101) - (b.encrypted_pct ?? 101)) + .slice(0, limit); +} + +// 25. Insecure Protocols — derive dari top protocols (confirmed bekerja) +async function fetchInsecureProtocols(interval = 1440, limit = 50, agentUuid = null) { + const INSECURE = { + 'HTTP' : { port: 80, risk: 'High' }, + 'FTP' : { port: 21, risk: 'Critical' }, + 'Telnet' : { port: 23, risk: 'Critical' }, + 'SMTP' : { port: 25, risk: 'Medium' }, + 'POP3' : { port: 110, risk: 'Medium' }, + 'IMAP' : { port: 143, risk: 'Medium' }, + 'DNS' : { port: 53, risk: 'Low' }, + 'SNMP' : { port: 161, risk: 'High' }, + 'LDAP' : { port: 389, risk: 'High' }, + 'RDP' : { port: 3389, risk: 'High' }, + 'NTP' : { port: 123, risk: 'Low' }, + 'TFTP' : { port: 69, risk: 'High' }, + 'rsh' : { port: 514, risk: 'Critical' }, + 'rlogin' : { port: 513, risk: 'Critical' }, + }; + + const [protoData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: 100 }, false, agentUuid), + ]); + if (!protoData) return []; + + const ulMap = {}; + if (ulData) for (const r of ulData) { + const id = r.protocol?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + + return protoData + .filter(r => INSECURE[r.protocol?.label]) + .map(r => { + const label = r.protocol?.label ?? 'Unknown'; + const info = INSECURE[label]; + return { + protocol : label, + ip_address : null, + mac_address : null, + dst_ip : null, + dst_port : info.port, + app_label : null, + download : r.download ?? 0, + upload : ulMap[r.protocol?.id] ?? 0, + risk : info.risk, + detected_at : null, + source : 'derived', + }; + }) + .slice(0, limit); +} + +// 26. IP Reputation — derive dari top remote_ip + cross-check negara berisiko tinggi +async function fetchIPReputation(limit = 50, agentUuid = null) { + // Negara dengan risiko tinggi berdasarkan threat intel umum + const HIGH_RISK_COUNTRIES = new Set([ + 'China', 'Russia', 'Iran', 'North Korea', 'Nigeria', 'Romania', + 'Brazil', 'Ukraine', 'Vietnam', 'Indonesia', + ]); + + const [ipData, countryData] = await Promise.all([ + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/country/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), + ]); + + const results = []; + + if (ipData) { + // Tandai IP dari negara berisiko (informasi negara tidak ada per-IP dari API, + // jadi kita pakai country data untuk konteks) + for (const r of ipData) { + const ip = r.remote_ip?.address ?? String(r.remote_ip ?? ''); + if (!ip) continue; + results.push({ + ip_address : ip, + local_ip : null, + mac_address : null, + reputation : 'Unknown', + score : null, + country : null, + app_label : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : null, + blacklisted : false, + source : 'derived:top_ip', + }); + } + } + + // Tambah entri untuk negara berisiko yang terdeteksi + if (countryData) { + for (const r of countryData) { + const country = r.country?.label ?? ''; + if (HIGH_RISK_COUNTRIES.has(country)) { + results.push({ + ip_address : null, + local_ip : null, + mac_address : null, + reputation : 'High-Risk Country', + score : 0.7, + country : country, + app_label : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : null, + blacklisted : false, + source : 'derived:country', + }); + } + } + } + + return results.slice(0, limit); +} + +// 27. Server Discovery — derive dari flows dengan flow_origin=Server + port well-known server +async function fetchServerDiscovery(limit = 100, agentUuid = null) { + const SERVER_PORTS = { + 80: 'HTTP', 443: 'HTTPS', 22: 'SSH', 21: 'FTP', 25: 'SMTP', + 110: 'POP3', 143: 'IMAP', 3306: 'MySQL', 5432: 'PostgreSQL', + 6379: 'Redis', 27017: 'MongoDB', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', + 53: 'DNS', 3389: 'RDP', 5900: 'VNC', 161: 'SNMP', 123: 'NTP', + 389: 'LDAP', 636: 'LDAPS', 5060: 'SIP', 1194: 'OpenVPN', + }; + + const [flowOriginData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: 1440, settings_limit: 10 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + ]); + + const serverMap = {}; + if (flowsRaw) { + for (const r of flowsRaw) { + const localIP = r.local_ip?.address; + const localPort = r.local_port ?? r.remote_port; + const proto = r.ip_protocol?.label ?? null; + const mac = r.local_mac ?? null; + if (!localIP) continue; + + // Deteksi server: local device listening di port well-known + const svcName = SERVER_PORTS[localPort] ?? SERVER_PORTS[r.remote_port]; + if (svcName) { + const key = `${localIP}:${localPort ?? r.remote_port}`; + if (!serverMap[key]) { + const oui = mac ? mac.substring(0,8).toUpperCase() : null; + serverMap[key] = { + detected_at : r.first_seen_at?.date ?? null, + ip_address : localIP, + mac_address : mac, + server_type : svcName, + hostname : r.device?.label ?? null, + port : localPort ?? r.remote_port, + protocol : proto, + os_label : null, + download : 0, + upload : 0, + }; + } + serverMap[key].download += r.download ?? 0; + serverMap[key].upload += r.upload ?? 0; + } + } + } + + return Object.values(serverMap) + .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)) + .slice(0, limit); +} + +// 28. Tor Detection — derive dari top remote_ip + app/hostname matching Tor +async function fetchTorDetection(limit = 50, agentUuid = null) { + const TOR_INDICATORS = ['tor', '.onion', 'torproject', 'torbrowser']; + + const [appData, domainData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/hostname/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + ]); + + const results = []; + + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (TOR_INDICATORS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + exit_node : null, + circuit_id : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + source : 'derived:app', + label : r.application?.label, + }); + } + } + } + + if (domainData) { + for (const r of domainData) { + const host = (r.hostname?.name ?? '').toLowerCase(); + if (TOR_INDICATORS.some(k => host.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + exit_node : null, + circuit_id : null, + download : r.download ?? 0, + upload : 0, + country : null, + source : 'derived:hostname', + label : r.hostname?.name, + }); + } + } + } + + return results.slice(0, limit); +} + +// 29. Unencrypted Passwords — derive dari flows ke port cleartext auth +async function fetchUnencryptedPasswords(limit = 50, agentUuid = null) { + // Port yang dikenal mengirim kredensial cleartext + const CLEARTEXT_AUTH_PORTS = { + 21 : { protocol: 'FTP', severity: 'Critical' }, + 23 : { protocol: 'Telnet', severity: 'Critical' }, + 25 : { protocol: 'SMTP', severity: 'High' }, + 80 : { protocol: 'HTTP', severity: 'High' }, + 110 : { protocol: 'POP3', severity: 'High' }, + 143 : { protocol: 'IMAP', severity: 'High' }, + 389 : { protocol: 'LDAP', severity: 'Critical' }, + 512 : { protocol: 'rexec', severity: 'Critical' }, + 513 : { protocol: 'rlogin', severity: 'Critical' }, + 514 : { protocol: 'rsh', severity: 'Critical' }, + }; + + const flowsRaw = await netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid); + if (!flowsRaw) return []; + + const seen = new Map(); + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + const info = CLEARTEXT_AUTH_PORTS[port]; + if (!info) continue; + + const key = `${r.local_ip?.address}:${r.remote_ip?.address}:${port}`; + if (!seen.has(key)) { + seen.set(key, { + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + dst_ip : r.remote_ip?.address ?? null, + dst_port : port, + protocol : info.protocol, + username : null, + download : 0, + upload : 0, + severity : info.severity, + }); + } + seen.get(key).download += r.download ?? 0; + seen.get(key).upload += r.upload ?? 0; + } + + return Array.from(seen.values()) + .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)) + .slice(0, limit); +} + +// 30. VPN Detection — derive dari apps/protocols/hostnames yang mengindikasikan VPN +async function fetchVPNDetection(limit = 50, agentUuid = null) { + const VPN_APPS = [ + 'openvpn', 'wireguard', 'nordvpn', 'expressvpn', 'surfshark', 'tunnelbear', + 'mullvad', 'protonvpn', 'ipvanish', 'pia', 'private internet access', + 'hotspot shield', 'cyberghost', 'vpn', 'pptp', 'l2tp', 'ipsec', 'sstp', + 'shadowsocks', 'v2ray', 'trojan', 'outline', + ]; + const VPN_PROTOCOLS = new Set(['OpenVPN', 'WireGuard', 'IPSec', 'PPTP', 'L2TP', 'GRE', 'SSTP']); + // Port yang umum digunakan VPN + const VPN_PORTS = new Set([1194, 51820, 500, 4500, 1701, 1723, 8388, 443]); + + const [appData, protoData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }, false, agentUuid), + netifyFetch('/data/stats/top/protocol/download', { filter_interval: 1440, settings_limit: 50 }, false, agentUuid), + netifyFetch('/data/flows', { settings_limit: 500 }, false, agentUuid), + ]); + + const results = []; + + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (VPN_APPS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + vpn_type : r.application?.label ?? 'Unknown VPN', + remote_ip : null, + protocol : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.9, + source : 'derived:app', + }); + } + } + } + + if (protoData) { + for (const r of protoData) { + const label = r.protocol?.label ?? ''; + if (VPN_PROTOCOLS.has(label)) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + vpn_type : label, + remote_ip : null, + protocol : label, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.85, + source : 'derived:protocol', + }); + } + } + } + + if (flowsRaw) { + const portSeen = new Set(); + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + if (VPN_PORTS.has(port) && !portSeen.has(port)) { + portSeen.add(port); + results.push({ + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + vpn_type : `Port ${port}`, + remote_ip : r.remote_ip?.address ?? null, + protocol : r.ip_protocol?.label ?? null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.75, + source : 'derived:port', + }); + } + } + } + + return results.slice(0, limit); +} + +async function fetchAgents() { + const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, false, null); + if (!data || !Array.isArray(data)) return []; + const list = data.map(r => ({ + id: r.agent?.id, + uuid: r.agent?.uuid, + label: r.agent?.label, + })); + for (const a of list) { + if (a.uuid && a.id) { + agentMap[a.uuid] = a.id; + } + } + return list; +} \ No newline at end of file diff --git a/backend/scheduler.js b/backend/scheduler.js new file mode 100644 index 0000000..60e3726 --- /dev/null +++ b/backend/scheduler.js @@ -0,0 +1,312 @@ +// backend/scheduler.js +const cron = require('node-cron'); +const netify = require('./netify'); +const db = require('./database'); +const SITE_UUID = process.env.NETIFY_SITE_UUID || 'dummy_site_uuid'; + +let isRunning = false; + +async function runPoll() { + if (isRunning) { + console.log('[Scheduler] Poll sedang berjalan, skip.'); + return; + } + isRunning = true; + const fetchedAt = new Date().toISOString(); + console.log(`[Scheduler] Mulai polling... (${fetchedAt})`); + + try { + // 0. Sync agents and seed default user accounts dynamically + try { + apiAgents = await netify.fetchAgents(); + if (apiAgents && apiAgents.length > 0) { + db.syncAgentUsers(apiAgents); + console.log(`[Scheduler] OK Sync Agents : ${apiAgents.length} agen terdeteksi`); + } + } catch (err) { + console.error('[Scheduler] Gagal sync agent users:', err.message); + } + + async function fetchAndStore(fetchedAt, agentUuid) { + const agentLabel = agentUuid ? agentUuid : 'Global'; + console.log(`[Scheduler] Fetching data for ${agentLabel}`); +// 1. Top Aplikasi + const apps = await netify.fetchTopApps(1440, 20, agentUuid); + if (apps && Array.isArray(apps)) { + db.insertBandwidthApps(apps, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Apps : ${apps.length} baris`); + } else { + console.log(`[Scheduler] -- Apps : tidak ada data`); + } + + // 2. Top Devices — pakai fetchDiscoveredDevices yg sudah dinormalisasi + const devices = await netify.fetchDiscoveredDevices(1440, 200, agentUuid); + if (devices && Array.isArray(devices)) { + db.insertDevices(devices, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Devices : ${devices.length} baris`); + } else { + console.log(`[Scheduler] -- Devices : tidak ada data`); + } + + // 3. Top Protokol + const protocols = await netify.fetchTopProtocols(1440, 20, agentUuid); + if (protocols && Array.isArray(protocols)) { + db.insertProtocols(protocols, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Protocols : ${protocols.length} baris`); + } else { + console.log(`[Scheduler] -- Protocols : tidak ada data`); + } + + // 4. Top Negara + const countries = await netify.fetchTopCountries(1440, 15, agentUuid); + if (countries && Array.isArray(countries)) { + db.insertCountries(countries, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Countries : ${countries.length} baris`); + } else { + console.log(`[Scheduler] -- Countries : tidak ada data`); + } + + // 5. Top Domain/DNS + const domains = await netify.fetchTopDomains(1440, 20, agentUuid); + if (domains && Array.isArray(domains)) { + db.insertDNS(domains, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK DNS : ${domains.length} baris`); + } else { + console.log(`[Scheduler] -- DNS : tidak ada data`); + } + + // 6. Flows — pakai local_ip sebagai proxy + const flows = await netify.fetchFlows(200, agentUuid); + if (flows && Array.isArray(flows)) { + db.insertFlows(flows, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Flows : ${flows.length} baris`); + } else { + console.log(`[Scheduler] -- Flows : tidak ada data`); + } + + // 7. Threats — dari Events Status + const threats = await netify.fetchCyberThreats(1440, 50, agentUuid); + if (threats && Array.isArray(threats)) { + db.insertThreats(threats, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Threats : ${threats.length} baris`); + } else { + console.log(`[Scheduler] -- Threats : tidak ada data`); + } + + // 8. Events Log + const events = await netify.fetchEvents(50, agentUuid); + if (events && Array.isArray(events)) { + db.insertEvents(events, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Events : ${events.length} baris`); + } else { + console.log(`[Scheduler] -- Events : tidak ada data`); + } + + // 10. App Categories + const appCats = await netify.fetchTopAppCategories(1440, 15, agentUuid); + if (appCats?.length) { db.insertAppCategories(appCats, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK AppCats : ${appCats.length} baris`); } + else console.log(`[Scheduler] -- AppCats : tidak ada data`); + + // 11. Continents + const continents = await netify.fetchTopContinents(1440, 10, agentUuid); + if (continents?.length) { db.insertContinents(continents, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Continents : ${continents.length} baris`); } + else console.log(`[Scheduler] -- Continents : tidak ada data`); + + // 12. Regions + const regions = await netify.fetchTopRegions(1440, 20, agentUuid); + if (regions?.length) { db.insertRegions(regions, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Regions : ${regions.length} baris`); } + else console.log(`[Scheduler] -- Regions : tidak ada data`); + + // 13. Cities + const cities = await netify.fetchTopCities(1440, 20, agentUuid); + if (cities?.length) { db.insertCities(cities, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Cities : ${cities.length} baris`); } + else console.log(`[Scheduler] -- Cities : tidak ada data`); + + // 14. VLANs + const vlans = await netify.fetchTopVLANs(1440, 20, agentUuid); + if (vlans?.length) { db.insertVLANs(vlans, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK VLANs : ${vlans.length} baris`); } + else console.log(`[Scheduler] -- VLANs : tidak ada data`); + + // 15. Interfaces + const ifaces = await netify.fetchTopInterfaces(1440, 20, agentUuid); + if (ifaces?.length) { db.insertInterfaces(ifaces, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK Interfaces : ${ifaces.length} baris`); } + else console.log(`[Scheduler] -- Interfaces : tidak ada data`); + + // 16. Flow Types + const flowTypes = await netify.fetchTopFlowTypes(1440, 10, agentUuid); + if (flowTypes?.length) { db.insertFlowTypes(flowTypes, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK FlowTypes : ${flowTypes.length} baris`); } + else console.log(`[Scheduler] -- FlowTypes : tidak ada data`); + + // 17. Flow Origins + const flowOrigins = await netify.fetchTopFlowOrigins(1440, 10, agentUuid); + if (flowOrigins?.length) { db.insertFlowOrigins(flowOrigins, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK FlowOrigin : ${flowOrigins.length} baris`); } + else console.log(`[Scheduler] -- FlowOrigin : tidak ada data`); + + // 18. IP Versions + const ipVersions = await netify.fetchTopIPVersions(1440, 5, agentUuid); + if (ipVersions?.length) { db.insertIPVersions(ipVersions, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK IPVersions : ${ipVersions.length} baris`); } + else console.log(`[Scheduler] -- IPVersions : tidak ada data`); + + // 19. Remote IPs + const remoteIPs = await netify.fetchTopRemoteIPs(1440, 20, agentUuid); + if (remoteIPs?.length) { db.insertRemoteIPs(remoteIPs, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK RemoteIPs : ${remoteIPs.length} baris`); } + else console.log(`[Scheduler] -- RemoteIPs : tidak ada data`); + + // 20. MAC Bandwidth + const macBW = await netify.fetchTopLocalMACs(1440, 50, agentUuid); + if (macBW?.length) { db.insertMACBandwidth(macBW, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK MACBandwdh : ${macBW.length} baris`); } + else console.log(`[Scheduler] -- MACBandwdh : tidak ada data`); + + // 9. Bandwidth Timeline + const summary = await netify.fetchBandwidthSummary(1440, agentUuid); + const devCount = devices?.length ?? 0; + if (summary) { + db.insertBandwidthTimeline({ ...summary, devices: devCount }, fetchedAt, SITE_UUID, agentUuid); + console.log(`[Scheduler] OK Timeline : saved`); + } else { + console.log(`[Scheduler] -- Timeline : gagal ambil data`); + } + + // 21. TLS Versions + const tlsVer = await netify.fetchTLSVersions(1440, 10, agentUuid); + if (tlsVer?.length) { db.insertTLSVersions(tlsVer, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Ver : ${tlsVer.length} baris`); } + else console.log(`[Scheduler] -- TLS Ver : tidak ada data`); + + // 22. TLS Ciphers + const tlsCipher = await netify.fetchTLSCiphers(1440, 15, agentUuid); + if (tlsCipher?.length) { db.insertTLSCiphers(tlsCipher, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Cipher : ${tlsCipher.length} baris`); } + else console.log(`[Scheduler] -- TLS Cipher : tidak ada data`); + + // 23. TLS Security + const tlsSec = await netify.fetchTLSSecurity(1440, 10, agentUuid); + if (tlsSec?.length) { db.insertTLSSecurity(tlsSec, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TLS Sec : ${tlsSec.length} baris`); } + else console.log(`[Scheduler] -- TLS Sec : tidak ada data`); + + // 24. NetBIOS Hostnames + const netbios = await netify.fetchNetBIOSHostnames(1440, 30, agentUuid); + if (netbios?.length) { db.insertNetBIOSHostnames(netbios, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK NetBIOS : ${netbios.length} baris`); } + else console.log(`[Scheduler] -- NetBIOS : tidak ada data`); + + // 25. Discovery OS (standalone — OS yang terdeteksi di jaringan) + const discOs = await netify.fetchTopDiscoveryOS(1440, 20, agentUuid); + if (discOs?.length) { db.insertDiscoveryOS(discOs, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DiscOS : ${discOs.length} baris`); } + else console.log(`[Scheduler] -- DiscOS : tidak ada data`); + + // 26. DHCP Class Fingerprint + const dhcpFp = await netify.fetchDHCPClassFingerprints(1440, 30, agentUuid); + if (dhcpFp?.length) { db.insertDHCPFingerprints(dhcpFp, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DHCP FP : ${dhcpFp.length} baris`); } + else console.log(`[Scheduler] -- DHCP FP : tidak ada data`); + + // 27. HTTP User-Agent + const userAgents = await netify.fetchHTTPUserAgents(1440, 30, agentUuid); + if (userAgents?.length) { db.insertHTTPUserAgents(userAgents, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK UserAgent : ${userAgents.length} baris`); } + else console.log(`[Scheduler] -- UserAgent : tidak ada data`); + + // 28. HTTPS SNI Hostname + const sniHosts = await netify.fetchSNIHostnames(1440, 30, agentUuid); + if (sniHosts?.length) { db.insertSNIHostnames(sniHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SNI Host : ${sniHosts.length} baris`); } + else console.log(`[Scheduler] -- SNI Host : tidak ada data`); + + // 29. SSL Server Common Name + const sslCN = await netify.fetchSSLServerCN(1440, 30, agentUuid); + if (sslCN?.length) { db.insertSSLServerCN(sslCN, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSL CN : ${sslCN.length} baris`); } + else console.log(`[Scheduler] -- SSL CN : tidak ada data`); + + // 30. QUIC Hostname + const quicHosts = await netify.fetchQUICHostnames(1440, 30, agentUuid); + if (quicHosts?.length) { db.insertQUICHostnames(quicHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK QUIC Host : ${quicHosts.length} baris`); } + else console.log(`[Scheduler] -- QUIC Host : tidak ada data`); + + // 31. BitTorrent Info Hash + const btHashes = await netify.fetchBitTorrentInfoHashes(1440, 30, agentUuid); + if (btHashes?.length) { db.insertBitTorrentHashes(btHashes, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK BT Hash : ${btHashes.length} baris`); } + else console.log(`[Scheduler] -- BT Hash : tidak ada data`); + + // 32. SSH Client (field: ssh_client) + const sshClient = await netify.fetchSSHClients(1440, 20, agentUuid); + if (sshClient?.length) { db.insertSSHVersions(sshClient, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSH Client : ${sshClient.length} baris`); } + else console.log(`[Scheduler] -- SSH Client : tidak ada data`); + + // 32b. SSH Server (field: ssh_server) + const sshServer = await netify.fetchSSHServers(1440, 20, agentUuid); + if (sshServer?.length) { db.insertSSHVersions(sshServer, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SSH Server : ${sshServer.length} baris`); } + else console.log(`[Scheduler] -- SSH Server : tidak ada data`); + + // 33. mDNS Hostname (Chromecast, Apple TV, etc.) + const mdnsHosts = await netify.fetchMDNSHostnames(1440, 30, agentUuid); + if (mdnsHosts?.length) { db.insertMDNSHostnames(mdnsHosts, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK mDNS Host : ${mdnsHosts.length} baris`); } + else console.log(`[Scheduler] -- mDNS Host : tidak ada data`); + + // ─── INTELLIGENCE 22-30 (derive dari data yang tersedia) ───────────────── + + // 34. Cryptocurrency Mining (derive dari apps + flows ke port mining) + const cryptoMining = await netify.fetchCryptoMining(50, agentUuid); + if (cryptoMining?.length) { db.insertCryptoMining(cryptoMining, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK CryptoMine : ${cryptoMining.length} baris`); } + else console.log(`[Scheduler] -- CryptoMine : tidak ada data`); + + // 35. Device Discovery (derive dari flows + bandwidth per-IP) + const devDisc = await netify.fetchDeviceDiscovery(100, agentUuid); + if (devDisc?.length) { db.insertDeviceDiscovery(devDisc, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK DevDisc : ${devDisc.length} baris`); } + else console.log(`[Scheduler] -- DevDisc : tidak ada data`); + + // 36. Encryption Audit (derive dari flows per-IP: port encrypted vs plain) + const encAudit = await netify.fetchEncryptionAudit(50, agentUuid); + if (encAudit?.length) { db.insertEncryptionAudit(encAudit, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK EncAudit : ${encAudit.length} baris`); } + else console.log(`[Scheduler] -- EncAudit : tidak ada data`); + + // 37. Insecure Protocols (derive dari top protocols) + const insecProto = await netify.fetchInsecureProtocols(1440, 50, agentUuid); + if (insecProto?.length) { db.insertInsecureProtocols(insecProto, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK InsecProto : ${insecProto.length} baris`); } + else console.log(`[Scheduler] -- InsecProto : tidak ada data`); + + // 38. IP Reputation (derive dari top remote_ip + high-risk countries) + const ipRep = await netify.fetchIPReputation(50, agentUuid); + if (ipRep?.length) { db.insertIPReputation(ipRep, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK IPRepute : ${ipRep.length} baris`); } + else console.log(`[Scheduler] -- IPRepute : tidak ada data`); + + // 39. Server Discovery (derive dari flows ke port server well-known) + const srvDisc = await netify.fetchServerDiscovery(100, agentUuid); + if (srvDisc?.length) { db.insertServerDiscovery(srvDisc, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK SrvDisc : ${srvDisc.length} baris`); } + else console.log(`[Scheduler] -- SrvDisc : tidak ada data`); + + // 40. Tor Detection (derive dari apps/hostnames mengandung "tor") + const torDet = await netify.fetchTorDetection(50, agentUuid); + if (torDet?.length) { db.insertTorDetection(torDet, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK TorDet : ${torDet.length} baris`); } + else console.log(`[Scheduler] -- TorDet : tidak ada data`); + + // 41. Unencrypted Password (derive dari flows ke port cleartext auth) + const unencPwd = await netify.fetchUnencryptedPasswords(50, agentUuid); + if (unencPwd?.length) { db.insertUnencryptedPasswords(unencPwd, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK UnencPwd : ${unencPwd.length} baris`); } + else console.log(`[Scheduler] -- UnencPwd : tidak ada data`); + + // 42. VPN Detection (derive dari apps/protocols/ports VPN) + const vpnDet = await netify.fetchVPNDetection(50, agentUuid); + if (vpnDet?.length) { db.insertVPNDetection(vpnDet, fetchedAt, SITE_UUID, agentUuid); console.log(`[Scheduler] OK VPNDet : ${vpnDet.length} baris`); } + else console.log(`[Scheduler] -- VPNDet : tidak ada data`); + + } + + // --- Main loop + await fetchAndStore(fetchedAt, null); + if (apiAgents && apiAgents.length > 0) { + for (const agent of apiAgents) { + if (agent && agent.uuid) { + await fetchAndStore(fetchedAt, agent.uuid); + } + } + } + } catch (err) { + console.error('[Scheduler] ERROR:', err); + } finally { + isRunning = false; + console.log(`[Scheduler] Poll selesai.\n`); + } +} + +function startScheduler() { + runPoll(); + cron.schedule('* * * * *', () => runPoll()); + console.log('[Scheduler] Aktif. Polling setiap 1 menit.\n'); +} + +module.exports = { startScheduler, runPoll }; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 98b1224..dff63ab 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,123 +1,163 @@ -// backend/server.js -// ───────────────────────────────────────────────────────────────────────────── -// BackOne Backend API Server -// -// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB. -// Semua data collection (ingestion) dilakukan oleh Proxy Server (port 4000). -// Backend TIDAK memanggil DPI API secara langsung. -// -// Environment Variables: -// MONGODB_URI - MongoDB connection string -// BACKEND_PORT - Port server ini (default: 3001) -// JWT_SECRET - Secret untuk JWT auth -// ALLOWED_ORIGINS- Comma-separated allowed CORS origins -// PROXY_URL - URL proxy server (untuk trigger manual refresh) -// ───────────────────────────────────────────────────────────────────────────── - -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); - -const express = require('express'); -const cors = require('cors'); -const cookieParser = require('cookie-parser'); -const jwt = require('jsonwebtoken'); -const connectDB = require('./db/mongoose'); - -// ─── Connect to MongoDB (read-only mode) ────────────────────────────────────── -connectDB(); - -const app = express(); -const PORT = process.env.BACKEND_PORT || 3001; - -// ─── Middleware ──────────────────────────────────────────────────────────────── -const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS - ? process.env.ALLOWED_ORIGINS.split(',') - : ['http://localhost:3000', 'http://127.0.0.1:3000']; - -app.use(cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - if (ALLOWED_ORIGINS.includes(origin)) { - callback(null, true); - } else { - callback(new Error('Blocked by CORS policy (Unauthorized Origin)')); - } - }, - credentials: true -})); -app.use(express.json()); -app.use(cookieParser()); - -// ─── Public Routes ──────────────────────────────────────────────────────────── -const authRoutes = require('./routes/auth'); -const { getUploadsDir } = require('./routes/auth/helpers'); -app.use('/api/auth', authRoutes); -app.use('/api/uploads', express.static(getUploadsDir())); - -// ─── Auth Middleware ────────────────────────────────────────────────────────── -const { requireAuth } = require('./middleware/auth'); -const { getTimeFilter, getBaseFilter } = require('./routes/dashboard/helpers'); -const { - generateMacFromIp, - resolveVendorFromIp, - resolveDeviceTypeFromIp, - resolveOSFromIp, - generateAutoLabel -} = require('./deviceResolver'); - -// ─── Protected Dashboard Routes ─────────────────────────────────────────────── -const dashboardRoutes = require('./routes/dashboard'); - -// Override /api/dashboard/app-details to show real-time device mapping per application -app.get('/api/dashboard/app-details', requireAuth, (req, res) => { - require('./routes/appDetailsHandler')(req, res, { - getTimeFilter, - getBaseFilter - }); -}); - -// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.) -app.get('/api/dashboard/device-details', requireAuth, (req, res) => { - require('./routes/deviceDetailsHandler')(req, res, { - getTimeFilter, - getBaseFilter, - generateMacFromIp, - resolveDeviceTypeFromIp, - resolveOSFromIp, - resolveVendorFromIp, - generateAutoLabel - }); -}); - -app.get('/api/dashboard/remote-ip-details', requireAuth, async (req, res) => { - require('./routes/remoteIpDetailsHandler')(req, res, { - getTimeFilter - }); -}); - -const metadataDetailRoutes = require('./routes/metadataDetail'); -app.use('/api/dashboard/metadata-detail', requireAuth, metadataDetailRoutes); - -const categoryDetailRoutes = require('./routes/categoryDetail'); -app.use('/api/dashboard/category-detail', requireAuth, categoryDetailRoutes); - -app.use('/api/dashboard', requireAuth, dashboardRoutes); - - - - -// ─── Health Check ───────────────────────────────────────────────────────────── -app.get('/api/health', (req, res) => { - res.json({ - ok: true, - message: 'BackOne Backend berjalan (MongoDB read-only mode)', - time: new Date().toISOString() - }); -}); - -// ─── Start Server ───────────────────────────────────────────────────────────── -app.listen(PORT, () => { - console.log(`\n🚀 BackOne API Server berjalan di http://localhost:${PORT}`); - console.log(`🔌 API Health : http://localhost:${PORT}/api/health`); - console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)\n`); -}); +// backend/server.js +// ───────────────────────────────────────────────────────────────────────────── +// Polyfill global crypto for Node 18 compatibility (required by mongodb driver) +if (typeof globalThis.crypto === 'undefined') { + globalThis.crypto = require('crypto'); +} + +// BackOne Backend API Server +// +// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB. +// Semua data collection (ingestion) dilakukan oleh Proxy Server (port 4000). +// Backend TIDAK memanggil DPI API secara langsung. +// +// Environment Variables: +// MONGODB_URI - MongoDB connection string +// BACKEND_PORT - Port server ini (default: 3001) +// JWT_SECRET - Secret untuk JWT auth +// ALLOWED_ORIGINS- Comma-separated allowed CORS origins +// PROXY_URL - URL proxy server (untuk trigger manual refresh) +// ───────────────────────────────────────────────────────────────────────────── + +const path = require('path'); +const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'; +require('dotenv').config({ path: path.join(__dirname, '..', envFile) }); + +const express = require('express'); +const cors = require('cors'); +const cookieParser = require('cookie-parser'); +const jwt = require('jsonwebtoken'); +const connectDB = require('./db/mongoose'); + +// ─── Connect to MongoDB (read-only mode) ────────────────────────────────────── +connectDB(); + +const app = express(); +const PORT = process.env.BACKEND_PORT || 3001; + +// ─── Middleware ──────────────────────────────────────────────────────────────── +const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',') + : ['http://localhost:3000', 'http://127.0.0.1:3000']; + +app.use(cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + if (ALLOWED_ORIGINS.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Blocked by CORS policy (Unauthorized Origin)')); + } + }, + credentials: true +})); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); + +app.use((req, res, next) => { + if (req.originalUrl && req.originalUrl.includes('/api/dashboard')) { + try { + const fs = require('fs'); + const path = require('path'); + const logPath = path.join(__dirname, '../scratch/http_requests.log'); + const logLine = `[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - Query: ${JSON.stringify(req.query)}\n`; + fs.appendFileSync(logPath, logLine); + } catch (e) { + console.error('Logger error:', e.message); + } + } + next(); +}); + + +// ─── Public Routes ──────────────────────────────────────────────────────────── +const authRoutes = require('./routes/auth'); +const { getUploadsDir } = require('./routes/auth/helpers'); +app.use('/api/auth', authRoutes); +app.use('/api/uploads', express.static(getUploadsDir())); + +// ─── Auth Middleware ────────────────────────────────────────────────────────── +const { requireAuth } = require('./middleware/auth'); +const { getTimeFilter, getBaseFilter } = require('./routes/dashboard/helpers'); +const { + generateMacFromIp, + resolveVendorFromIp, + resolveDeviceTypeFromIp, + resolveOSFromIp, + generateAutoLabel +} = require('./deviceResolver'); + +// ─── Protected Dashboard Routes ─────────────────────────────────────────────── +const dashboardRoutes = require('./routes/dashboard'); + +// Override /api/dashboard/app-details to show real-time device mapping per application +app.get('/api/dashboard/app-details', requireAuth, (req, res) => { + require('./routes/appDetailsHandler')(req, res, { + getTimeFilter, + getBaseFilter + }); +}); + +// Override /api/dashboard/device-details to map real-time classifications (Facebook, YouTube, etc.) +app.get('/api/dashboard/device-details', requireAuth, (req, res) => { + require('./routes/deviceDetailsHandler')(req, res, { + getTimeFilter, + getBaseFilter, + generateMacFromIp, + resolveDeviceTypeFromIp, + resolveOSFromIp, + resolveVendorFromIp, + generateAutoLabel + }); +}); + +app.get('/api/dashboard/remote-ip-details', requireAuth, async (req, res) => { + require('./routes/remoteIpDetailsHandler')(req, res, { + getTimeFilter + }); +}); + +const metadataDetailRoutes = require('./routes/metadataDetail'); +app.use('/api/dashboard/metadata-detail', requireAuth, metadataDetailRoutes); + +const categoryDetailRoutes = require('./routes/categoryDetail'); +app.use('/api/dashboard/category-detail', requireAuth, categoryDetailRoutes); + +app.use('/api/dashboard', requireAuth, dashboardRoutes); + + + + +// ─── Health Check ───────────────────────────────────────────────────────────── +app.get('/api/health', (req, res) => { + res.json({ + ok: true, + message: 'BackOne Backend berjalan (MongoDB read-only mode)', + time: new Date().toISOString() + }); +}); + +// ─── Global JSON Error Handler ──────────────────────────────────────────────── +// Menangkap semua error yang tidak di-handle (termasuk multer, mongoose, dll.) +// dan memastikan response selalu JSON, BUKAN HTML default Express. +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + console.error('[Global Error Handler]', err.message || err); + const status = err.status || err.statusCode || 500; + res.status(status).json({ + error: err.message || 'Internal server error', + code: err.code || undefined, + }); +}); + +// ─── Start Server ───────────────────────────────────────────────────────────── +// Bind to 127.0.0.1 in production to prevent direct external access to port 3001. +// All external traffic must go through the reverse proxy (Apache/Nginx) at port 80/443. +const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0'; +app.listen(PORT, BIND_HOST, () => { + console.log(`\n🚀 BackOne API Server berjalan di http://${BIND_HOST}:${PORT}`); + console.log(`🔌 API Health : http://${BIND_HOST}:${PORT}/api/health`); + console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)`); + console.log(`🔒 Security : Bound to ${BIND_HOST} (internal only in production)\n`); +}); diff --git a/backend/services/ingestionService.js b/backend/services/ingestionService.js new file mode 100644 index 0000000..53397d5 --- /dev/null +++ b/backend/services/ingestionService.js @@ -0,0 +1,135 @@ +const cron = require('node-cron'); +const netify = require('../netify'); +const { Summary, AppStat, ProtocolStat, DeviceStat, Flow, Threat } = require('../models/Schemas'); + +const SITE_UUID = process.env.NETIFY_SITE_UUID || process.env.BACKONE_SITE_UUID; +let isRunning = false; + +async function runPoll() { + if (isRunning) return; + isRunning = true; + const timestamp = new Date(); + console.log(`[Mongo-Ingestion] Started polling at ${timestamp.toISOString()}`); + + try { + const agents = await netify.fetchAgents(); + const agentList = agents && agents.length > 0 ? agents.map(a => a.uuid) : [null]; // null for global + + for (const agentUuid of agentList) { + console.log(`[Mongo-Ingestion] Fetching data for Agent: ${agentUuid || 'Global'}`); + + // 1. Summary + const summary = await netify.fetchBandwidthSummary(1440, agentUuid); + if (summary) { + await new Summary({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + ...summary + }).save(); + } + + // 2. Apps + const apps = await netify.fetchTopApps(1440, 200, agentUuid); // high limit for data lake + if (apps && apps.length > 0) { + const appDocs = apps.map(app => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + app_label: app.application?.label || 'Unknown', + download: app.download || 0, + upload: app.upload || 0, + flows: app.flows || 0 + })); + await AppStat.insertMany(appDocs); + } + + const devices = await netify.fetchDiscoveredDevices(1440, 500, agentUuid); + if (devices && devices.length > 0) { + const devDocs = devices.map(d => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + ip_address: d.ip_address, + mac_address: d.mac_address, + device_label: d.device_label, + device_type: d.device_type, + os_label: d.os_label, + manufacturer: d.manufacturer, + download: d.download || 0, + upload: d.upload || 0, + flows: d.flows || 0, + last_seen: d.last_seen + })).filter(d => d.ip_address); // Ensure ip_address exists to avoid validation error + if (devDocs.length > 0) { + await DeviceStat.insertMany(devDocs); + } + } + + // 4. Flows + const flows = await netify.fetchFlows(500, agentUuid); + if (flows && flows.length > 0) { + const flowDocs = flows.map(f => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + flow_id: f.flow_id, + src_ip: f.src_ip, + src_mac: f.src_mac, + dst_ip: f.dst_ip, + dst_port: f.dst_port, + protocol: f.protocol, + app_label: f.app_label, + domain: f.domain, + download: f.download || 0, + upload: f.upload || 0, + first_seen: f.first_seen, + last_seen: f.last_seen + })).filter(f => f.src_ip); + if (flowDocs.length > 0) { + await Flow.insertMany(flowDocs); + } + } + + // 5. Threats + const threats = await netify.fetchCyberThreats(agentUuid); + if (threats && threats.length > 0) { + const threatDocs = threats.map(t => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + threat_type: t.threat_type || 'Unknown Threat', + severity: t.severity || 'Medium', + src_ip: t.src_ip, + dst_ip: t.dst_ip, + dst_port: t.dst_port, + protocol: t.protocol, + description: t.description, + event_at: t.event_at || new Date().toISOString() + })); + if (threatDocs.length > 0) { + await Threat.insertMany(threatDocs); + } + } + } + } catch (error) { + console.error('[Mongo-Ingestion] Error during polling:', error); + } finally { + isRunning = false; + } +} + +function startScheduler() { + // Run every 5 minutes + cron.schedule('*/5 * * * *', () => { + runPoll(); + }); + console.log('[Mongo-Ingestion] Scheduler started (every 5 minutes)'); + + // Initial run + runPoll(); +} + +module.exports = { startScheduler }; + + diff --git a/backend/test_api_tls.js b/backend/test_api_tls.js new file mode 100644 index 0000000..0f8ef0c --- /dev/null +++ b/backend/test_api_tls.js @@ -0,0 +1,14 @@ +const http = require('http'); + +http.get('http://localhost:3001/api/dashboard/tls-versions', { + headers: { + 'Cookie': 'token=test', // Just checking schema, if it requires auth we might need to mock or use the proxy + } +}, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + console.log("Response TLS Versions:"); + console.log(data.slice(0, 500)); + }); +}); diff --git a/backend/uploads/profile-1783856980162-234747538.png b/backend/uploads/profile-1783856980162-234747538.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/backend/uploads/profile-1783856980162-234747538.png differ diff --git a/backend/uploads/profile-1783925846234-644654635.png b/backend/uploads/profile-1783925846234-644654635.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/backend/uploads/profile-1783925846234-644654635.png differ diff --git a/backend/uploads/profile-1783926643277-796221976.png b/backend/uploads/profile-1783926643277-796221976.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/backend/uploads/profile-1783926643277-796221976.png differ diff --git a/check-mongo.js b/check-mongo.js new file mode 100644 index 0000000..0af2eb1 --- /dev/null +++ b/check-mongo.js @@ -0,0 +1,60 @@ +// check-mongo.js +// Script diagnostik untuk memverifikasi koneksi ke database Source 2 (backone_inspect_0) +// Jalankan: node check-mongo.js + +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env.local') }); + +const mongoose = require('mongoose'); + +const MONGODB_URI = process.env.MONGODB_URI; + +if (!MONGODB_URI) { + console.error('[ERROR] MONGODB_URI tidak ditemukan di .env.local'); + process.exit(1); +} + +console.log('\n╔════════════════════════════════════════════════╗'); +console.log('║ Source 2 — MongoDB Connection Diagnostic ║'); +console.log('╚════════════════════════════════════════════════╝\n'); +console.log(`[Check] Mencoba koneksi ke: ${MONGODB_URI}\n`); + +async function checkMongo() { + try { + await mongoose.connect(MONGODB_URI, { + serverSelectionTimeoutMS: 10000, + connectTimeoutMS: 10000, + }); + + const db = mongoose.connection.db; + const dbName = db.databaseName; + + console.log(`[OK] Berhasil terhubung ke MongoDB!`); + console.log(`[OK] Database: ${dbName}`); + + // Daftar koleksi yang ada + const collections = await db.listCollections().toArray(); + if (collections.length === 0) { + console.log('[INFO] Database masih kosong — belum ada koleksi.'); + } else { + console.log(`[INFO] Koleksi yang ada (${collections.length}):`); + for (const col of collections) { + const count = await db.collection(col.name).countDocuments(); + console.log(` - ${col.name}: ${count} dokumen`); + } + } + + console.log('\n[RESULT] ✅ STEP 8 PASS — Koneksi ke database Source 2 berhasil.\n'); + process.exit(0); + } catch (err) { + console.error(`[ERROR] Gagal terhubung ke MongoDB: ${err.message}`); + console.error('\nPossible causes:'); + console.error(' 1. Host "mongodb-netify" tidak bisa dijangkau (butuh VPN/SSH tunnel)'); + console.error(' 2. Kredensial backone_inspect:backone_inspect salah'); + console.error(' 3. MongoDB belum berjalan di server tujuan'); + console.error('\n[RESULT] ❌ STEP 8 FAIL — Hubungi atasan untuk verifikasi koneksi.\n'); + process.exit(1); + } +} + +checkMongo(); diff --git a/deploy-server-setup.sh b/deploy-server-setup.sh new file mode 100644 index 0000000..e6c312a --- /dev/null +++ b/deploy-server-setup.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# ============================================================================= +# deploy-server-setup.sh +# Script yang dijalankan di server setelah file di-upload +# Path: /home/adminbackend/web/dev.demoplace.my.id/public_html/ +# ============================================================================= + +set -e +DEPLOY_DIR="/home/adminbackend/web/dev.demoplace.my.id/public_html" +cd "$DEPLOY_DIR" + +echo "=== [1/6] Checking environment ===" +node --version +npm --version +pm2 --version || npm install -g pm2 + +echo "" +echo "=== [2/6] Installing backend dependencies ===" +cd "$DEPLOY_DIR/backend" +npm install --omit=dev --legacy-peer-deps +cd "$DEPLOY_DIR" + +echo "" +echo "=== [3/6] Installing proxy dependencies ===" +cd "$DEPLOY_DIR/proxy" +npm install --omit=dev --legacy-peer-deps +cd "$DEPLOY_DIR" + +echo "" +echo "=== [4/6] Creating required directories ===" +mkdir -p logs +mkdir -p scratch + +echo "" +echo "=== [5/6] Stopping old PM2 processes (if any) ===" +pm2 delete source2-proxy 2>/dev/null || echo "source2-proxy: not running" +pm2 delete source2-backend 2>/dev/null || echo "source2-backend: not running" +pm2 delete source2-frontend 2>/dev/null || echo "source2-frontend: not running" + +echo "" +echo "=== [6/6] Starting PM2 processes ===" +pm2 start ecosystem.config.js --env production +pm2 save +pm2 list + +echo "" +echo "=== DEPLOY COMPLETE ===" +echo "Frontend : http://127.0.0.1:3010" +echo "Backend : http://127.0.0.1:3011" +echo "Proxy : http://127.0.0.1:4010" +echo "" +echo "Check logs with: pm2 logs source2-backend --lines 30" diff --git a/docker-compose.yml b/docker-compose.yml index 049794e..98fd7f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,12 +53,9 @@ services: - MONGODB_URI=mongodb://mongodb:27017/backone_dpi - PROXY_PORT=4000 # Mode 1: kumpulkan SEMUA agent (default) - # Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik (1 agent) - # Ubah ke PROXY_COLLECT_MODE=agents dan isi PROXY_AGENT_UUIDS untuk mode multi-agent + # Ubah ke PROXY_COLLECT_MODE=agent dan isi PROXY_AGENT_UUID untuk mode spesifik - PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all} - PROXY_AGENT_UUID=${PROXY_AGENT_UUID:-} - - PROXY_AGENT_UUIDS=${PROXY_AGENT_UUIDS:-} - - PROXY_AGENT_DELAY_MS=${PROXY_AGENT_DELAY_MS:-5000} - PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *} networks: - backone-infra diff --git a/ecosystem.config.js b/ecosystem.config.js index f0395e6..e9ea1bf 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,22 +1,64 @@ +// ecosystem.config.js — PM2 Configuration for Source 2 (dev.demoplace.my.id) module.exports = { apps: [ { - name: "backone-proxy", - script: "./proxy/index.js", - env_file: ".env.production" + name: 'source2-proxy', + script: './proxy/index.js', + cwd: '/home/adminbackend/web/dev.demoplace.my.id/public_html', + instances: 1, + exec_mode: 'fork', + watch: false, + node_args: '--max-old-space-size=1024', + max_memory_restart: '1200M', + restart_delay: 5000, + max_restarts: 10, + env_file: '.env.production', + env: { NODE_ENV: 'production' }, + error_file: './logs/proxy-error.log', + out_file: './logs/proxy-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, }, { - name: "backone-backend", - script: "./backend/server.js", - env_file: ".env.production" + name: 'source2-backend', + script: './backend/server.js', + cwd: '/home/adminbackend/web/dev.demoplace.my.id/public_html', + instances: 1, + exec_mode: 'fork', + watch: false, + node_args: '--max-old-space-size=256', + max_memory_restart: '400M', + restart_delay: 3000, + max_restarts: 10, + env_file: '.env.production', + env: { NODE_ENV: 'production' }, + error_file: './logs/backend-error.log', + out_file: './logs/backend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, }, { - name: "backone-frontend", - script: "server.js", - env_file: ".env.production", + name: 'source2-frontend', + script: 'start-with-env.js', + cwd: '/home/adminbackend/web/dev.demoplace.my.id/public_html', + instances: 1, + exec_mode: 'fork', + watch: false, + node_args: '--max-old-space-size=512', + max_memory_restart: '700M', + restart_delay: 3000, + max_restarts: 10, + env_file: '.env.production', env: { - PORT: 8009 - } - } - ] + NODE_ENV: 'production', + PORT: 3010, + HOSTNAME: '127.0.0.1', + NEXT_TELEMETRY_DISABLED: '1', + }, + error_file: './logs/frontend-error.log', + out_file: './logs/frontend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + }, + ], }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 3e3fe71..13b020a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,21 +1,21 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - "backend/**", - "proxy/**", - "test/**", - ]), -]); - -export default eslintConfig; +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + "backend/**", + "proxy/**", + "test/**", + ]), +]); + +export default eslintConfig; diff --git a/git-push-iso.js b/git-push-iso.js new file mode 100644 index 0000000..32b850a --- /dev/null +++ b/git-push-iso.js @@ -0,0 +1,259 @@ +/** + * git-push-iso.js + * Push ke Gitea & GitHub menggunakan isomorphic-git (pure JS, tanpa system git) + * + * CARA KERJA: + * 1. Clone dari Gitea (untuk dapat history 75 commits) + * 2. Salin file proyek terbaru ke folder clone + * 3. Commit perubahan + * 4. Push ke Gitea + * 5. Push ke GitHub dengan remote tambahan + */ + +const git = require('isomorphic-git'); +const http = require('isomorphic-git/http/node'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// ─── CONFIG ──────────────────────────────────────────────────────────────── +const PROJECT_DIR = path.resolve(__dirname); + +// Gitea +const GITEA_URL = 'https://git.proit.id/rafif/Deep-Package-Inspection'; +const GITEA_BRANCH = 'Proxy_Server_API_backone.cloud'; +const GITEA_USER = 'rafif'; +const GITEA_PASS = 'NetWorking.0'; + +// GitHub +const GITHUB_URL = 'https://github.com/Rafif-Riqullah-Siregar/BackOne-Deep-Package-Inspection'; +const GITHUB_BRANCH = 'Proxy-Server-API-backone.cloud'; +// GitHub token (diisi nanti, atau bisa kosong dulu untuk test) +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; + +// Commit message +const COMMIT_MSG = `feat(source2): push all latest files - device labeling, help system, proxy docs, database isolation fix + +- Added DOKUMENTASI-FILTER-PER-SITE.md (site isolation docs) +- Fixed start-with-env.js to force-load .env.production +- Fixed MONGODB_URI hostname from mongodb-netify to mongodb.prod.proit.id +- Updated .gitignore to exclude sensitive scripts and credential files +- Minor UI and labeling improvements`; + +// Author +const AUTHOR = { name: 'Rafif-Riqullah-Siregar', email: 'rafif@databisnis.id' }; + +// ─── GITIGNORE PATTERNS ────────────────────────────────────────────────────── +// File/folder yang TIDAK boleh di-push (dari .gitignore) +const EXCLUDED_PATTERNS = [ + 'node_modules', + '.next', + '.env', + '.env.local', + '.env.production', + '.env.development', + 'coverage', + 'build', + 'out', + '.DS_Store', + '*.log', + '*.pem', + '.vercel', + '*.tsbuildinfo', + 'next-env.d.ts', + '*.db', '*.db-shm', '*.db-wal', '*.sqlite', + 'Laporan_*.docx', + 'temp_docx', + '*.zip', + 'AGENTS.md', 'CLAUDE.md', '.agents', + 'docs', 'plans', + 'temp.json', 'scratch', + // Sensitive scripts + 'compare-netify-vs-dashboard.js', + 'ssh-read-source1-proxy.js', + 'check-frontend-uri-now.js', + 'verify-final.js', + 'check-frontend-uri.js', + 'ssh-check-logs.js', + // Sensitive docs + 'BUKTI-AKSES-MONGODB.txt', + 'DOKUMENTASI-PROXY-NETIFY.md', +]; + +function shouldExclude(filePath) { + const parts = filePath.split(/[/\\]/); + for (const pattern of EXCLUDED_PATTERNS) { + for (const part of parts) { + if (pattern.startsWith('*')) { + const ext = pattern.slice(1); + if (part.endsWith(ext)) return true; + } else if (part === pattern || filePath.includes(pattern)) { + return true; + } + } + } + return false; +} + +async function getGitFiles(dir, baseDir = dir) { + const files = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/'); + if (shouldExclude(relPath) || entry.name === '.git') continue; + if (entry.isDirectory()) { + files.push(...await getGitFiles(fullPath, baseDir)); + } else { + files.push(relPath); + } + } + return files; +} + +async function main() { + console.log('╔══════════════════════════════════════════════════════════════╗'); + console.log('║ GIT PUSH (isomorphic-git) → Gitea + GitHub ║'); + console.log('╚══════════════════════════════════════════════════════════════╝\n'); + + // ─── STEP 1: Clone dari Gitea ke temp dir ────────────────────────────────── + const tmpDir = path.join(os.tmpdir(), `dpi-push-${Date.now()}`); + console.log(`[1] Cloning dari Gitea ke temp: ${tmpDir}`); + fs.mkdirSync(tmpDir, { recursive: true }); + + try { + await git.clone({ + fs, http, + dir: tmpDir, + url: GITEA_URL, + ref: GITEA_BRANCH, + singleBranch: true, + depth: 10, // ambil 10 commit terakhir saja (cukup untuk push) + onAuth: () => ({ username: GITEA_USER, password: GITEA_PASS }), + onProgress: ({ phase, loaded, total }) => { + if (total) process.stdout.write(`\r ${phase}: ${loaded}/${total} `); + }, + }); + console.log(`\n ✅ Clone berhasil dari Gitea branch ${GITEA_BRANCH}`); + } catch (err) { + console.error(`\n ❌ Clone dari Gitea gagal: ${err.message}`); + console.log(' → Coba dengan username tanpa domain (tanpa @databisnis.id)'); + process.exit(1); + } + + // ─── STEP 2: Salin file project terbaru ke temp dir ──────────────────────── + console.log(`\n[2] Menyalin file terbaru dari project ke clone...`); + const projectFiles = await getGitFiles(PROJECT_DIR); + let copied = 0; + for (const relPath of projectFiles) { + const src = path.join(PROJECT_DIR, relPath); + const dst = path.join(tmpDir, relPath); + fs.mkdirSync(path.dirname(dst), { recursive: true }); + fs.copyFileSync(src, dst); + copied++; + } + console.log(` ✅ ${copied} file disalin ke clone`); + + // ─── STEP 3: Stage semua perubahan ───────────────────────────────────────── + console.log(`\n[3] Staging semua perubahan...`); + const statusMatrix = await git.statusMatrix({ fs, dir: tmpDir }); + let staged = 0; + for (const [filepath, head, workdir, stage] of statusMatrix) { + if (workdir !== stage) { + if (workdir === 0) { + // File dihapus + await git.remove({ fs, dir: tmpDir, filepath }); + } else { + // File baru atau dimodifikasi + await git.add({ fs, dir: tmpDir, filepath }); + } + staged++; + } + } + console.log(` ✅ ${staged} file di-stage`); + + if (staged === 0) { + console.log(' ℹ️ Tidak ada perubahan yang perlu di-commit!'); + return cleanup(tmpDir); + } + + // ─── STEP 4: Commit ───────────────────────────────────────────────────────── + console.log(`\n[4] Membuat commit...`); + const sha = await git.commit({ + fs, + dir: tmpDir, + author: AUTHOR, + committer: AUTHOR, + message: COMMIT_MSG, + }); + console.log(` ✅ Commit dibuat: ${sha.slice(0, 8)}`); + + // ─── STEP 5: Push ke Gitea ────────────────────────────────────────────────── + console.log(`\n[5] Push ke Gitea (${GITEA_URL})...`); + try { + await git.push({ + fs, http, + dir: tmpDir, + remote: 'origin', + ref: GITEA_BRANCH, + onAuth: () => ({ username: GITEA_USER, password: GITEA_PASS }), + onProgress: ({ phase, loaded, total }) => { + if (total) process.stdout.write(`\r ${phase}: ${loaded}/${total} `); + }, + }); + console.log(`\n ✅ Push ke Gitea BERHASIL! Branch: ${GITEA_BRANCH}`); + } catch (err) { + console.error(`\n ❌ Push ke Gitea gagal: ${err.message}`); + } + + // ─── STEP 6: Push ke GitHub ───────────────────────────────────────────────── + console.log(`\n[6] Push ke GitHub (${GITHUB_URL})...`); + + // Tambah remote GitHub + const remotes = await git.listRemotes({ fs, dir: tmpDir }); + const hasGithub = remotes.some(r => r.remote === 'github'); + if (!hasGithub) { + await git.addRemote({ fs, dir: tmpDir, remote: 'github', url: GITHUB_URL }); + } + + // Coba push ke GitHub + if (!GITHUB_TOKEN) { + console.log(' ⚠️ GITHUB_TOKEN tidak di-set. GitHub push membutuhkan Personal Access Token.'); + console.log(' → Set environment variable: $env:GITHUB_TOKEN = "ghp_XXXX"'); + console.log(' → Lalu jalankan: node git-push-iso.js'); + } else { + try { + await git.push({ + fs, http, + dir: tmpDir, + remote: 'github', + ref: GITHUB_BRANCH, + remoteRef: GITHUB_BRANCH, + onAuth: () => ({ username: 'Rafif-Riqullah-Siregar', password: GITHUB_TOKEN }), + onProgress: ({ phase, loaded, total }) => { + if (total) process.stdout.write(`\r ${phase}: ${loaded}/${total} `); + }, + }); + console.log(`\n ✅ Push ke GitHub BERHASIL! Branch: ${GITHUB_BRANCH}`); + } catch (err) { + console.error(`\n ❌ Push ke GitHub gagal: ${err.message}`); + } + } + + cleanup(tmpDir); + console.log('\n╔══════════════════════════════════════════════════════════════╗'); + console.log('║ SELESAI ║'); + console.log('╚══════════════════════════════════════════════════════════════╝'); +} + +function cleanup(tmpDir) { + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + console.log(`\n[cleanup] Temp dir dihapus: ${tmpDir}`); + } catch(e) {} +} + +main().catch(err => { + console.error('\n[FATAL]', err.message); + process.exit(1); +}); diff --git a/migration-temp/customagentlocations.json b/migration-temp/customagentlocations.json new file mode 100644 index 0000000..963a932 --- /dev/null +++ b/migration-temp/customagentlocations.json @@ -0,0 +1,35 @@ +[ + { + "_id": "6a584fdb36539224fa4cbfd9", + "agent_uuid": "F6-2V-DT-8A", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "latitude": -6.2263304, + "longitude": 106.4247322, + "label": "CPI Balaraja Agent Office", + "created_at": "2026-07-14T04:03:09.294Z", + "updated_at": "2026-07-14T04:03:09.294Z", + "__v": 0 + }, + { + "_id": "6a584fdb36539224fa4cbfda", + "agent_uuid": "2F-TF-1D-GK", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "latitude": -6.3763318, + "longitude": 106.8983017, + "label": "JRP Cibubur Agent", + "created_at": "2026-07-14T04:03:09.303Z", + "updated_at": "2026-07-14T04:57:22.288Z", + "__v": 0 + }, + { + "_id": "6a584fdb36539224fa4cbfdb", + "agent_uuid": "8A-V3-PB-85", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "latitude": -6.2253265, + "longitude": 106.8061484, + "label": "IFG LT.18 Agent HQ", + "created_at": "2026-07-14T04:03:09.322Z", + "updated_at": "2026-07-14T04:03:09.322Z", + "__v": 0 + } +] \ No newline at end of file diff --git a/migration-temp/tenantconfigs.json b/migration-temp/tenantconfigs.json new file mode 100644 index 0000000..99d89f2 --- /dev/null +++ b/migration-temp/tenantconfigs.json @@ -0,0 +1,35 @@ +[ + { + "_id": "6a584fdb36539224fa4cbfd6", + "site_uuid": "default", + "brand_name": "BackOne", + "brand_logo": "/backone-logo.png", + "footer_copyright": "PT. Data Bisnis Solusi", + "primary_color": "#E11D48", + "created_at": "2026-07-14T02:15:21.201Z", + "updated_at": "2026-07-27T01:03:28.716Z", + "__v": 0 + }, + { + "_id": "6a584fdb36539224fa4cbfd7", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "brand_name": "SIAB", + "brand_logo": "/siab-logo.png", + "footer_copyright": "PT. Data Bisnis Solusi", + "primary_color": "#3B82F6", + "created_at": "2026-07-14T02:15:21.204Z", + "updated_at": "2026-07-27T01:03:28.796Z", + "__v": 0 + }, + { + "_id": "6a584fdb36539224fa4cbfd8", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "brand_name": "Nexus", + "brand_logo": "/nexus-logo.png", + "footer_copyright": "PT. Nexus Solusi", + "primary_color": "#8B5CF6", + "created_at": "2026-07-14T02:15:21.206Z", + "updated_at": "2026-07-27T01:03:28.799Z", + "__v": 0 + } +] \ No newline at end of file diff --git a/migration-temp/users.json b/migration-temp/users.json new file mode 100644 index 0000000..5b06da8 --- /dev/null +++ b/migration-temp/users.json @@ -0,0 +1,217 @@ +[ + { + "_id": "6a509b014fa14ba76d96ed33", + "username": "admin", + "password_hash": "$2a$10$uaBO91aVN9kwWS3rhCBvmu3uV00QFzMjorwqPK/AkhsGKKaLoFJoG", + "account_name": "BackOne Administrator", + "role": "SUPER_ADMIN", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "is_active": true, + "created_at": "2026-07-08T06:20:27.502Z", + "updated_at": "2026-07-21T06:57:52.516Z", + "profile_picture": "profile-1784254528937-270868858.png", + "__v": 0, + "agent_uuid": null, + "created_by": "admin", + "login_attempts": 0 + }, + { + "_id": "6a509b254fa14ba76d96ed35", + "username": "cibubur", + "password_hash": "$2a$10$OjvVNyBXRogLWcBS07GHUOkBVtBMZ6iue6U71PgWWlbMXDWe9kCu.", + "account_name": "JRP Cibubur Agent", + "role": "AGENT_VIEWER", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "agent_uuid": "2F-TF-1D-GK", + "is_active": true, + "created_at": "2026-07-14T03:39:34.474Z", + "__v": 0, + "created_by": "admin", + "profile_picture": null, + "updated_at": "2026-07-17T13:57:02.823Z", + "login_attempts": 0 + }, + { + "_id": "6a509b2e4fa14ba76d96ed36", + "username": "ifg", + "password_hash": "$2a$10$MsoJJqgY98DmgGMGyQIUD.PRA5kdbpXWhvNHFRakeipuJGF2F/73q", + "account_name": "IFG LT.18 Agent", + "role": "AGENT_VIEWER", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "agent_uuid": "8A-V3-PB-85", + "is_active": true, + "created_at": "2026-07-14T03:39:52.757Z", + "__v": 0, + "created_by": "admin", + "profile_picture": null, + "updated_at": "2026-07-14T03:40:22.272Z" + }, + { + "_id": "6a509b394fa14ba76d96ed37", + "username": "balaraja", + "password_hash": "$2a$10$sx7XNdylDOr1XCy4PjjEuOrCoIWhayMNYwNlsAjspHVnmrz0xmQ9a", + "account_name": "CPI Balaraja Agent", + "role": "AGENT_VIEWER", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "agent_uuid": "F6-2V-DT-8A", + "is_active": true, + "created_at": "2026-07-14T03:40:14.386Z", + "__v": 0, + "created_by": "admin", + "profile_picture": null, + "updated_at": "2026-07-14T03:40:14.386Z" + }, + { + "_id": "6a509b424fa14ba76d96ed38", + "username": "007", + "password_hash": "$2a$10$CDc8GOc0aTQaqMm/jD8E5OvYTxr.lbTPdrRbC6O1D2MDRzClbgyA6", + "account_name": "Gateway 007 Agent", + "role": "AGENT_VIEWER", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "agent_uuid": "YW-6I-61-LL", + "is_active": true, + "created_at": "2026-07-14T03:40:51.583Z", + "__v": 0, + "created_by": "admin", + "profile_picture": null, + "updated_at": "2026-07-17T09:10:21.716Z", + "login_attempts": 3, + "lockout_until": "2026-07-17T09:25:21.716Z" + }, + { + "_id": "6a54b314301004e28818f8e2", + "username": "bsd", + "password_hash": "$2a$10$IIZtN8coQVLfptktA0bO2eIzaCpy3yIGtr9EUWsSf9MdTUaztx8eW", + "account_name": "Fazza BSD", + "profile_picture": null, + "role": "AGENT_VIEWER", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "agent_uuid": "1T-5Q-RC-AS", + "is_active": true, + "created_at": "2026-07-14T03:38:04.913Z", + "updated_at": "2026-07-14T03:38:04.913Z", + "__v": 0, + "created_by": "admin" + }, + { + "_id": "6a54b332301004e28818f8e8", + "username": "jkt", + "password_hash": "$2a$10$EE0G6vQ6qbN6MYj2BSjqWOdJN2q/LmBcYRLZ578higbfLjnOzRKuW", + "account_name": "Fazza JKT", + "profile_picture": null, + "role": "AGENT_VIEWER", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "agent_uuid": "2N-ID-VQ-AL", + "is_active": true, + "created_at": "2026-07-14T03:38:25.721Z", + "updated_at": "2026-07-14T03:38:25.721Z", + "__v": 0, + "created_by": "admin" + }, + { + "_id": "6a56617fe7a6bc10808db750", + "username": "siab", + "__v": 0, + "account_name": "SIAB Administrator", + "agent_uuid": null, + "created_at": "2026-07-14T03:13:43.107Z", + "created_by": "admin", + "is_active": true, + "password_hash": "$2a$10$lGP.s16GBImnBWKlFCg9Ge7d0k0vwwhFGrlfExRs6KlhO/fW6yJE.", + "profile_picture": "profile-1784254601947-954448750.png", + "role": "TENANT_ADMIN", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "updated_at": "2026-07-17T02:16:41.972Z" + }, + { + "_id": "6a56617fe7a6bc10808db751", + "username": "nexus", + "__v": 0, + "account_name": "Nexus Administrator", + "agent_uuid": null, + "created_at": "2026-07-14T03:23:28.022Z", + "created_by": "nexus", + "is_active": true, + "password_hash": "$2a$10$nwhAiFodTWGZChddy10uvOV7jjo1aNMoJqrr9yB58GqGJE9oixaSe", + "profile_picture": "profile-1784254553441-339825741.png", + "role": "TENANT_ADMIN", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "updated_at": "2026-07-17T09:37:28.382Z", + "login_attempts": 0 + }, + { + "_id": "6a56617fe7a6bc10808db752", + "username": "sulist", + "__v": 0, + "account_name": "BackOne Super SOC Analyst", + "agent_uuid": null, + "created_at": "2026-07-14T03:41:18.852Z", + "created_by": "admin", + "is_active": true, + "password_hash": "$2a$10$jNV0a9uQrtnvNJwkHVe2b.m0NBOXFSbGN/6cku/wCdeuV9p9gpmSq", + "profile_picture": "profile-1784254618510-383483774.png", + "role": "SOC_ANALYST", + "site_uuid": null, + "updated_at": "2026-07-17T02:16:58.532Z" + }, + { + "_id": "6a56617fe7a6bc10808db753", + "username": "nelis", + "__v": 0, + "account_name": "Nexus SOC Analyst", + "agent_uuid": null, + "created_at": "2026-07-14T03:42:02.608Z", + "created_by": "nexus", + "is_active": true, + "password_hash": "$2a$10$tKdI9yz1e9V2mSW4H/gj.udLtAtSrHJy0QxMbq6hN3mym5XxJpH.W", + "profile_picture": "profile-1784254567483-97901195.png", + "role": "SOC_ANALYST", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "updated_at": "2026-07-17T02:16:07.500Z" + }, + { + "_id": "6a56617fe7a6bc10808db754", + "username": "nener", + "__v": 0, + "account_name": "Nexus Engineer", + "agent_uuid": null, + "created_at": "2026-07-14T03:44:55.970Z", + "created_by": "nexus", + "is_active": true, + "password_hash": "$2a$10$OoaKeuEeSHkqYMy1W50tvegaQm7u3qpP1YWuBcfmyJ45aH1kwL.Oq", + "profile_picture": "profile-1784254581808-854860134.png", + "role": "ENGINEER", + "site_uuid": "d7902405_0dc2_458b_8584_ed4d24b64f24", + "updated_at": "2026-07-17T02:16:21.824Z" + }, + { + "_id": "6a56617fe7a6bc10808db755", + "username": "silis", + "__v": 0, + "account_name": "SIAB SOC Analyst", + "agent_uuid": null, + "created_at": "2026-07-14T03:51:30.034Z", + "created_by": "siab", + "is_active": true, + "password_hash": "$2a$10$LrDCN9PzBjBV5uKKDIkcjOZcfq3WADJM408.VBrwlQmeqO/PbZtoG", + "profile_picture": "profile-1784254634906-830642874.png", + "role": "SOC_ANALYST", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "updated_at": "2026-07-17T02:17:14.925Z" + }, + { + "_id": "6a56617fe7a6bc10808db756", + "username": "siner", + "__v": 0, + "account_name": "SIAB Engineer", + "agent_uuid": null, + "created_at": "2026-07-14T03:51:50.884Z", + "created_by": "siab", + "is_active": true, + "password_hash": "$2a$10$3CISy5oSUYnC23Whfwf36OSE3y7DHYBXDXRvJwZBldyQD0ehc5Nlm", + "profile_picture": "profile-1784254648483-456467829.png", + "role": "ENGINEER", + "site_uuid": "6681452d_9cae_4ff4_8ae8_0d504774265e", + "updated_at": "2026-07-17T02:17:28.503Z" + } +] \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 43935a7..8c7c359 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,17 +2,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", - serverExternalPackages: ["mongoose"], experimental: { serverActions: { - allowedOrigins: ["demoplace.my.id", "www.demoplace.my.id"], + allowedOrigins: ["dev.demoplace.my.id", "www.dev.demoplace.my.id"], }, }, async rewrites() { return [ { source: '/api/:path*', - destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'}/api/:path*`, + destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3011'}/api/:path*`, }, ]; }, diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index ba3f186..7a283a1 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -1,8 +1,14 @@ +limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; +limit_req_zone $binary_remote_addr zone=uploads:10m rate=5r/s; + server { listen 80; - server_name demoplace.my.id; - - server_tokens off; # Hide NGINX version + server_name demoplace.my.id www.demoplace.my.id; + + server_tokens off; + + # Allow large file uploads for profile pictures (max 10MB) + client_max_body_size 10m; # Security Headers add_header X-Frame-Options "SAMEORIGIN" always; @@ -10,12 +16,12 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - # Rate Limiting zone configuration should be in nginx.conf (http block), but we can configure basic protection - # We will pass everything to the frontend container - - location / { - proxy_pass http://frontend:3000; + # Rate limiting on API endpoints + location /api/auth/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -24,8 +30,37 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header X-Powered-By; + proxy_read_timeout 30s; + } - # Hide internal technologies from being sent back to the client + # Profile picture uploads — rate limited more strictly + location /api/auth/upload-profile-picture { + limit_req zone=uploads burst=5 nodelay; + client_max_body_size 10m; + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; proxy_hide_header X-Powered-By; } + + # All other traffic goes to Next.js frontend + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header X-Powered-By; + proxy_read_timeout 30s; + proxy_connect_timeout 10s; + } } diff --git a/package-lock.json b/package-lock.json index 8c2ce9b..216c77c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { - "name": "netify-app", - "version": "0.1.0", + "name": "backone-dpi", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "netify-app", - "version": "0.1.0", + "name": "backone-dpi", + "version": "1.0.0", "dependencies": { + "@react-pdf/renderer": "^4.5.1", "@types/leaflet": "^1.9.21", "axios": "^1.18.1", "bcryptjs": "^3.0.3", @@ -21,6 +22,7 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "framer-motion": "^12.42.2", + "isomorphic-git": "^1.40.0", "jsonwebtoken": "^9.0.3", "leaflet": "^1.9.4", "lucide-react": "^1.21.0", @@ -30,6 +32,7 @@ "next": "16.2.9", "next-themes": "^0.4.6", "node-cron": "^4.6.0", + "node-fetch": "^3.3.2", "react": "19.2.4", "react-dom": "19.2.4", "react-globe.gl": "^2.38.0", @@ -57,6 +60,9 @@ "ssh2-sftp-client": "^12.1.1", "tailwindcss": "^4", "typescript": "^5" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@alloc/quick-lru": { @@ -1259,6 +1265,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1307,6 +1337,183 @@ "node": ">=12.4.0" } }, + "node_modules/@react-pdf/fns": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.3.tgz", + "integrity": "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.8.tgz", + "integrity": "sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^5.1.1", + "@react-pdf/types": "^2.11.1", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.1.0.tgz", + "integrity": "sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==", + "license": "MIT", + "dependencies": { + "@react-pdf/svg": "^1.1.0", + "jay-peg": "^1.1.1", + "png-js": "^2.0.0" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.6.1.tgz", + "integrity": "sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/image": "^3.1.0", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/stylesheet": "^6.2.1", + "@react-pdf/textkit": "^6.3.0", + "@react-pdf/types": "^2.11.1", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-5.1.1.tgz", + "integrity": "sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "browserify-zlib": "^0.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^2.0.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.3.0.tgz", + "integrity": "sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz", + "integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/reconciler/node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/@react-pdf/render": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.5.1.tgz", + "integrity": "sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/textkit": "^6.3.0", + "@react-pdf/types": "^2.11.1", + "abs-svg-path": "^0.1.1", + "color-string": "^2.1.4", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.5.1.tgz", + "integrity": "sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/font": "^4.0.8", + "@react-pdf/layout": "^4.6.1", + "@react-pdf/pdfkit": "^5.1.1", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/reconciler": "^2.0.0", + "@react-pdf/render": "^4.5.1", + "@react-pdf/types": "^2.11.1", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.2.1.tgz", + "integrity": "sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/types": "^2.11.1", + "color-string": "^2.1.4", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/svg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@react-pdf/svg/-/svg-1.1.0.tgz", + "integrity": "sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==", + "license": "MIT", + "dependencies": { + "@react-pdf/primitives": "^4.3.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.3.0.tgz", + "integrity": "sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.11.1.tgz", + "integrity": "sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.8", + "@react-pdf/primitives": "^4.3.0", + "@react-pdf/stylesheet": "^6.2.1" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -2601,7 +2808,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -2610,6 +2816,12 @@ "node": ">=6.5" } }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -3005,6 +3217,12 @@ "node": ">= 0.4" } }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, "node_modules/async-mutex": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", @@ -3024,7 +3242,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -3246,6 +3463,15 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3327,6 +3553,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.28.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", @@ -3450,7 +3694,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3559,6 +3802,12 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "license": "Apache-2.0" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3579,6 +3828,15 @@ "node": ">=20" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3608,6 +3866,27 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3883,7 +4162,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" @@ -4298,6 +4576,15 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -4410,7 +4697,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -4487,6 +4773,18 @@ "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4561,6 +4859,12 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5245,7 +5549,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5261,7 +5564,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -5341,7 +5643,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -5404,11 +5705,33 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { @@ -5553,11 +5876,27 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5606,6 +5945,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5941,7 +6292,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -6022,6 +6372,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -6055,6 +6420,12 @@ "node": ">= 6" } }, + "node_modules/hyphen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", + "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", + "license": "ISC" + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -6095,7 +6466,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -6290,7 +6660,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6584,7 +6953,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6596,6 +6964,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -6646,7 +7020,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -6656,6 +7029,71 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-git": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.40.0.tgz", + "integrity": "sha512-/CbnxwZqIm17y3c/z0INbkgEKSvFerXtO/NGgaRxZ8nvL3eoMtbjuAS7f4Pj7lZzj8HaultvDD1ClJTBVDl89g==", + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/isomorphic-git/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/isomorphic-git/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6674,6 +7112,15 @@ "node": ">= 0.4" } }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, "node_modules/jerrypick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", @@ -6693,6 +7140,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7225,6 +7678,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7361,6 +7833,12 @@ "node": ">= 0.4" } }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -7478,6 +7956,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -7949,6 +8436,26 @@ "node": ">=20" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -7968,6 +8475,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", @@ -7988,6 +8513,15 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8207,6 +8741,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8220,6 +8760,12 @@ "node": ">=6" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8290,6 +8836,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8354,6 +8909,14 @@ "node": ">=8" } }, + "node_modules/png-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz", + "integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==", + "dependencies": { + "fflate": "^0.8.2" + } + }, "node_modules/point-in-polygon-hao": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", @@ -8379,7 +8942,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8414,6 +8976,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/preact": { "version": "10.29.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", @@ -8465,7 +9033,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -8546,6 +9113,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8876,6 +9452,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", @@ -8926,6 +9511,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9137,7 +9728,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -9188,6 +9778,26 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -9746,6 +10356,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, "node_modules/swr": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", @@ -9959,6 +10575,12 @@ "three": ">=0.154" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10019,6 +10641,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10188,7 +10824,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -10332,6 +10967,32 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10466,6 +11127,29 @@ "d3-timer": "^3.0.1" } }, + "node_modules/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10575,7 +11259,6 @@ "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -10705,6 +11388,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zip-stream": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", diff --git a/package.json b/package.json index b2ae3b7..e32a97f 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { - "name": "netify-app", - "version": "0.1.0", + "name": "backone-dpi", + "version": "1.0.0", "private": true, + "engines": { + "node": ">=18.0.0" + }, "scripts": { - "dev": "concurrently --names \"NEXT,BACKEND,PROXY\" --prefix-colors \"cyan,green,yellow\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"", - "dev:next": "next dev", + "dev": "concurrently --names \"MONGO,NEXT,BACKEND,PROXY\" --prefix-colors \"magenta,cyan,green,yellow\" \"node scripts/start-mongo.js\" \"next dev -p 3010\" \"node backend/server.js\" \"node proxy/index.js\"", + "dev:central": "concurrently --names \"NEXT,BACKEND\" --prefix-colors \"cyan,green\" \"next dev -p 3010\" \"node backend/server.js\"", + "dev:next": "next dev -p 3010", "dev:backend": "node backend/server.js", "dev:proxy": "node proxy/index.js", - "kill:ports": "powershell -Command \"@(3000,3001,4000) | ForEach-Object { $port = $_; $pids = netstat -ano | Select-String \\\":$port\\s+.+LISTENING\\\" | ForEach-Object { ($_ -split '\\s+')[-1] }; $pids | Where-Object { $_ -match '^\\d+$' } | ForEach-Object { taskkill /PID $_ /F 2>$null } }; Write-Host 'Ports 3000/3001/4000 cleared.'\"", + "kill:ports": "powershell -Command \"@(3010,3011,4010) | ForEach-Object { $port = $_; $pids = netstat -ano | Select-String \\\":$port\\s+.+LISTENING\\\" | ForEach-Object { ($_ -split '\\s+')[-1] }; $pids | Where-Object { $_ -match '^\\d+$' } | ForEach-Object { taskkill /PID $_ /F 2>$null } }; Write-Host 'Ports 3010/3011/4010 cleared.'\"", "build": "next build", "start": "next start", "start:prod": "concurrently \"next start\" \"node backend/server.js\" \"node proxy/index.js\"", @@ -15,9 +19,12 @@ "backend": "node backend/server.js", "proxy": "node proxy/index.js", "proxy:bun": "bun proxy/index.js", - "install:all": "npm install && cd backend && npm install && cd ../proxy && npm install && cd .." + "install:all": "npm install && cd backend && npm install && cd ../proxy && npm install && cd ..", + "deploy": "npm run build && node scripts/deploy-sftp.js", + "deploy:sftp": "node scripts/deploy-sftp.js" }, "dependencies": { + "@react-pdf/renderer": "^4.5.1", "@types/leaflet": "^1.9.21", "axios": "^1.18.1", "bcryptjs": "^3.0.3", @@ -31,6 +38,7 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "framer-motion": "^12.42.2", + "isomorphic-git": "^1.40.0", "jsonwebtoken": "^9.0.3", "leaflet": "^1.9.4", "lucide-react": "^1.21.0", @@ -40,6 +48,7 @@ "next": "16.2.9", "next-themes": "^0.4.6", "node-cron": "^4.6.0", + "node-fetch": "^3.3.2", "react": "19.2.4", "react-dom": "19.2.4", "react-globe.gl": "^2.38.0", diff --git a/postcss.config.mjs b/postcss.config.mjs index 61e3684..bc52b4a 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,7 +1,7 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/proxy/Dockerfile.bun b/proxy/Dockerfile.bun index 80a56ad..1f3fbd3 100644 --- a/proxy/Dockerfile.bun +++ b/proxy/Dockerfile.bun @@ -1,20 +1,20 @@ -FROM oven/bun:1-alpine - -WORKDIR /app - -# Install dependencies first (layer caching) -# bun install is compatible with npm package.json / package-lock.json -COPY package*.json ./ -RUN bun install --production - -# Copy application source -COPY . . - -# Expose proxy REST API port -EXPOSE 4000 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ - CMD bun -e "require('http').get('http://localhost:4000/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))" - -CMD ["bun", "run", "index.js"] +FROM oven/bun:1-alpine + +WORKDIR /app + +# Install dependencies first (layer caching) +# bun install is compatible with npm package.json / package-lock.json +COPY package*.json ./ +RUN bun install --production + +# Copy application source +COPY . . + +# Expose proxy REST API port +EXPOSE 4000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD bun -e "require('http').get('http://localhost:4000/health', r => r.statusCode === 200 ? process.exit(0) : process.exit(1)).on('error', () => process.exit(1))" + +CMD ["bun", "run", "index.js"] diff --git a/proxy/INFO.md b/proxy/INFO.md index e3ab5db..b94cff9 100644 --- a/proxy/INFO.md +++ b/proxy/INFO.md @@ -1,108 +1,108 @@ -# BackOne DPI Proxy — Deployment Reference - -Standalone Docker image for collecting Netify DPI data and writing to MongoDB. - ---- - -## Environment Variables - -### Required - -| Variable | Description | -|---|---| -| `NETIFY_SITE_UUIDS` | Comma-separated Netify site UUIDs (or single `NETIFY_SITE_UUID`) | -| `NETIFY_TOKEN` | Netify JWT token (or `NETIFY_JWT_TOKEN` / `NETIFY_API_KEY`) | - -### MongoDB - -| Variable | Default | Description | -|---|---|---| -| `MONGODB_URI` | `mongodb://127.0.0.1:27017/backone_dpi` | MongoDB connection string | - -### Collection Mode - -| Variable | Default | Description | -|---|---|---| -| `PROXY_COLLECT_MODE` | `all` | `all` = all agents, `agent` = single agent, `agents` = list of agents | -| `PROXY_AGENT_UUID` | _(none)_ | Single agent UUID (required if `mode=agent`) | -| `PROXY_AGENT_UUIDS` | _(none)_ | Comma-separated agent UUIDs (required if `mode=agents`) | -| `PROXY_AGENT_DELAY_MS` | `5000` | Delay (ms) between each agent collection to avoid rate-limiting | - -### Scheduling - -| Variable | Default | Description | -|---|---|---| -| `PROXY_CRON_SCHEDULE` | `*/5 * * * *` | Cron expression for collection interval | -| `PROXY_CAPACITY_LOG_INTERVAL_MS` | `86400000` | How often to log DB capacity usage (default: 24h) | - -### Limits - -| Variable | Default | Description | -|---|---|---| -| `PROXY_FLOW_LIMIT` | `1000000` | Max flows to fetch per agent per cycle | -| `PROXY_PORT` | `4000` | REST API listen port | - -### Netify API - -| Variable | Default | Description | -|---|---|---| -| `NETIFY_INFORMATICS_BASE_URL` | `https://informatics.netify.ai/api/v1` | Netify API base URL | - ---- - -## Docker Run Example - -```bash -docker run -d \ - --name backone_proxy \ - -p 4000:4000 \ - -e MONGODB_URI=mongodb://host.docker.internal:27017/backone_dpi \ - -e NETIFY_SITE_UUIDS=site-uuid-1,site-uuid-2 \ - -e NETIFY_TOKEN=your-jwt-token \ - -e PROXY_COLLECT_MODE=agents \ - -e PROXY_AGENT_UUIDS=agent-uuid-1,agent-uuid-2,agent-uuid-3 \ - backone-proxy -``` - -## Docker Compose Example - -```yaml -services: - proxy: - build: ./proxy - container_name: backone_proxy - restart: always - ports: - - "4000:4000" - environment: - - MONGODB_URI=mongodb://mongodb:27017/backone_dpi - - PROXY_PORT=4000 - - PROXY_COLLECT_MODE=all - - PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all} - - PROXY_AGENT_UUID=${PROXY_AGENT_UUID:-} - - PROXY_AGENT_UUIDS=${PROXY_AGENT_UUIDS:-} - - PROXY_AGENT_DELAY_MS=${PROXY_AGENT_DELAY_MS:-5000} - - PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *} - - NETIFY_SITE_UUIDS=${NETIFY_SITE_UUIDS} - - NETIFY_TOKEN=${NETIFY_TOKEN} - - NETIFY_INFORMATICS_BASE_URL=${NETIFY_INFORMATICS_BASE_URL:-https://informatics.netify.ai/api/v1} -``` - -## REST API Endpoints - -| Method | Endpoint | Description | -|---|---|---| -| `GET` | `/health` | Liveness check (MongoDB status) | -| `GET` | `/status` | Scheduler status, mode, last run | -| `GET` | `/agents` | List agent UUIDs in MongoDB | -| `POST` | `/collect/all` | Manual trigger — all agents | -| `POST` | `/collect/:uuid` | Manual trigger — single agent | -| `POST` | `/collect/agents` | Manual trigger — multiple agents `{"uuids":[...], "delay_ms":5000}` | -| `GET` | `/latest` | Latest data from all collections (debug) | -| `GET` | `/domain-details?domain=...` | IP/MAC details for a domain | - -## Health Check - -```bash -curl http://localhost:4000/health -``` +# BackOne DPI Proxy — Deployment Reference + +Standalone Docker image for collecting Netify DPI data and writing to MongoDB. + +--- + +## Environment Variables + +### Required + +| Variable | Description | +|---|---| +| `NETIFY_SITE_UUIDS` | Comma-separated Netify site UUIDs (or single `NETIFY_SITE_UUID`) | +| `NETIFY_TOKEN` | Netify JWT token (or `NETIFY_JWT_TOKEN` / `NETIFY_API_KEY`) | + +### MongoDB + +| Variable | Default | Description | +|---|---|---| +| `MONGODB_URI` | `mongodb://127.0.0.1:27017/backone_dpi` | MongoDB connection string | + +### Collection Mode + +| Variable | Default | Description | +|---|---|---| +| `PROXY_COLLECT_MODE` | `all` | `all` = all agents, `agent` = single agent, `agents` = list of agents | +| `PROXY_AGENT_UUID` | _(none)_ | Single agent UUID (required if `mode=agent`) | +| `PROXY_AGENT_UUIDS` | _(none)_ | Comma-separated agent UUIDs (required if `mode=agents`) | +| `PROXY_AGENT_DELAY_MS` | `5000` | Delay (ms) between each agent collection to avoid rate-limiting | + +### Scheduling + +| Variable | Default | Description | +|---|---|---| +| `PROXY_CRON_SCHEDULE` | `*/5 * * * *` | Cron expression for collection interval | +| `PROXY_CAPACITY_LOG_INTERVAL_MS` | `86400000` | How often to log DB capacity usage (default: 24h) | + +### Limits + +| Variable | Default | Description | +|---|---|---| +| `PROXY_FLOW_LIMIT` | `1000000` | Max flows to fetch per agent per cycle | +| `PROXY_PORT` | `4000` | REST API listen port | + +### Netify API + +| Variable | Default | Description | +|---|---|---| +| `NETIFY_INFORMATICS_BASE_URL` | `https://informatics.netify.ai/api/v1` | Netify API base URL | + +--- + +## Docker Run Example + +```bash +docker run -d \ + --name backone_proxy \ + -p 4000:4000 \ + -e MONGODB_URI=mongodb://host.docker.internal:27017/backone_dpi \ + -e NETIFY_SITE_UUIDS=site-uuid-1,site-uuid-2 \ + -e NETIFY_TOKEN=your-jwt-token \ + -e PROXY_COLLECT_MODE=agents \ + -e PROXY_AGENT_UUIDS=agent-uuid-1,agent-uuid-2,agent-uuid-3 \ + backone-proxy +``` + +## Docker Compose Example + +```yaml +services: + proxy: + build: ./proxy + container_name: backone_proxy + restart: always + ports: + - "4000:4000" + environment: + - MONGODB_URI=mongodb://mongodb:27017/backone_dpi + - PROXY_PORT=4000 + - PROXY_COLLECT_MODE=all + - PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all} + - PROXY_AGENT_UUID=${PROXY_AGENT_UUID:-} + - PROXY_AGENT_UUIDS=${PROXY_AGENT_UUIDS:-} + - PROXY_AGENT_DELAY_MS=${PROXY_AGENT_DELAY_MS:-5000} + - PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *} + - NETIFY_SITE_UUIDS=${NETIFY_SITE_UUIDS} + - NETIFY_TOKEN=${NETIFY_TOKEN} + - NETIFY_INFORMATICS_BASE_URL=${NETIFY_INFORMATICS_BASE_URL:-https://informatics.netify.ai/api/v1} +``` + +## REST API Endpoints + +| Method | Endpoint | Description | +|---|---|---| +| `GET` | `/health` | Liveness check (MongoDB status) | +| `GET` | `/status` | Scheduler status, mode, last run | +| `GET` | `/agents` | List agent UUIDs in MongoDB | +| `POST` | `/collect/all` | Manual trigger — all agents | +| `POST` | `/collect/:uuid` | Manual trigger — single agent | +| `POST` | `/collect/agents` | Manual trigger — multiple agents `{"uuids":[...], "delay_ms":5000}` | +| `GET` | `/latest` | Latest data from all collections (debug) | +| `GET` | `/domain-details?domain=...` | IP/MAC details for a domain | + +## Health Check + +```bash +curl http://localhost:4000/health +``` diff --git a/proxy/check_device.js b/proxy/check_device.js new file mode 100644 index 0000000..7e33bd1 --- /dev/null +++ b/proxy/check_device.js @@ -0,0 +1,34 @@ +// check_device.js - Detailed check of 10.6.10.44 records +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +async function run() { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'); + const db = mongoose.connection.db; + + // Get all records for 10.6.10.44 + const docs = await db.collection('devicestats') + .find({ ip_address: '10.6.10.44' }) + .sort({ timestamp: 1 }) + .toArray(); + + console.log(`Total docs for 10.6.10.44: ${docs.length}`); + docs.forEach((d, i) => { + console.log(`\n--- Doc ${i + 1} ---`); + console.log(' _id: ', d._id); + console.log(' agent_uuid: ', d.agent_uuid); + console.log(' timestamp: ', d.timestamp); + console.log(' created_at: ', d.created_at); + console.log(' updated_at: ', d.updated_at); + console.log(' download: ', d.download); + console.log(' device_label:', d.device_label); + }); + + // Check if there are different agent_uuids + const agents = [...new Set(docs.map(d => d.agent_uuid))]; + console.log('\nDistinct agent_uuids for this IP:', agents); + + await mongoose.disconnect(); +} +run().catch(err => { console.error(err.message); process.exit(1); }); diff --git a/proxy/check_summary_data.js b/proxy/check_summary_data.js new file mode 100644 index 0000000..620d7e4 --- /dev/null +++ b/proxy/check_summary_data.js @@ -0,0 +1,63 @@ +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; +const SIAB = '6681452d_9cae_4ff4_8ae8_0d504774265e'; + +mongoose.connect(MONGODB_URI).then(async () => { + const db = mongoose.connection.db; + const since24h = new Date(Date.now() - 24 * 3600000); + const since7d = new Date(Date.now() - 7 * 24 * 3600000); + + // Count site-level summary docs + const count24h = await db.collection('summaries').countDocuments({ + site_uuid: SIAB, agent_uuid: null, timestamp: { $gte: since24h } + }); + const countAll = await db.collection('summaries').countDocuments({ + site_uuid: SIAB, agent_uuid: null + }); + + // Sum bandwidth for last 24h (site-level, agent_uuid: null) + const agg24h = await db.collection('summaries').aggregate([ + { $match: { site_uuid: SIAB, agent_uuid: null, timestamp: { $gte: since24h } } }, + { $group: { _id: null, totalDown: { $sum: '$bandwidth_down' }, totalUp: { $sum: '$bandwidth_up' }, count: { $sum: 1 } } } + ]).toArray(); + + // Sum bandwidth ALL time (site-level) + const aggAll = await db.collection('summaries').aggregate([ + { $match: { site_uuid: SIAB, agent_uuid: null } }, + { $group: { _id: null, totalDown: { $sum: '$bandwidth_down' }, totalUp: { $sum: '$bandwidth_up' }, count: { $sum: 1 } } } + ]).toArray(); + + // Oldest and newest + const oldest = await db.collection('summaries').findOne({ site_uuid: SIAB, agent_uuid: null }, { sort: { timestamp: 1 }, projection: { timestamp: 1 } }); + const newest = await db.collection('summaries').findOne({ site_uuid: SIAB, agent_uuid: null }, { sort: { timestamp: -1 }, projection: { timestamp: 1, bandwidth_down: 1, bandwidth_up: 1 } }); + + console.log('\n=== MongoDB Summary Check (SIAB site) ==='); + console.log('Total site-level docs:', countAll); + console.log('Site-level docs in last 24h:', count24h); + console.log('\nBandwidth SUM (last 24h):'); + console.log(' Down:', agg24h[0] ? (agg24h[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', agg24h[0] ? (agg24h[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log('\nBandwidth SUM (ALL time):'); + console.log(' Down:', aggAll[0] ? (aggAll[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', aggAll[0] ? (aggAll[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log('\nOldest entry :', oldest?.timestamp); + console.log('Newest entry :', newest?.timestamp); + console.log('Latest bandwidth_down per 5min:', newest ? (newest.bandwidth_down / 1024 / 1024).toFixed(4) + ' MB' : 'N/A'); + console.log('Latest bandwidth_up per 5min :', newest ? (newest.bandwidth_up / 1024 / 1024).toFixed(4) + ' MB' : 'N/A'); + + // Also check flow data for comparison + const flowAgg = await db.collection('flows').aggregate([ + { $match: { site_uuid: SIAB, timestamp: { $gte: since24h } } }, + { $group: { _id: null, totalDown: { $sum: '$download' }, totalUp: { $sum: '$upload' }, count: { $sum: 1 } } } + ]).toArray(); + console.log('\nFlow-level bandwidth (last 24h from flows collection):'); + console.log(' Down:', flowAgg[0] ? (flowAgg[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', flowAgg[0] ? (flowAgg[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Flow count:', flowAgg[0]?.count || 0); + console.log('=====================================\n'); + + process.exit(0); +}).catch(e => { console.error(e.message); process.exit(1); }); diff --git a/proxy/clean_devicestat_duplicates.js b/proxy/clean_devicestat_duplicates.js new file mode 100644 index 0000000..974598c --- /dev/null +++ b/proxy/clean_devicestat_duplicates.js @@ -0,0 +1,73 @@ +// proxy/clean_devicestat_duplicates.js +// ───────────────────────────────────────────────────────────────────────────── +// One-time cleanup script to deduplicate historical DeviceStat records. +// Keeps only the LATEST document per (agent_uuid, ip_address) pair, +// removing all older duplicates accumulated before the upsert fix. +// +// Usage: node proxy/clean_devicestat_duplicates.js +// ───────────────────────────────────────────────────────────────────────────── + +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); + +const mongoose = require('mongoose'); +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'; + +const DeviceStatSchema = new mongoose.Schema({ + timestamp: { type: Date }, + agent_uuid: { type: String }, + site_uuid: { type: String }, + ip_address: { type: String }, + mac_address: { type: String }, + device_label: String, + device_type: String, + os_label: String, + manufacturer: String, + download: Number, + upload: Number, + flows: Number, + last_seen: String, +}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } }); + +const DeviceStat = mongoose.model('DeviceStat', DeviceStatSchema); + +async function run() { + console.log('[Cleanup] Connecting to MongoDB...'); + await mongoose.connect(MONGODB_URI); + console.log('[Cleanup] Connected.'); + + // Find all unique (agent_uuid, ip_address) combinations + const groups = await DeviceStat.aggregate([ + { $group: { + _id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' }, + ids: { $push: '$_id' }, + timestamps: { $push: '$timestamp' }, + count: { $sum: 1 }, + }}, + { $match: { count: { $gt: 1 } } }, + ]); + + console.log(`[Cleanup] Found ${groups.length} (agent_uuid, ip_address) pairs with duplicates.`); + let totalDeleted = 0; + + for (const group of groups) { + // Sort the ids by matching timestamps - keep the latest + const paired = group.ids.map((id, i) => ({ id, ts: group.timestamps[i] })); + paired.sort((a, b) => new Date(b.ts) - new Date(a.ts)); + + // Keep the first (newest), delete the rest + const toDelete = paired.slice(1).map(p => p.id); + const result = await DeviceStat.deleteMany({ _id: { $in: toDelete } }); + totalDeleted += result.deletedCount; + } + + const remaining = await DeviceStat.countDocuments(); + console.log(`[Cleanup] Done. Deleted ${totalDeleted} duplicate DeviceStat records.`); + console.log(`[Cleanup] Remaining DeviceStat documents: ${remaining}`); + await mongoose.disconnect(); +} + +run().catch(err => { + console.error('[Cleanup] Fatal error:', err.message); + process.exit(1); +}); diff --git a/proxy/cleanup_duplicate_agents.js b/proxy/cleanup_duplicate_agents.js index ae5d73b..644fef0 100644 --- a/proxy/cleanup_duplicate_agents.js +++ b/proxy/cleanup_duplicate_agents.js @@ -8,8 +8,8 @@ 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'; +const SIAB_UUID = '6681452d_9cae_4ff4_8ae8_0d504774265e'; +const OFFICE_UUID = '1959bb55_045b_47c7_bbdd_f33b7db197b9'; // 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']; @@ -27,13 +27,13 @@ async function cleanup() { for (const colName of collections) { const col = db.collection(colName); - // 1. Delete SIAB agents that are stored under NEXUS site_uuid + // 1. Delete SIAB agents that are stored under OFFICE site_uuid const r1 = await col.deleteMany({ - site_uuid: NEXUS_UUID, + site_uuid: OFFICE_UUID, agent_uuid: { $in: SIAB_AGENTS } }); if (r1.deletedCount > 0) { - console.log(`[${colName}] Removed ${r1.deletedCount} docs (SIAB agents from NEXUS)`); + console.log(`[${colName}] Removed ${r1.deletedCount} docs (SIAB agents from OFFICE)`); totalDeleted += r1.deletedCount; } diff --git a/proxy/collector.js b/proxy/collector.js index 5017b69..6d60171 100644 --- a/proxy/collector.js +++ b/proxy/collector.js @@ -1,262 +1,263 @@ -// proxy/collector.js -// ───────────────────────────────────────────────────────────────────────────── -// Core data collection logic for the BackOne Proxy Server -// Supports 2 modes: ALL Agents and ONE Agent by UUID -// All data is stored in MongoDB, tagged with agent_uuid + site_uuid. -// ───────────────────────────────────────────────────────────────────────────── - -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); - -const netify = require('./netifyClient'); -const { collectSecondaryTelemetry } = require('./collectorHelper'); -const { - collectDevicesAndApps, - collectFlows, - collectThreats, - collectEvents -} = require('./collectorHelperDpi2'); - -const { Summary, AppStat } = require('./models/Schemas'); -const { pruneOldData } = require('./dataRetention'); - -const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID; -const SITE_UUIDS = SITE_UUIDS_STR ? SITE_UUIDS_STR.split(',').map(s => s.trim()).filter(Boolean) : []; - -async function collectForAgent(agentUuid, timestamp, siteUuid) { - const label = agentUuid || 'GLOBAL'; - console.log(`[Collector] → Fetching data for Agent: ${label}`); - - try { - // 1. Summary / Bandwidth (including derived speed, packet_drops, and peak_flow_rate) - const summary = await netify.fetchBandwidthSummary(1440, agentUuid, siteUuid); - if (summary) { - let download_speed = 0; - let upload_speed = 0; - let packet_drops = 0; - let peak_flow_rate = summary.active_flows || 0; - - try { - const prev = await Summary.findOne({ agent_uuid: agentUuid }).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, summary.bandwidth_down - (prev.bandwidth_down || 0)); - const bytesDiffUp = Math.max(0, summary.bandwidth_up - (prev.bandwidth_up || 0)); - download_speed = bytesDiffDown / timeDiffSec; - upload_speed = bytesDiffUp / timeDiffSec; - } - } - } catch (err) { - console.error('[Collector] Error calculating summary speeds:', err.message); - } - - packet_drops = Math.floor((summary.active_flows || 0) * 0.015); - peak_flow_rate = Math.floor((summary.active_flows || 0) * 1.18); - - const activeFlows = summary.active_flows || 0; - const totalBandwidth = (summary.bandwidth_down || 0) + (summary.bandwidth_up || 0); - - 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: agentUuid, - site_uuid: siteUuid, - ...summary, - download_speed, - upload_speed, - packet_drops, - peak_flow_rate, - cpu_usage, - memory_usage, - queue_depth - }).save(); - console.log(`[Collector] ✓ Summary saved for ${label}`); - } - - // 2. Top Apps - const apps = await netify.fetchTopApps(1440, 200, agentUuid, siteUuid); - if (apps && apps.length > 0) { - const appDocs = apps.map(app => ({ - timestamp, agent_uuid: agentUuid, site_uuid: siteUuid, - app_label: app.application?.label || 'Unknown', - download: app.download || 0, upload: app.upload || 0, flows: app.flows || 0, - })); - await AppStat.insertMany(appDocs); - console.log(`[Collector] ✓ ${appDocs.length} apps saved for ${label}`); - } - - // Collect Secondary Telemetry (categories, TLS, countries, DHCP, User Agents, BitTorrent) - await collectSecondaryTelemetry(agentUuid, timestamp, siteUuid, netify, label); - - // 2. Devices & App Records - const ipToMacMap = await collectDevicesAndApps(agentUuid, timestamp, siteUuid, netify, label); - - // 3. Flows - await collectFlows(agentUuid, timestamp, siteUuid, netify, label, ipToMacMap); - - // 5. Threats - await collectThreats(agentUuid, timestamp, siteUuid, netify, label); - - // 6. Events - await collectEvents(agentUuid, timestamp, siteUuid, netify, label); - - return { success: true, agent_uuid: agentUuid }; - } catch (err) { - console.error(`[Collector] ✗ Error collecting for ${label}:`, err.message); - return { success: false, agent_uuid: agentUuid, error: err.message }; - } -} - -async function collectAllAgents() { - const timestamp = new Date(); - const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] === MODE: ALL AGENTS === Started at ${timeString}`); - - const results = []; - let totalAgents = 0; - - if (SITE_UUIDS.length === 0) { - console.warn('[Collector] No NETIFY_SITE_UUIDS configured.'); - 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); - if (!rawAgents || rawAgents.length === 0) { - console.warn(`[Collector] No agents found for site ${siteUuid}.`); - continue; - } - - // 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 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 }); - } - } - - const successful = results.filter(r => r.success).length; - console.log(`[Collector] === ALL AGENTS DONE === ${successful}/${totalAgents} successful across ${SITE_UUIDS.length} sites`); - - await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); - - return { success: true, mode: 'all', agents_count: totalAgents, successful }; -} - -async function collectSpecificAgent(agentUuid, siteUuid = SITE_UUIDS[0]) { - const timestamp = new Date(); - const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] === MODE: SPECIFIC AGENT ${agentUuid} === Started at ${timeString}`); - const result = await collectForAgent(agentUuid, timestamp, siteUuid); - - await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); - - return { success: result.success, mode: 'specific', agent_uuid: agentUuid }; -} - -async function collectSpecificAgents(agentUuids, siteUuid = SITE_UUIDS[0], delayMs = 5000) { - const timestamp = new Date(); - const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] === MODE: SPECIFIC AGENTS [${agentUuids.join(', ')}] === Started at ${timeString}`); - const results = []; - for (let i = 0; i < agentUuids.length; i++) { - if (i > 0) { - console.log(`[Collector] Waiting ${delayMs}ms before next agent...`); - await new Promise(resolve => setTimeout(resolve, delayMs)); - } - const result = await collectForAgent(agentUuids[i], timestamp, siteUuid); - results.push(result); - } - const successful = results.filter(r => r.success).length; - console.log(`[Collector] === SPECIFIC AGENTS DONE === ${successful}/${agentUuids.length} successful`); - - await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); - - return { success: true, mode: 'specific_agents', agents_count: agentUuids.length, successful, results }; -} - -module.exports = { - collectAllAgents, - collectSpecificAgent, - collectSpecificAgents -}; +// proxy/collector.js +// ───────────────────────────────────────────────────────────────────────────── +// Core data collection logic for the BackOne Proxy Server +// Supports 2 modes: ALL Agents and ONE Agent by UUID +// All data is stored in MongoDB, tagged with agent_uuid + site_uuid. +// ───────────────────────────────────────────────────────────────────────────── + +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); + +const netify = require('./netifyClient'); +const { collectSecondaryTelemetry } = require('./collectorHelper'); +const { collectDevicesAndApps, collectFlows } = require('./collectorHelperDpi2'); +const { collectThreats, collectEvents } = require('./collectorHelperDpi3'); + +const { Summary, AppStat, AgentRegistry } = require('./models/Schemas'); +const { pruneOldData } = require('./dataRetention'); + +const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID; +const SITE_UUIDS = SITE_UUIDS_STR ? SITE_UUIDS_STR.split(',').map(s => s.trim()).filter(Boolean) : []; + +async function collectForAgent(agentUuid, timestamp, siteUuid) { + const label = agentUuid || 'GLOBAL'; + console.log(`[Collector] → Fetching data for Agent: ${label}`); + + try { + // 1. Summary / Bandwidth (including derived speed, packet_drops, and peak_flow_rate) + const summary = await netify.fetchBandwidthSummary(5, agentUuid, siteUuid); + if (summary) { + let download_speed = 0; + let upload_speed = 0; + let packet_drops = 0; + let peak_flow_rate = summary.active_flows || 0; + + try { + const prev = await Summary.findOne({ agent_uuid: agentUuid }).sort({ timestamp: -1 }).lean(); + const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300; + const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300; + download_speed = summary.bandwidth_down / activeTimeDiff; + upload_speed = summary.bandwidth_up / activeTimeDiff; + } catch (err) { + console.error('[Collector] Error calculating summary speeds:', err.message); + } + + packet_drops = Math.floor((summary.active_flows || 0) * 0.015); + peak_flow_rate = Math.floor((summary.active_flows || 0) * 1.18); + + const activeFlows = summary.active_flows || 0; + const totalBandwidth = (summary.bandwidth_down || 0) + (summary.bandwidth_up || 0); + + 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: agentUuid, + site_uuid: siteUuid, + ...summary, + download_speed, + upload_speed, + packet_drops, + peak_flow_rate, + cpu_usage, + memory_usage, + queue_depth + }).save(); + console.log(`[Collector] ✓ Summary saved for ${label}`); + } + + // 2. Top Apps + const apps = await netify.fetchTopApps(5, 200, agentUuid, siteUuid); + if (apps && apps.length > 0) { + const appDocs = apps.map(app => ({ + timestamp, agent_uuid: agentUuid, site_uuid: siteUuid, + app_label: app.application?.label || 'Unknown', + download: app.download || 0, upload: app.upload || 0, flows: app.flows || 0, + })); + await AppStat.insertMany(appDocs); + console.log(`[Collector] ✓ ${appDocs.length} apps saved for ${label}`); + } + + // Collect Secondary Telemetry (categories, TLS, countries, DHCP, User Agents, BitTorrent) + await collectSecondaryTelemetry(agentUuid, timestamp, siteUuid, netify, label); + + // 2. Devices & App Records + const ipToMacMap = await collectDevicesAndApps(agentUuid, timestamp, siteUuid, netify, label); + + // 3. Flows + await collectFlows(agentUuid, timestamp, siteUuid, netify, label, ipToMacMap); + + // 5. Threats + await collectThreats(agentUuid, timestamp, siteUuid, netify, label); + + // 6. Events + await collectEvents(agentUuid, timestamp, siteUuid, netify, label); + + return { success: true, agent_uuid: agentUuid }; + } catch (err) { + console.error(`[Collector] ✗ Error collecting for ${label}:`, err.message); + return { success: false, agent_uuid: agentUuid, error: err.message }; + } +} + +async function collectAllAgents() { + const timestamp = new Date(); + const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; + console.log(`[Collector] === MODE: ALL AGENTS === Started at ${timeString}`); + + const results = []; + let totalAgents = 0; + + if (SITE_UUIDS.length === 0) { + console.warn('[Collector] No NETIFY_SITE_UUIDS configured.'); + 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); + if (!rawAgents || rawAgents.length === 0) { + console.warn(`[Collector] No agents found for site ${siteUuid}.`); + continue; + } + + // 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 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); + + // ── Upsert all agents into agent_registry collection ─────────────────── + // This ensures ALL agents appear in the frontend even with no telemetry data. + await Promise.allSettled(agents.map(a => + AgentRegistry.findOneAndUpdate( + { uuid: a.uuid }, + { + $set: { + uuid: a.uuid, + serial: a.serial || a.uuid, + label: a.label, + site_uuid: siteUuid, + provisioned: a.provisioned ?? true, + activated: a.activated ?? false, + last_seen_at: a.last_seen_at ?? null, + netify_id: a.id ?? null, + } + }, + { upsert: true, new: true } + ) + )); + console.log(`[Collector] ✓ ${agents.length} agents upserted into registry for site ${siteUuid}`); + + 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 { + const siteSummary = await netify.fetchBandwidthSummary(5, 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(); + const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300; + const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300; + download_speed = siteSummary.bandwidth_down / activeTimeDiff; + upload_speed = siteSummary.bandwidth_up / activeTimeDiff; + } 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 }); + } + } + + const successful = results.filter(r => r.success).length; + console.log(`[Collector] === ALL AGENTS DONE === ${successful}/${totalAgents} successful across ${SITE_UUIDS.length} sites`); + + await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); + + return { success: true, mode: 'all', agents_count: totalAgents, successful }; +} + +async function collectSpecificAgent(agentUuid, siteUuid = SITE_UUIDS[0]) { + const result = await collectSpecificAgents([agentUuid], siteUuid, 0); + return { success: result.successful > 0, mode: 'specific', agent_uuid: agentUuid }; +} + +async function collectSpecificAgents(agentUuids, siteUuid = SITE_UUIDS[0], delayMs = 5000) { + const timestamp = new Date(); + const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; + console.log(`[Collector] === MODE: SPECIFIC AGENTS [${agentUuids.join(', ')}] === Started at ${timeString}`); + const results = []; + for (let i = 0; i < agentUuids.length; i++) { + if (i > 0) { + console.log(`[Collector] Waiting ${delayMs}ms before next agent...`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + const result = await collectForAgent(agentUuids[i], timestamp, siteUuid); + results.push(result); + } + const successful = results.filter(r => r.success).length; + console.log(`[Collector] === SPECIFIC AGENTS DONE === ${successful}/${agentUuids.length} successful`); + + await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); + + return { success: true, mode: 'specific_agents', agents_count: agentUuids.length, successful, results }; +} + +module.exports = { + collectAllAgents, + collectSpecificAgent, + collectSpecificAgents +}; diff --git a/proxy/collectorHelper.js b/proxy/collectorHelper.js index a7701e6..aea58fd 100644 --- a/proxy/collectorHelper.js +++ b/proxy/collectorHelper.js @@ -1,167 +1,167 @@ -// proxy/collectorHelper.js -// ───────────────────────────────────────────────────────────────────────────── -// Supplementary Telemetry collection steps for BackOne Proxy Server. -// Split from collector.js to satisfy the 256-line file size limit. -// ───────────────────────────────────────────────────────────────────────────── - -const { - AppCategoryStat, - TlsVersionStat, - TlsCipherStat, - TlsSecurityStat, - CountryStat, - ProtocolStat, - SslSubjectAltNameStat, - SniHostnameStat, - SslServerCnStat, - QuicHostnameStat, -} = require('./models/Schemas'); - -async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify, label) { - try { - // 2b. App Categories - const categories = await netify.fetchTopAppCategories(1440, 50, agentUuid, SITE_UUID); - if (categories && categories.length > 0) { - const catDocs = categories.map(c => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - category_label: c.category_label, - download: c.download || 0, - upload: c.upload || 0, - flows: c.flows || 0, - })); - await AppCategoryStat.insertMany(catDocs); - console.log(`[Collector] ✓ ${catDocs.length} categories saved for ${label}`); - } - - // 2c. TLS Versions - const tlsVersions = await netify.fetchTlsVersions(1440, 50, agentUuid, SITE_UUID); - if (tlsVersions && tlsVersions.length > 0) { - const tvDocs = tlsVersions.map(v => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - tls_version: v.tls_version, - download: v.download || 0, - upload: v.upload || 0, - flows: v.flows || 0, - })); - await TlsVersionStat.insertMany(tvDocs); - console.log(`[Collector] ✓ ${tvDocs.length} TLS versions saved for ${label}`); - } - - // 2d. TLS Ciphers - const tlsCiphers = await netify.fetchTlsCiphers(1440, 50, agentUuid, SITE_UUID); - if (tlsCiphers && tlsCiphers.length > 0) { - const tcDocs = tlsCiphers.map(c => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - tls_cipher: c.tls_cipher, - download: c.download || 0, - upload: c.upload || 0, - flows: c.flows || 0, - })); - await TlsCipherStat.insertMany(tcDocs); - console.log(`[Collector] ✓ ${tcDocs.length} TLS ciphers saved for ${label}`); - } - - // 2e. TLS Security - const tlsSecurity = await netify.fetchTlsSecurity(1440, 50, agentUuid, SITE_UUID); - if (tlsSecurity && tlsSecurity.length > 0) { - const tsDocs = tlsSecurity.map(s => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - tls_security: s.tls_security, - download: s.download || 0, - upload: s.upload || 0, - flows: s.flows || 0, - })); - await TlsSecurityStat.insertMany(tsDocs); - console.log(`[Collector] ✓ ${tsDocs.length} TLS security stats saved for ${label}`); - } - - // 2f. Top Countries - const countries = await netify.fetchTopCountries(1440, 100, agentUuid, SITE_UUID); - if (countries && countries.length > 0) { - const coDocs = countries.map(c => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - country_code: c.country_code, - country_name: c.country_name || '', - download: c.download || 0, - upload: c.upload || 0, - flows: c.flows || 0, - })); - await CountryStat.insertMany(coDocs); - console.log(`[Collector] ✓ ${coDocs.length} countries saved for ${label}`); - } - - // 2g. Top Protocols - const protocols = await netify.fetchTopProtocols(1440, 50, agentUuid, SITE_UUID); - if (protocols && protocols.length > 0) { - const protoDocs = protocols.map(p => ({ - timestamp, - agent_uuid: agentUuid, - site_uuid: SITE_UUID, - protocol_label: p.protocol_label, - download: p.download || 0, - upload: p.upload || 0, - flows: p.flows || 0, - })); - await ProtocolStat.insertMany(protoDocs); - console.log(`[Collector] ✓ ${protoDocs.length} protocols saved for ${label}`); - } - - // 2h. SNI Hostnames - const snis = await netify.fetchSniHostnames(1440, 10000, agentUuid, SITE_UUID); - if (snis && snis.length > 0) { - const sniDocs = snis.map(s => ({ - timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, - sni_hostname: (s.sni_hostname && String(s.sni_hostname).trim() !== '') ? s.sni_hostname : 'Unknown', - download: s.download || 0, upload: s.upload || 0, flows: s.flows || 0, - })); - await SniHostnameStat.insertMany(sniDocs); - console.log(`[Collector] ✓ ${sniDocs.length} SNI hostnames saved for ${label}`); - } - - /* -- COMMENTED OUT DUE TO NETIFY API HTTP 422 (UNSUPPORTED TIER) -- - // 2i. SSL Server Common Names - const cns = await netify.fetchSslServerCn(1440, 50, agentUuid); - if (cns && cns.length > 0) { - const cnDocs = cns.map(c => ({ - timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, - ssl_server_cn: c.ssl_server_cn, download: c.download || 0, upload: c.upload || 0, flows: c.flows || 0, - })); - await SslServerCnStat.insertMany(cnDocs); - console.log(`[Collector] ✓ ${cnDocs.length} SSL Server CNs saved for ${label}`); - } - - // 2j. QUIC Hostnames - const quics = await netify.fetchQuicHostnames(1440, 50, agentUuid); - if (quics && quics.length > 0) { - const quicDocs = quics.map(q => ({ - timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, - quic_hostname: q.quic_hostname, download: q.download || 0, upload: q.upload || 0, flows: q.flows || 0, - })); - await QuicHostnameStat.insertMany(quicDocs); - console.log(`[Collector] ✓ ${quicDocs.length} QUIC hostnames saved for ${label}`); - } - */ - - // NOTE: The following API fields are not supported on this subscription (HTTP 422): - // dhcp_class, http_useragent, bittorrent_info_hash, ssl_subject_alt_name - // These sections are intentionally skipped to avoid wasted API calls. - // Re-enable when API access is upgraded to a tier that supports these fields. - - } catch (err) { - console.error(`[CollectorHelper] Error saving secondary telemetry:`, err.message); - } -} - -module.exports = { - collectSecondaryTelemetry, -}; +// proxy/collectorHelper.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary Telemetry collection steps for BackOne Proxy Server. +// Split from collector.js to satisfy the 256-line file size limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { + AppCategoryStat, + TlsVersionStat, + TlsCipherStat, + TlsSecurityStat, + CountryStat, + ProtocolStat, + SslSubjectAltNameStat, + SniHostnameStat, + SslServerCnStat, + QuicHostnameStat, +} = require('./models/Schemas'); + +async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify, label) { + try { + // 2b. App Categories + const categories = await netify.fetchTopAppCategories(5, 50, agentUuid, SITE_UUID); + if (categories && categories.length > 0) { + const catDocs = categories.map(c => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + category_label: c.category_label, + download: c.download || 0, + upload: c.upload || 0, + flows: c.flows || 0, + })); + await AppCategoryStat.insertMany(catDocs); + console.log(`[Collector] ✓ ${catDocs.length} categories saved for ${label}`); + } + + // 2c. TLS Versions + const tlsVersions = await netify.fetchTlsVersions(5, 50, agentUuid, SITE_UUID); + if (tlsVersions && tlsVersions.length > 0) { + const tvDocs = tlsVersions.map(v => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + tls_version: v.tls_version, + download: v.download || 0, + upload: v.upload || 0, + flows: v.flows || 0, + })); + await TlsVersionStat.insertMany(tvDocs); + console.log(`[Collector] ✓ ${tvDocs.length} TLS versions saved for ${label}`); + } + + // 2d. TLS Ciphers + const tlsCiphers = await netify.fetchTlsCiphers(5, 50, agentUuid, SITE_UUID); + if (tlsCiphers && tlsCiphers.length > 0) { + const tcDocs = tlsCiphers.map(c => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + tls_cipher: c.tls_cipher, + download: c.download || 0, + upload: c.upload || 0, + flows: c.flows || 0, + })); + await TlsCipherStat.insertMany(tcDocs); + console.log(`[Collector] ✓ ${tcDocs.length} TLS ciphers saved for ${label}`); + } + + // 2e. TLS Security + const tlsSecurity = await netify.fetchTlsSecurity(5, 50, agentUuid, SITE_UUID); + if (tlsSecurity && tlsSecurity.length > 0) { + const tsDocs = tlsSecurity.map(s => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + tls_security: s.tls_security, + download: s.download || 0, + upload: s.upload || 0, + flows: s.flows || 0, + })); + await TlsSecurityStat.insertMany(tsDocs); + console.log(`[Collector] ✓ ${tsDocs.length} TLS security stats saved for ${label}`); + } + + // 2f. Top Countries + const countries = await netify.fetchTopCountries(5, 100, agentUuid, SITE_UUID); + if (countries && countries.length > 0) { + const coDocs = countries.map(c => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + country_code: c.country_code, + country_name: c.country_name || '', + download: c.download || 0, + upload: c.upload || 0, + flows: c.flows || 0, + })); + await CountryStat.insertMany(coDocs); + console.log(`[Collector] ✓ ${coDocs.length} countries saved for ${label}`); + } + + // 2g. Top Protocols + const protocols = await netify.fetchTopProtocols(5, 50, agentUuid, SITE_UUID); + if (protocols && protocols.length > 0) { + const protoDocs = protocols.map(p => ({ + timestamp, + agent_uuid: agentUuid, + site_uuid: SITE_UUID, + protocol_label: p.protocol_label, + download: p.download || 0, + upload: p.upload || 0, + flows: p.flows || 0, + })); + await ProtocolStat.insertMany(protoDocs); + console.log(`[Collector] ✓ ${protoDocs.length} protocols saved for ${label}`); + } + + // 2h. SNI Hostnames + const snis = await netify.fetchSniHostnames(5, 10000, agentUuid, SITE_UUID); + if (snis && snis.length > 0) { + const sniDocs = snis.map(s => ({ + timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, + sni_hostname: (s.sni_hostname && String(s.sni_hostname).trim() !== '') ? s.sni_hostname : 'Unknown', + download: s.download || 0, upload: s.upload || 0, flows: s.flows || 0, + })); + await SniHostnameStat.insertMany(sniDocs); + console.log(`[Collector] ✓ ${sniDocs.length} SNI hostnames saved for ${label}`); + } + + /* -- COMMENTED OUT DUE TO NETIFY API HTTP 422 (UNSUPPORTED TIER) -- + // 2i. SSL Server Common Names + const cns = await netify.fetchSslServerCn(1440, 50, agentUuid); + if (cns && cns.length > 0) { + const cnDocs = cns.map(c => ({ + timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, + ssl_server_cn: c.ssl_server_cn, download: c.download || 0, upload: c.upload || 0, flows: c.flows || 0, + })); + await SslServerCnStat.insertMany(cnDocs); + console.log(`[Collector] ✓ ${cnDocs.length} SSL Server CNs saved for ${label}`); + } + + // 2j. QUIC Hostnames + const quics = await netify.fetchQuicHostnames(1440, 50, agentUuid); + if (quics && quics.length > 0) { + const quicDocs = quics.map(q => ({ + timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, + quic_hostname: q.quic_hostname, download: q.download || 0, upload: q.upload || 0, flows: q.flows || 0, + })); + await QuicHostnameStat.insertMany(quicDocs); + console.log(`[Collector] ✓ ${quicDocs.length} QUIC hostnames saved for ${label}`); + } + */ + + // NOTE: The following API fields are not supported on this subscription (HTTP 422): + // dhcp_class, http_useragent, bittorrent_info_hash, ssl_subject_alt_name + // These sections are intentionally skipped to avoid wasted API calls. + // Re-enable when API access is upgraded to a tier that supports these fields. + + } catch (err) { + console.error(`[CollectorHelper] Error saving secondary telemetry:`, err.message); + } +} + +module.exports = { + collectSecondaryTelemetry, +}; diff --git a/proxy/collectorHelperDpi2.js b/proxy/collectorHelperDpi2.js index 408330f..bc44c80 100644 --- a/proxy/collectorHelperDpi2.js +++ b/proxy/collectorHelperDpi2.js @@ -14,7 +14,7 @@ const { } = require('./deviceResolver'); async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, label) { - const devices = await netify.fetchDiscoveredDevices(1440, 500, agentUuid, SITE_UUID); + const devices = await netify.fetchDiscoveredDevices(5, 500, agentUuid, SITE_UUID); const ipToMacMap = {}; if (devices && devices.length > 0) { @@ -56,7 +56,7 @@ async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, la let deviceAppCount = 0; for (let i = 0; i < topDevices.length; i += 5) { const batch = topDevices.slice(i, i + 5); - const results = await Promise.allSettled(batch.map(d => netify.fetchDeviceApps(d.ip_address, 1440, 50, agentUuid, SITE_UUID))); + const results = await Promise.allSettled(batch.map(d => netify.fetchDeviceApps(d.ip_address, 5, 50, agentUuid, SITE_UUID))); const appDocs = []; results.forEach((res, idx) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { @@ -81,7 +81,7 @@ async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, la } async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipToMacMap) { - // Netify API has a hard limit of 1,000,000 for settings_limit. + // Netify API has a hard limit of 1,000,000 for settings_limit. Use 1000000 as default per rule. const flowLimit = parseInt(process.env.PROXY_FLOW_LIMIT || '1000000'); const flows = await netify.fetchFlows(flowLimit, agentUuid, SITE_UUID); if (flows && flows.length > 0) { @@ -115,16 +115,34 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo 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 flowIdsInBatch = flowDocs.map(f => f.flow_id).filter(Boolean); + const existingFlowThreats = new Set( + await Threat.find({ flow_id: { $in: flowIdsInBatch } }).distinct('flow_id') + ); + const threatDocs = []; + const eventDocs = []; + 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; + for (const r of blacklistRules) { + if (r.type === 'domain') { + const val = r.value.toLowerCase(); + // Direct domain match + if (f.domain && f.domain.toLowerCase().includes(val)) { + isViolation = true; + break; + } + // Main domain part match against app label (e.g. "google" from "google.com") + const mainDomainPart = val.split('.')[0]; + if (mainDomainPart && f.app_label && f.app_label.toLowerCase().includes(mainDomainPart)) { + isViolation = true; + break; + } + } } // Look up app details to check category @@ -138,7 +156,7 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo } } - if (isViolation) { + if (isViolation && !existingFlowThreats.has(f.flow_id)) { threatDocs.push({ timestamp, agent_uuid: f.agent_uuid, @@ -150,7 +168,21 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo 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() + event_at: new Date().toISOString(), + flow_id: f.flow_id + }); + + eventDocs.push({ + timestamp, + agent_uuid: f.agent_uuid, + site_uuid: f.site_uuid, + event_type: "blacklist_violation", + severity: "Warning", + description: `Access to blacklisted app/domain: ${f.app_label} (${f.domain || 'N/A'})${categoryLabel ? ' - Category: ' + categoryLabel : ''}`, + ip_address: f.src_ip, + mac_address: f.src_mac, + event_at: new Date(), + flow_id: f.flow_id }); } } @@ -159,6 +191,10 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo await Threat.insertMany(threatDocs); console.log(`[Collector] ✓ ${threatDocs.length} blacklist policy violation threats recorded for ${label}`); } + if (eventDocs.length > 0) { + await Event.insertMany(eventDocs); + console.log(`[Collector] ✓ ${eventDocs.length} blacklist policy violation events recorded for ${label}`); + } } } catch (err) { console.error('[Collector] Blacklist detection failed:', err.message); @@ -169,58 +205,7 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo } } -async function collectThreats(agentUuid, timestamp, SITE_UUID, netify, label) { - const threats = await netify.fetchCyberThreats(agentUuid, SITE_UUID); - if (threats && threats.length > 0) { - const threatDocs = threats.map(t => ({ - timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, - threat_type: t.threat_type || 'Unknown Threat', severity: t.severity || 'Medium', - src_ip: t.src_ip, dst_ip: t.dst_ip, dst_port: t.dst_port, protocol: t.protocol, - description: t.description, event_at: t.event_at || new Date().toISOString(), - })); - await Threat.insertMany(threatDocs); - console.log(`[Collector] ✓ ${threatDocs.length} threats saved for ${label}`); - } -} - -async function collectEvents(agentUuid, timestamp, SITE_UUID, netify, label) { - const events = await netify.fetchEvents(100, agentUuid, SITE_UUID); - if (events && events.length > 0) { - const eventIds = events.map(e => e.event_id).filter(id => id !== null); - const existing = await Event.find({ site_uuid: SITE_UUID, event_id: { $in: eventIds } }).distinct('event_id'); - const existingSet = new Set(existing); - - const macToAgentMap = {}; - const eventMacs = [...new Set(events.map(e => e.mac_address).filter(Boolean))]; - if (eventMacs.length > 0) { - const storedDevices = await DeviceStat.find( - { site_uuid: SITE_UUID, mac_address: { $in: eventMacs } }, - { mac_address: 1, agent_uuid: 1 } - ).lean(); - for (const d of storedDevices) { - if (d.mac_address && d.agent_uuid) macToAgentMap[d.mac_address] = d.agent_uuid; - } - } - - const eventDocs = events.filter(e => e.event_id === null || !existingSet.has(e.event_id)).map(e => { - const resolvedAgentUuid = (e.mac_address && macToAgentMap[e.mac_address]) || agentUuid; - return { - timestamp, agent_uuid: resolvedAgentUuid, site_uuid: SITE_UUID, - event_id: e.event_id, event_type: e.event_type, severity: e.severity, - description: e.description, category_label: e.category_label, - ip_address: e.ip_address, mac_address: e.mac_address, event_at: e.event_at, - }; - }); - if (eventDocs.length > 0) { - await Event.insertMany(eventDocs); - console.log(`[Collector] ✓ ${eventDocs.length} new events saved for ${label}`); - } - } -} - module.exports = { collectDevicesAndApps, - collectFlows, - collectThreats, - collectEvents + collectFlows }; diff --git a/proxy/collectorHelperDpi3.js b/proxy/collectorHelperDpi3.js new file mode 100644 index 0000000..ddc720a --- /dev/null +++ b/proxy/collectorHelperDpi3.js @@ -0,0 +1,61 @@ +// proxy/collectorHelperDpi3.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary Telemetry collection steps for threats and events. +// Split from collectorHelperDpi2.js to satisfy the 256-line file size limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { DeviceStat, Threat, Event } = require('./models/Schemas'); + +async function collectThreats(agentUuid, timestamp, SITE_UUID, netify, label) { + const threats = await netify.fetchCyberThreats(agentUuid, SITE_UUID); + if (threats && threats.length > 0) { + const threatDocs = threats.map(t => ({ + timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, + threat_type: t.threat_type || 'Unknown Threat', severity: t.severity || 'Medium', + src_ip: t.src_ip, dst_ip: t.dst_ip, dst_port: t.dst_port, protocol: t.protocol, + description: t.description, event_at: t.event_at || new Date().toISOString(), + })); + await Threat.insertMany(threatDocs); + console.log(`[Collector] ✓ ${threatDocs.length} threats saved for ${label}`); + } +} + +async function collectEvents(agentUuid, timestamp, SITE_UUID, netify, label) { + const events = await netify.fetchEvents(100, agentUuid, SITE_UUID); + if (events && events.length > 0) { + const eventIds = events.map(e => e.event_id).filter(id => id !== null); + const existing = await Event.find({ site_uuid: SITE_UUID, event_id: { $in: eventIds } }).distinct('event_id'); + const existingSet = new Set(existing); + + const macToAgentMap = {}; + const eventMacs = [...new Set(events.map(e => e.mac_address).filter(Boolean))]; + if (eventMacs.length > 0) { + const storedDevices = await DeviceStat.find( + { site_uuid: SITE_UUID, mac_address: { $in: eventMacs } }, + { mac_address: 1, agent_uuid: 1 } + ).lean(); + for (const d of storedDevices) { + if (d.mac_address && d.agent_uuid) macToAgentMap[d.mac_address] = d.agent_uuid; + } + } + + const eventDocs = events.filter(e => e.event_id === null || !existingSet.has(e.event_id)).map(e => { + const resolvedAgentUuid = (e.mac_address && macToAgentMap[e.mac_address]) || agentUuid; + return { + timestamp, agent_uuid: resolvedAgentUuid, site_uuid: SITE_UUID, + event_id: e.event_id, event_type: e.event_type, severity: e.severity, + description: e.description, category_label: e.category_label, + ip_address: e.ip_address, mac_address: e.mac_address, event_at: e.event_at, + }; + }); + if (eventDocs.length > 0) { + await Event.insertMany(eventDocs); + console.log(`[Collector] ✓ ${eventDocs.length} new events saved for ${label}`); + } + } +} + +module.exports = { + collectThreats, + collectEvents +}; diff --git a/proxy/dataRetention.js b/proxy/dataRetention.js index cf940bf..57bf96c 100644 --- a/proxy/dataRetention.js +++ b/proxy/dataRetention.js @@ -11,9 +11,9 @@ const Schemas = require('./models/Schemas'); * Prune all time-series documents older than 7 days. */ async function pruneOldData() { - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - const timeString = sevenDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] [Retention] Checking for telemetry data older than 7 days (before ${timeString})...`); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const timeString = thirtyDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; + console.log(`[Collector] [Retention] Checking for telemetry data older than 30 days (before ${timeString})...`); // Prune from all collections in Schemas except CustomDeviceLabel const collections = Object.keys(Schemas).filter(name => name !== 'CustomDeviceLabel'); @@ -22,7 +22,7 @@ async function pruneOldData() { try { const Model = Schemas[name]; if (typeof Model.deleteMany === 'function') { - const res = await Model.deleteMany({ timestamp: { $lt: sevenDaysAgo } }); + const res = await Model.deleteMany({ timestamp: { $lt: thirtyDaysAgo } }); if (res.deletedCount > 0) { console.log(`[Collector] [Retention] ✓ Cleaned up ${res.deletedCount} old records from ${name}`); } diff --git a/proxy/diagnostic.js b/proxy/diagnostic.js new file mode 100644 index 0000000..d1510cd --- /dev/null +++ b/proxy/diagnostic.js @@ -0,0 +1,44 @@ +// diagnostic.js - Run: node proxy/diagnostic.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'; + +async function run() { + await mongoose.connect(MONGODB_URI); + const db = mongoose.connection.db; + + // 1. Total count + const total = await db.collection('devicestats').countDocuments(); + console.log('=== DeviceStat Total:', total); + + // 2. Count for 10.6.10.44 + const specific = await db.collection('devicestats').countDocuments({ ip_address: '10.6.10.44' }); + console.log('=== Count for 10.6.10.44:', specific); + + // 3. Sample doc for 10.6.10.44 + const sample = await db.collection('devicestats').findOne({ ip_address: '10.6.10.44' }); + console.log('=== Sample doc for 10.6.10.44:', JSON.stringify(sample, null, 2)); + + // 4. Duplicate groups (top 10) + const dups = await db.collection('devicestats').aggregate([ + { $group: { _id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' }, count: { $sum: 1 } } }, + { $match: { count: { $gt: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 10 } + ]).toArray(); + console.log('=== Top duplicate groups:', JSON.stringify(dups, null, 2)); + + // 5. Check what collection name is actually used + const collections = await db.listCollections().toArray(); + console.log('=== Collections:', collections.map(c => c.name)); + + // 6. Check indexes on devicestats + const indexes = await db.collection('devicestats').indexes(); + console.log('=== Indexes on devicestats:', JSON.stringify(indexes, null, 2)); + + await mongoose.disconnect(); +} + +run().catch(err => { console.error('ERROR:', err.message); process.exit(1); }); diff --git a/proxy/fix_devicestat_index.js b/proxy/fix_devicestat_index.js new file mode 100644 index 0000000..9b72724 --- /dev/null +++ b/proxy/fix_devicestat_index.js @@ -0,0 +1,79 @@ +// fix_devicestat_index.js +// Creates a unique compound index on (agent_uuid, ip_address) in DeviceStat +// and deduplicates any remaining duplicates before creating the index. +// Run: node proxy/fix_devicestat_index.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'; + +async function run() { + console.log('[Fix] Connecting to MongoDB...'); + await mongoose.connect(MONGODB_URI); + const db = mongoose.connection.db; + const col = db.collection('devicestats'); + + // Step 1: Find all duplicates grouped by (agent_uuid, ip_address) + console.log('[Fix] Scanning for duplicates...'); + const groups = await col.aggregate([ + { + $group: { + _id: { agent_uuid: '$agent_uuid', ip_address: '$ip_address' }, + ids: { $push: '$_id' }, + timestamps: { $push: '$timestamp' }, + count: { $sum: 1 }, + } + }, + { $match: { count: { $gt: 1 } } }, + ]).toArray(); + + console.log(`[Fix] Found ${groups.length} duplicate groups.`); + let deleted = 0; + + for (const group of groups) { + // Sort by timestamp descending — keep the newest + const paired = group.ids.map((id, i) => ({ id, ts: new Date(group.timestamps[i] || 0) })); + paired.sort((a, b) => b.ts - a.ts); + const toDelete = paired.slice(1).map(p => p.id); + const result = await col.deleteMany({ _id: { $in: toDelete } }); + deleted += result.deletedCount; + } + + console.log(`[Fix] Deleted ${deleted} duplicate documents.`); + const remaining = await col.countDocuments(); + console.log(`[Fix] Remaining DeviceStat documents: ${remaining}`); + + // Step 2: Drop old non-unique compound index if it exists + try { + await col.dropIndex('agent_uuid_1_timestamp_-1_ip_address_1'); + console.log('[Fix] Dropped old compound index.'); + } catch (e) { + console.log('[Fix] Old index not found or already dropped:', e.message); + } + + // Step 3: Create UNIQUE compound index on (agent_uuid, ip_address) + try { + await col.createIndex( + { agent_uuid: 1, ip_address: 1 }, + { unique: true, name: 'agent_uuid_1_ip_address_1_unique', background: true } + ); + console.log('[Fix] Created unique index on (agent_uuid, ip_address).'); + } catch (e) { + console.error('[Fix] Failed to create unique index:', e.message); + } + + // Step 4: Verify indexes + const indexes = await col.indexes(); + console.log('[Fix] Current indexes:'); + indexes.forEach(idx => console.log(` - ${idx.name}: ${JSON.stringify(idx.key)} ${idx.unique ? '[UNIQUE]' : ''}`)); + + // Step 5: Verify 10.6.10.44 + const cnt = await col.countDocuments({ ip_address: '10.6.10.44' }); + console.log(`\n[Fix] Count for 10.6.10.44: ${cnt} (should be 1)`); + + await mongoose.disconnect(); + console.log('[Fix] Done.'); +} + +run().catch(err => { console.error('[Fix] Fatal:', err.message); process.exit(1); }); diff --git a/proxy/index.js b/proxy/index.js index d942ac9..d5bc907 100644 --- a/proxy/index.js +++ b/proxy/index.js @@ -1,10 +1,16 @@ // proxy/index.js // ───────────────────────────────────────────────────────────────────────────── +// Polyfill global crypto for Node 18 compatibility (required by mongodb driver) +if (typeof globalThis.crypto === 'undefined') { + globalThis.crypto = require('crypto'); +} + // BackOne Proxy Server - Entry Point // ───────────────────────────────────────────────────────────────────────────── const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'; +require('dotenv').config({ path: path.join(__dirname, '..', envFile) }); const express = require('express'); const cors = require('cors'); @@ -57,11 +63,13 @@ async function main() { console.log('╚════════════════════════════════════════════════╝\n'); console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`); - // 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}`); + // Bind to 127.0.0.1 in production — port 4000 must never be exposed externally + const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0'; + app.listen(PORT, BIND_HOST, () => { + console.log(`\n🚀 Proxy REST API running at http://${BIND_HOST}:${PORT}`); console.log(` GET /health → liveness check`); - console.log(` GET /status → scheduler + DB status\n`); + console.log(` GET /status → scheduler + DB status`); + console.log(`🔒 Security : Bound to ${BIND_HOST} (internal only in production)\n`); }); const connected = await connectDB(); diff --git a/proxy/models/Schemas.js b/proxy/models/Schemas.js index 1af95ca..a7ceb88 100644 --- a/proxy/models/Schemas.js +++ b/proxy/models/Schemas.js @@ -1,182 +1,199 @@ -// proxy/models/Schemas.js -// MongoDB schemas shared between the proxy server (write) and backend (read). -// Each document is tagged with agent_uuid + site_uuid for tenant isolation. -// -// IMPORTANT: Indexes are set for common query patterns: -// - timestamp (for time-range queries) -// - agent_uuid (for per-tenant filtering) -// - site_uuid (for site-level aggregation) - -const mongoose = require('mongoose'); - -const baseOptions = { - timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } -}; - -// ─── Bandwidth Summary (per agent, per collection cycle) ─────────────────────── -const SummarySchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, // null = global/all agents - site_uuid: { type: String, index: true }, - bandwidth_down: Number, - bandwidth_up: Number, - active_flows: Number, - download_speed: Number, - upload_speed: Number, - total_devices: Number, - total_threats: Number, - packet_drops: Number, - peak_flow_rate: Number, - cpu_usage: Number, - memory_usage: Number, - queue_depth: Number, -}, baseOptions); - -// ─── Top Applications (per agent) ───────────────────────────────────────────── -const AppStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - app_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── Protocol Statistics (per agent) ────────────────────────────────────────── -const ProtocolStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - protocol_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── Discovered Devices (per agent, includes IP + MAC + device info) ─────────── -const DeviceStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - ip_address: { type: String, required: true, index: true }, - mac_address: { type: String, index: true }, - device_label: String, - device_type: String, - os_label: String, - manufacturer: String, - download: Number, - upload: Number, - flows: Number, - last_seen: String, -}, baseOptions); - -// ─── Network Flows (per agent) ───────────────────────────────────────────────── -const FlowSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - flow_id: String, - src_ip: { type: String, index: true }, - 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, - app_label: String, - domain: String, - download: Number, - upload: Number, - first_seen: String, - last_seen: String, -}, baseOptions); - -// ─── Cyber Threats (per agent) ───────────────────────────────────────────────── -const ThreatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - threat_type: String, - severity: String, - src_ip: String, - dst_ip: String, - dst_port: Number, - protocol: String, - description: String, - event_at: String, -}, baseOptions); - -// ─── App Categories (per agent) ─────────────────────────────────────────────── -const AppCategoryStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - category_label: { type: String, required: true }, - download: Number, - upload: Number, - flows: Number, -}, baseOptions); - -// ─── System Events (per agent) ───────────────────────────────────────────────── -const EventSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - event_id: Number, - event_type: String, - severity: String, - description: String, - category_label: String, - ip_address: String, - mac_address: String, - event_at: Date, -}, baseOptions); - -// ─── Compound indexes for common dashboard queries ───────────────────────────── -SummarySchema.index({ agent_uuid: 1, timestamp: -1 }); -AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 }); -DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); -FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); -FlowSchema.index({ agent_uuid: 1, flow_id: 1 }); -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 }); -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 -// Allows showing "YouTube 134GB" in Device Detail modal per specific IP -const DeviceAppStatSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true, index: true, expires: '7d' }, - agent_uuid: { type: String, index: true }, - site_uuid: { type: String, index: true }, - ip_address: { type: String, required: true, index: true }, - app_label: { type: String, required: true }, - app_id: Number, - download: { type: Number, default: 0 }, - upload: { type: Number, default: 0 }, - flows: { type: Number, default: 0 }, - last_seen: String, -}, baseOptions); -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 }); - -const telemetrySchemas = require('./SchemasTelemetry'); -const auxSchemas = require('./SchemasAux'); - -module.exports = { - Summary: mongoose.model('Summary', SummarySchema), - AppStat: mongoose.model('AppStat', AppStatSchema), - ProtocolStat:mongoose.model('ProtocolStat',ProtocolStatSchema), - DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), - DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema), - Flow: mongoose.model('Flow', FlowSchema), - Threat: mongoose.model('Threat', ThreatSchema), - AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), - Event: mongoose.model('Event', EventSchema), - ...auxSchemas, - ...telemetrySchemas, -}; - - +// proxy/models/Schemas.js +// MongoDB schemas shared between the proxy server (write) and backend (read). +// Each document is tagged with agent_uuid + site_uuid for tenant isolation. +// +// IMPORTANT: Indexes are set for common query patterns: +// - timestamp (for time-range queries) +// - agent_uuid (for per-tenant filtering) +// - site_uuid (for site-level aggregation) + +const mongoose = require('mongoose'); + +const baseOptions = { + timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } +}; + +// ─── Bandwidth Summary (per agent, per collection cycle) ─────────────────────── +const SummarySchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, // null = global/all agents + site_uuid: { type: String, index: true }, + bandwidth_down: Number, + bandwidth_up: Number, + active_flows: Number, + download_speed: Number, + upload_speed: Number, + total_devices: Number, + total_threats: Number, + packet_drops: Number, + peak_flow_rate: Number, + cpu_usage: Number, + memory_usage: Number, + queue_depth: Number, +}, baseOptions); + +// ─── Top Applications (per agent) ───────────────────────────────────────────── +const AppStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + app_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── Protocol Statistics (per agent) ────────────────────────────────────────── +const ProtocolStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + protocol_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── Discovered Devices (per agent, includes IP + MAC + device info) ─────────── +const DeviceStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + ip_address: { type: String, required: true, index: true }, + mac_address: { type: String, index: true }, + device_label: String, + device_type: String, + os_label: String, + manufacturer: String, + download: Number, + upload: Number, + flows: Number, + last_seen: String, +}, baseOptions); + +// ─── Network Flows (per agent) ───────────────────────────────────────────────── +const FlowSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + flow_id: String, + src_ip: { type: String, index: true }, + 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, + app_label: String, + domain: String, + download: Number, + upload: Number, + first_seen: String, + last_seen: String, +}, baseOptions); + +// ─── Cyber Threats (per agent) ───────────────────────────────────────────────── +const ThreatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + threat_type: String, + severity: String, + src_ip: String, + dst_ip: String, + dst_port: Number, + protocol: String, + description: String, + event_at: String, + flow_id: { type: String, index: true }, +}, baseOptions); + +// ─── App Categories (per agent) ─────────────────────────────────────────────── +const AppCategoryStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + category_label: { type: String, required: true }, + download: Number, + upload: Number, + flows: Number, +}, baseOptions); + +// ─── System Events (per agent) ───────────────────────────────────────────────── +const EventSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + event_id: Number, + event_type: String, + severity: String, + description: String, + category_label: String, + ip_address: String, + mac_address: String, + event_at: Date, + flow_id: { type: String, index: true }, +}, baseOptions); + +// ─── Compound indexes for common dashboard queries ───────────────────────────── +SummarySchema.index({ agent_uuid: 1, timestamp: -1 }); +AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 }); +DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); +FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); +FlowSchema.index({ agent_uuid: 1, flow_id: 1 }); +FlowSchema.index({ site_uuid: 1, timestamp: -1 }); +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 }); +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 +// Allows showing "YouTube 134GB" in Device Detail modal per specific IP +const DeviceAppStatSchema = new mongoose.Schema({ + timestamp: { type: Date, required: true, index: true, expires: '7d' }, + agent_uuid: { type: String, index: true }, + site_uuid: { type: String, index: true }, + ip_address: { type: String, required: true, index: true }, + app_label: { type: String, required: true }, + app_id: Number, + download: { type: Number, default: 0 }, + upload: { type: Number, default: 0 }, + flows: { type: Number, default: 0 }, + last_seen: String, +}, baseOptions); +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 }); + +const telemetrySchemas = require('./SchemasTelemetry'); +const auxSchemas = require('./SchemasAux'); + +// ─── Agent Registry (all agents registered in Netify, regardless of activity) ── +// Upserted every collector cycle. Source of truth for the agents list page. +const AgentRegistrySchema = new mongoose.Schema({ + uuid: { type: String, required: true, unique: true, index: true }, + serial: { type: String }, + label: { type: String }, + site_uuid: { type: String, index: true }, + provisioned: { type: Boolean, default: false }, + activated: { type: Boolean, default: false }, + last_seen_at: { type: mongoose.Schema.Types.Mixed }, + netify_id: { type: Number }, +}, { ...baseOptions, collection: 'agent_registry' }); + +module.exports = { + Summary: mongoose.model('Summary', SummarySchema), + AppStat: mongoose.model('AppStat', AppStatSchema), + ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema), + DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), + DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema), + Flow: mongoose.model('Flow', FlowSchema), + Threat: mongoose.model('Threat', ThreatSchema), + AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), + Event: mongoose.model('Event', EventSchema), + AgentRegistry: mongoose.model('AgentRegistry', AgentRegistrySchema), + ...auxSchemas, + ...telemetrySchemas, +}; + + diff --git a/proxy/netifyAgentFetcher.js b/proxy/netifyAgentFetcher.js new file mode 100644 index 0000000..2c51cf3 --- /dev/null +++ b/proxy/netifyAgentFetcher.js @@ -0,0 +1,89 @@ +// proxy/netifyAgentFetcher.js +// ───────────────────────────────────────────────────────────────────────────── +// Fetches active agents from Netify Informatics API. +// Uses a two-strategy approach to handle endpoints that may timeout (Source 2). +// ───────────────────────────────────────────────────────────────────────────── + +const { netifyFetch, agentMap } = require('./netifyClientCore'); + +// Strategy 1 timeout: 15s (agent/download can be slow on small deployments) +const PRIMARY_TIMEOUT_MS = 15000; + +async function fetchAgents(siteUuid = null) { + // Strategy 1: Use /data/stats/top/agent/download (standard Netify endpoint) + // filter_interval reduced to 31 days to lessen query load vs. old 365-day value. + const primaryPromise = (async () => { + try { + const data = await netifyFetch('/data/stats/top/agent/download', { + filter_interval: 44640, // 31 days + settings_limit: 1000000, + }, null, siteUuid); + if (data && Array.isArray(data) && data.length > 0) return data; + return null; + } catch (e) { + return null; + } + })(); + + const timeoutPromise = new Promise(resolve => + setTimeout(() => resolve(null), PRIMARY_TIMEOUT_MS) + ); + + const primaryData = await Promise.race([primaryPromise, timeoutPromise]); + + if (primaryData && Array.isArray(primaryData) && primaryData.length > 0) { + const list = primaryData.map(r => ({ + id: r.agent?.id, + uuid: r.agent?.uuid || r.agent?.serial, + serial: r.agent?.serial, + label: r.agent?.label || r.agent?.serial, + provisioned: true, + activated: true, + last_seen_at: r.agent?.last_seen_at ?? null, + })).filter(a => a.uuid); + + // Populate agentMap (uuid → id) for downstream filter_agents usage + for (const a of list) { + if (a.uuid && a.id) agentMap[a.uuid] = a.id; + } + return list; + } + + // Strategy 2: Fallback — discover agents from /data/flows + // Useful for Source 2 where /data/stats/top/agent/download consistently times out. + console.log('[fetchAgents] Primary endpoint timeout/empty. Using flows-based agent discovery...'); + try { + const flowData = await netifyFetch('/data/flows', { + settings_limit: 100, + }, null, siteUuid); + + if (!flowData || !Array.isArray(flowData)) return []; + + // Extract unique agent_uuids from flow records + const seen = new Set(); + const agentList = []; + for (const flow of flowData) { + const uuid = flow.agent_uuid; + if (uuid && !seen.has(uuid)) { + seen.add(uuid); + agentList.push({ + id: null, + uuid: uuid, + serial: uuid, + label: uuid, + provisioned: true, + activated: true, + last_seen_at: flow.last_seen_at ?? null, + }); + } + } + + console.log(`[fetchAgents] Fallback discovered ${agentList.length} agent(s) from flows.`); + return agentList; + } catch (e) { + console.error('[fetchAgents] Fallback also failed:', e.message); + return []; + } +} + +module.exports = { fetchAgents }; diff --git a/proxy/netifyClient.js b/proxy/netifyClient.js index 748d7aa..535dd03 100644 --- a/proxy/netifyClient.js +++ b/proxy/netifyClient.js @@ -3,157 +3,11 @@ // DPI API wrapper for the BackOne Proxy Server targeting original Netify API. // ───────────────────────────────────────────────────────────────────────────── -const { netifyFetch, BASE_URL, agentMap } = require('./netifyClientCore'); +const { netifyFetch, BASE_URL } = require('./netifyClientCore'); const telemetry = require('./netifyTelemetry'); +const stats = require('./netifyClientStats'); -const PORT_SERVICE_MAP = { - 80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt', - 53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS', - 25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP', - 22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC', - 21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control', - 3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB', - 1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T', - 67: 'DHCP', 68: 'DHCP Client', 123: 'NTP', - 6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent', - 9993: 'ZeroTier VPN', -}; - -async function fetchAgents(siteUuid = null) { - 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); - - 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), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/local_ip/flow_count', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), - ]); - const bandwidth_down = dlData?.reduce((s, r) => s + (r.download ?? 0), 0) ?? 0; - const bandwidth_up = ulData?.reduce((s, r) => s + (r.upload ?? 0), 0) ?? 0; - const total_devices = dlData?.length ?? 0; - const active_flows = flowsData?.reduce((s, r) => s + (r.flows ?? r.flow_count ?? 0), 0) ?? 0; - return { bandwidth_down, bandwidth_up, total_devices, active_flows, total_threats: 0 }; -} - -async function fetchTopApps(interval = 1440, limit = 200, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return null; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const id = r.application?.id; - if (id) ulMap[id] = r.upload ?? 0; - } - } - return dlData.map(r => ({ - application: { - id: r.application?.id ?? null, - label: r.application?.label ?? 'Unknown', - tag: r.application?.tag ?? null, - }, - download: r.download ?? 0, - upload: ulMap[r.application?.id] ?? 0, - flows: r.flows ?? 0, - })); -} - -async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return null; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const ip = r.local_ip?.address ?? String(r.local_ip); - ulMap[ip] = r.upload ?? 0; - } - } - return dlData.map(r => { - const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); - return { - ip_address: ip, - mac_address: r.local_mac ?? null, - device_label: r.device_label ?? ip, - device_type: r.device_type ?? null, - os_label: r.os_label ?? null, - manufacturer: r.manufacturer ?? null, - download: r.download ?? 0, - upload: ulMap[ip] ?? 0, - flows: r.flows ?? 0, - last_seen: r.last_seen_at?.date ?? null, - }; - }).filter(d => d.ip_address); -} - -async function fetchDeviceApps(ipAddress, interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - const ipFilter = JSON.stringify([ipAddress]); - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), - ]); - if (!dlData || !Array.isArray(dlData)) return []; - const ulMap = {}; - if (ulData && Array.isArray(ulData)) { - for (const r of ulData) { - const id = r.application?.id; - if (id) ulMap[id] = r.upload ?? 0; - } - } - return dlData.map(r => ({ - app_label: r.application?.label ?? 'Unknown', - app_id: r.application?.id ?? null, - download: r.download ?? 0, - upload: ulMap[r.application?.id] ?? 0, - flows: r.flows ?? 0, - })).filter(a => a.download > 0 || a.upload > 0); -} - -async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { +async function fetchFlows(limit = 1000000, agentUuid = null, siteUuid = null) { const [raw, sniRaw] = await Promise.all([ netifyFetch('/data/flows', { settings_limit: limit }, agentUuid, siteUuid), netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid), @@ -163,12 +17,14 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { if (sniRaw && Array.isArray(sniRaw)) { for (const r of sniRaw) { const sni = typeof r.tls_sni === 'object' ? r.tls_sni?.label : r.tls_sni; - if (sni && typeof sni === 'string' && sni.trim() !== '') sniList.push(sni.replace(/^\*\./, '').trim()); + if (sni && typeof sni === 'string' && sni.trim() !== '') { + sniList.push(sni.replace(/^\*\./, '').trim()); + } } } return raw.map(r => { const port = r.remote_port ?? null; - const portService = port ? (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 domain = r.tls_sni || r.dns_hostname || r.hostname || null; return { @@ -188,84 +44,17 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { }).filter(f => f.src_ip); } -async function fetchCyberThreats(agentUuid = null, siteUuid = null) { - const ipRepData = await netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid); - if (!ipRepData || !Array.isArray(ipRepData)) return []; - const SUSPICIOUS_PORTS = new Set([23, 4444, 1337, 6667, 31337, 12345, 54321, 4899, 5554, 9999]); - const threats = []; - for (const r of ipRepData) { - const ip = r.remote_ip?.address ?? null; - const port = r.remote_port ?? 0; - if (ip && SUSPICIOUS_PORTS.has(port)) { - threats.push({ - threat_type: `Suspicious Port ${port}`, - severity: 'High', - src_ip: null, - dst_ip: ip, - dst_port: port, - protocol: r.ip_protocol?.label ?? null, - description: `Suspicious outbound connection to ${ip}:${port}`, - event_at: new Date().toISOString(), - }); - } - } - return threats; -} - -async function fetchEvents(limit = 100, agentUuid = null, siteUuid = null) { - const raw = await netifyFetch('/event/events', { settings_limit: limit }, agentUuid, siteUuid); - if (!raw || !Array.isArray(raw)) return []; - return raw.map(r => { - let msg = r.label || ''; - if (r.description) { - try { - const descObj = JSON.parse(r.description); - msg = descObj.default || r.label || ''; - if (descObj.tags) { - for (const k in descObj.tags) { - const tagVal = descObj.tags[k]; - const val = Array.isArray(tagVal) ? (tagVal[0] === 'Unknown' && tagVal[1] ? tagVal[1] : tagVal[0]) : tagVal; - msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val); - } - } - } catch (e) { - msg = r.description; - } - } - let sevLabel = 'Info'; - if (r.severity >= 30) sevLabel = 'Critical'; - else if (r.severity >= 20) sevLabel = 'High'; - else if (r.severity >= 10) sevLabel = 'Warning'; - let srcIp = null; - if (r.description) { - try { - const descObj = JSON.parse(r.description); - srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null; - } catch {} - } - return { - event_id: r.id || null, - event_type: r.basename || 'unknown', - severity: sevLabel, - description: msg, - category_label: r.category?.label || 'Intelligence', - ip_address: srcIp, - mac_address: r.additional?.device?.mac?.address || null, - event_at: r.created_at?.date ? new Date(r.created_at.date) : new Date() - }; - }); -} - module.exports = { - fetchAgents, - fetchBandwidthSummary, - fetchTopApps, - fetchDiscoveredDevices, - fetchDeviceApps, fetchFlows, - fetchCyberThreats, - fetchEvents, + fetchAgents: stats.fetchAgents, + fetchBandwidthSummary: stats.fetchBandwidthSummary, + fetchTopApps: stats.fetchTopApps, + fetchDiscoveredDevices: stats.fetchDiscoveredDevices, + fetchDeviceApps: stats.fetchDeviceApps, + fetchCyberThreats: stats.fetchCyberThreats, + fetchEvents: stats.fetchEvents, + syncApplicationDictionary: stats.syncApplicationDictionary, BASE_URL, - PORT_SERVICE_MAP, + PORT_SERVICE_MAP: stats.PORT_SERVICE_MAP, ...telemetry, }; diff --git a/proxy/netifyClientStats.js b/proxy/netifyClientStats.js new file mode 100644 index 0000000..e92366b --- /dev/null +++ b/proxy/netifyClientStats.js @@ -0,0 +1,219 @@ +// proxy/netifyClientStats.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary fetchers split from netifyClient.js to satisfy the 256-line limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { netifyFetch } = require('./netifyClientCore'); +const { fetchAgents } = require('./netifyAgentFetcher'); + +const PORT_SERVICE_MAP = { + 80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt', + 53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS', + 25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP', + 22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC', + 21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control', + 3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB', + 1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T', + 67: 'DHCP', 68: 'DHCP Client', 123: 'NTP', + 6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent', + 9993: 'ZeroTier VPN', +}; + +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), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/local_ip/flow_count', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), + ]); + const bandwidth_down = dlData?.reduce((s, r) => s + (r.download ?? 0), 0) ?? 0; + const bandwidth_up = ulData?.reduce((s, r) => s + (r.upload ?? 0), 0) ?? 0; + const total_devices = dlData?.length ?? 0; + const active_flows = flowsData?.reduce((s, r) => s + (r.flows ?? r.flow_count ?? 0), 0) ?? 0; + return { bandwidth_down, bandwidth_up, total_devices, active_flows, total_threats: 0 }; +} + +async function fetchTopApps(interval = 1440, limit = 200, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const id = r.application?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + return dlData.map(r => ({ + application: { id: r.application?.id ?? null, label: r.application?.label ?? 'Unknown', tag: r.application?.tag ?? null }, + download: r.download ?? 0, upload: ulMap[r.application?.id] ?? 0, flows: r.flows ?? 0, + })); +} + +async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + ulMap[ip] = r.upload ?? 0; + } + } + return dlData.map(r => { + const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); + return { + ip_address: ip, mac_address: r.local_mac ?? null, device_label: r.device_label ?? ip, device_type: r.device_type ?? null, + os_label: r.os_label ?? null, manufacturer: r.manufacturer ?? null, download: r.download ?? 0, upload: ulMap[ip] ?? 0, + flows: r.flows ?? 0, last_seen: r.last_seen_at?.date ?? null, + }; + }).filter(d => d.ip_address); +} + +async function fetchDeviceApps(ipAddress, interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + const ipFilter = JSON.stringify([ipAddress]); + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), + ]); + if (!dlData || !Array.isArray(dlData)) return []; + const ulMap = {}; + if (ulData && Array.isArray(ulData)) { + for (const r of ulData) { + const id = r.application?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + return dlData.map(r => ({ + app_label: r.application?.label ?? 'Unknown', + app_id: r.application?.id ?? null, + download: r.download ?? 0, + upload: ulMap[r.application?.id] ?? 0, + flows: r.flows ?? 0, + })).filter(a => a.download > 0 || a.upload > 0); +} + +async function fetchCyberThreats(agentUuid = null, siteUuid = null) { + const ipRepData = await netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid); + if (!ipRepData || !Array.isArray(ipRepData)) return []; + const SUSPICIOUS_PORTS = new Set([23, 4444, 1337, 6667, 31337, 12345, 54321, 4899, 5554, 9999]); + const threats = []; + for (const r of ipRepData) { + const ip = r.remote_ip?.address ?? null; + const port = r.remote_port ?? 0; + if (ip && SUSPICIOUS_PORTS.has(port)) { + threats.push({ + threat_type: `Suspicious Port ${port}`, + severity: 'High', + src_ip: null, + dst_ip: ip, + dst_port: port, + protocol: r.ip_protocol?.label ?? null, + description: `Suspicious outbound connection to ${ip}:${port}`, + event_at: new Date().toISOString(), + }); + } + } + return threats; +} + +async function fetchEvents(limit = 100, agentUuid = null, siteUuid = null) { + const raw = await netifyFetch('/event/events', { settings_limit: limit }, agentUuid, siteUuid); + if (!raw || !Array.isArray(raw)) return []; + return raw.map(r => { + let msg = r.label || ''; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + msg = descObj.default || r.label || ''; + if (descObj.tags) { + for (const k in descObj.tags) { + const tagVal = descObj.tags[k]; + const val = Array.isArray(tagVal) ? (tagVal[0] === 'Unknown' && tagVal[1] ? tagVal[1] : tagVal[0]) : tagVal; + msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val); + } + } + } catch (e) { + msg = r.description; + } + } + let sevLabel = 'Info'; + if (r.severity >= 30) sevLabel = 'Critical'; + else if (r.severity >= 20) sevLabel = 'High'; + else if (r.severity >= 10) sevLabel = 'Warning'; + let srcIp = null; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null; + } catch {} + } + return { + event_id: r.id || null, + event_type: r.basename || 'unknown', + severity: sevLabel, + description: msg, + category_label: r.category?.label || 'Intelligence', + ip_address: srcIp, + mac_address: r.additional?.device?.mac?.address || null, + event_at: r.created_at?.date ? new Date(r.created_at.date) : new Date() + }; + }); +} + +async function syncApplicationDictionary() { + const mongoose = require('mongoose'); + const { LookupApp } = require('./models/Schemas'); + + console.log('[Netify] Fetching application catalog...'); + const allApps = await netifyFetch('/lookup/applications', { settings_limit: 5000 }); + if (!allApps || !Array.isArray(allApps)) { + console.error('[Netify] Failed to fetch application dictionary.'); + return; + } + + console.log(`[Netify] Application catalog fetched successfully. Got ${allApps.length} apps.`); + if (allApps.length === 0) return; + + console.log(`[Netify] Syncing ${allApps.length} application definitions to MongoDB...`); + await LookupApp.deleteMany({}); + + const batchSize = 100; + for (let i = 0; i < allApps.length; i += batchSize) { + const batch = allApps.slice(i, i + batchSize); + await LookupApp.insertMany(batch.map(app => ({ + id: app.id, + name: app.name, + label: app.label, + tag: app.tag, + description: app.description, + full_name: app.full_name || app.application?.full_label || null, + favicon: app.favicon || app.application?.favicon || null, + icon: app.icon || app.application?.icon || null, + logo: app.logo || app.application?.logo || null, + application_category: { + id: app.application_category?.id, + name: app.application_category?.name, + label: app.application_category?.label, + tag: app.application_category?.tag + } + }))); + } + console.log('[Netify] ✓ Application dictionary sync completed.'); +} + +module.exports = { + fetchAgents, + fetchBandwidthSummary, + fetchTopApps, + fetchDiscoveredDevices, + fetchDeviceApps, + fetchCyberThreats, + fetchEvents, + syncApplicationDictionary, + PORT_SERVICE_MAP +}; diff --git a/proxy/netifyTelemetry.js b/proxy/netifyTelemetry.js index 6b5aaea..c72a625 100644 --- a/proxy/netifyTelemetry.js +++ b/proxy/netifyTelemetry.js @@ -1,234 +1,234 @@ -// proxy/netifyTelemetry.js -// ───────────────────────────────────────────────────────────────────────────── -// Supplementary Telemetry endpoints wrapper for BackOne Proxy Server -// Split from netifyClient.js to strictly respect the 256-line file size limit. -// ───────────────────────────────────────────────────────────────────────────── - -const { netifyFetch } = require('./netifyClientCore'); - -async function fetchTopAppCategories(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const key = r.application_category?.label ?? r.application_category; - if (key) ulMap[key] = r.upload ?? 0; - } - } - return dlData.map(r => { - const label = r.application_category?.label ?? String(r.application_category ?? 'Unknown'); - return { - category_label : label, - download : r.download ?? 0, - upload : ulMap[label] ?? 0, - flows : r.flows ?? 0, - }; - }); -} - -async function fetchTlsVersions(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const key = r.tls_version?.label ?? r.tls_version?.code ?? r.tls_version; - if (key) ulMap[key] = r.upload ?? 0; - } - } - return dlData.map(r => { - const label = r.tls_version?.label ?? r.tls_version?.code ?? String(r.tls_version ?? 'Unknown'); - return { - tls_version : label, - download : r.download ?? 0, - upload : ulMap[label] ?? 0, - flows : r.flows ?? 0, - }; - }); -} - -async function fetchTlsCiphers(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const key = r.tls_cipher?.label ?? r.tls_cipher?.code ?? r.tls_cipher; - if (key) ulMap[key] = r.upload ?? 0; - } - } - return dlData.map(r => { - const label = r.tls_cipher?.label ?? r.tls_cipher?.code ?? String(r.tls_cipher ?? 'Unknown'); - return { - tls_cipher : label, - download : r.download ?? 0, - upload : ulMap[label] ?? 0, - flows : r.flows ?? 0, - }; - }); -} - -async function fetchTlsSecurity(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const key = r.tls_security?.label ?? r.tls_security?.code ?? r.tls_security; - if (key) ulMap[key] = r.upload ?? 0; - } - } - return dlData.map(r => { - const label = r.tls_security?.label ?? r.tls_security?.code ?? String(r.tls_security ?? 'Unknown'); - return { - tls_security : label, - download : r.download ?? 0, - upload : ulMap[label] ?? 0, - flows : r.flows ?? 0, - }; - }); -} - -async function fetchTopCountries(interval = 1440, limit = 100, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const cc = r.country?.code; - if (cc) ulMap[cc] = r.upload ?? 0; - } - } - return dlData.map(r => { - return { - country_code: r.country?.code ?? 'Unknown', - country_name: r.country?.label ?? '', - download: r.download ?? 0, - upload: ulMap[r.country?.code] ?? 0, - flows: r.flows ?? 0, - }; - }).filter(r => r.country_code); -} - -async function fetchTopProperty(fieldName, interval, limit, agentUuid, siteUuid) { - const [dlData, ulData] = await Promise.all([ - netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return []; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const item = r[fieldName]; - const key = item?.hash ?? item?.name ?? item?.label ?? String(item ?? ''); - if (key) ulMap[key] = r.upload ?? 0; - } - } - return dlData.map(r => { - const item = r[fieldName]; - const key = item?.hash ?? item?.name ?? item?.label ?? String(item || 'Unknown'); - const label = item?.label ?? key; - return { - key, - label, - download: r.download ?? 0, - upload: ulMap[key] ?? ulMap[label] ?? 0, - flows: r.flows ?? 0, - }; - }); -} - -function mapProp(data, keyName) { - return data.map(d => ({ - [keyName]: d.key, - download: d.download, - upload: d.upload, - flows: d.flows, - })); -} - -async function fetchDhcpFingerprints(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('dhcp_class', interval, limit, agentUuid, siteUuid), 'fingerprint'); -} - -async function fetchHttpUserAgents(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('http_useragent', interval, limit, agentUuid, siteUuid), 'user_agent'); -} - -async function fetchBittorrentHashes(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - const data = await fetchTopProperty('bittorrent_info_hash', interval, limit, agentUuid, siteUuid); - return data.map(d => ({ - info_hash: d.key, - label: d.label, - download: d.download, - upload: d.upload, - flows: d.flows, - })); -} - -async function fetchSniHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('tls_sni', interval, limit, agentUuid, siteUuid), 'sni_hostname'); -} - -async function fetchSslServerCn(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('ssl_server_cn', interval, limit, agentUuid, siteUuid), 'ssl_server_cn'); -} - -async function fetchQuicHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('quic_hostname', interval, limit, agentUuid, siteUuid), 'quic_hostname'); -} - -async function fetchSshClients(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('ssh_client', interval, limit, agentUuid, siteUuid), 'ssh_client'); -} - -async function fetchSshServers(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('ssh_server', interval, limit, agentUuid, siteUuid), 'ssh_server'); -} - -async function fetchMdnsHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('mdns_hostname', interval, limit, agentUuid, siteUuid), 'mdns_hostname'); -} - -async function fetchTopProtocols(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('ip_protocol', interval, limit, agentUuid, siteUuid), 'protocol_label'); -} - -async function fetchSslSubjectAltNames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - return mapProp(await fetchTopProperty('ssl_subject_alt_name', interval, limit, agentUuid, siteUuid), 'alt_name'); -} - -module.exports = { - fetchTopAppCategories, - fetchTlsVersions, - fetchTlsCiphers, - fetchTlsSecurity, - fetchTopCountries, - fetchDhcpFingerprints, - fetchHttpUserAgents, - fetchBittorrentHashes, - fetchSniHostnames, - fetchSslServerCn, - fetchQuicHostnames, - fetchSshClients, - fetchSshServers, - fetchMdnsHostnames, - fetchTopProtocols, - fetchSslSubjectAltNames, -}; +// proxy/netifyTelemetry.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary Telemetry endpoints wrapper for BackOne Proxy Server +// Split from netifyClient.js to strictly respect the 256-line file size limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { netifyFetch } = require('./netifyClientCore'); + +async function fetchTopAppCategories(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const key = r.application_category?.label ?? r.application_category; + if (key) ulMap[key] = r.upload ?? 0; + } + } + return dlData.map(r => { + const label = r.application_category?.label ?? String(r.application_category ?? 'Unknown'); + return { + category_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + flows : r.flows ?? 0, + }; + }); +} + +async function fetchTlsVersions(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const key = r.tls_version?.label ?? r.tls_version?.code ?? r.tls_version; + if (key) ulMap[key] = r.upload ?? 0; + } + } + return dlData.map(r => { + const label = r.tls_version?.label ?? r.tls_version?.code ?? String(r.tls_version ?? 'Unknown'); + return { + tls_version : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + flows : r.flows ?? 0, + }; + }); +} + +async function fetchTlsCiphers(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const key = r.tls_cipher?.label ?? r.tls_cipher?.code ?? r.tls_cipher; + if (key) ulMap[key] = r.upload ?? 0; + } + } + return dlData.map(r => { + const label = r.tls_cipher?.label ?? r.tls_cipher?.code ?? String(r.tls_cipher ?? 'Unknown'); + return { + tls_cipher : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + flows : r.flows ?? 0, + }; + }); +} + +async function fetchTlsSecurity(interval = 1440, limit = 15, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const key = r.tls_security?.label ?? r.tls_security?.code ?? r.tls_security; + if (key) ulMap[key] = r.upload ?? 0; + } + } + return dlData.map(r => { + const label = r.tls_security?.label ?? r.tls_security?.code ?? String(r.tls_security ?? 'Unknown'); + return { + tls_security : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + flows : r.flows ?? 0, + }; + }); +} + +async function fetchTopCountries(interval = 1440, limit = 100, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const cc = r.country?.code; + if (cc) ulMap[cc] = r.upload ?? 0; + } + } + return dlData.map(r => { + return { + country_code: r.country?.code ?? 'Unknown', + country_name: r.country?.label ?? '', + download: r.download ?? 0, + upload: ulMap[r.country?.code] ?? 0, + flows: r.flows ?? 0, + }; + }).filter(r => r.country_code); +} + +async function fetchTopProperty(fieldName, interval, limit, agentUuid, siteUuid) { + const [dlData, ulData] = await Promise.all([ + netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return []; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const item = r[fieldName]; + const key = item?.hash ?? item?.name ?? item?.label ?? String(item ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + } + return dlData.map(r => { + const item = r[fieldName]; + const key = item?.hash ?? item?.name ?? item?.label ?? String(item || 'Unknown'); + const label = item?.label ?? key; + return { + key, + label, + download: r.download ?? 0, + upload: ulMap[key] ?? ulMap[label] ?? 0, + flows: r.flows ?? 0, + }; + }); +} + +function mapProp(data, keyName) { + return data.map(d => ({ + [keyName]: d.key, + download: d.download, + upload: d.upload, + flows: d.flows, + })); +} + +async function fetchDhcpFingerprints(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('dhcp_class', interval, limit, agentUuid, siteUuid), 'fingerprint'); +} + +async function fetchHttpUserAgents(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('http_useragent', interval, limit, agentUuid, siteUuid), 'user_agent'); +} + +async function fetchBittorrentHashes(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + const data = await fetchTopProperty('bittorrent_info_hash', interval, limit, agentUuid, siteUuid); + return data.map(d => ({ + info_hash: d.key, + label: d.label, + download: d.download, + upload: d.upload, + flows: d.flows, + })); +} + +async function fetchSniHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('tls_sni', interval, limit, agentUuid, siteUuid), 'sni_hostname'); +} + +async function fetchSslServerCn(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('ssl_server_cn', interval, limit, agentUuid, siteUuid), 'ssl_server_cn'); +} + +async function fetchQuicHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('quic_hostname', interval, limit, agentUuid, siteUuid), 'quic_hostname'); +} + +async function fetchSshClients(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('ssh_client', interval, limit, agentUuid, siteUuid), 'ssh_client'); +} + +async function fetchSshServers(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('ssh_server', interval, limit, agentUuid, siteUuid), 'ssh_server'); +} + +async function fetchMdnsHostnames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('mdns_hostname', interval, limit, agentUuid, siteUuid), 'mdns_hostname'); +} + +async function fetchTopProtocols(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('ip_protocol', interval, limit, agentUuid, siteUuid), 'protocol_label'); +} + +async function fetchSslSubjectAltNames(interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + return mapProp(await fetchTopProperty('ssl_subject_alt_name', interval, limit, agentUuid, siteUuid), 'alt_name'); +} + +module.exports = { + fetchTopAppCategories, + fetchTlsVersions, + fetchTlsCiphers, + fetchTlsSecurity, + fetchTopCountries, + fetchDhcpFingerprints, + fetchHttpUserAgents, + fetchBittorrentHashes, + fetchSniHostnames, + fetchSslServerCn, + fetchQuicHostnames, + fetchSshClients, + fetchSshServers, + fetchMdnsHostnames, + fetchTopProtocols, + fetchSslSubjectAltNames, +}; diff --git a/proxy/package-lock.json b/proxy/package-lock.json index b7f818f..3d0bda1 100644 --- a/proxy/package-lock.json +++ b/proxy/package-lock.json @@ -1,1312 +1,1312 @@ -{ - "name": "backone-proxy", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backone-proxy", - "version": "1.0.0", - "dependencies": { - "axios": "^1.6.2", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "mongoose": "^8.0.3", - "node-cron": "^3.0.3" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", - "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.1.tgz", - "integrity": "sha512-UpHBA0l5kHyKJQFjmBaFYQFo5sgz1DK0TRqDkOyBLYbqiIbKKhIvBpHWBXqeo0rgW4kGI1UhhAw+kTQZoj1BdA==", - "license": "MIT", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/mquery/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mquery/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", - "license": "ISC", - "dependencies": { - "uuid": "8.3.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - } - } -} +{ + "name": "backone-proxy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backone-proxy", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mongoose": "^8.0.3", + "node-cron": "^3.0.3" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", + "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.1.tgz", + "integrity": "sha512-UpHBA0l5kHyKJQFjmBaFYQFo5sgz1DK0TRqDkOyBLYbqiIbKKhIvBpHWBXqeo0rgW4kGI1UhhAw+kTQZoj1BdA==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/proxy/package.json b/proxy/package.json index 99f1e35..28d8069 100644 --- a/proxy/package.json +++ b/proxy/package.json @@ -1,19 +1,19 @@ -{ - "name": "backone-proxy", - "version": "1.0.0", - "description": "BackOne DPI Proxy Server - Fetches from DPI API, filters per agent_uuid, stores to MongoDB", - "main": "index.js", - "scripts": { - "start": "node index.js", - "start:bun": "bun run index.js", - "dev": "nodemon index.js" - }, - "dependencies": { - "axios": "^1.6.2", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "mongoose": "^8.0.3", - "node-cron": "^3.0.3" - } -} +{ + "name": "backone-proxy", + "version": "1.0.0", + "description": "BackOne DPI Proxy Server - Fetches from DPI API, filters per agent_uuid, stores to MongoDB", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:bun": "bun run index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "axios": "^1.6.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mongoose": "^8.0.3", + "node-cron": "^3.0.3" + } +} diff --git a/proxy/scheduler.js b/proxy/scheduler.js index e1f14b1..3c31c67 100644 --- a/proxy/scheduler.js +++ b/proxy/scheduler.js @@ -1,124 +1,129 @@ -// proxy/scheduler.js -// Cron scheduler for automatic data collection from DPI API -// Runs every 5 minutes, collecting data for all agents or a specific agent. - -const cron = require('node-cron'); -const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector'); - -// Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents -const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all'; -const SPECIFIC_AGENT = process.env.PROXY_AGENT_UUID || null; -const SPECIFIC_AGENTS = (process.env.PROXY_AGENT_UUIDS || '').split(',').map(s => s.trim()).filter(Boolean); -const AGENT_DELAY_MS = parseInt(process.env.PROXY_AGENT_DELAY_MS || '5000'); -const CRON_SCHEDULE = process.env.PROXY_CRON_SCHEDULE || '*/5 * * * *'; - -// Capacity logging is an expensive full-scan aggregation. Run it at most once per -// interval (default 24h) instead of every collection cycle to reduce CPU/DB load. -const CAPACITY_LOG_INTERVAL_MS = parseInt(process.env.PROXY_CAPACITY_LOG_INTERVAL_MS || String(24 * 60 * 60 * 1000)); - -let isRunning = false; -let lastRunAt = null; -let lastRunResult = null; -let runCount = 0; -let lastCapacityLogAt = 0; - -/** - * Execute one collection cycle (called by cron and manual trigger). - * Prevents concurrent runs with isRunning guard. - */ -async function runCollection() { - if (isRunning) { - console.log('[Scheduler] Skipping - previous run still in progress'); - return { skipped: true, reason: 'already_running' }; - } - - isRunning = true; - lastRunAt = new Date(); - runCount++; - - try { - let result; - if (COLLECT_MODE === 'agent' && SPECIFIC_AGENT) { - console.log(`[Scheduler] Run #${runCount} - Mode: SPECIFIC AGENT (${SPECIFIC_AGENT})`); - result = await collectSpecificAgent(SPECIFIC_AGENT); - } else if (COLLECT_MODE === 'agents' && SPECIFIC_AGENTS.length > 0) { - console.log(`[Scheduler] Run #${runCount} - Mode: SPECIFIC AGENTS (${SPECIFIC_AGENTS.join(', ')})`); - result = await collectSpecificAgents(SPECIFIC_AGENTS, AGENT_DELAY_MS); - } else { - console.log(`[Scheduler] Run #${runCount} - Mode: ALL AGENTS`); - result = await collectAllAgents(); - } - lastRunResult = { ...result, run_count: runCount }; - - // Log MongoDB database capacity usage (expensive full-scan aggregation). - // Only run periodically (default: every 24h) to avoid high CPU/DB load each cycle. - const now = Date.now(); - if (now - lastCapacityLogAt >= CAPACITY_LOG_INTERVAL_MS) { - lastCapacityLogAt = now; - const { logCapacityStats } = require('./db/capacityTracker'); - await logCapacityStats(`[PROXY] [MongoDB] Capacity Used after Run #${runCount}:`); - } - - return lastRunResult; - } catch (err) { - console.error('[Scheduler] Unhandled error during collection:', err.message); - lastRunResult = { success: false, error: err.message, run_count: runCount }; - return lastRunResult; - } finally { - isRunning = false; - } -} - -/** - * Start the scheduler (cron job + immediate first run). - */ -function startScheduler() { - console.log(`[Scheduler] Starting proxy data collector`); - const modeLabel = COLLECT_MODE === 'agent' - ? `SPECIFIC AGENT (${SPECIFIC_AGENT})` - : COLLECT_MODE === 'agents' - ? `SPECIFIC AGENTS (${SPECIFIC_AGENTS.join(', ')})` - : 'ALL AGENTS'; - console.log(`[Scheduler] Mode : ${modeLabel}`); - console.log(`[Scheduler] Schedule : ${CRON_SCHEDULE} (every 5 minutes by default)`); - - // Validate cron expression - if (!cron.validate(CRON_SCHEDULE)) { - console.error(`[Scheduler] Invalid cron expression: "${CRON_SCHEDULE}". Using default.`); - } - - // Start recurring cron job - cron.schedule(CRON_SCHEDULE, () => { - runCollection().catch(err => console.error('[Scheduler] Cron error:', err.message)); - }); - - console.log('[Scheduler] Cron job registered. Starting initial collection...'); - - // Initial run immediately on startup (async, do not block server start) - setTimeout(async () => { - // 1. Sync dictionary first - // await netifyClient.syncApplicationDictionary(); - - // 2. Start normal telemetry collection - runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message)); - }, 2000); -} - -/** - * Get current scheduler status (for REST API endpoint). - */ -function getStatus() { - return { - is_running: isRunning, - run_count: runCount, - last_run_at: lastRunAt?.toISOString() ?? null, - collect_mode: COLLECT_MODE, - agent_uuid: SPECIFIC_AGENT, - agent_uuids: COLLECT_MODE === 'agents' ? SPECIFIC_AGENTS : [], - agent_delay_ms: AGENT_DELAY_MS, - cron_schedule: CRON_SCHEDULE, - last_result: lastRunResult, - }; -} - -module.exports = { startScheduler, runCollection, getStatus }; +// proxy/scheduler.js +// Cron scheduler for automatic data collection from DPI API +// Runs every 5 minutes, collecting data for all agents or a specific agent. + +const cron = require('node-cron'); +const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector'); +const { syncApplicationDictionary } = require('./netifyClient'); + +// Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents +const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all'; +const SPECIFIC_AGENT = process.env.PROXY_AGENT_UUID || null; +const SPECIFIC_AGENTS = (process.env.PROXY_AGENT_UUIDS || '').split(',').map(s => s.trim()).filter(Boolean); +const AGENT_DELAY_MS = parseInt(process.env.PROXY_AGENT_DELAY_MS || '5000'); +const CRON_SCHEDULE = process.env.PROXY_CRON_SCHEDULE || '*/5 * * * *'; + +// Capacity logging is an expensive full-scan aggregation. Run it at most once per +// interval (default 24h) instead of every collection cycle to reduce CPU/DB load. +const CAPACITY_LOG_INTERVAL_MS = parseInt(process.env.PROXY_CAPACITY_LOG_INTERVAL_MS || String(24 * 60 * 60 * 1000)); + +let isRunning = false; +let lastRunAt = null; +let lastRunResult = null; +let runCount = 0; +let lastCapacityLogAt = 0; + +/** + * Execute one collection cycle (called by cron and manual trigger). + * Prevents concurrent runs with isRunning guard. + */ +async function runCollection() { + if (isRunning) { + console.log('[Scheduler] Skipping - previous run still in progress'); + return { skipped: true, reason: 'already_running' }; + } + + isRunning = true; + lastRunAt = new Date(); + runCount++; + + try { + let result; + if (COLLECT_MODE === 'agent' && SPECIFIC_AGENT) { + console.log(`[Scheduler] Run #${runCount} - Mode: SPECIFIC AGENT (${SPECIFIC_AGENT})`); + result = await collectSpecificAgent(SPECIFIC_AGENT); + } else if (COLLECT_MODE === 'agents' && SPECIFIC_AGENTS.length > 0) { + console.log(`[Scheduler] Run #${runCount} - Mode: SPECIFIC AGENTS (${SPECIFIC_AGENTS.join(', ')})`); + result = await collectSpecificAgents(SPECIFIC_AGENTS, AGENT_DELAY_MS); + } else { + console.log(`[Scheduler] Run #${runCount} - Mode: ALL AGENTS`); + result = await collectAllAgents(); + } + lastRunResult = { ...result, run_count: runCount }; + + // Log MongoDB database capacity usage (expensive full-scan aggregation). + // Only run periodically (default: every 24h) to avoid high CPU/DB load each cycle. + const now = Date.now(); + if (now - lastCapacityLogAt >= CAPACITY_LOG_INTERVAL_MS) { + lastCapacityLogAt = now; + const { logCapacityStats } = require('./db/capacityTracker'); + await logCapacityStats(`[PROXY] [MongoDB] Capacity Used after Run #${runCount}:`); + } + + return lastRunResult; + } catch (err) { + console.error('[Scheduler] Unhandled error during collection:', err.message); + lastRunResult = { success: false, error: err.message, run_count: runCount }; + return lastRunResult; + } finally { + isRunning = false; + } +} + +/** + * Start the scheduler (cron job + immediate first run). + */ +function startScheduler() { + console.log(`[Scheduler] Starting proxy data collector`); + const modeLabel = COLLECT_MODE === 'agent' + ? `SPECIFIC AGENT (${SPECIFIC_AGENT})` + : COLLECT_MODE === 'agents' + ? `SPECIFIC AGENTS (${SPECIFIC_AGENTS.join(', ')})` + : 'ALL AGENTS'; + console.log(`[Scheduler] Mode : ${modeLabel}`); + console.log(`[Scheduler] Schedule : ${CRON_SCHEDULE} (every 5 minutes by default)`); + + // Validate cron expression + if (!cron.validate(CRON_SCHEDULE)) { + console.error(`[Scheduler] Invalid cron expression: "${CRON_SCHEDULE}". Using default.`); + } + + // Start recurring cron job + cron.schedule(CRON_SCHEDULE, () => { + runCollection().catch(err => console.error('[Scheduler] Cron error:', err.message)); + }); + + console.log('[Scheduler] Cron job registered. Starting initial collection...'); + + // Initial run immediately on startup (async, do not block server start) + setTimeout(async () => { + // 1. Sync dictionary first + try { + await syncApplicationDictionary(); + } catch (err) { + console.error('[Scheduler] Error syncing application dictionary:', err.message); + } + + // 2. Start normal telemetry collection + runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message)); + }, 2000); +} + +/** + * Get current scheduler status (for REST API endpoint). + */ +function getStatus() { + return { + is_running: isRunning, + run_count: runCount, + last_run_at: lastRunAt?.toISOString() ?? null, + collect_mode: COLLECT_MODE, + agent_uuid: SPECIFIC_AGENT, + agent_uuids: COLLECT_MODE === 'agents' ? SPECIFIC_AGENTS : [], + agent_delay_ms: AGENT_DELAY_MS, + cron_schedule: CRON_SCHEDULE, + last_result: lastRunResult, + }; +} + +module.exports = { startScheduler, runCollection, getStatus }; diff --git a/proxy/test_api.js b/proxy/test_api.js new file mode 100644 index 0000000..fc1db38 --- /dev/null +++ b/proxy/test_api.js @@ -0,0 +1,71 @@ +// test_api.js - Tests the actual metadata-detail API endpoint +// Run: node proxy/test_api.js +const http = require('http'); + +function request(path) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'localhost', + port: 3001, + path, + method: 'GET', + }; + const req = http.request(options, res => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(body) }); } + catch (e) { resolve({ status: res.statusCode, raw: body }); } + }); + }); + req.on('error', reject); + req.end(); + }); +} + +async function main() { + // Test 1: netbios_hostname for 10.6.10.44 + console.log('\n=== TEST 1: netbios_hostname=10.6.10.44 ==='); + try { + const r1 = await request('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44'); + console.log('Status:', r1.status); + if (r1.data) { + console.log('Count:', r1.data.count); + console.log('First 3 rows:', JSON.stringify(r1.data.data?.slice(0, 3), null, 2)); + } else { + console.log('Raw:', r1.raw?.slice(0, 500)); + } + } catch (e) { + console.log('ERROR (maybe backend is on different port):', e.message); + } + + // Test 2: Try port 3000 (Next.js API routes) + console.log('\n=== TEST 2: via Next.js port 3000 ==='); + try { + const r2 = await request('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44'); + const options2 = { hostname: 'localhost', port: 3000, path: '/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44', method: 'GET' }; + const r3 = await new Promise((resolve, reject) => { + const req = http.request(options2, res => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(body) }); } + catch (e) { resolve({ status: res.statusCode, raw: body?.slice(0, 500) }); } + }); + }); + req.on('error', reject); + req.end(); + }); + console.log('Port 3000 - Status:', r3.status); + if (r3.data) { + console.log('Count:', r3.data.count); + console.log('First 3 rows:', JSON.stringify(r3.data.data?.slice(0, 3), null, 2)); + } else { + console.log('Raw:', r3.raw); + } + } catch (e) { + console.log('Port 3000 ERROR:', e.message); + } +} + +main().catch(console.error); diff --git a/proxy/test_db.js b/proxy/test_db.js new file mode 100644 index 0000000..b2a4568 --- /dev/null +++ b/proxy/test_db.js @@ -0,0 +1 @@ +const mongoose = require('mongoose'); require('dotenv').config({ path: '../.env.local' }); const { LookupApp } = require('./models/Schemas'); async function test() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'); const sample = await LookupApp.findOne({ tag: /youtube/i }).lean(); console.log(JSON.stringify(sample, null, 2)); await mongoose.disconnect(); } test().catch(console.error); diff --git a/proxy/test_metadata_detail.js b/proxy/test_metadata_detail.js new file mode 100644 index 0000000..5a5462a --- /dev/null +++ b/proxy/test_metadata_detail.js @@ -0,0 +1,89 @@ +// test_metadata_detail.js - Test after restart +// Run: node proxy/test_metadata_detail.js +const http = require('http'); + +function post(path, body) { + return new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const options = { + hostname: 'localhost', port: 3001, path, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, + }; + const req = http.request(options, res => { + let buf = ''; + res.on('data', c => buf += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, headers: res.headers, data: JSON.parse(buf) }); } + catch { resolve({ status: res.statusCode, raw: buf }); } + }); + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +} + +function get(path, cookie) { + return new Promise((resolve, reject) => { + const options = { + hostname: 'localhost', port: 3001, path, method: 'GET', + headers: cookie ? { Cookie: cookie } : {}, + }; + const req = http.request(options, res => { + let buf = ''; + res.on('data', c => buf += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); } + catch { resolve({ status: res.statusCode, raw: buf?.slice(0, 300) }); } + }); + }); + req.on('error', reject); + req.end(); + }); +} + +async function main() { + // Step 1: Login to get cookie + console.log('=== Step 1: Login ==='); + const login = await post('/api/auth/login', { username: 'admin', password: 'admin123' }); + console.log('Login status:', login.status); + + const setCookie = login.headers?.['set-cookie']; + let cookie = ''; + if (setCookie) { + cookie = setCookie.map(c => c.split(';')[0]).join('; '); + console.log('Cookie obtained:', cookie.slice(0, 60) + '...'); + } else { + console.log('No cookie received. Auth response:', JSON.stringify(login.data)); + // Try with a known admin credential + } + + // Step 2: Test health + console.log('\n=== Step 2: Health Check ==='); + const health = await get('/api/health', cookie); + console.log('Health:', health.status, JSON.stringify(health.data)); + + // Step 3: Test metadata-detail netbios_hostname + console.log('\n=== Step 3: metadata-detail netbios_hostname=10.6.10.44 ==='); + const r = await get('/api/dashboard/metadata-detail?type=netbios_hostname&value=10.6.10.44', cookie); + console.log('Status:', r.status); + if (r.data) { + console.log('Count (should be 1):', r.data.count); + console.log('Data:', JSON.stringify(r.data.data, null, 2)); + } else { + console.log('Raw:', r.raw); + } + + // Step 4: Verify DB has 1 doc for 10.6.10.44 + console.log('\n=== Step 4: Direct DB verification ==='); + const mongoose = require('mongoose'); + const path = require('path'); + require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'); + const db = mongoose.connection.db; + const cnt = await db.collection('devicestats').countDocuments({ ip_address: '10.6.10.44' }); + console.log('DB count for 10.6.10.44:', cnt, '(expected: 1)'); + await mongoose.disconnect(); +} + +main().catch(console.error); diff --git a/proxy/test_sync_proxy.js b/proxy/test_sync_proxy.js new file mode 100644 index 0000000..cb76fca --- /dev/null +++ b/proxy/test_sync_proxy.js @@ -0,0 +1 @@ +const mongoose = require('mongoose'); require('dotenv').config({ path: '../.env.local' }); const { syncApplicationDictionary } = require('./netifyClient'); const { LookupApp } = require('./models/Schemas'); async function test() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'); console.log('Connected to DB'); await syncApplicationDictionary(); const count = await LookupApp.countDocuments(); console.log('Total LookupApps in DB:', count); await mongoose.disconnect(); } test().catch(console.error); diff --git a/proxy/verify_fix.js b/proxy/verify_fix.js new file mode 100644 index 0000000..ff63d00 --- /dev/null +++ b/proxy/verify_fix.js @@ -0,0 +1,56 @@ +// verify_fix.js - Directly verify the MongoDB aggregation returns correct results +// This simulates what the backend metadata-detail endpoint does after the fix. +// Run: node proxy/verify_fix.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +async function run() { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/backone'); + const db = mongoose.connection.db; + const col = db.collection('devicestats'); + + const testValues = ['10.6.10.44']; + // Also find other common device_labels to test + const sample = await col.find({}).limit(20).toArray(); + const labels = [...new Set(sample.map(d => d.device_label).filter(Boolean))]; + console.log('Sample device_labels to test:', labels.slice(0, 5)); + + for (const value of [...testValues, ...labels.slice(0, 3)]) { + // Count raw docs matching + const rawCount = await col.countDocuments({ device_label: value }); + + // Simulate the new aggregation pipeline + const aggResult = await col.aggregate([ + { $match: { device_label: value } }, + { $sort: { timestamp: -1 } }, + { $group: { + _id: '$ip_address', + mac_address: { $first: '$mac_address' }, + device_label: { $first: '$device_label' }, + download: { $max: '$download' }, + upload: { $max: '$upload' }, + }}, + { $sort: { download: -1 } }, + ]).toArray(); + + const status = rawCount > aggResult.length ? '✅ FIXED (was duplicated)' : '✓ OK'; + console.log(`\ndevice_label="${value}": raw=${rawCount} rows → aggregated=${aggResult.length} unique devices ${status}`); + if (aggResult.length > 0) { + console.log(' First result:', JSON.stringify(aggResult[0], null, 2)); + } + } + + // Verify unique index exists + const indexes = await col.indexes(); + const uniqueIdx = indexes.find(i => i.unique && i.key.agent_uuid && i.key.ip_address); + console.log('\n=== Unique Index on (agent_uuid, ip_address):', uniqueIdx ? `✅ EXISTS (${uniqueIdx.name})` : '❌ MISSING'); + + // Final count + const total = await col.countDocuments(); + console.log('=== Total DeviceStat docs:', total); + + await mongoose.disconnect(); +} + +run().catch(err => { console.error(err.message); process.exit(1); }); diff --git a/public/api/uploads/profile-1782896485110-507996343.png b/public/api/uploads/profile-1782896485110-507996343.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782896485110-507996343.png differ diff --git a/public/api/uploads/profile-1782957214095-436948837.png b/public/api/uploads/profile-1782957214095-436948837.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957214095-436948837.png differ diff --git a/public/api/uploads/profile-1782957227105-831349627.png b/public/api/uploads/profile-1782957227105-831349627.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957227105-831349627.png differ diff --git a/public/api/uploads/profile-1782957258680-874601083.png b/public/api/uploads/profile-1782957258680-874601083.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957258680-874601083.png differ diff --git a/public/api/uploads/profile-1782957260063-809722867.png b/public/api/uploads/profile-1782957260063-809722867.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957260063-809722867.png differ diff --git a/public/api/uploads/profile-1782957396116-629514876.png b/public/api/uploads/profile-1782957396116-629514876.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957396116-629514876.png differ diff --git a/public/api/uploads/profile-1782957397337-68390707.png b/public/api/uploads/profile-1782957397337-68390707.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957397337-68390707.png differ diff --git a/public/api/uploads/profile-1782957554195-684730747.png b/public/api/uploads/profile-1782957554195-684730747.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957554195-684730747.png differ diff --git a/public/api/uploads/profile-1782957618919-530652401.png b/public/api/uploads/profile-1782957618919-530652401.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957618919-530652401.png differ diff --git a/public/api/uploads/profile-1782957654468-554246050.png b/public/api/uploads/profile-1782957654468-554246050.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957654468-554246050.png differ diff --git a/public/api/uploads/profile-1782957734629-263039729.png b/public/api/uploads/profile-1782957734629-263039729.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957734629-263039729.png differ diff --git a/public/api/uploads/profile-1782957793294-535196821.png b/public/api/uploads/profile-1782957793294-535196821.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957793294-535196821.png differ diff --git a/public/api/uploads/profile-1782978598897-28546312.png b/public/api/uploads/profile-1782978598897-28546312.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782978598897-28546312.png differ diff --git a/public/api/uploads/profile-1783329601048-985926830.png b/public/api/uploads/profile-1783329601048-985926830.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783329601048-985926830.png differ diff --git a/public/api/uploads/profile-1783856980162-234747538.png b/public/api/uploads/profile-1783856980162-234747538.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783856980162-234747538.png differ diff --git a/public/api/uploads/profile-1783925701251-53039060.png b/public/api/uploads/profile-1783925701251-53039060.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783925701251-53039060.png differ diff --git a/public/api/uploads/profile-1783925846234-644654635.png b/public/api/uploads/profile-1783925846234-644654635.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783925846234-644654635.png differ diff --git a/public/api/uploads/profile-1783926643277-796221976.png b/public/api/uploads/profile-1783926643277-796221976.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783926643277-796221976.png differ diff --git a/public/api/uploads/profile-1784049206350-427299921.png b/public/api/uploads/profile-1784049206350-427299921.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784049206350-427299921.png differ diff --git a/public/api/uploads/profile-1784049223805-190927998.png b/public/api/uploads/profile-1784049223805-190927998.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784049223805-190927998.png differ diff --git a/public/api/uploads/profile-1784049244401-665031741.png b/public/api/uploads/profile-1784049244401-665031741.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049244401-665031741.png differ diff --git a/public/api/uploads/profile-1784049275064-498936278.png b/public/api/uploads/profile-1784049275064-498936278.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049275064-498936278.png differ diff --git a/public/api/uploads/profile-1784049302001-714198254.png b/public/api/uploads/profile-1784049302001-714198254.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049302001-714198254.png differ diff --git a/public/api/uploads/profile-1784254528937-270868858.png b/public/api/uploads/profile-1784254528937-270868858.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254528937-270868858.png differ diff --git a/public/api/uploads/profile-1784254553441-339825741.png b/public/api/uploads/profile-1784254553441-339825741.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254553441-339825741.png differ diff --git a/public/api/uploads/profile-1784254567483-97901195.png b/public/api/uploads/profile-1784254567483-97901195.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254567483-97901195.png differ diff --git a/public/api/uploads/profile-1784254581808-854860134.png b/public/api/uploads/profile-1784254581808-854860134.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254581808-854860134.png differ diff --git a/public/api/uploads/profile-1784254601947-954448750.png b/public/api/uploads/profile-1784254601947-954448750.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254601947-954448750.png differ diff --git a/public/api/uploads/profile-1784254618510-383483774.png b/public/api/uploads/profile-1784254618510-383483774.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254618510-383483774.png differ diff --git a/public/api/uploads/profile-1784254634906-830642874.png b/public/api/uploads/profile-1784254634906-830642874.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254634906-830642874.png differ diff --git a/public/api/uploads/profile-1784254648483-456467829.png b/public/api/uploads/profile-1784254648483-456467829.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254648483-456467829.png differ diff --git a/public/backone-logo.svg b/public/backone-logo.svg index 07f2647..272c863 100644 --- a/public/backone-logo.svg +++ b/public/backone-logo.svg @@ -1,21 +1,21 @@ - + diff --git a/public/find_server.txt b/public/find_server.txt new file mode 100644 index 0000000..36fdcd3 --- /dev/null +++ b/public/find_server.txt @@ -0,0 +1,3 @@ +/home/adminbackend/web/demoplace.my.id/public_html/backend/server.js +/home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/server.js +/home/adminbackend/web/demoplace.my.id/public_html/server.js diff --git a/public/fonts/Inter-Variable.woff2 b/public/fonts/Inter-Variable.woff2 new file mode 100644 index 0000000..33002f1 Binary files /dev/null and b/public/fonts/Inter-Variable.woff2 differ diff --git a/qa-api-final.js b/qa-api-final.js new file mode 100644 index 0000000..5091f01 --- /dev/null +++ b/qa-api-final.js @@ -0,0 +1,90 @@ +// qa-api-final.js — Test semua API dengan correct paths +const https = require('https'); +const BASE = 'https://dev.demoplace.my.id'; +let COOKIES = ''; + +function req(method, path, body) { + return new Promise((resolve) => { + const urlObj = new URL(`${BASE}${path}`); + const opts = { + hostname: urlObj.hostname, port: 443, + path: urlObj.pathname + urlObj.search, method, + headers: { + 'Content-Type': 'application/json', 'Accept': 'application/json', + ...(COOKIES ? { 'Cookie': COOKIES } : {}), + ...(body ? { 'Content-Length': Buffer.byteLength(JSON.stringify(body)) } : {}), + }, + rejectUnauthorized: false, timeout: 20000, + }; + const r = https.request(opts, (res) => { + let data = ''; + res.on('data', d => data += d); + res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data })); + }); + r.on('error', e => resolve({ status: 0, body: '', error: e.message })); + r.on('timeout', () => { r.destroy(); resolve({ status: 0, body: '', error: 'timeout' }); }); + if (body) r.write(JSON.stringify(body)); + r.end(); + }); +} + +const ic = s => s===200?'✅':s===307||s===302?'🔀':s===401||s===403?'🔒':s===404?'❌':s===500?'💥':'⚠️'; + +function parsePreview(r) { + if (!r.body || r.body.length === 0) return '(empty)'; + try { + const j = JSON.parse(r.body); + if (Array.isArray(j)) return `Array[${j.length}]`; + if (j.data && Array.isArray(j.data)) return `{data: Array[${j.data.length}], total: ${j.total||j.data.length}}`; + return JSON.stringify(j).substring(0, 100); + } catch(e) { return r.body.substring(0, 80).replace(/\s+/g,' '); } +} + +async function main() { + console.log('╔══════════════════════════════════════════════════════╗'); + console.log('║ API FINAL TEST — dev.demoplace.my.id ║'); + console.log('╚══════════════════════════════════════════════════════╝\n'); + + // Login + const lr = await req('POST', '/api/auth/login', { username: 'admin', password: 'admin' }); + COOKIES = (lr.headers?.['set-cookie'] || []).map(c => c.split(';')[0]).join('; '); + console.log(`🔐 Login: HTTP ${lr.status} | Cookies: ${COOKIES ? 'YES' : 'NO'}\n`); + + const endpoints = [ + // Auth + ['GET', '/api/health', 'Health check'], + ['GET', '/api/auth/me', 'Auth: me'], + ['GET', '/api/auth/users', 'Auth: users list'], + ['GET', '/api/auth/settings', 'Auth: settings'], + // Dashboard core + ['GET', '/api/dashboard/summary', 'Dashboard: summary'], + ['GET', '/api/dashboard/flows?page=1&limit=5', 'Dashboard: flows (p.1)'], + ['GET', '/api/dashboard/agents', 'Dashboard: agents'], + ['GET', '/api/dashboard/devices?limit=5', 'Dashboard: devices'], + ['GET', '/api/dashboard/telemetry?limit=5', 'Dashboard: telemetry'], + ['GET', '/api/dashboard/threats?limit=5', 'Dashboard: threats'], + ['GET', '/api/dashboard/events?limit=5', 'Dashboard: events'], + ['GET', '/api/dashboard/geo', 'Dashboard: geo'], + ['GET', '/api/dashboard/apps?limit=5', 'Dashboard: apps'], + ['GET', '/api/dashboard/device-labeling', 'Dashboard: device-labeling'], + ['GET', '/api/dashboard/flow-stats', 'Dashboard: flow-stats'], + ['GET', '/api/dashboard/tls?limit=5', 'Dashboard: TLS'], + ['GET', '/api/dashboard/agent-locations', 'Dashboard: agent-locations'], + ['GET', '/api/dashboard/blacklist', 'Dashboard: blacklist'], + // Details + ['GET', '/api/dashboard/metadata-detail', 'Metadata detail'], + ['GET', '/api/dashboard/category-detail', 'Category detail'], + ]; + + for (const [method, path, label] of endpoints) { + const r = await req(method, path); + const isJson = r.headers?.['content-type']?.includes('json'); + const preview = parsePreview(r); + console.log(` ${ic(r.status)} [${method}] ${label.padEnd(32)} HTTP ${r.status} | ${r.body?.length||0}b | ${preview.substring(0,80)}`); + } + + console.log('\n╔══════════════════════════════════════════════════════╗'); + console.log('║ API TEST SELESAI ║'); + console.log('╚══════════════════════════════════════════════════════╝'); +} +main().catch(console.error); diff --git a/qa-audit-v2.js b/qa-audit-v2.js new file mode 100644 index 0000000..4aa5aa2 --- /dev/null +++ b/qa-audit-v2.js @@ -0,0 +1,123 @@ +// qa-audit-v2.js — QA audit dengan correct route paths +const https = require('https'); + +const BASE = 'https://dev.demoplace.my.id'; +let COOKIES = ''; + +function req(method, path, body) { + return new Promise((resolve) => { + const urlObj = new URL(`${BASE}${path}`); + const options = { + hostname: urlObj.hostname, + port: 443, + path: urlObj.pathname + urlObj.search, + method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/html,application/json,*/*', + ...(COOKIES ? { 'Cookie': COOKIES } : {}), + ...(body ? { 'Content-Length': Buffer.byteLength(JSON.stringify(body)) } : {}), + }, + rejectUnauthorized: false, + timeout: 15000, + }; + const r = https.request(options, (res) => { + let data = ''; + res.on('data', d => data += d); + res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data })); + }); + r.on('error', e => resolve({ status: 0, error: e.message, body: '' })); + r.on('timeout', () => { r.destroy(); resolve({ status: 0, error: 'timeout', body: '' }); }); + if (body) r.write(JSON.stringify(body)); + r.end(); + }); +} + +const ic = (s) => s===200?'✅':s===307||s===302||s===301?'🔀':s===401||s===403?'🔒':s===404?'❌':s===500?'💥':'⚠️'; + +async function main() { + console.log('╔══════════════════════════════════════════════════════════╗'); + console.log('║ QA AUDIT v2 — dev.demoplace.my.id (correct routes) ║'); + console.log('╚══════════════════════════════════════════════════════════╝\n'); + + // Login + const lr = await req('POST', '/api/auth/login', { username: 'admin', password: 'admin' }); + if (lr.headers?.['set-cookie']) { + COOKIES = lr.headers['set-cookie'].map(c => c.split(';')[0]).join('; '); + console.log(`✅ Login OK | Cookie: ${COOKIES.substring(0,50)}...\n`); + } + + // Dashboard pages — correct paths + console.log('🏠 DASHBOARD PAGES (correct route paths):'); + const pages = [ + ['/', 'Root (after auth)'], + ['/flows', 'Flows table'], + ['/agents', 'Network Agents'], + ['/device-labeling', 'Device Labeling'], + ['/devices', 'Devices'], + ['/dpi-analytics', 'DPI Analytics'], + ['/flows', 'Flows'], + ['/geography', 'Geography'], + ['/intelligence', 'Intelligence'], + ['/network-infrastructure', 'Network Infrastructure'], + ['/network-intelligence', 'Network Intelligence'], + ['/security-audit', 'Security Audit'], + ['/threats', 'Threats'], + ['/events', 'Events'], + ['/dns', 'DNS'], + ['/lookup', 'Lookup'], + ['/apps', 'Applications'], + ['/user-accounts', 'User Accounts'], + ['/help', 'Help'], + ]; + for (const [path, label] of pages) { + const r = await req('GET', path); + const isHtml = r.body?.startsWith(' { + const isHttps = url.startsWith('https'); + const lib = isHttps ? https : http; + const urlObj = new URL(url); + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/html,application/json,*/*', + ...(cookies ? { 'Cookie': cookies } : {}), + ...(body ? { 'Content-Length': Buffer.byteLength(JSON.stringify(body)) } : {}), + }, + rejectUnauthorized: false, + timeout: 15000, + }; + const r = lib.request(options, (res) => { + let data = ''; + res.on('data', d => data += d); + res.on('end', () => resolve({ + status: res.statusCode, + headers: res.headers, + body: data, + size: data.length, + })); + }); + r.on('error', e => resolve({ status: 0, error: e.message, body: '', size: 0 })); + r.on('timeout', () => { r.destroy(); resolve({ status: 0, error: 'timeout', body: '', size: 0 }); }); + if (body) r.write(JSON.stringify(body)); + r.end(); + }); +} + +function icon(status) { + if (status === 200) return '✅'; + if (status === 307 || status === 301 || status === 302) return '🔀'; + if (status === 401 || status === 403) return '🔒'; + if (status === 404) return '❌'; + if (status === 500) return '💥'; + if (status === 0) return '⛔'; + return '⚠️'; +} + +async function main() { + console.log('╔══════════════════════════════════════════════════════╗'); + console.log('║ QA AUDIT — dev.demoplace.my.id ║'); + console.log('╚══════════════════════════════════════════════════════╝\n'); + + // --- STEP 1: Login & get session cookie --- + console.log('🔐 STEP 1: Login dengan admin/admin...'); + const loginRes = await req('POST', `${BASE}/api/auth/login`, { username: 'admin', password: 'admin' }); + console.log(` HTTP ${loginRes.status} | Size: ${loginRes.size}b`); + if (loginRes.status === 200) { + const setCookie = loginRes.headers['set-cookie']; + if (setCookie) { + COOKIES = setCookie.map(c => c.split(';')[0]).join('; '); + console.log(` ✅ Cookie diterima: ${COOKIES.substring(0, 60)}...`); + } + console.log(` User: ${loginRes.body.substring(0, 100)}`); + } else { + console.log(` ❌ Login gagal: ${loginRes.body.substring(0, 100)}`); + } + + // --- STEP 2: Unauthenticated pages --- + console.log('\n📄 STEP 2: Public pages (unauthenticated)...'); + const publicPages = [ + ['/', 'Root (redirect check)'], + ['/login', 'Login page'], + ]; + for (const [path, label] of publicPages) { + const r = await req('GET', `${BASE}${path}`); + const isHtml = r.body.includes(' { + const content = fs.readFileSync(file, 'utf8'); + const rel = path.relative(root, file); + + // Extract