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
This commit is contained in:
rafif committed 2026-07-29 14:14:29 +07:00
1 parent a403f752f3
commit dd4c8f6876
359 files changed
+26144 -3396

No files matched your search

+16
View File
@@ -66,3 +66,19 @@ CLAUDE.md
docs/ docs/
plans/ plans/
.env* .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
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+252
View File
@@ -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*
+2 -15
View File
@@ -41,7 +41,7 @@ Produk BackOne oleh **PT. Data Bisnis Solusi** — Dashboard monitoring jaringan
## 📡 Proxy — 2 Mode Pengambilan Data ## 📡 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) ### 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. 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 ### 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 | | Kantor BackOne (Admin) | `all` | _(kosong)_ | Semua agent |
| Pihak A | `agent` | `UUID-AGENT-A` | Hanya data Pihak A | | Pihak A | `agent` | `UUID-AGENT-A` | Hanya data Pihak A |
| Pihak B | `agent` | `UUID-AGENT-B` | Hanya data Pihak B | | Pihak B | `agent` | `UUID-AGENT-B` | Hanya data Pihak B |
| Pihak C | `agent` | `UUID-AGENT-C` | Hanya data Pihak C | | 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 | | GET | `/agents` | List semua agent UUID yang ada di MongoDB |
| POST | `/collect/all` | Trigger manual — kumpulkan semua agent | | POST | `/collect/all` | Trigger manual — kumpulkan semua agent |
| POST | `/collect/:uuid` | Trigger manual — kumpulkan agent spesifik | | POST | `/collect/:uuid` | Trigger manual — kumpulkan agent spesifik |
| POST | `/collect/agents` | Trigger manual — kumpulkan multiple agents (body: `{"uuids": [...], "delay_ms": 5000}`) |
--- ---
+2 -5
View File
@@ -26,9 +26,7 @@ contract, not just a task description.
**Responsibilities** **Responsibilities**
- Implement API/data-layer logic. - Implement API/data-layer logic.
- Wire the mock-vs-live routing required by the Demo/Live switch (`AGENTS.md` §5) and - Ensure all endpoints aggregate real-time data from MongoDB and Netify, avoiding any mock or simulated responses.
the Cloud/Local endpoint switch (`AGENTS.md` §6) — both must resolve through the
same contract so swapping either setting never changes calling code.
- Keep business logic out of route handlers; route handlers stay thin. - Keep business logic out of route handlers; route handlers stay thin.
**When invoked**: any task touching data, APIs, or service integration. **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 ## 3. Frontend Engineer
**Responsibilities** **Responsibilities**
- Implement UI for the task, including the Demo/Live and Cloud/Local switcher - Implement UI for the task.
controls where relevant.
- Consume the Backend's contract rather than reaching around it. - Consume the Backend's contract rather than reaching around it.
- Keep components small and composable, respecting the 256-LOC rule. - Keep components small and composable, respecting the 256-LOC rule.
+15
View File
@@ -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"]
+3
View File
@@ -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());
+22
View File
@@ -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));
+44
View File
@@ -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); });
+31
View File
@@ -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));
+15
View File
@@ -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));
+10
View File
@@ -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);
});
+2146
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -5,7 +5,8 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const path = require('path'); 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'; const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi';
@@ -22,7 +23,7 @@ async function connectDB() {
serverSelectionTimeoutMS: 10000, serverSelectionTimeoutMS: 10000,
connectTimeoutMS: 10000, connectTimeoutMS: 10000,
}); });
console.log('[MongoDB] ✓ Connected successfully'); console.log('[MongoDB] ✓ Connected successfully to', MONGODB_URI);
const { logCapacityStats } = require('./capacityTracker'); const { logCapacityStats } = require('./capacityTracker');
logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message)); logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message));
return; return;
+47 -6
View File
@@ -1,5 +1,6 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const User = require('../models/User'); const User = require('../models/User');
const Session = require('../models/Session');
const { Summary } = require('../models/Schemas'); const { Summary } = require('../models/Schemas');
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key'; const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-backone-key';
@@ -11,19 +12,46 @@ async function requireAuth(req, res, next) {
try { try {
req.user = jwt.verify(token, JWT_SECRET); 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 ────────────────────────────────────────────────────────── // ── VIEW-AS MODE ──────────────────────────────────────────────────────────
const viewAsHeader = req.headers['x-view-as-agent']; 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 { try {
const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET); const viewDecoded = jwt.verify(viewAsHeader, JWT_SECRET);
if (viewDecoded.type === 'view-as' && viewDecoded.adminId === req.user.id && viewDecoded.viewAs) { 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; let targetSiteUuid = req.user.site_uuid;
if (targetAgentUser && targetAgentUser.site_uuid) { if (targetAgentUser && targetAgentUser.site_uuid) {
targetSiteUuid = targetAgentUser.site_uuid; targetSiteUuid = targetAgentUser.site_uuid;
} else { } 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) { if (summaryDoc && summaryDoc.site_uuid) {
targetSiteUuid = summaryDoc.site_uuid; targetSiteUuid = summaryDoc.site_uuid;
} }
@@ -32,7 +60,7 @@ async function requireAuth(req, res, next) {
req.user = { req.user = {
...req.user, ...req.user,
role: 'AGENT_VIEWER', role: 'AGENT_VIEWER',
agent_uuid: viewDecoded.viewAs, agent_uuid: targetAgent,
agent_label: viewDecoded.viewAsLabel, agent_label: viewDecoded.viewAsLabel,
site_uuid: targetSiteUuid, site_uuid: targetSiteUuid,
_viewAsMode: true, _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; const token = req.cookies?.token;
if (!token) return res.status(401).json({ error: 'Not authenticated' }); if (!token) return res.status(401).json({ error: 'Not authenticated' });
try { try {
const decoded = jwt.verify(token, JWT_SECRET); 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' }); return res.status(403).json({ error: 'Forbidden' });
} }
req.adminUser = decoded; req.adminUser = decoded;
+4 -1
View File
@@ -82,7 +82,7 @@ const FlowSchema = new mongoose.Schema({
site_uuid: { type: String, index: true }, site_uuid: { type: String, index: true },
flow_id: String, flow_id: String,
src_ip: { type: String, index: true }, src_ip: { type: String, index: true },
src_mac: String, src_mac: { type: String, index: true },
dst_ip: { type: String, index: true }, dst_ip: { type: String, index: true },
dst_port: Number, dst_port: Number,
protocol: String, protocol: String,
@@ -107,6 +107,7 @@ const ThreatSchema = new mongoose.Schema({
protocol: String, protocol: String,
description: String, description: String,
event_at: String, event_at: String,
flow_id: { type: String, index: true },
}, baseOptions); }, baseOptions);
// ─── App Categories (per agent) ─────────────────────────────────────────────── // ─── App Categories (per agent) ───────────────────────────────────────────────
@@ -133,6 +134,7 @@ const EventSchema = new mongoose.Schema({
ip_address: String, ip_address: String,
mac_address: String, mac_address: String,
event_at: Date, event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions); }, baseOptions);
// ─── Compound Indexes for common dashboard queries ───────────────────────────── // ─── Compound Indexes for common dashboard queries ─────────────────────────────
@@ -144,6 +146,7 @@ FlowSchema.index({ agent_uuid: 1, flow_id: 1 });
FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 }); FlowSchema.index({ agent_uuid: 1, protocol: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, domain: 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, app_label: 1, timestamp: -1 });
FlowSchema.index({ site_uuid: 1, src_mac: 1, timestamp: -1 });
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 }); ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 }); AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
EventSchema.index({ agent_uuid: 1, timestamp: -1 }); EventSchema.index({ agent_uuid: 1, timestamp: -1 });
+22
View File
@@ -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);
+5 -1
View File
@@ -14,11 +14,15 @@ const UserSchema = new mongoose.Schema({
password_hash: { type: String, required: true }, password_hash: { type: String, required: true },
account_name: { type: String, default: null }, account_name: { type: String, default: null },
profile_picture: { 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 }, site_uuid: { type: String, default: null, index: true },
agent_uuid: { type: String, default: null }, 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 }, created_by: { type: String, default: null, index: true },
is_active: { type: Boolean, default: 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' } timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
}); });
+2718
View File
File diff suppressed because it is too large. Load diff
+312
View File
@@ -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 };
+46 -6
View File
@@ -1,5 +1,10 @@
// backend/server.js // 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 // BackOne Backend API Server
// //
// Tanggung jawab backend ini adalah READ-ONLY dari MongoDB. // Tanggung jawab backend ini adalah READ-ONLY dari MongoDB.
@@ -15,7 +20,8 @@
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
const path = require('path'); 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 express = require('express');
const cors = require('cors'); const cors = require('cors');
@@ -45,9 +51,26 @@ app.use(cors({
}, },
credentials: true credentials: true
})); }));
app.use(express.json()); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser()); 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 ──────────────────────────────────────────────────────────── // ─── Public Routes ────────────────────────────────────────────────────────────
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
const { getUploadsDir } = require('./routes/auth/helpers'); const { getUploadsDir } = require('./routes/auth/helpers');
@@ -115,9 +138,26 @@ app.get('/api/health', (req, res) => {
}); });
}); });
// ─── 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 ───────────────────────────────────────────────────────────── // ─── Start Server ─────────────────────────────────────────────────────────────
app.listen(PORT, () => { // Bind to 127.0.0.1 in production to prevent direct external access to port 3001.
console.log(`\n🚀 BackOne API Server berjalan di http://localhost:${PORT}`); // All external traffic must go through the reverse proxy (Apache/Nginx) at port 80/443.
console.log(`🔌 API Health : http://localhost:${PORT}/api/health`); const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0';
console.log(`📡 Mode : READ-ONLY dari MongoDB (data dikirim oleh Proxy Server)\n`); 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`);
}); });
+135
View File
@@ -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 };
+14
View File
@@ -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));
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

+60
View File
@@ -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();
+52
View File
@@ -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"
+1 -4
View File
@@ -53,12 +53,9 @@ services:
- MONGODB_URI=mongodb://mongodb:27017/backone_dpi - MONGODB_URI=mongodb://mongodb:27017/backone_dpi
- PROXY_PORT=4000 - PROXY_PORT=4000
# Mode 1: kumpulkan SEMUA agent (default) # 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=agent dan isi PROXY_AGENT_UUID untuk mode spesifik
# Ubah ke PROXY_COLLECT_MODE=agents dan isi PROXY_AGENT_UUIDS untuk mode multi-agent
- PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all} - PROXY_COLLECT_MODE=${PROXY_COLLECT_MODE:-all}
- PROXY_AGENT_UUID=${PROXY_AGENT_UUID:-} - 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 * * * *} - PROXY_CRON_SCHEDULE=${PROXY_CRON_SCHEDULE:-*/5 * * * *}
networks: networks:
- backone-infra - backone-infra
+55 -13
View File
@@ -1,22 +1,64 @@
// ecosystem.config.js — PM2 Configuration for Source 2 (dev.demoplace.my.id)
module.exports = { module.exports = {
apps: [ apps: [
{ {
name: "backone-proxy", name: 'source2-proxy',
script: "./proxy/index.js", script: './proxy/index.js',
env_file: ".env.production" 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", name: 'source2-backend',
script: "./backend/server.js", script: './backend/server.js',
env_file: ".env.production" 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", name: 'source2-frontend',
script: "server.js", script: 'start-with-env.js',
env_file: ".env.production", 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: { 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,
},
],
}; };
+259
View File
@@ -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);
});
+35
View File
@@ -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
}
]
+35
View File
@@ -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
}
]
+217
View File
@@ -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"
}
]
+2 -3
View File
@@ -2,17 +2,16 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
serverExternalPackages: ["mongoose"],
experimental: { experimental: {
serverActions: { serverActions: {
allowedOrigins: ["demoplace.my.id", "www.demoplace.my.id"], allowedOrigins: ["dev.demoplace.my.id", "www.dev.demoplace.my.id"],
}, },
}, },
async rewrites() { async rewrites() {
return [ return [
{ {
source: '/api/:path*', 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*`,
}, },
]; ];
}, },
+43 -8
View File
@@ -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 { server {
listen 80; listen 80;
server_name demoplace.my.id; server_name demoplace.my.id www.demoplace.my.id;
server_tokens off; # Hide NGINX version server_tokens off;
# Allow large file uploads for profile pictures (max 10MB)
client_max_body_size 10m;
# Security Headers # Security Headers
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
@@ -10,12 +16,12 @@ server {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" 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 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 # Rate limiting on API endpoints
# We will pass everything to the frontend container location /api/auth/ {
limit_req zone=api burst=20 nodelay;
location / { proxy_pass http://127.0.0.1:3000;
proxy_pass http://frontend:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
@@ -24,8 +30,37 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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; 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;
}
} }
+713 -24
View File
File diff suppressed because it is too large. Load diff
+15 -6
View File
@@ -1,13 +1,17 @@
{ {
"name": "netify-app", "name": "backone-dpi",
"version": "0.1.0", "version": "1.0.0",
"private": true, "private": true,
"engines": {
"node": ">=18.0.0"
},
"scripts": { "scripts": {
"dev": "concurrently --names \"NEXT,BACKEND,PROXY\" --prefix-colors \"cyan,green,yellow\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"", "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:next": "next dev", "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:backend": "node backend/server.js",
"dev:proxy": "node proxy/index.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", "build": "next build",
"start": "next start", "start": "next start",
"start:prod": "concurrently \"next start\" \"node backend/server.js\" \"node proxy/index.js\"", "start:prod": "concurrently \"next start\" \"node backend/server.js\" \"node proxy/index.js\"",
@@ -15,9 +19,12 @@
"backend": "node backend/server.js", "backend": "node backend/server.js",
"proxy": "node proxy/index.js", "proxy": "node proxy/index.js",
"proxy:bun": "bun 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": { "dependencies": {
"@react-pdf/renderer": "^4.5.1",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"axios": "^1.18.1", "axios": "^1.18.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
@@ -31,6 +38,7 @@
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"framer-motion": "^12.42.2", "framer-motion": "^12.42.2",
"isomorphic-git": "^1.40.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
@@ -40,6 +48,7 @@
"next": "16.2.9", "next": "16.2.9",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"node-cron": "^4.6.0", "node-cron": "^4.6.0",
"node-fetch": "^3.3.2",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-globe.gl": "^2.38.0", "react-globe.gl": "^2.38.0",
+34
View File
@@ -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); });
+63
View File
@@ -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); });
+73
View File
@@ -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);
});
+4 -4
View File
@@ -9,7 +9,7 @@ const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../../../..', '.env.local') }); require('dotenv').config({ path: path.join(__dirname, '../../../..', '.env.local') });
const SIAB_UUID = '6681452d_9cae_4ff4_8ae8_0d504774265e'; const SIAB_UUID = '6681452d_9cae_4ff4_8ae8_0d504774265e';
const NEXUS_UUID = 'd7902405_0dc2_458b_8584_ed4d24b64f24'; const OFFICE_UUID = '1959bb55_045b_47c7_bbdd_f33b7db197b9';
// Definitive SIAB agent list (from most recent collector run) // 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']; 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) { for (const colName of collections) {
const col = db.collection(colName); 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({ const r1 = await col.deleteMany({
site_uuid: NEXUS_UUID, site_uuid: OFFICE_UUID,
agent_uuid: { $in: SIAB_AGENTS } agent_uuid: { $in: SIAB_AGENTS }
}); });
if (r1.deletedCount > 0) { 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; totalDeleted += r1.deletedCount;
} }
+38 -37
View File
@@ -10,14 +10,10 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
const netify = require('./netifyClient'); const netify = require('./netifyClient');
const { collectSecondaryTelemetry } = require('./collectorHelper'); const { collectSecondaryTelemetry } = require('./collectorHelper');
const { const { collectDevicesAndApps, collectFlows } = require('./collectorHelperDpi2');
collectDevicesAndApps, const { collectThreats, collectEvents } = require('./collectorHelperDpi3');
collectFlows,
collectThreats,
collectEvents
} = require('./collectorHelperDpi2');
const { Summary, AppStat } = require('./models/Schemas'); const { Summary, AppStat, AgentRegistry } = require('./models/Schemas');
const { pruneOldData } = require('./dataRetention'); const { pruneOldData } = require('./dataRetention');
const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID; const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID;
@@ -29,7 +25,7 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) {
try { try {
// 1. Summary / Bandwidth (including derived speed, packet_drops, and peak_flow_rate) // 1. Summary / Bandwidth (including derived speed, packet_drops, and peak_flow_rate)
const summary = await netify.fetchBandwidthSummary(1440, agentUuid, siteUuid); const summary = await netify.fetchBandwidthSummary(5, agentUuid, siteUuid);
if (summary) { if (summary) {
let download_speed = 0; let download_speed = 0;
let upload_speed = 0; let upload_speed = 0;
@@ -38,15 +34,10 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) {
try { try {
const prev = await Summary.findOne({ agent_uuid: agentUuid }).sort({ timestamp: -1 }).lean(); const prev = await Summary.findOne({ agent_uuid: agentUuid }).sort({ timestamp: -1 }).lean();
if (prev && prev.timestamp) { const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300;
const timeDiffSec = (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000; const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300;
if (timeDiffSec > 0) { download_speed = summary.bandwidth_down / activeTimeDiff;
const bytesDiffDown = Math.max(0, summary.bandwidth_down - (prev.bandwidth_down || 0)); upload_speed = summary.bandwidth_up / activeTimeDiff;
const bytesDiffUp = Math.max(0, summary.bandwidth_up - (prev.bandwidth_up || 0));
download_speed = bytesDiffDown / timeDiffSec;
upload_speed = bytesDiffUp / timeDiffSec;
}
}
} catch (err) { } catch (err) {
console.error('[Collector] Error calculating summary speeds:', err.message); console.error('[Collector] Error calculating summary speeds:', err.message);
} }
@@ -78,7 +69,7 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) {
} }
// 2. Top Apps // 2. Top Apps
const apps = await netify.fetchTopApps(1440, 200, agentUuid, siteUuid); const apps = await netify.fetchTopApps(5, 200, agentUuid, siteUuid);
if (apps && apps.length > 0) { if (apps && apps.length > 0) {
const appDocs = apps.map(app => ({ const appDocs = apps.map(app => ({
timestamp, agent_uuid: agentUuid, site_uuid: siteUuid, timestamp, agent_uuid: agentUuid, site_uuid: siteUuid,
@@ -154,6 +145,28 @@ async function collectAllAgents() {
// Register these agents as belonging to this site // Register these agents as belonging to this site
for (const agent of agents) processedAgentUuids.add(agent.uuid); 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; totalAgents += agents.length;
console.log(`[Collector] Processing ${agents.length} agents for site ${siteUuid}: ${agents.map(a => a.uuid).join(', ')}`); console.log(`[Collector] Processing ${agents.length} agents for site ${siteUuid}: ${agents.map(a => a.uuid).join(', ')}`);
@@ -161,23 +174,17 @@ async function collectAllAgents() {
// Collect bandwidth at site level (no agentUuid filter) so numbers match // Collect bandwidth at site level (no agentUuid filter) so numbers match
// Netify portal exactly and avoid double-counting across agents. // Netify portal exactly and avoid double-counting across agents.
try { try {
console.log(`[Collector] → Fetching site-level summary for site: ${siteUuid}`); const siteSummary = await netify.fetchBandwidthSummary(5, null, siteUuid);
const siteSummary = await netify.fetchBandwidthSummary(1440, null, siteUuid);
if (siteSummary) { if (siteSummary) {
let download_speed = 0; let download_speed = 0;
let upload_speed = 0; let upload_speed = 0;
try { try {
const prev = await Summary.findOne({ agent_uuid: null, site_uuid: siteUuid }).sort({ timestamp: -1 }).lean(); const prev = await Summary.findOne({ agent_uuid: null, site_uuid: siteUuid }).sort({ timestamp: -1 }).lean();
if (prev && prev.timestamp) { const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300;
const timeDiffSec = (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000; const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300;
if (timeDiffSec > 0) { download_speed = siteSummary.bandwidth_down / activeTimeDiff;
const bytesDiffDown = Math.max(0, siteSummary.bandwidth_down - (prev.bandwidth_down || 0)); upload_speed = siteSummary.bandwidth_up / activeTimeDiff;
const bytesDiffUp = Math.max(0, siteSummary.bandwidth_up - (prev.bandwidth_up || 0));
download_speed = bytesDiffDown / timeDiffSec;
upload_speed = bytesDiffUp / timeDiffSec;
}
}
} catch (err) { } catch (err) {
console.error('[Collector] Error calculating site summary speeds:', err.message); console.error('[Collector] Error calculating site summary speeds:', err.message);
} }
@@ -224,14 +231,8 @@ async function collectAllAgents() {
} }
async function collectSpecificAgent(agentUuid, siteUuid = SITE_UUIDS[0]) { async function collectSpecificAgent(agentUuid, siteUuid = SITE_UUIDS[0]) {
const timestamp = new Date(); const result = await collectSpecificAgents([agentUuid], siteUuid, 0);
const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; return { success: result.successful > 0, mode: 'specific', agent_uuid: agentUuid };
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) { async function collectSpecificAgents(agentUuids, siteUuid = SITE_UUIDS[0], delayMs = 5000) {
+7 -7
View File
@@ -20,7 +20,7 @@ const {
async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify, label) { async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify, label) {
try { try {
// 2b. App Categories // 2b. App Categories
const categories = await netify.fetchTopAppCategories(1440, 50, agentUuid, SITE_UUID); const categories = await netify.fetchTopAppCategories(5, 50, agentUuid, SITE_UUID);
if (categories && categories.length > 0) { if (categories && categories.length > 0) {
const catDocs = categories.map(c => ({ const catDocs = categories.map(c => ({
timestamp, timestamp,
@@ -36,7 +36,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2c. TLS Versions // 2c. TLS Versions
const tlsVersions = await netify.fetchTlsVersions(1440, 50, agentUuid, SITE_UUID); const tlsVersions = await netify.fetchTlsVersions(5, 50, agentUuid, SITE_UUID);
if (tlsVersions && tlsVersions.length > 0) { if (tlsVersions && tlsVersions.length > 0) {
const tvDocs = tlsVersions.map(v => ({ const tvDocs = tlsVersions.map(v => ({
timestamp, timestamp,
@@ -52,7 +52,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2d. TLS Ciphers // 2d. TLS Ciphers
const tlsCiphers = await netify.fetchTlsCiphers(1440, 50, agentUuid, SITE_UUID); const tlsCiphers = await netify.fetchTlsCiphers(5, 50, agentUuid, SITE_UUID);
if (tlsCiphers && tlsCiphers.length > 0) { if (tlsCiphers && tlsCiphers.length > 0) {
const tcDocs = tlsCiphers.map(c => ({ const tcDocs = tlsCiphers.map(c => ({
timestamp, timestamp,
@@ -68,7 +68,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2e. TLS Security // 2e. TLS Security
const tlsSecurity = await netify.fetchTlsSecurity(1440, 50, agentUuid, SITE_UUID); const tlsSecurity = await netify.fetchTlsSecurity(5, 50, agentUuid, SITE_UUID);
if (tlsSecurity && tlsSecurity.length > 0) { if (tlsSecurity && tlsSecurity.length > 0) {
const tsDocs = tlsSecurity.map(s => ({ const tsDocs = tlsSecurity.map(s => ({
timestamp, timestamp,
@@ -84,7 +84,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2f. Top Countries // 2f. Top Countries
const countries = await netify.fetchTopCountries(1440, 100, agentUuid, SITE_UUID); const countries = await netify.fetchTopCountries(5, 100, agentUuid, SITE_UUID);
if (countries && countries.length > 0) { if (countries && countries.length > 0) {
const coDocs = countries.map(c => ({ const coDocs = countries.map(c => ({
timestamp, timestamp,
@@ -101,7 +101,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2g. Top Protocols // 2g. Top Protocols
const protocols = await netify.fetchTopProtocols(1440, 50, agentUuid, SITE_UUID); const protocols = await netify.fetchTopProtocols(5, 50, agentUuid, SITE_UUID);
if (protocols && protocols.length > 0) { if (protocols && protocols.length > 0) {
const protoDocs = protocols.map(p => ({ const protoDocs = protocols.map(p => ({
timestamp, timestamp,
@@ -117,7 +117,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify
} }
// 2h. SNI Hostnames // 2h. SNI Hostnames
const snis = await netify.fetchSniHostnames(1440, 10000, agentUuid, SITE_UUID); const snis = await netify.fetchSniHostnames(5, 10000, agentUuid, SITE_UUID);
if (snis && snis.length > 0) { if (snis && snis.length > 0) {
const sniDocs = snis.map(s => ({ const sniDocs = snis.map(s => ({
timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID,
+44 -59
View File
@@ -14,7 +14,7 @@ const {
} = require('./deviceResolver'); } = require('./deviceResolver');
async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, label) { 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 = {}; const ipToMacMap = {};
if (devices && devices.length > 0) { if (devices && devices.length > 0) {
@@ -56,7 +56,7 @@ async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, la
let deviceAppCount = 0; let deviceAppCount = 0;
for (let i = 0; i < topDevices.length; i += 5) { for (let i = 0; i < topDevices.length; i += 5) {
const batch = topDevices.slice(i, 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 = []; const appDocs = [];
results.forEach((res, idx) => { results.forEach((res, idx) => {
if (res.status === 'fulfilled' && Array.isArray(res.value)) { 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) { 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 flowLimit = parseInt(process.env.PROXY_FLOW_LIMIT || '1000000');
const flows = await netify.fetchFlows(flowLimit, agentUuid, SITE_UUID); const flows = await netify.fetchFlows(flowLimit, agentUuid, SITE_UUID);
if (flows && flows.length > 0) { 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 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 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 threatDocs = [];
const eventDocs = [];
for (const f of flowDocs) { for (const f of flowDocs) {
let isViolation = false; let isViolation = false;
let categoryLabel = ""; let categoryLabel = "";
// Check if domain is blacklisted // Check if domain is blacklisted
if (f.domain && blacklistedDomains.has(f.domain.toLowerCase())) { 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; isViolation = true;
} else if (f.app_label && blacklistedDomains.has(f.app_label.toLowerCase())) { 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; isViolation = true;
break;
}
}
} }
// Look up app details to check category // 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({ threatDocs.push({
timestamp, timestamp,
agent_uuid: f.agent_uuid, agent_uuid: f.agent_uuid,
@@ -150,7 +168,21 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo
dst_port: f.dst_port, dst_port: f.dst_port,
protocol: f.protocol, protocol: f.protocol,
description: `Access to blacklisted app/domain: ${f.app_label} (${f.domain || 'N/A'})${categoryLabel ? ' - Category: ' + categoryLabel : ''}`, 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); await Threat.insertMany(threatDocs);
console.log(`[Collector] ✓ ${threatDocs.length} blacklist policy violation threats recorded for ${label}`); 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) { } catch (err) {
console.error('[Collector] Blacklist detection failed:', err.message); 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 = { module.exports = {
collectDevicesAndApps, collectDevicesAndApps,
collectFlows, collectFlows
collectThreats,
collectEvents
}; };
+61
View File
@@ -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
};
+4 -4
View File
@@ -11,9 +11,9 @@ const Schemas = require('./models/Schemas');
* Prune all time-series documents older than 7 days. * Prune all time-series documents older than 7 days.
*/ */
async function pruneOldData() { async function pruneOldData() {
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const timeString = sevenDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; const timeString = thirtyDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB';
console.log(`[Collector] [Retention] Checking for telemetry data older than 7 days (before ${timeString})...`); console.log(`[Collector] [Retention] Checking for telemetry data older than 30 days (before ${timeString})...`);
// Prune from all collections in Schemas except CustomDeviceLabel // Prune from all collections in Schemas except CustomDeviceLabel
const collections = Object.keys(Schemas).filter(name => name !== 'CustomDeviceLabel'); const collections = Object.keys(Schemas).filter(name => name !== 'CustomDeviceLabel');
@@ -22,7 +22,7 @@ async function pruneOldData() {
try { try {
const Model = Schemas[name]; const Model = Schemas[name];
if (typeof Model.deleteMany === 'function') { 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) { if (res.deletedCount > 0) {
console.log(`[Collector] [Retention] ✓ Cleaned up ${res.deletedCount} old records from ${name}`); console.log(`[Collector] [Retention] ✓ Cleaned up ${res.deletedCount} old records from ${name}`);
} }
+44
View File
@@ -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); });
+79
View File
@@ -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); });
+13 -5
View File
@@ -1,10 +1,16 @@
// proxy/index.js // 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 // BackOne Proxy Server - Entry Point
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
const path = require('path'); 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 express = require('express');
const cors = require('cors'); const cors = require('cors');
@@ -57,11 +63,13 @@ async function main() {
console.log('╚════════════════════════════════════════════════╝\n'); console.log('╚════════════════════════════════════════════════╝\n');
console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`); console.log(`[Proxy] Mode: ${process.env.PROXY_COLLECT_MODE || 'all'}`);
// Start REST API server first so liveness probes remain active // Bind to 127.0.0.1 in production — port 4000 must never be exposed externally
app.listen(PORT, '0.0.0.0', () => { const BIND_HOST = process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0';
console.log(`\n🚀 Proxy REST API running at http://0.0.0.0:${PORT}`); 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 /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(); const connected = await connectDB();
+17
View File
@@ -103,6 +103,7 @@ const ThreatSchema = new mongoose.Schema({
protocol: String, protocol: String,
description: String, description: String,
event_at: String, event_at: String,
flow_id: { type: String, index: true },
}, baseOptions); }, baseOptions);
// ─── App Categories (per agent) ─────────────────────────────────────────────── // ─── App Categories (per agent) ───────────────────────────────────────────────
@@ -129,6 +130,7 @@ const EventSchema = new mongoose.Schema({
ip_address: String, ip_address: String,
mac_address: String, mac_address: String,
event_at: Date, event_at: Date,
flow_id: { type: String, index: true },
}, baseOptions); }, baseOptions);
// ─── Compound indexes for common dashboard queries ───────────────────────────── // ─── Compound indexes for common dashboard queries ─────────────────────────────
@@ -137,6 +139,7 @@ AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 });
DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true });
FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); FlowSchema.index({ agent_uuid: 1, timestamp: -1 });
FlowSchema.index({ agent_uuid: 1, flow_id: 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 }); FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
ThreatSchema.index({ agent_uuid: 1, timestamp: -1 }); ThreatSchema.index({ agent_uuid: 1, timestamp: -1 });
AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 }); AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 });
@@ -165,6 +168,19 @@ DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 });
const telemetrySchemas = require('./SchemasTelemetry'); const telemetrySchemas = require('./SchemasTelemetry');
const auxSchemas = require('./SchemasAux'); 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 = { module.exports = {
Summary: mongoose.model('Summary', SummarySchema), Summary: mongoose.model('Summary', SummarySchema),
AppStat: mongoose.model('AppStat', AppStatSchema), AppStat: mongoose.model('AppStat', AppStatSchema),
@@ -175,6 +191,7 @@ module.exports = {
Threat: mongoose.model('Threat', ThreatSchema), Threat: mongoose.model('Threat', ThreatSchema),
AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema),
Event: mongoose.model('Event', EventSchema), Event: mongoose.model('Event', EventSchema),
AgentRegistry: mongoose.model('AgentRegistry', AgentRegistrySchema),
...auxSchemas, ...auxSchemas,
...telemetrySchemas, ...telemetrySchemas,
}; };
+89
View File
@@ -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 };
+16 -227
View File
@@ -3,157 +3,11 @@
// DPI API wrapper for the BackOne Proxy Server targeting original Netify API. // 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 telemetry = require('./netifyTelemetry');
const stats = require('./netifyClientStats');
const PORT_SERVICE_MAP = { async function fetchFlows(limit = 1000000, agentUuid = null, siteUuid = null) {
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) {
const [raw, sniRaw] = await Promise.all([ const [raw, sniRaw] = await Promise.all([
netifyFetch('/data/flows', { settings_limit: limit }, agentUuid, siteUuid), netifyFetch('/data/flows', { settings_limit: limit }, agentUuid, siteUuid),
netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, 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)) { if (sniRaw && Array.isArray(sniRaw)) {
for (const r of sniRaw) { for (const r of sniRaw) {
const sni = typeof r.tls_sni === 'object' ? r.tls_sni?.label : r.tls_sni; 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 => { return raw.map(r => {
const port = r.remote_port ?? null; 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 appLabel = r.application?.label || portService;
const domain = r.tls_sni || r.dns_hostname || r.hostname || null; const domain = r.tls_sni || r.dns_hostname || r.hostname || null;
return { return {
@@ -188,84 +44,17 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) {
}).filter(f => f.src_ip); }).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 = { module.exports = {
fetchAgents,
fetchBandwidthSummary,
fetchTopApps,
fetchDiscoveredDevices,
fetchDeviceApps,
fetchFlows, fetchFlows,
fetchCyberThreats, fetchAgents: stats.fetchAgents,
fetchEvents, fetchBandwidthSummary: stats.fetchBandwidthSummary,
fetchTopApps: stats.fetchTopApps,
fetchDiscoveredDevices: stats.fetchDiscoveredDevices,
fetchDeviceApps: stats.fetchDeviceApps,
fetchCyberThreats: stats.fetchCyberThreats,
fetchEvents: stats.fetchEvents,
syncApplicationDictionary: stats.syncApplicationDictionary,
BASE_URL, BASE_URL,
PORT_SERVICE_MAP, PORT_SERVICE_MAP: stats.PORT_SERVICE_MAP,
...telemetry, ...telemetry,
}; };
+219
View File
@@ -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
};
+6 -1
View File
@@ -4,6 +4,7 @@
const cron = require('node-cron'); const cron = require('node-cron');
const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector'); const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector');
const { syncApplicationDictionary } = require('./netifyClient');
// Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents // Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents
const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all'; const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all';
@@ -97,7 +98,11 @@ function startScheduler() {
// Initial run immediately on startup (async, do not block server start) // Initial run immediately on startup (async, do not block server start)
setTimeout(async () => { setTimeout(async () => {
// 1. Sync dictionary first // 1. Sync dictionary first
// await netifyClient.syncApplicationDictionary(); try {
await syncApplicationDictionary();
} catch (err) {
console.error('[Scheduler] Error syncing application dictionary:', err.message);
}
// 2. Start normal telemetry collection // 2. Start normal telemetry collection
runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message)); runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message));
+71
View File
@@ -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);
+1
View File
@@ -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);
+89
View File
@@ -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);
+1
View File
@@ -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);
+56
View File
@@ -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); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

+3
View File
@@ -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
Binary file not shown.
+90
View File
@@ -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);
+123
View File
@@ -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('<!DOCTYPE') || r.body?.includes('<html');
const hasData = r.body?.includes('BackOne') || r.body?.includes('dashboard') || r.body?.includes('flows');
const loc = r.headers?.location || '-';
console.log(` ${ic(r.status)} ${label.padEnd(28)} HTTP ${r.status} | HTML:${isHtml?'Y':'N'} | Data:${hasData?'Y':'N'} | ${r.body?.length||0}b ${loc !== '-' ? `→ ${loc}` : ''}`);
}
// API endpoints
console.log('\n🔌 API ENDPOINTS:');
const apis = [
['GET', '/api/health', 'Health'],
['GET', '/api/auth/me', 'Auth/me'],
['GET', '/api/dashboard/summary', 'Dashboard summary'],
['GET', '/api/dashboard/stats', 'Dashboard stats'],
['GET', '/api/flows?page=1&limit=5', 'Flows list'],
['GET', '/api/telemetry?limit=5', 'Telemetry'],
['GET', '/api/agents', 'Agents'],
['GET', '/api/devices?limit=5', 'Devices'],
['GET', '/api/users', 'Users'],
['GET', '/api/sites', 'Sites'],
];
for (const [method, path, label] of apis) {
const r = await req(method, path);
const isJson = r.headers?.['content-type']?.includes('json');
let preview = '';
if (r.status !== 200) preview = ` | ${r.body?.substring(0, 60).replace(/\s+/g,' ')}`;
else if (isJson) {
try { const j = JSON.parse(r.body); preview = ` | keys: ${Object.keys(j).join(', ')}`; } catch(e){}
}
console.log(` ${ic(r.status)} [${method}] ${label.padEnd(20)} HTTP ${r.status} | JSON:${isJson?'Y':'N'} | ${r.body?.length||0}b${preview.substring(0,80)}`);
}
// All accounts
console.log('\n👤 ALL ACCOUNTS:');
const accounts = [['admin','admin'],['siab','siab'],['office','office']];
for (const [u, p] of accounts) {
const r = await req('POST', '/api/auth/login', { username: u, password: p });
if (r.status === 200) {
const d = JSON.parse(r.body);
console.log(` ✅ ${u}/${p} → ${d.user?.account_name} (${d.user?.role}) | site: ${d.user?.site_uuid?.substring(0,8)}...`);
} else {
console.log(` ❌ ${u}/${p} → HTTP ${r.status}: ${r.body?.substring(0,60)}`);
}
}
console.log('\n╔══════════════════════════════════════════════════════════╗');
console.log('║ AUDIT SELESAI ║');
console.log('╚══════════════════════════════════════════════════════════╝');
}
main().catch(console.error);
+164
View File
@@ -0,0 +1,164 @@
// qa-audit.js — Komprehensif QA audit semua halaman & API endpoint production
const https = require('https');
const http = require('http');
const BASE = 'https://dev.demoplace.my.id';
const BACKEND = 'http://103.185.47.52'; // test via external
// Cookies dari login untuk test authenticated pages
let COOKIES = '';
function req(method, url, body, cookies) {
return new Promise((resolve) => {
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('<!DOCTYPE') || r.body.includes('<html');
console.log(` ${icon(r.status)} ${label}: HTTP ${r.status} | HTML: ${isHtml} | ${r.size}b | Location: ${r.headers?.location || '-'}`);
}
// --- STEP 3: Authenticated pages ---
console.log('\n🏠 STEP 3: Dashboard pages (authenticated)...');
const dashPages = [
['/dashboard', 'Dashboard main'],
['/dashboard/flows', 'Flows table'],
['/dashboard/telemetry', 'Telemetry'],
['/dashboard/devices', 'Device labeling'],
['/dashboard/agents', 'Network agents'],
['/dashboard/users', 'User management'],
['/dashboard/settings', 'Settings'],
['/dashboard/profile', 'Profile'],
];
for (const [path, label] of dashPages) {
const r = await req('GET', `${BASE}${path}`, null, COOKIES);
const isHtml = r.body.includes('<!DOCTYPE') || r.body.includes('<html');
const hasError = r.body.includes('error') || r.body.includes('Error');
console.log(` ${icon(r.status)} ${label}: HTTP ${r.status} | HTML: ${isHtml} | Error: ${hasError} | ${r.size}b`);
}
// --- STEP 4: API endpoints ---
console.log('\n🔌 STEP 4: API endpoints...');
const apiEndpoints = [
['GET', '/api/health', 'Health check', null],
['GET', '/api/auth/me', 'Auth me', null],
['GET', '/api/flows?page=1&limit=10', 'Flows (paged)', null],
['GET', '/api/telemetry', 'Telemetry data', null],
['GET', '/api/devices', 'Devices list', null],
['GET', '/api/agents', 'Network agents', null],
['GET', '/api/users', 'Users list (admin)', null],
['GET', '/api/dashboard/stats', 'Dashboard stats', null],
['GET', '/api/dashboard/summary', 'Dashboard summary', null],
];
for (const [method, path, label] of apiEndpoints) {
const r = await req(method, `${BASE}${path}`, null, COOKIES);
const isJson = r.headers?.['content-type']?.includes('json');
const preview = r.body.replace(/\s+/g, ' ').substring(0, 80);
console.log(` ${icon(r.status)} [${method}] ${label}: HTTP ${r.status} | JSON: ${isJson} | ${r.size}b`);
if (r.status !== 200) console.log(` └─ ${preview}`);
}
// --- STEP 5: Static assets ---
console.log('\n🎨 STEP 5: Static assets...');
const assets = [
['/backone-logo.png', 'Logo PNG'],
['/favicon.ico', 'Favicon'],
['/_next/static/chunks/1e--nra3xanpi.css', 'CSS chunk 1'],
['/_next/static/media/BackOneLogo_Regular-s.p.27fxep_pjgchi.ttf', 'Custom font'],
];
for (const [path, label] of assets) {
const r = await req('GET', `${BASE}${path}`);
console.log(` ${icon(r.status)} ${label}: HTTP ${r.status} | CT: ${r.headers?.['content-type']?.split(';')[0] || '-'} | ${r.size}b`);
}
// --- STEP 6: Login with all accounts ---
console.log('\n👤 STEP 6: Test semua akun...');
const accounts = [
['admin', 'admin', 'SUPER_ADMIN'],
['siab', 'siab', 'TENANT_ADMIN (SIAB)'],
['office', 'office', 'TENANT_ADMIN (Office)'],
];
for (const [user, pass, role] of accounts) {
const r = await req('POST', `${BASE}/api/auth/login`, { username: user, password: pass });
if (r.status === 200) {
const data = JSON.parse(r.body);
console.log(` ✅ ${user}/${pass} → ${data.user?.account_name || role} (${data.user?.role})`);
} else {
console.log(` ❌ ${user}/${pass} → HTTP ${r.status}: ${r.body.substring(0, 60)}`);
}
}
console.log('\n╔══════════════════════════════════════════════════════╗');
console.log('║ QA AUDIT SELESAI ║');
console.log('╚══════════════════════════════════════════════════════╝');
}
main().catch(console.error);
+66
View File
@@ -0,0 +1,66 @@
const { Client } = require('ssh2');
const remoteScript = `
const fs = require('fs');
const path = require('path');
const root = '/home/adminbackend/web/demoplace.my.id/public_html/.next/standalone/.next/server/app/(dashboard)';
function scanDir(dir) {
const results = [];
if (!fs.existsSync(dir)) return results;
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
results.push(...scanDir(fullPath));
} else if (item.name.endsWith('.html') || item.name.endsWith('.js')) {
results.push(fullPath);
}
}
return results;
}
console.log('=== AUDITING ALL PAGE HEADERS AND TITLES ON DEMOPLACE.MY.ID ===\\n');
const files = scanDir(root);
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const rel = path.relative(root, file);
// Extract <h2> or <h1> or header titles
const h2s = [];
const h2Match = content.matchAll(/<h2[^>]*>(.*?)<\/h2>/gi);
for (const m of h2Match) {
const clean = m[1].replace(/<[^>]+>/g, '').trim();
if (clean && !h2s.includes(clean)) h2s.push(clean);
}
// Extract Learn This Page triggers
const hasHelpTrigger = content.includes('Learn This Page');
if (h2s.length > 0 || hasHelpTrigger) {
console.log('Page Route File:', rel);
if (h2s.length > 0) console.log(' Headers (H2):', h2s.join(' | '));
console.log(' Has Learn This Page Button:', hasHelpTrigger);
console.log('---');
}
});
`;
const conn = new Client();
conn.on('ready', () => {
conn.exec(`node -e ${JSON.stringify(remoteScript)}`, (err, stream) => {
let out = '';
stream.on('data', d => out += d);
stream.on('close', () => {
console.log(out);
conn.end();
});
});
}).connect({
host: '103.185.47.52',
port: 2222,
username: 'adminbackend',
password: 'htEo7x6LsBQiEHHH'
});
+67
View File
@@ -0,0 +1,67 @@
// scripts/check-agents-error.js
'use strict';
const { Client } = require('ssh2');
const https = require('https');
const SSH = { host: '103.185.47.52', port: 2222, username: 'adminbackend', password: 'htEo7x6LsBQiEHHH', readyTimeout: 60000 };
const PUB = '/home/adminbackend/web/demoplace.my.id/public_html';
function exec(conn, cmd, label) {
if (label) console.log('\n[' + label + ']');
console.log('$ ' + cmd.substring(0, 80));
return new Promise(resolve => {
conn.exec(cmd, (err, s) => {
if (err) { resolve(''); return; }
let out = '';
s.on('data', d => { out += d; process.stdout.write(d.toString()); })
.stderr.on('data', d => { out += d; process.stdout.write(d.toString()); })
.on('close', () => resolve(out));
});
});
}
async function httpGet(url, opts) {
return new Promise(resolve => {
const req = https.get(url, { timeout: 10000, rejectUnauthorized: false, ...opts }, res => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => resolve({ status: res.statusCode, body: body.substring(0, 500) }));
});
req.on('error', e => resolve({ status: 0, error: e.message }));
req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
});
}
async function main() {
const conn = new Client();
await new Promise((r, j) => { conn.on('ready', r).on('error', j).connect(SSH); });
console.log('[SSH] Connected\n');
// 1. Cek error log frontend
await exec(conn, 'tail -50 ' + PUB + '/logs/frontend-error.log 2>/dev/null | head -50', '1. Frontend error log');
// 2. Cek frontend out log terbaru
await exec(conn, 'tail -30 ' + PUB + '/logs/frontend-out.log 2>/dev/null', '2. Frontend out log');
// 3. Test API agents langsung dari server
await exec(conn, 'curl -sk http://127.0.0.1:3001/api/agents 2>/dev/null | head -c 500', '3. Test API /api/agents langsung');
// 4. Test API health backend
await exec(conn, 'curl -sk http://127.0.0.1:3001/api/health 2>/dev/null', '4. Test backend health');
// 5. Cek .env.production - apakah BACKEND_URL benar
await exec(conn, 'cat ' + PUB + '/.env.production 2>/dev/null | grep -v PASSWORD | grep -v SECRET | grep -v MONGO | head -20', '5. Environment variables (safe)');
// 6. Cek apakah backend bisa akses agents collection dari MongoDB
await exec(conn, 'curl -sk http://127.0.0.1:3001/api/agents/list 2>/dev/null | head -c 300', '6. Test /api/agents/list');
conn.end();
// 7. Test dari luar via domain
console.log('\n[7. Test API agents via domain...]');
const r = await httpGet('https://demoplace.my.id/api/agents');
console.log(' Status: ' + r.status);
console.log(' Body: ' + (r.body || r.error || 'empty'));
}
main().catch(e => console.error('[FATAL]', e.message));
+94
View File
@@ -0,0 +1,94 @@
// scripts/check-static-files.js
// Cek apakah static chunk files ada di disk dan bisa diakses via domain
'use strict';
const { Client } = require('ssh2');
const https = require('https');
const SSH = {
host: '103.185.47.52', port: 2222,
username: 'adminbackend', password: 'htEo7x6LsBQiEHHH',
readyTimeout: 60000
};
function sshExec(conn, cmd) {
return new Promise(resolve => {
conn.exec(cmd, (err, s) => {
if (err) { resolve('SSH_ERROR'); return; }
let out = '';
s.on('data', d => { out += d; process.stdout.write(d.toString()); })
.stderr.on('data', d => { out += d; process.stdout.write(d.toString()); })
.on('close', () => resolve(out));
});
});
}
function httpGet(url) {
return new Promise(resolve => {
const req = https.get(url, { timeout: 10000, rejectUnauthorized: false }, res => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => resolve({ status: res.statusCode, type: res.headers['content-type'], size: body.length }));
});
req.on('error', e => resolve({ status: 0, error: e.message }));
req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
});
}
async function main() {
const conn = new Client();
await new Promise((r, j) => { conn.on('ready', r).on('error', j).connect(SSH); });
console.log('[SSH] Connected\n');
const PUB = '/home/adminbackend/web/demoplace.my.id/public_html';
// 1. List chunks directory
console.log('=== .next/static/chunks/ contents ===');
await sshExec(conn, `ls -la ${PUB}/.next/static/chunks/ | head -20`);
// 2. Cek CSS files yang direferensikan oleh HTML
console.log('\n=== Cek file CSS spesifik ===');
await sshExec(conn, `ls -la ${PUB}/.next/static/chunks/*.css 2>/dev/null | head -5 || echo "Tidak ada .css di chunks/"`);
// 3. Cek apakah JS bootstrap file ada
console.log('\n=== Cek file JS yang diperlukan ===');
await sshExec(conn, `ls ${PUB}/.next/static/chunks/ | grep -E "(3km6z|21luguo|1-8s9)" | head -10 || echo "File JS tidak ditemukan"`);
// 4. Test akses static file via domain (dari dalam server)
console.log('\n=== Test static file via Apache ===');
await sshExec(conn,
`curl -sk -D - https://demoplace.my.id/_next/static/chunks/3km6z-n6wbgrs.js 2>/dev/null | head -15 || ` +
`curl -sk -D - http://103.185.47.52:8080/_next/static/chunks/3km6z-n6wbgrs.js 2>/dev/null | head -15`
);
// 5. Test apakah Nginx melayani file static langsung
console.log('\n=== Cek Nginx serving static (port 443) ===');
await sshExec(conn,
`curl -sk -w "\\nHTTP: %{http_code} | Type: %{content_type} | Size: %{size_download}" ` +
`-o /dev/null https://demoplace.my.id/_next/static/chunks/3km6z-n6wbgrs.js`
);
// 6. Cek apakah ada .next/server/ directory
console.log('\n=== Status .next/server/ ===');
await sshExec(conn, `ls ${PUB}/.next/server/ 2>/dev/null | head -5 || echo ".next/server/ TIDAK ADA - standalone build mungkin tidak lengkap"`);
conn.end();
// 7. Test dari luar - JS dan CSS
console.log('\n=== Test static resources dari luar ===');
const files = [
'/_next/static/chunks/3km6z-n6wbgrs.js',
'/_next/static/chunks/1u6dw_tm45utz.css',
'/_next/static/chunks/1oskchnhjm4n8.css',
'/_next/static/chunks/21luguo5_g2uu.js',
];
for (const f of files) {
const r = await httpGet(`https://demoplace.my.id${f}`);
const icon = r.status === 200 ? '✅' : '❌';
console.log(` ${icon} ${f}`);
console.log(` HTTP ${r.status} | Type: ${r.type || 'N/A'} | Size: ${r.size || 0} bytes`);
}
}
main().catch(err => console.error('[FATAL]', err.message));
+61
View File
@@ -0,0 +1,61 @@
const https = require('https');
const PAGES = [
'/',
'/network-infrastructure',
'/agents',
'/devices',
'/flows',
'/apps',
'/network-intelligence',
'/dns',
'/geography',
'/dpi-analytics',
'/lookup',
'/intelligence',
'/threats',
'/events',
'/security-audit',
'/help'
];
function fetchPage(route) {
return new Promise((resolve) => {
https.get(`https://demoplace.my.id${route}`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const titleMatch = data.match(/<title>(.*?)<\/title>/i);
const descMatch = data.match(/<meta name="description" content="(.*?)"/i);
const title = titleMatch ? titleMatch[1] : 'N/A';
const desc = descMatch ? descMatch[1] : 'N/A';
// Find logo references
const logos = [];
const logoRegex = /src="([^"]*(?:logo|icon)[^"]*)"/gi;
let match;
while ((match = logoRegex.exec(data)) !== null) {
if (!logos.includes(match[1])) logos.push(match[1]);
}
resolve({ route, statusCode: res.statusCode, title, desc, logos });
});
}).on('error', (err) => {
resolve({ route, error: err.message });
});
});
}
async function main() {
console.log('=== AUDITING DEMOPLACE.MY.ID PAGES ===\n');
for (const page of PAGES) {
const res = await fetchPage(page);
console.log(`Route: ${res.route}`);
console.log(` Title : ${res.title}`);
console.log(` Description : ${res.desc}`);
console.log(` Logos/Icons : ${res.logos ? res.logos.join(', ') : 'None'}`);
console.log('---');
}
}
main();
Loaded 100 of 359 files, more files were not shown because too many files have changed in this diff. Show more