From 674462bef3500fd907f357642c018d6862513bfa Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 25 Aug 2026 01:29:20 -0700 Subject: [PATCH] Restore ARGUS v4 agent-native (was clobbered by stale v2) + fixes - Restore full v4: API keys, /api/scan, /docs, /openapi.json, /mcp MCP endpoint, subscriptions, AD-audit + creds scans - Add missing /api/login (premium owner login, permanent sub key) - Fix agent_report CoT leak: enable_thinking:false + reasoning_effort:low - Add Ollama fallback chain (.128 qwen3.8fast -> .186 ornith-1.5:9b) - Fix Kali API params: naabu uses 'host', wpscan uses 'url' - Fix creds_scan hydra contract (target+service+username) - Fix run_osint: missing 'ssh' command (sshpass -p X ssh -o ...) - Harden sub_active against naive datetime --- app.py | 1086 +++++++++++++++++++++++++++++++++++++++++----- argus.service | 13 + nginx-argus.conf | 13 + 3 files changed, 1003 insertions(+), 109 deletions(-) create mode 100644 argus.service create mode 100644 nginx-argus.conf diff --git a/app.py b/app.py index 97a20e8..3b5d7e5 100644 --- a/app.py +++ b/app.py @@ -1,20 +1,32 @@ #!/usr/bin/env python3 """ -ARGUS v2 — Autonomous Security Agent +ARGUS v4 — The Hundred-Eyed Watchman ===================================== Rentable AI security agent. Real tool calls, real results, in the browser. -Bitcoin (sats) settled. No KYC. qwen3.8fast interprets every result. +Bitcoin (sats) settled. No KYC. qwen3.8-fast interprets every result. + +ARGUS PANOPTES — the hundred-eyed giant of Greek myth, the all-seeing +watchman who never sleeps. Every eye open. Nothing escapes the gaze. Tooling: - - Web vuln -> SUPERkali (.177): nuclei / ffuf / nmap / subfinder / httpx - - OSINT -> OSINT VM (.66): sherlock / theHarvester / spiderfoot / holehe + - Web vuln -> SUPERkali (.177) 70-tool REST API: nuclei/ffuf/nmap/sqlmap/nikto/wpscan + - OSINT -> OSINT VM (.66): sherlock / theHarvester / spiderfoot / holehe / maigret + - AD audit -> Commando (.47): impacket secretsdump/kerberoast/asreproast + winPEAS + hashcat + - Creds -> SUPERkali: crackmapexec / enum4linux / hydra / smbclient - Agent -> qwen3.8fast @ shadow-death (.128) for professional reports +API: agent-native. Mint a key, submit scans as JSON, poll results. +Docs live at /docs — human and machine readable (OpenAPI at /openapi.json). + +MCP: paid weekly subscription unlocks ARGUS as a red-team MCP endpoint +at /mcp — agents connect over the Model Context Protocol, list tools, +and call scans programmatically. Subscription settled in Bitcoin. + drjones — indianaholmes@thetempleofdoom.com """ -import os, sqlite3, json, subprocess, uuid, hashlib, hmac, threading, urllib.request, ssl -from datetime import datetime, timezone -from flask import Flask, request, jsonify, render_template_string, g +import os, sqlite3, json, subprocess, uuid, hashlib, hmac, threading, urllib.request, ssl, base64 +from datetime import datetime, timezone, timedelta +from flask import Flask, request, jsonify, render_template_string, g, Response # BTCPay uses a self-signed cert on the LAN — trust it _SSL = ssl.create_default_context() @@ -26,6 +38,8 @@ PROXMOX_HOST = "10.30.20.85" PROXMOX_USER = "root" KALI_VMID = "109" OSINT_VMID = "547" +COMMANDO_VMID = "601" +KALI_API = "http://10.30.30.177:5000" BTCPAY_URL = "https://10.30.20.140" BTCPAY_PUBLIC = "https://btcpay.thetempleofdoom.com" BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22" @@ -34,11 +48,31 @@ STORE_ADDR = "bc1qvj4x74dnvzw3ku2f8lrmhz4g6r4p63djcs2y39" WEBHOOK_SECRET = "BsvyofWDqri5mDbQxhWhW3" OLLAMA_URL = "http://10.30.20.128:11434" OLLAMA_MODEL = "qwen3.8fast" +# Fallback chain — .128 (nightmare 4080S) is flaky; .186 (light_reaper 3070) is the reliable backup. +OLLAMA_HOSTS = [ + ("http://10.30.20.128:11434", "qwen3.8fast"), # primary — the main brain + ("http://10.30.20.186:11434", "ornith-1.5:9b"), # fallback — long-context specialist +] +ADMIN_USER = "drjones" +ADMIN_PASS = "Czapiewski1!" SECRET = os.environ.get("ARGUS_SECRET", uuid.uuid4().hex) DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "argus.db") +PUBLIC_BASE = "https://argus.thetempleofdoom.com" # Pricing in SATS (1 BTC = 100,000,000 sats) -PRICES_SATS = {"web-vuln": 4000, "osint": 2500, "monitor": 15000} +PRICES_SATS = { + "web-vuln": 4000, + "osint": 2500, + "ad-audit": 5000, + "creds": 3500, + "monitor": 15000, +} + +# MCP subscription (weekly, in sats) +MCP_PLANS = { + "mcp-weekly": {"sats": 20000, "label": "Red-Team MCP Endpoint — 7 days", "days": 7}, + "mcp-monthly": {"sats": 60000, "label": "Red-Team MCP Endpoint — 30 days", "days": 30}, +} # ─── DB ────────────────────────────────────────────────────────────────────── def get_db(): @@ -53,10 +87,24 @@ def init_db(): CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, email TEXT, kind TEXT, target TEXT, status TEXT DEFAULT 'pending', -- unpaid | running | done | failed - invoice_id TEXT, raw TEXT, report TEXT, + invoice_id TEXT, raw TEXT, report TEXT, params TEXT DEFAULT '{}', + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS api_keys ( + key TEXT PRIMARY KEY, email TEXT, label TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS subscriptions ( + key TEXT PRIMARY KEY, email TEXT, plan TEXT, + expires_at TEXT, -- ISO UTC timestamp created_at TEXT DEFAULT (datetime('now')) ); """) + # migrate: ensure raw/report/params columns exist (older DBs used 'result' or lacked params) + cols = [r[1] for r in db.execute("PRAGMA table_info(jobs)").fetchall()] + for name, ddl in [("raw", "TEXT"), ("report", "TEXT"), ("params", "TEXT DEFAULT '{}'")]: + if name not in cols: + db.execute(f"ALTER TABLE jobs ADD COLUMN {name} {ddl}") db.commit(); db.close() # ─── TOOL EXECUTORS ────────────────────────────────────────────────────────── @@ -67,27 +115,76 @@ def _ssh(cmd, timeout): return r.stdout + r.stderr def run_guest(vmid, cmd, timeout=420): - """Run a command inside a VM via Proxmox guest exec; return decoded stdout.""" - out = _ssh(f"qm guest exec {vmid} --timeout 300 -- bash -c {cmd!r}", timeout) + """Run a command inside a Linux VM via Proxmox guest exec; return decoded stdout.""" + out = _ssh(f"qm guest exec {vmid} -- bash -c {cmd!r}", timeout) try: return json.loads(out).get("out-data", "") except Exception: return out -OSINT_HOST = "10.30.20.66" +def run_commando(ps_script, timeout=420): + """Run PowerShell on the Commando VM (Windows) via guest exec (base64 UTF-16LE).""" + enc = base64.b64encode(ps_script.encode("utf-16-le")).decode() + out = _ssh(f"qm guest exec {COMMANDO_VMID} -- powershell -NoProfile -EncodedCommand {enc}", timeout) + try: + d = json.loads(out) + return d.get("out-data", "") or d.get("err-data", "") + except Exception: + return out + +def kali_api(tool, params, timeout=600): + """Call the Kali REST API (70 tools) at /api/tools/.""" + body = json.dumps(params).encode() + req = urllib.request.Request(f"{KALI_API}/api/tools/{tool}", data=body, + headers={"Content-Type": "application/json"}) + try: + r = urllib.request.urlopen(req, timeout=timeout) + d = json.loads(r.read()) + if isinstance(d, dict) and "stdout" in d: + return (d.get("stdout") or "") + (("\nERR: " + d["stderr"]) if d.get("stderr") else "") + if isinstance(d, dict) and "error" in d: + return "ERR: " + str(d["error"]) + return json.dumps(d, indent=2) + except Exception as e: + return f"ERR: {e}" + +OSINT_HOST = "10.30.30.66" OSINT_USER = "osint" OSINT_KEY = "osint" def run_osint(cmd, timeout=300): - """Run a command on the OSINT VM (10.30.20.66) over SSH.""" - r = subprocess.run(["sshpass", "-p", OSINT_KEY, "-o", "StrictHostKeyChecking=no", - f"{OSINT_USER}@{OSINT_HOST}", cmd], + """Run a command on the OSINT VM (10.30.30.66) over SSH.""" + r = subprocess.run(["sshpass", "-p", OSINT_KEY, "ssh", "-o", "StrictHostKeyChecking=no", + "-o", "ConnectTimeout=10", f"{OSINT_USER}@{OSINT_HOST}", cmd], capture_output=True, text=True, timeout=timeout) return r.stdout + r.stderr # ─── AGENT (qwen3.8) ───────────────────────────────────────────────────────── +def _llm_generate(prompt): + """Try each Ollama host/model in order; return text or None. + + qwen3.8fast needs enable_thinking:false + reasoning_effort:low (its template + keys on those fields, NOT Ollama's `think` field) or it leaks ////// CoT junk. + ornith-1.5:9b honours a top-level think:false.""" + for url, model in OLLAMA_HOSTS: + opts = {"num_predict": 1024} + if model.startswith("qwen"): + opts.update({"enable_thinking": False, "reasoning_effort": "low"}) + body = json.dumps({"model": model, "prompt": prompt, "stream": False, + "think": False, "options": opts}).encode() + req = urllib.request.Request(f"{url}/api/generate", data=body, + headers={"Content-Type": "application/json"}) + try: + r = urllib.request.urlopen(req, timeout=180) + out = json.loads(r.read()).get("response", "").strip() + if out: + return out + except Exception: + continue + return None + def agent_report(kind, target, raw): - """qwen3.8fast turns raw tool output into a professional findings report.""" + """Turn raw tool output into a professional findings report (LLM).""" if not raw or not raw.strip(): return "No findings returned from the tooling." prompt = ( @@ -97,41 +194,64 @@ def agent_report(kind, target, raw): f"technical — do not invent findings that are not in the raw output. " f"Format as markdown.\n\n=== RAW OUTPUT ===\n{raw[:8000]}" ) - body = json.dumps({"model": OLLAMA_MODEL, "prompt": prompt, "stream": False, - "think": False, - "options": {"num_predict": 1500, "enable_thinking": False, "reasoning_effort": "low"}}).encode() - req = urllib.request.Request(f"{OLLAMA_URL}/api/generate", data=body, - headers={"Content-Type": "application/json"}) - try: - r = urllib.request.urlopen(req, timeout=240) - return json.loads(r.read()).get("response", "").strip() - except Exception as e: - return f"(agent interpretation unavailable: {e})\n\n{raw[:3000]}" + resp = _llm_generate(prompt) + if resp is None: + return f"(agent interpretation unavailable — no LLM host reachable)\n\n{raw[:3000]}" + return resp # ─── SCANS ─────────────────────────────────────────────────────────────────── def web_vuln_scan(target): host = target.split("://")[-1].split("/")[0].split(":")[0] stages = {} - stages["subdomain_discovery"] = run_guest(KALI_VMID, - f"subfinder -d {host} -silent 2>/dev/null | head -40") - stages["port_scan"] = run_guest(KALI_VMID, - f"naabu -host {host} -silent 2>/dev/null | head -40") - stages["vulnerability_scan"] = run_guest(KALI_VMID, - f"nuclei -u https://{host} -silent -severity low,medium,high,critical -timeout 8 2>/dev/null | head -80") - stages["service_enumeration"] = run_guest(KALI_VMID, - f"nmap -sV -sC -Pn --top-ports 100 {host} 2>/dev/null | tail -40") - stages["directory_fuzzing"] = run_guest(KALI_VMID, - f"ffuf -u https://{host}/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -mc 200,204,301,302,307,401,403 -t 40 -timeout 5 2>/dev/null | head -40") + stages["subdomain_discovery"] = kali_api("subfinder", {"domain": host}) + stages["port_scan"] = kali_api("naabu", {"host": host}) + stages["http_probe"] = kali_api("httpx", {"target": host}) + stages["vulnerability_scan"] = kali_api("nuclei", {"target": f"https://{host}", "additional_args": "-severity low,medium,high,critical -timeout 8"}) + stages["cms_scan"] = kali_api("wpscan", {"url": f"https://{host}"}) + stages["sql_injection"] = kali_api("sqlmap", {"target": f"https://{host}", "additional_args": "--batch --level 1 --risk 1"}) + stages["service_enumeration"] = kali_api("nmap", {"target": host}) + stages["directory_fuzzing"] = kali_api("ffuf", {"url": f"https://{host}", "wordlist": "/usr/share/seclists/Discovery/Web-Content/common.txt"}) return json.dumps(stages, indent=2) def osint_scan(target): stages = {} - stages["username_search"] = run_osint( - f"timeout 90 sherlock {target} --timeout 10 2>/dev/null | head -50") - stages["email_domain_recon"] = run_osint( - f"timeout 60 theHarvester -d {target} -b all 2>/dev/null | head -50") - stages["subdomain_enumeration"] = run_osint( - f"timeout 60 sublist3r -d {target} 2>/dev/null | head -40") + stages["username_search"] = run_osint(f"timeout 90 sherlock {target} --timeout 10 2>/dev/null | head -50") + stages["email_domain_recon"] = run_osint(f"timeout 60 theHarvester -d {target} -b all 2>/dev/null | head -50") + stages["subdomain_enumeration"] = run_osint(f"timeout 60 sublist3r -d {target} 2>/dev/null | head -40") + stages["email_breach"] = run_osint(f"timeout 60 holehe {target} 2>/dev/null | head -40") + return json.dumps(stages, indent=2) + +def ad_audit_scan(target, username="", password="", domain=""): + """Windows / Active Directory assessment via Commando (impacket + recon).""" + host = target.split("://")[-1].split("/")[0].split(":")[0] + stages = {} + # unauthenticated recon + stages["port_scan"] = run_commando( + f'Import-Module "C:\\\\Tools\\\\PowerSploit\\\\Recon\\\\Invoke-Portscan.ps1"; ' + f'Invoke-Portscan -Hosts "{host}" -Ports "21,22,53,80,88,135,139,389,443,445,464,593,636,3268,3269,3389,5985,5986,8080,9389" -T 4' + ) + cred = f"{domain}/{username}:{password}" if (domain and username) else (f"{username}:{password}" if username else "") + if cred: + stages["samrdump"] = run_commando(f'python C:\\\\Python311\\\\Scripts\\\\samrdump.py "{cred}@{host}" 2>&1 | Select-Object -First 50') + stages["kerberoast"] = run_commando(f'python C:\\\\Python311\\\\Scripts\\\\GetUserSPNs.py "{cred}@{host}" -request 2>&1 | Select-Object -First 60') + stages["asreproast"] = run_commando(f'python C:\\\\Python311\\\\Scripts\\\\GetNPUsers.py "{cred}@{host}" -request -format hashcat 2>&1 | Select-Object -First 40') + stages["laps_passwords"] = run_commando(f'python C:\\\\Python311\\\\Scripts\\\\GetLAPSPassword.py "{cred}@{host}" 2>&1 | Select-Object -First 30') + stages["secretsdump"] = run_commando(f'python C:\\\\Python311\\\\Scripts\\\\secretsdump.py "{cred}@{host}" 2>&1 | Select-Object -First 80') + else: + stages["asreproast_enum"] = run_commando( + f'python C:\\\\Python311\\\\Scripts\\\\GetNPUsers.py "{domain}/" -no-pass -dc-ip "{host}" -request -format hashcat 2>&1 | Select-Object -First 30' + ) + return json.dumps(stages, indent=2) + +def creds_scan(target, username="", password=""): + """Credential / SMB attacks via Kali (crackmapexec, enum4linux, hydra, smbclient).""" + host = target.split("://")[-1].split("/")[0].split(":")[0] + stages = {} + stages["smb_enum"] = kali_api("enum4linux", {"target": host}) + stages["smb_client"] = kali_api("smbclient", {"target": host}) + stages["smb_attack"] = kali_api("crackmapexec", {"target": f"smb {host}"}) + if username: + stages["hydra_smb"] = kali_api("hydra", {"target": host, "service": "smb", "username": username}) return json.dumps(stages, indent=2) def run_job(job_id): @@ -139,24 +259,37 @@ def run_job(job_id): job = db.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone() db.execute("UPDATE jobs SET status='running' WHERE id=?", (job_id,)); db.commit() try: - raw = web_vuln_scan(job["target"]) if job["kind"] == "web-vuln" else osint_scan(job["target"]) - report = agent_report(job["kind"], job["target"], raw) - db.execute("UPDATE jobs SET raw=?, report=?, status='done' WHERE id=?", - (raw, report, job_id)) + params = json.loads(job["params"] or "{}") + username = params.get("username", "") + password = params.get("password", "") + domain = params.get("domain", "") + kind = job["kind"] + target = job["target"] + if kind == "web-vuln": + raw = web_vuln_scan(target) + elif kind == "ad-audit": + raw = ad_audit_scan(target, username, password, domain) + elif kind == "creds": + raw = creds_scan(target, username, password) + else: + raw = osint_scan(target) + report = agent_report(kind, target, raw) + db.execute("UPDATE jobs SET raw=?, report=?, status='done' WHERE id=?", (raw, report, job_id)) except Exception as e: - db.execute("UPDATE jobs SET report=?, status='failed' WHERE id=?", - (f"Scan error: {e}", job_id)) + db.execute("UPDATE jobs SET report=?, status='failed' WHERE id=?", (f"Scan error: {e}", job_id)) db.commit(); db.close() # ─── BTCPAY ────────────────────────────────────────────────────────────────── -def btcpay_create_invoice(sats, order_id, desc): +def btcpay_create_invoice(sats, order_id, desc, redirect=None): btc = sats / 1e8 - body = json.dumps({"amount": str(btc), "currency": "BTC", "orderId": order_id, - "checkout": {"redirectURL": f"https://argus.thetempleofdoom.com/report/{order_id}"}, - "metadata": {"orderId": order_id, "description": desc}}).encode() + body = {"amount": str(btc), "currency": "BTC", "orderId": order_id, + "metadata": {"orderId": order_id, "description": desc}} + if redirect: + body["checkout"] = {"redirectURL": redirect} req = urllib.request.Request(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices", - data=body, headers={"Authorization": f"token {BTCPAY_KEY}", - "Content-Type": "application/json"}) + data=json.dumps(body).encode(), + headers={"Authorization": f"token {BTCPAY_KEY}", + "Content-Type": "application/json"}) try: r = urllib.request.urlopen(req, timeout=20, context=_SSL) inv = json.loads(r.read()) @@ -172,9 +305,60 @@ def btcpay_verify(sig, raw): expected = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest() return hmac.compare_digest(f"sha256={expected}", sig) +# ─── API KEYS & SUBSCRIPTIONS ──────────────────────────────────────────────── +def mint_key(email, label=""): + key = "argus_" + uuid.uuid4().hex + db = get_db() + db.execute("INSERT INTO api_keys (key, email, label) VALUES (?,?,?)", (key, email, label)) + db.commit() + return key + +def key_valid(key): + if not key: return False + db = get_db() + return db.execute("SELECT 1 FROM api_keys WHERE key=?", (key,)).fetchone() is not None + +def key_email(key): + db = get_db() + row = db.execute("SELECT email FROM api_keys WHERE key=?", (key,)).fetchone() + return row["email"] if row else None + +def sub_active(key): + """True if the key has an unexpired subscription.""" + if not key: return False + db = get_db() + row = db.execute("SELECT expires_at FROM subscriptions WHERE key=?", (key,)).fetchone() + if not row or not row["expires_at"]: return False + try: + exp = datetime.fromisoformat(row["expires_at"]) + if exp.tzinfo is None: + exp = exp.replace(tzinfo=timezone.utc) + except Exception: + return False + return exp > datetime.now(timezone.utc) + +def extend_subscription(key, email, plan): + days = MCP_PLANS[plan]["days"] + db = get_db() + row = db.execute("SELECT expires_at FROM subscriptions WHERE key=?", (key,)).fetchone() + base = datetime.now(timezone.utc) + if row and row["expires_at"]: + try: + cur = datetime.fromisoformat(row["expires_at"]) + if cur > base: + base = cur + except Exception: + pass + exp = (base + timedelta(days=days)).isoformat() + db.execute("""INSERT INTO subscriptions (key, email, plan, expires_at) VALUES (?,?,?,?) + ON CONFLICT(key) DO UPDATE SET plan=excluded.plan, expires_at=excluded.expires_at, email=excluded.email""", + (key, email, plan, exp)) + db.commit() + return exp + # ─── UI (premium, animated) ────────────────────────────────────────────────── STYLE = """ -:root{--bg:#05070d;--panel:rgba(13,20,38,.7);--line:rgba(56,89,152,.35);--txt:#e8f0fb;--dim:#8fa3c8;--acc:#22d3ee;--acc2:#8b5cf6;--good:#34d399;--warn:#fbbf24} +:root{--bg:#05070d;--panel:rgba(13,20,38,.7);--line:rgba(56,89,152,.35);--txt:#e8f0fb;--dim:#8fa3c8;--acc:#22d3ee;--acc2:#8b5cf6;--good:#34d399;--warn:#fbbf24;--blood:#e11d48} *{box-sizing:border-box;margin:0;padding:0} html{scroll-behavior:smooth} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--txt);line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden} @@ -184,11 +368,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .wrap{max-width:1140px;margin:0 auto;padding:0 24px} nav{display:flex;justify-content:space-between;align-items:center;padding:22px 0;position:relative;z-index:2} .logo{font-weight:900;font-size:1.5rem;letter-spacing:2px}.logo span{background:linear-gradient(90deg,var(--acc),var(--acc2));-webkit-background-clip:text;-webkit-text-fill-color:transparent} +.logo .eye{display:inline-block;width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 35% 35%,var(--acc),#041018 70%);box-shadow:0 0 12px var(--acc);margin-right:10px;vertical-align:middle} nav a{color:var(--dim);text-decoration:none;margin-left:24px;font-size:.92rem;transition:.2s} nav a:hover{color:var(--txt)} nav a.cta{background:linear-gradient(90deg,var(--acc),var(--acc2));color:#041018;padding:10px 20px;border-radius:10px;font-weight:700} .hero{padding:110px 0 70px;text-align:center} .hero .eyebrow{letter-spacing:4px;color:var(--acc);font-size:.8rem;text-transform:uppercase;margin-bottom:18px} +.hero .eyebrow .blink{animation:blink 1.4s infinite} +@keyframes blink{50%{opacity:.2}} .hero h1{font-size:3.6rem;font-weight:900;letter-spacing:-1.5px;line-height:1.04} .hero h1 span{background:linear-gradient(90deg,var(--acc),var(--acc2));-webkit-background-clip:text;-webkit-text-fill-color:transparent} .hero p{color:var(--dim);font-size:1.22rem;max-width:680px;margin:24px auto} @@ -214,10 +401,14 @@ input:focus{outline:none;border-color:var(--acc);box-shadow:0 0 0 3px rgba(34,21 .radios{display:flex;gap:18px;flex-wrap:wrap} .radios label{display:flex;align-items:center;gap:8px;margin:0;color:var(--txt)} .radios input{width:auto} +.opt{display:none} +.opt.show{display:block} .tools{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px} .tool{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px} .tool b{color:var(--acc);display:block;margin-bottom:4px;font-size:.95rem} .tool span{color:var(--dim);font-size:.82rem} +.cat{margin:40px 0 16px;font-size:1.3rem;font-weight:800;color:var(--txt)} +.cat .tag{font-size:.7rem;color:var(--acc2);letter-spacing:2px;text-transform:uppercase;display:block;margin-bottom:2px} footer{text-align:center;color:var(--dim);font-size:.85rem;padding:50px 0;position:relative;z-index:1} footer a{color:var(--dim)} .addr{font-family:monospace;background:rgba(5,7,13,.7);border:1px solid var(--line);border-radius:8px;padding:8px 14px;font-size:.82rem;word-break:break-all;color:var(--good)} @@ -228,6 +419,33 @@ pre{background:rgba(5,7,13,.8);border:1px solid var(--line);border-radius:12px;p .report h1,.report h2,.report h3{color:var(--acc);margin:16px 0 8px} .report li{margin-left:20px;color:var(--dim)} .report code{background:rgba(5,7,13,.8);padding:2px 6px;border-radius:4px} +.terminal{font-family:ui-monospace,'SF Mono',Menlo,monospace;background:rgba(2,4,9,.92);border:1px solid var(--line);border-radius:14px;max-width:640px;margin:34px auto 0;text-align:left;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5)} +.terminal .bar{display:flex;align-items:center;gap:6px;padding:12px 16px;background:rgba(13,20,38,.6);border-bottom:1px solid var(--line)} +.terminal .bar i{width:11px;height:11px;border-radius:50%;display:inline-block} +.terminal .bar i.r{background:#ff5f56}.terminal .bar i.y{background:#ffbd2e}.terminal .bar i.g{background:#27c93f} +.terminal .bar span{color:var(--dim);font-size:.75rem;margin-left:8px;letter-spacing:1px} +.terminal .body{padding:18px 20px;font-size:.83rem;line-height:1.7;min-height:180px} +.terminal .body .ln{white-space:pre-wrap;word-break:break-word} +.terminal .body .ln .c{color:var(--good)}.terminal .body .ln .k{color:var(--acc)}.terminal .body .ln .d{color:var(--dim)}.terminal .body .ln .w{color:var(--warn)}.terminal .body .ln .b{color:var(--blood)} +.cursor{display:inline-block;width:8px;height:15px;background:var(--acc);vertical-align:-2px;animation:blink 1s infinite} +.redact{background:#1a2035;color:transparent;border-radius:3px;user-select:none} +.stamp{display:inline-block;border:2px solid var(--blood);color:var(--blood);padding:3px 14px;border-radius:4px;font-weight:800;letter-spacing:3px;font-size:.72rem;transform:rotate(-4deg);opacity:.8;text-transform:uppercase} +.docs{margin-top:50px} +.docs .endpoint{background:var(--panel);border:1px solid var(--line);border-radius:14px;margin:22px 0;overflow:hidden} +.docs .endpoint .head{display:flex;align-items:center;gap:14px;padding:18px 22px;background:rgba(13,20,38,.5);border-bottom:1px solid var(--line);flex-wrap:wrap} +.method{font-family:ui-monospace,monospace;font-weight:800;font-size:.8rem;padding:4px 12px;border-radius:6px;letter-spacing:1px} +.m-GET{background:#0f2a1e;color:var(--good)}.m-POST{background:#0f3050;color:#6cc3ff} +.docs .endpoint .head code.path{font-family:ui-monospace,monospace;color:var(--txt);font-size:1rem;font-weight:600} +.docs .endpoint .desc{padding:18px 22px;color:var(--dim);font-size:.92rem} +.docs .endpoint .desc b{color:var(--txt)} +.docs h3{color:var(--acc);margin:34px 0 6px;font-size:1.2rem} +.docs .auth-note{background:rgba(139,92,246,.08);border:1px solid rgba(139,92,246,.3);border-radius:10px;padding:14px 18px;color:var(--dim);font-size:.88rem;margin:14px 0} +.code-block{position:relative;margin:14px 0} +.code-block .lang{position:absolute;top:10px;right:16px;color:var(--dim);font-size:.7rem;letter-spacing:1px;text-transform:uppercase} +.tbl{width:100%;border-collapse:collapse;font-size:.86rem;margin:10px 0} +.tbl th{text-align:left;color:var(--acc);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line);font-size:.8rem} +.tbl td{padding:8px 10px;border-bottom:1px solid rgba(56,89,152,.15);color:var(--dim)} +.tbl td code{color:var(--txt);background:rgba(5,7,13,.6);padding:1px 6px;border-radius:4px;font-size:.8rem} @media(max-width:680px){.hero h1{font-size:2.4rem}} """ @@ -257,98 +475,229 @@ BG_JS = """ """ -TOOLS_LIST = [ - ("nuclei", "Fast CVE / misconfiguration scanner — 8,000+ templates"), - ("ffuf", "High-speed directory & parameter fuzzer"), - ("nmap", "Port + service + NSE script enumeration"), - ("subfinder", "Passive subdomain discovery across 30+ sources"), - ("httpx", "Live HTTP probing of discovered hosts"), - ("katana", "Crawler that maps a site's attack surface"), - ("naabu", "Fast port scanner for target discovery"), - ("sherlock", "Username footprint across 300+ social networks"), - ("theHarvester", "Email + subdomain harvesting from public sources"), - ("spiderfoot", "Automated OSINT reconnaissance framework"), - ("holehe", "Checks which services a given email is registered on"), - ("maigret", "Deep username → profile enumeration"), - ("instaloader", "Instagram profile + post intelligence"), - ("toutatis", "Phone-number OSINT lookups"), - ("qwen3.8 agent", "Interprets raw findings into a professional report"), +TERMINAL_JS = """ + +""" + +# Full arsenal by category (rendered on /capabilities) +ARSENAL = [ + ("WEB RECON · KALI", [ + ("nuclei", "Fast CVE / misconfiguration scanner — 8,000+ templates"), + ("subfinder", "Passive subdomain discovery across 30+ sources"), + ("httpx", "Live HTTP probing of discovered hosts"), + ("katana", "Crawler that maps a site's attack surface"), + ("naabu", "Fast port scanner for target discovery"), + ("ffuf", "High-speed directory & parameter fuzzer"), + ("gobuster", "Directory / DNS / VHOST brute-forcer"), + ("feroxbuster", "Recursive content discovery"), + ("dirsearch", "Web path scanner with JSON output"), + ("arjun", "HTTP parameter discovery"), + ("nmap", "Port + service + NSE script enumeration"), + ("masscan", "Internet-scale port scanning"), + ("dnsx", "Fast DNS toolkit"), + ("findomain", "Fast subdomain enumerator"), + ("amass", "In-depth attack-surface mapping"), + ("rustscan", "Lightning-fast port scanner"), + ]), + ("EXPLOITATION · KALI", [ + ("sqlmap", "Automatic SQL injection detection & takeover"), + ("wpscan", "WordPress vulnerability scanner"), + ("nikto", "Web server vulnerability scanner"), + ("commix", "Command injection exploiter"), + ("wfuzz", "Web application fuzzer"), + ("dirb", "Web content scanner"), + ("metasploit", "Full exploitation framework (msfconsole)"), + ("searchsploit", "Exploit-DB offline search"), + ("burpsuite", "Web app pentest proxy"), + ]), + ("CREDENTIALS · KALI", [ + ("crackmapexec", "Swiss-army SMB/WinRM/MSSQL post-exploitation"), + ("enum4linux", "SMB / Samba enumeration"), + ("smbclient", "SMB share access & enumeration"), + ("hydra", "Fast network logon cracker"), + ("john", "Password hash cracker"), + ("hashcat", "GPU-accelerated hash cracking"), + ("evil-winrm", "WinRM shell for lateral movement"), + ("responder", "LLMNR/NBT-NS/mDNS poisoning"), + ("impacket", "Python SMB/AD attack library"), + ("bloodhound", "AD attack-path visualization"), + ]), + ("AD / WINDOWS · COMMANDO", [ + ("mimikatz", "Credential extraction from memory / SAM / LSA"), + ("impacket-secretsdump", "Remote SAM/LSA/NTDS secret dump"), + ("kerberoast", "GetUserSPNs — service-ticket hash extraction"), + ("asreproast", "GetNPUsers — AS-REP hash extraction"), + ("GetLAPSPassword", "Read LAPS-managed local admin passwords"), + ("wmiexec / psexec / smbexec", "Remote command execution over WMI/SMB"), + ("samrdump / lookupsid", "SAMR user & SID enumeration"), + ("SharpHound", "BloodHound data collector"), + ("PowerView", "AD enumeration (users/groups/sessions/GPO)"), + ("winPEAS", "Local privilege-escalation enumeration"), + ("hashcat", "Crack NTLM / Kerberoast / AS-REP hashes"), + ("CrackMapExec", "SMB/LDAP/WinRM network attacks"), + ]), + ("OSINT · TRACE LABS", [ + ("sherlock", "Username footprint across 300+ social networks"), + ("theHarvester", "Email + subdomain harvesting"), + ("spiderfoot", "Automated OSINT recon framework"), + ("holehe", "Email-registered-service enumeration"), + ("maigret", "Deep username → profile enumeration"), + ("instaloader", "Instagram profile + post intelligence"), + ("toutatis", "Phone-number OSINT lookups"), + ("shodan", "Internet device search engine"), + ]), + ("AGENT", [ + ("qwen3.8fast", "Interprets raw findings into a professional report"), + ]), ] +def _nav(active=""): + return f"""""" + +def capabilities_page(): + blocks = "" + for cat, tools in ARSENAL: + tools_html = "".join(f'
{n}{d}
' for n, d in tools) + blocks += f'
arsenal{cat}
{tools_html}
' + return f""" + +ARGUS — The Arsenal +
+{_nav()} +
+

The arsenal

Every weapon ARGUS turns on your target — real binaries, real results, across three attack VMs. classified

+{blocks} +
+ +
{BG_JS}""" + def landing(): cards = "" for key, name, sats, desc in [ - ("web-vuln", "Vulnerability Scan", 4000, "nuclei + ffuf + nmap + subfinder against your target. Real CVE checks."), - ("osint", "OSINT Investigation", 2500, "Usernames, emails, domains — a 24-tool deep-recon pass."), + ("web-vuln", "Vulnerability Scan", 4000, "nuclei + ffuf + nmap + sqlmap + wpscan against your target. Real CVE checks."), + ("osint", "OSINT Investigation", 2500, "Usernames, emails, domains — a deep-recon pass across 24 tools."), + ("ad-audit", "AD / Windows Audit", 5000, "impacket secretsdump + kerberoast + asreproast + winPEAS against an Active Directory target."), + ("creds", "Credential Attack", 3500, "crackmapexec + enum4linux + hydra SMB credential testing."), ("monitor", "Continuous Monitoring", 15000, "The agent re-scans on schedule and alerts on change.")]: cards += f"""

{name}

{desc}

{sats:,} sats
""" + # MCP subscription card + cards += f"""

Red-Team MCP Endpoint

+

Subscribe weekly and plug ARGUS straight into your AI agent as a Model Context Protocol server — list tools, call scans, read reports, all over /mcp.

+
20,000 sats / wk
""" return f""" -ARGUS — Autonomous Security Agent +ARGUS — The Hundred-Eyed Watchman
- +{_nav()}
-
Autonomous Offensive Security
-

Your attack surface,
hunted by an agent.

-

ARGUS runs a real offensive toolkit — not a theme scan — and an AI agent interprets every result into a report you can act on. Settled in Bitcoin.

- +
ARGUS PANOPTES the hundred-eyed watchman
+

Every eye open.
Nothing escapes the gaze.

+

ARGUS is an autonomous offensive security agent. It turns a hundred real attack tools across three war machines — Kali, Commando, and Trace Labs — on whatever you point it at, then an AI cortex reads every result back as a report you can act on. Settled in Bitcoin.

+
argus — watchtower session
+
+
-

What ARGUS does

Three services, one agent, zero hand-holding.

+

What ARGUS watches

Five services, plus a paid MCP endpoint for your agents.

{cards}
-

Launch a scan

Pick a service, enter a target, pay in sats. Results appear live in your browser.

+

Open an eye

Pick a service, name a target, pay in sats. Results appear live in your browser.

- - - + + + + +
- + +
+ + + + + + +
+
+

Subscribe for MCP access

Pay weekly in Bitcoin to expose ARGUS as a Model Context Protocol endpoint at /mcp — your AI agents list tools and call scans directly.

+
+ +
+ + +
+ + + + +
+
+
+
-{BG_JS}""" - -def capabilities_page(): - tools = "".join(f'
{n}{d}
' for n, d in TOOLS_LIST) - return f""" - -ARGUS — Capabilities -
- -
-

The arsenal

Every tool ARGUS runs against your target — real binaries, real results.

-
{tools}
-
- -
{BG_JS}""" +{BG_JS} +{TERMINAL_JS} +""" REPORT = """ ARGUS — Live Report
- +{_nav()}

Scan Report

Target: {{job.target}}  ·  Type: {{job.kind}}  ·  {{job.status}}

@@ -365,7 +714,8 @@ REPORT = """ {% endif %}
-{BG_JS} + +{BG_JS} """ +# ─── API DOCS (code snippets kept OUTSIDE f-strings to avoid quote clash) ──── +SNIP_KEYS = """POST /api/keys HTTP/1.1 +Content-Type: application/json + +{ "email": "you@example.com", "label": "my first agent" }""" +SNIP_KEYS_RES = """200 OK +{ "api_key": "argus_9f3a...", "email": "you@example.com" }""" +SNIP_SCAN = """POST /api/scan HTTP/1.1 +Content-Type: application/json +X-API-Key: argus_9f3a... + +{ + "kind": "web-vuln", + "target": "example.com", + "email": "you@example.com" +}""" +SNIP_SCAN_RES = """200 OK +{ + "job_id": "9f3a2b1c0d4e", + "status": "unpaid", + "sats": 4000, + "checkout": "https://btcpay.thetempleofdoom.com/i/...", + "report_url": "https://argus.thetempleofdoom.com/report/9f3a2b1c0d4e", + "poll": "/api/job/9f3a2b1c0d4e" +}""" +SNIP_JOB_CURL = """curl -s https://argus.thetempleofdoom.com/api/job/9f3a2b1c0d4e | jq .""" +SNIP_JOB_RES = """200 OK +{ "status": "done", "report": "# Findings\\n...", "raw": "{...tool output...}" }""" +SNIP_JOBS_CURL = """curl -s "https://argus.thetempleofdoom.com/api/jobs?key=argus_9f3a..." | jq .""" +SNIP_TOOLS_CURL = """curl -s https://argus.thetempleofdoom.com/api/tools | jq .""" +SNIP_SUB = """POST /api/subscribe HTTP/1.1 +Content-Type: application/json + +{ "email": "you@example.com", "plan": "mcp-weekly" }""" +SNIP_SUB_RES = """200 OK +{ + "api_key": "argus_9f3a...", + "plan": "mcp-weekly", + "sats": 20000, + "checkout": "https://btcpay.thetempleofdoom.com/i/..." +}""" +SNIP_MCP_INIT = """POST /mcp HTTP/1.1 +Content-Type: application/json +X-API-Key: argus_9f3a... + +{ "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": { "protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": { "name": "my-agent", "version": "1.0" } } }""" +SNIP_MCP_TOOLS = """POST /mcp HTTP/1.1 +Content-Type: application/json +X-API-Key: argus_9f3a... + +{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }""" +SNIP_MCP_CALL = """POST /mcp HTTP/1.1 +Content-Type: application/json +X-API-Key: argus_9f3a... + +{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", + "params": { "name": "argus_scan", + "arguments": { "kind": "web-vuln", "target": "example.com", "email": "you@example.com" } } }""" + +def _code(lang, code): + import html + return '
' + lang + '
' + html.escape(code) + '
' + +def _tbl(rows): + head = "".join("" + h + "" for h in rows[0]) + body = "".join("" + "".join("" + c + "" for c in r) + "" for r in rows[1:]) + return '' + head + '' + body + '
' + +def docs_page(): + parts = [] + parts.append(f""" + +ARGUS — API Reference +
+{_nav()} + +
+

API Reference

+

ARGUS is agent-native — built to be driven by code, not clicks. Mint an API key, submit scans as JSON, poll for results. Machine-readable OpenAPI at /openapi.json.

+ +
Base URL: https://argus.thetempleofdoom.com  ·  Auth: pass your key as X-API-Key: <key>. Keys are free; scans are settled in Bitcoin (sats). MCP access requires a paid subscription.
+ +

Authentication

+
+
POST/api/keys
+
Mint an API key. Identifies you so the watchman can tie scans to a caller. No cost.
+
""") + parts.append(_code("json", SNIP_KEYS)) + parts.append(_tbl([ + ("Field", "Type", "Notes"), + ("email", "string", "required — where results are delivered"), + ("label", "string", "optional — human name for this key"), + ])) + parts.append(_code("response", SNIP_KEYS_RES)) + parts.append("""
+
+ +

Launching scans

+
+
POST/api/scan
+
Submit a scan programmatically. Returns a job id and a Bitcoin invoice. The scan fires the moment the invoice settles.
+
""") + parts.append(_code("json", SNIP_SCAN)) + parts.append(_tbl([ + ("Field", "Type", "Notes"), + ("kind", "string", "web-vuln · osint · ad-audit · creds · monitor"), + ("target", "string", "domain, IP, username, or email"), + ("email", "string", "required — result delivery"), + ("username", "string", "optional — for ad-audit / creds"), + ("password", "string", "optional — for ad-audit / creds"), + ("domain", "string", "optional — AD domain"), + ])) + parts.append(_code("response", SNIP_SCAN_RES)) + parts.append("""
+
+ +

Reading results

+
+
GET/api/job/<job_id>
+
Poll a job's status. The watchman works asynchronously — call this until status is done or failed.
+
""") + parts.append(_code("bash", SNIP_JOB_CURL)) + parts.append(_code("response", SNIP_JOB_RES)) + parts.append(_tbl([ + ("status", "meaning"), + ("unpaid", "invoice not yet settled"), + ("running", "tools are executing"), + ("done", "report ready"), + ("failed", "scan errored — report holds the reason"), + ])) + parts.append("""
+
+ +
+
GET/api/jobs?key=<key>
+
List your scans. Every job tied to a given API key.
+
""") + parts.append(_code("bash", SNIP_JOBS_CURL)) + parts.append("""
+
+ +

MCP subscription (paid)

+
+
POST/api/subscribe
+
Subscribe for MCP access. Pay a weekly (or monthly) Bitcoin invoice; on settlement your key is authorized to drive the /mcp endpoint until expiry. No KYC.
+
""") + parts.append(_code("json", SNIP_SUB)) + parts.append(_tbl([ + ("Field", "Type", "Notes"), + ("email", "string", "required"), + ("plan", "string", "mcp-weekly (20k sats/7d) · mcp-monthly (60k sats/30d)"), + ("api_key", "string", "optional — omit to mint a new key"), + ])) + parts.append(_code("response", SNIP_SUB_RES)) + parts.append("""
+
+ +
+
POST/mcp
+
The red-team MCP endpoint. A Model Context Protocol server over HTTP. Requires an active subscription (X-API-Key). Speaks JSON-RPC 2.0 — initialize, tools/list, tools/call.
+
""") + parts.append(_code("json", SNIP_MCP_INIT)) + parts.append(_code("json", SNIP_MCP_TOOLS)) + parts.append(_code("json", SNIP_MCP_CALL)) + parts.append(_tbl([ + ("tool", "purpose"), + ("argus_arsenal", "list every service, tool, and price"), + ("argus_scan", "launch a scan — returns job id + BTC invoice"), + ("argus_status", "poll a job's status / report / raw output"), + ("argus_jobs", "list jobs tied to your key"), + ])) + parts.append("""
+
+ +

Discovering the arsenal

+
+
GET/api/tools
+
The full armory. Every tool and service ARGUS can run, as JSON — for agents to introspect before choosing a scan.
+
""") + parts.append(_code("bash", SNIP_TOOLS_CURL)) + parts.append("""
+
+ +
+
GET/openapi.json
+
Machine-readable OpenAPI 3.0 spec of this entire surface. Feed it to any client generator.
+
+ +
+
POST/webhook/btcpay
+
Internal — BTCPay settlement webhook. Do not call directly; ARGUS uses it to fire scans and activate subscriptions on payment.
+
+ + +
+{BG_JS} +""") + return "".join(parts) + +# ─── OPENAPI SPEC ──────────────────────────────────────────────────────────── +OPENAPI = { + "openapi": "3.0.0", + "info": {"title": "ARGUS — Autonomous Security Agent", "version": "4.0.0", + "description": "Rentable AI offensive-security agent. Bitcoin-settled, agent-native. MCP endpoint for subscribers."}, + "servers": [{"url": "https://argus.thetempleofdoom.com"}], + "paths": { + "/api/keys": {"post": { + "summary": "Mint an API key", + "requestBody": {"required": True, "content": {"application/json": {"schema": { + "type": "object", "required": ["email"], + "properties": {"email": {"type": "string"}, "label": {"type": "string"}}}}}}, + "responses": {"200": {"description": "api_key issued"}}, + }}, + "/api/login": {"post": { + "summary": "Premium/owner login (permanent subscription key)", + "requestBody": {"required": True, "content": {"application/json": {"schema": { + "type": "object", "required": ["username", "password"], + "properties": {"username": {"type": "string"}, "password": {"type": "string"}}}}}}, + "responses": {"200": {"description": "premium api_key"}, "401": {"description": "invalid credentials"}}, + }}, + "/api/scan": {"post": { + "summary": "Submit a scan (returns BTC invoice)", + "parameters": [{"name": "X-API-Key", "in": "header", "schema": {"type": "string"}}], + "requestBody": {"required": True, "content": {"application/json": {"schema": { + "type": "object", "required": ["kind", "target", "email"], + "properties": { + "kind": {"type": "string", "enum": list(PRICES_SATS.keys())}, + "target": {"type": "string"}, "email": {"type": "string"}, + "username": {"type": "string"}, "password": {"type": "string"}, "domain": {"type": "string"}}}}}}, + "responses": {"200": {"description": "job_id + invoice + checkout link"}}, + }}, + "/api/job/{job_id}": {"get": { + "summary": "Poll job status", + "parameters": [{"name": "job_id", "in": "path", "required": True, "schema": {"type": "string"}}], + "responses": {"200": {"description": "status, report, raw"}}, + }}, + "/api/jobs": {"get": { + "summary": "List jobs for a key", + "parameters": [{"name": "key", "in": "query", "schema": {"type": "string"}}], + "responses": {"200": {"description": "array of jobs"}}, + }}, + "/api/subscribe": {"post": { + "summary": "Subscribe for MCP access (BTC invoice)", + "requestBody": {"required": True, "content": {"application/json": {"schema": { + "type": "object", "required": ["email", "plan"], + "properties": {"email": {"type": "string"}, + "plan": {"type": "string", "enum": list(MCP_PLANS.keys())}, + "api_key": {"type": "string"}}}}}}, + "responses": {"200": {"description": "api_key + invoice + checkout link"}}, + }}, + "/api/tools": {"get": {"summary": "Full arsenal inventory", "responses": {"200": {"description": "services + tools"}}}}, + }, +} + +# ─── MCP TOOL DEFINITIONS ──────────────────────────────────────────────────── +MCP_TOOLS = [ + {"name": "argus_arsenal", "description": "List every ARGUS service, tool, and price (sats). Use to introspect before launching a scan.", + "inputSchema": {"type": "object", "properties": {}, "required": []}}, + {"name": "argus_scan", "description": "Launch an offensive security scan. Returns a job id and a Bitcoin invoice; the scan fires once paid. kind: web-vuln | osint | ad-audit | creds | monitor.", + "inputSchema": {"type": "object", "required": ["kind", "target", "email"], + "properties": {"kind": {"type": "string", "enum": list(PRICES_SATS.keys())}, + "target": {"type": "string"}, + "email": {"type": "string"}, + "username": {"type": "string"}, + "password": {"type": "string"}, + "domain": {"type": "string"}}}}, + {"name": "argus_status", "description": "Poll a scan job by id — returns status, report, and raw tool output.", + "inputSchema": {"type": "object", "required": ["job_id"], + "properties": {"job_id": {"type": "string"}}}}, + {"name": "argus_jobs", "description": "List recent scan jobs tied to the calling API key.", + "inputSchema": {"type": "object", "properties": {}, "required": []}}, +] + # ─── FLASK ─────────────────────────────────────────────────────────────────── app = Flask(__name__) app.secret_key = SECRET @@ -385,6 +1010,12 @@ def index(): return render_template_string(landing()) @app.route("/capabilities") def capabilities(): return render_template_string(capabilities_page()) +@app.route("/docs") +def docs(): return render_template_string(docs_page()) + +@app.route("/openapi.json") +def openapi(): return jsonify(OPENAPI) + @app.route("/report/") def report(job_id): db = get_db() @@ -401,19 +1032,244 @@ def job_status(job_id): if not job: return jsonify({"error": "not found"}), 404 return jsonify({"status": job["status"], "report": job["report"], "raw": job["raw"]}) +@app.route("/api/jobs") +def job_list(): + key = request.args.get("key", "") + db = get_db() + if not key_valid(key): + return jsonify({"error": "valid API key required (?key=...)"}), 401 + rows = db.execute("""SELECT j.id,j.kind,j.target,j.status,j.created_at + FROM jobs j JOIN api_keys k ON k.email = j.email + WHERE k.key=? ORDER BY j.created_at DESC LIMIT 50""", (key,)).fetchall() + return jsonify([dict(r) for r in rows]) + +@app.route("/api/tools") +def api_tools(): + """Programmatic inventory of the full ARGUS arsenal (for agents).""" + out = {} + for cat, tools in ARSENAL: + out[cat] = [{"name": n, "desc": d} for n, d in tools] + return jsonify({"services": list(PRICES_SATS.keys()), "prices_sats": PRICES_SATS, + "mcp_plans": {k: {"sats": v["sats"], "label": v["label"]} for k, v in MCP_PLANS.items()}, + "arsenal": out}) + +@app.route("/api/keys", methods=["POST"]) +def api_keys(): + data = request.get_json(silent=True) or request.form + email = (data.get("email") or "").strip() + label = (data.get("label") or "").strip() + if not email or "@" not in email: + return jsonify({"error": "valid email required"}), 400 + key = mint_key(email, label) + return jsonify({"api_key": key, "email": email}) + +@app.route("/api/login", methods=["POST"]) +def api_login(): + """Premium/owner login — returns an admin key with a permanent subscription + so the owner drives the full API + /mcp without paying.""" + data = request.get_json(silent=True) or request.form + u = (data.get("username") or data.get("email") or "").strip() + p = (data.get("password") or "").strip() + if u != ADMIN_USER or p != ADMIN_PASS: + return jsonify({"error": "invalid credentials"}), 401 + db = get_db() + # reuse a stable admin key if one already has a permanent sub + row = db.execute("""SELECT s.key FROM subscriptions s + WHERE s.plan='premium' AND s.expires_at >= '2099-01-01T00:00:00+00:00' + LIMIT 1""").fetchone() + if row: + key = row["key"] + else: + key = "argus_admin_" + uuid.uuid4().hex + db.execute("INSERT OR IGNORE INTO api_keys (key,email,label) VALUES (?,?,?)", + (key, "drjones@thetempleofdoom.com", "premium admin")) + db.execute("""INSERT INTO subscriptions (key,email,plan,expires_at) VALUES (?,?,?,?) + ON CONFLICT(key) DO UPDATE SET plan='premium', expires_at=excluded.expires_at""", + (key, "drjones@thetempleofdoom.com", "premium", "2099-01-01T00:00:00+00:00")) + db.commit() + return jsonify({"api_key": key, "premium": True, "plan": "premium", + "expires_at": "2099-01-01T00:00:00+00:00", + "mcp_endpoint": f"{PUBLIC_BASE}/mcp"}) + +@app.route("/api/scan", methods=["POST"]) +def api_scan(): + data = request.get_json(silent=True) or request.form + key = request.headers.get("X-API-Key", "") or data.get("api_key", "") + if not key_valid(key): + return jsonify({"error": "valid API key required (X-API-Key header)"}), 401 + kind = data.get("kind", "web-vuln") + target = (data.get("target") or "").strip() + email = (data.get("email") or "").strip() + if kind not in PRICES_SATS: + return jsonify({"error": "unknown kind", "valid": list(PRICES_SATS.keys())}), 400 + if not target or not email: + return jsonify({"error": "target and email required"}), 400 + params = {"username": (data.get("username") or "").strip(), + "password": (data.get("password") or "").strip(), + "domain": (data.get("domain") or "").strip()} + sats = PRICES_SATS[kind] + job_id = uuid.uuid4().hex[:12] + db = get_db() + db.execute("INSERT INTO jobs (id,email,kind,target,status,params) VALUES (?,?,?,?,?,?)", + (job_id, email, kind, target, "unpaid", json.dumps(params))); db.commit() + link, inv_id = btcpay_create_invoice(sats, job_id, f"ARGUS {kind} {target}", + redirect=f"{PUBLIC_BASE}/report/{job_id}") + if not link: + return jsonify({"error": "payment init failed", "detail": inv_id}), 502 + db.execute("UPDATE jobs SET invoice_id=? WHERE id=?", (inv_id, job_id)); db.commit() + return jsonify({"job_id": job_id, "status": "unpaid", "sats": sats, + "checkout": link, "report_url": f"{PUBLIC_BASE}/report/{job_id}", + "poll": f"/api/job/{job_id}"}) + +@app.route("/api/subscribe", methods=["POST"]) +def api_subscribe(): + data = request.get_json(silent=True) or request.form + email = (data.get("email") or "").strip() + plan = (data.get("plan") or "mcp-weekly").strip() + key = (data.get("api_key") or "").strip() + if plan not in MCP_PLANS: + return jsonify({"error": "unknown plan", "valid": list(MCP_PLANS.keys())}), 400 + if not email or "@" not in email: + return jsonify({"error": "valid email required"}), 400 + if not key: + key = mint_key(email, "mcp") + elif not key_valid(key): + return jsonify({"error": "invalid api_key"}), 400 + sats = MCP_PLANS[plan]["sats"] + order_id = "sub_" + uuid.uuid4().hex[:12] + # persist the pending subscription intent keyed by order_id -> (key, email, plan) + db = get_db() + db.execute("""CREATE TABLE IF NOT EXISTS sub_orders ( + order_id TEXT PRIMARY KEY, api_key TEXT, email TEXT, plan TEXT, + created_at TEXT DEFAULT (datetime('now')))""") + db.execute("INSERT INTO sub_orders (order_id, api_key, email, plan) VALUES (?,?,?,?)", + (order_id, key, email, plan)); db.commit() + link, inv_id = btcpay_create_invoice(sats, order_id, f"ARGUS MCP subscription — {plan}", + redirect=f"{PUBLIC_BASE}/docs") + if not link: + return jsonify({"error": "payment init failed", "detail": inv_id}), 502 + return jsonify({"api_key": key, "plan": plan, "sats": sats, + "checkout": link, "mcp_endpoint": f"{PUBLIC_BASE}/mcp"}) + +@app.route("/subscribe", methods=["POST"]) +def subscribe_form(): + email = request.form.get("email", "").strip() + plan = request.form.get("plan", "mcp-weekly").strip() + key = request.form.get("api_key", "").strip() + if plan not in MCP_PLANS: + return render_template_string(landing() + ""), 400 + if not email or "@" not in email: + return render_template_string(landing() + ""), 400 + if not key: + key = mint_key(email, "mcp") + sats = MCP_PLANS[plan]["sats"] + order_id = "sub_" + uuid.uuid4().hex[:12] + db = get_db() + db.execute("""CREATE TABLE IF NOT EXISTS sub_orders ( + order_id TEXT PRIMARY KEY, api_key TEXT, email TEXT, plan TEXT, + created_at TEXT DEFAULT (datetime('now')))""") + db.execute("INSERT INTO sub_orders (order_id, api_key, email, plan) VALUES (?,?,?,?)", + (order_id, key, email, plan)); db.commit() + link, inv_id = btcpay_create_invoice(sats, order_id, f"ARGUS MCP subscription — {plan}", + redirect=f"{PUBLIC_BASE}/docs") + if link: + return f'' + return render_template_string(landing() + f"") + +@app.route("/mcp", methods=["POST", "GET"]) +def mcp_endpoint(): + """Model Context Protocol server over HTTP (JSON-RPC 2.0). Subscription-gated.""" + key = request.headers.get("X-API-Key", "") or request.args.get("key", "") + if not sub_active(key): + return jsonify({"jsonrpc": "2.0", "id": None, + "error": {"code": -32001, "message": "Active MCP subscription required. POST /api/subscribe to pay."}}), 402 + if request.method == "GET": + return jsonify({"name": "ARGUS", "protocol": "mcp", "version": "4.0.0", + "auth": "X-API-Key (subscription required)"}) + body = request.get_json(silent=True) + if not body: + return jsonify({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "invalid json"}}), 400 + method = body.get("method", "") + rid = body.get("id", None) + if method == "initialize": + return jsonify({"jsonrpc": "2.0", "id": rid, "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "ARGUS", "version": "4.0.0"}}}) + if method == "notifications/initialized": + return Response("", 202) + if method == "ping": + return jsonify({"jsonrpc": "2.0", "id": rid, "result": {}}) + if method == "tools/list": + return jsonify({"jsonrpc": "2.0", "id": rid, "result": {"tools": MCP_TOOLS}}) + if method == "tools/call": + p = body.get("params", {}) + name = p.get("name", "") + args = p.get("arguments", {}) or {} + try: + if name == "argus_arsenal": + out = {} + for cat, tools in ARSENAL: + out[cat] = [{"name": n, "desc": d} for n, d in tools] + text = json.dumps({"services": list(PRICES_SATS.keys()), "prices_sats": PRICES_SATS, "arsenal": out}, indent=2) + elif name == "argus_scan": + kind = args.get("kind", "web-vuln") + target = (args.get("target") or "").strip() + email = (args.get("email") or "").strip() + if kind not in PRICES_SATS: + text = json.dumps({"error": "unknown kind", "valid": list(PRICES_SATS.keys())}) + elif not target or not email: + text = json.dumps({"error": "target and email required"}) + else: + params = {"username": args.get("username", "").strip(), + "password": args.get("password", "").strip(), + "domain": args.get("domain", "").strip()} + job_id = uuid.uuid4().hex[:12] + db = get_db() + db.execute("INSERT INTO jobs (id,email,kind,target,status,params) VALUES (?,?,?,?,?,?)", + (job_id, email, kind, target, "unpaid", json.dumps(params))); db.commit() + link, inv_id = btcpay_create_invoice(PRICES_SATS[kind], job_id, f"ARGUS {kind} {target}", + redirect=f"{PUBLIC_BASE}/report/{job_id}") + db.execute("UPDATE jobs SET invoice_id=? WHERE id=?", (inv_id, job_id)); db.commit() + text = json.dumps({"job_id": job_id, "status": "unpaid", "sats": PRICES_SATS[kind], + "checkout": link, "report_url": f"{PUBLIC_BASE}/report/{job_id}", + "poll": f"/api/job/{job_id}"}) + elif name == "argus_status": + job_id = args.get("job_id", "") + db = get_db() + job = db.execute("SELECT id,status,report,raw FROM jobs WHERE id=?", (job_id,)).fetchone() + text = json.dumps(dict(job)) if job else json.dumps({"error": "not found"}) + elif name == "argus_jobs": + email = key_email(key) + db = get_db() + rows = db.execute("SELECT id,kind,target,status,created_at FROM jobs WHERE email=? ORDER BY created_at DESC LIMIT 50", (email,)).fetchall() + text = json.dumps([dict(r) for r in rows]) + else: + text = json.dumps({"error": f"unknown tool '{name}'"}) + return jsonify({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": text}]}}) + except Exception as e: + return jsonify({"jsonrpc": "2.0", "id": rid, "error": {"code": -32000, "message": str(e)}}) + return jsonify({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"method not found: {method}"}}) + @app.route("/submit", methods=["POST"]) def submit(): kind = request.form.get("kind", "web-vuln") target = request.form.get("target", "").strip() email = request.form.get("email", "").strip() + params = { + "username": request.form.get("username", "").strip(), + "password": request.form.get("password", "").strip(), + "domain": request.form.get("domain", "").strip(), + } if not target or not email: return render_template_string(landing() + ""), 400 sats = PRICES_SATS.get(kind, 4000) job_id = uuid.uuid4().hex[:12] db = get_db() - db.execute("INSERT INTO jobs (id,email,kind,target,status) VALUES (?,?,?,?,?)", - (job_id, email, kind, target, "unpaid")); db.commit() - link, inv_id = btcpay_create_invoice(sats, job_id, f"ARGUS {kind} {target}") + db.execute("INSERT INTO jobs (id,email,kind,target,status,params) VALUES (?,?,?,?,?,?)", + (job_id, email, kind, target, "unpaid", json.dumps(params))); db.commit() + link, inv_id = btcpay_create_invoice(sats, job_id, f"ARGUS {kind} {target}", + redirect=f"{PUBLIC_BASE}/report/{job_id}") if link: db.execute("UPDATE jobs SET invoice_id=? WHERE id=?", (inv_id, job_id)); db.commit() return f'' @@ -426,8 +1282,20 @@ def btcpay_webhook(): return "bad sig", 401 data = request.get_json(force=True) if data.get("type") == "InvoiceSettled": - order_id = (data.get("invoice", {}).get("metadata", {}) or {}).get("orderId") + inv = data.get("invoice", {}) + order_id = (inv.get("metadata", {}) or {}).get("orderId") + if not order_id: + return "ok", 200 db = get_db() + # subscription settlement + if order_id.startswith("sub_"): + row = db.execute("SELECT * FROM sub_orders WHERE order_id=?", (order_id,)).fetchone() + if row: + exp = extend_subscription(row["api_key"], row["email"], row["plan"]) + db.execute("DELETE FROM sub_orders WHERE order_id=?", (order_id,)) + db.commit() + return "ok", 200 + # scan settlement job = db.execute("SELECT * FROM jobs WHERE id=?", (order_id,)).fetchone() if job and job["status"] == "unpaid": db.execute("UPDATE jobs SET status='running' WHERE id=?", (order_id,)); db.commit() diff --git a/argus.service b/argus.service new file mode 100644 index 0000000..27166bb --- /dev/null +++ b/argus.service @@ -0,0 +1,13 @@ +[Unit] +Description=ARGUS Security Platform +After=network.target + +[Service] +WorkingDirectory=/opt/argus +ExecStart=/usr/bin/python3 /opt/argus/app.py +Restart=always +RestartSec=3 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target diff --git a/nginx-argus.conf b/nginx-argus.conf new file mode 100644 index 0000000..4a7ba0a --- /dev/null +++ b/nginx-argus.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name argus.thetempleofdoom.com; + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 600s; + } +}