#!/opt/threatmarket/venv/bin/python3 """THREATMARKET — threat-intel marketplace. Metadata public, payloads paid.""" import hashlib import hmac import json import os import secrets import sqlite3 import time import requests from flask import Flask, Response, jsonify, redirect, render_template_string, request, send_file BASE = os.environ.get("TM_BASE", "/opt/threatmarket") ARCHIVE = os.path.join(BASE, "archive") MANIFEST = os.path.join(ARCHIVE, "manifest.json") DB = os.path.join(BASE, "threatmarket.db") BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140").rstrip("/") BTCPAY_STORE = os.environ["BTCPAY_STORE"] BTCPAY_KEY = os.environ["BTCPAY_KEY"] WEBHOOK_SECRET = os.environ["BTCPAY_WEBHOOK_SECRET"] PACK_PRICE_USD = os.environ.get("TM_PRICE_USD", "12.00") app = Flask(__name__) def db(): con = sqlite3.connect(DB, timeout=10) con.row_factory = sqlite3.Row return con def init_db(): with db() as con: con.execute( """CREATE TABLE IF NOT EXISTS purchases ( token TEXT PRIMARY KEY, pack_id TEXT, invoice_id TEXT UNIQUE, status TEXT, created INTEGER, downloaded INTEGER DEFAULT 0)""" ) def packs(): with open(MANIFEST) as f: return json.load(f) def get_pack(pack_id): for p in packs(): if p["pack_id"] == pack_id: return p return None def file_sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 16), b""): h.update(chunk) return h.hexdigest() def btcpay_invoice(pack_id, token): r = requests.post( f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices", headers={"Authorization": f"token {BTCPAY_KEY}"}, json={ "amount": PACK_PRICE_USD, "currency": "USD", "metadata": {"orderId": token, "itemDesc": f"THREATMARKET intel pack {pack_id}"}, "checkout": {"redirectURL": f"http://10.30.20.102/status/{token}", "redirectAutomatically": True}, }, verify=False, timeout=30, ) r.raise_for_status() return r.json() # ---------- pages ---------- BASE_CSS = """ body{background:#0a0e14;color:#c9d1d9;font-family:'SF Mono',ui-monospace,Menlo,monospace;margin:0} a{color:#58a6ff;text-decoration:none}a:hover{text-decoration:underline} .wrap{max-width:960px;margin:0 auto;padding:40px 24px} h1{color:#f0f6fc;letter-spacing:.18em}h1 .tm{color:#3fb950} .tag{color:#8b949e;font-size:14px} .card{background:#11161f;border:1px solid #1f2733;border-radius:10px;padding:18px 22px;margin:14px 0} .card:hover{border-color:#3fb950} .grid{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap} .badge{background:#12261e;color:#3fb950;border:1px solid #1f4d33;border-radius:99px;padding:2px 10px;font-size:12px;margin-left:6px} .count{color:#58a6ff;font-weight:bold} .btn{display:inline-block;background:#238636;color:#fff;border-radius:8px;padding:10px 22px;font-weight:bold} .btn:hover{text-decoration:none;background:#2ea043} .price{color:#e3b341;font-size:20px;font-weight:bold} footer{border-top:1px solid #1f2733;margin-top:48px;padding:22px 24px;text-align:center;color:#8b949e;font-size:13px} .sha{color:#8b949e;font-size:11px;word-break:break-all} """ FOOTER = """ """ def page(title, body): return render_template_string( "" "{{t}}
{{body|safe}}
{{footer}}", t=title, css=BASE_CSS, body=body, footer=FOOTER, ) @app.get("/") def index(): rows = "" for p in packs(): cats = "".join(f'{c}' for c in p["categories"]) counts = " · ".join(f'{k}: {v}' for k, v in p["finding_counts"].items()) rows += f"""
{p['domain']}{cats}
{p['date']} · {counts}
sha256 {p['sha256'][:32]}… · {p['size_bytes']//1024} KB
${PACK_PRICE_USD}
Buy pack
""" body = f"""

THREATMARKET

Threat-intel packs harvested by a real scanning fleet. Certificate-transparency logs, DNS liveness, exposed-service inventory. Metadata below is public; payloads unlock after BTC payment.

{rows or '

No packs archived yet — the pipeline runs 2x daily.

'}""" return page("THREATMARKET", body) @app.get("/archive") def archive(): out = [ {k: p[k] for k in ("pack_id", "domain", "date", "categories", "finding_counts", "size_bytes", "sha256")} for p in packs() ] return jsonify(out) @app.get("/buy/") def buy_page(pack_id): p = get_pack(pack_id) if not p: return page("404", "

Pack not found

"), 404 counts = " · ".join(f"{k}: {v}" for k, v in p["finding_counts"].items()) body = f"""

Pack: {p['domain']}

{p['date']} · {counts}
sha256 {p['sha256']} · {p['size_bytes']//1024} KB zip

Price ${PACK_PRICE_USD} · paid in BTC over BTCPay. After settlement you get a one-time download link for the pack zip.

""" return page(f"Buy {pack_id}", body) @app.post("/buy/") def buy_go(pack_id): p = get_pack(pack_id) if not p: return page("404", "

Pack not found

"), 404 token = secrets.token_urlsafe(24) inv = btcpay_invoice(pack_id, token) with db() as con: con.execute( "INSERT INTO purchases (token, pack_id, invoice_id, status, created) VALUES (?,?,?,?,?)", (token, pack_id, inv["id"], "pending", int(time.time())), ) return redirect(inv["checkoutLink"]) @app.get("/status/") def status(token): with db() as con: row = con.execute("SELECT * FROM purchases WHERE token=?", (token,)).fetchone() if not row: return page("404", "

Unknown order

"), 404 if row["status"] == "paid" and not row["downloaded"]: body = f"""

Payment confirmed ✅

Your one-time download is ready. The link dies after the first successful download.

Download {row['pack_id']}.zip""" elif row["status"] == "paid": body = "

Already downloaded

This one-time link has been consumed.

" else: body = f"""

Awaiting payment…

Invoice {row['invoice_id']} is {row['status']}. This page refreshes every 15s.

""" return page("Order status", body) @app.get("/download//") def download(pack_id, token): with db() as con: row = con.execute("SELECT * FROM purchases WHERE token=?", (token,)).fetchone() if not row or row["pack_id"] != pack_id: return jsonify(error="invalid token"), 403 if row["status"] != "paid": return jsonify(error="payment not confirmed"), 402 if row["downloaded"]: return jsonify(error="one-time link already used"), 410 con.execute("UPDATE purchases SET downloaded=1 WHERE token=?", (token,)) p = get_pack(pack_id) path = os.path.join(ARCHIVE, p["file"]) return send_file(path, as_attachment=True, download_name=f"{pack_id}.zip") @app.post("/webhook/btcpay") def webhook(): sig = request.headers.get("BTCPay-Sig", "") expected = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), request.get_data(), hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): return jsonify(error="bad signature"), 400 data = request.get_json(force=True) if data.get("type") == "InvoiceSettled": inv = data["invoiceId"] with db() as con: con.execute("UPDATE purchases SET status='paid' WHERE invoice_id=? AND status='pending'", (inv,)) return "", 200 @app.get("/health") def health(): return jsonify(status="ok", service="threatmarket", packs=len(packs())) @app.get("/robots.txt") def robots(): return Response("User-agent: *\nAllow: /\nDisallow: /download/\n", mimetype="text/plain") @app.get("/llms.txt") def llms(): return Response( "# THREATMARKET\n\n" "Threat-intel marketplace selling archived reconnaissance packs (certificate-transparency " "subdomain enumeration, DNS liveness, exposed-service inventory) collected by real fleet scans.\n\n" "## Endpoints\n" "- GET /: landing page, pack catalog with metadata only\n" "- GET /archive: JSON list of packs (domain, date, categories, finding counts; no payloads)\n" "- GET|POST /buy/{PACK_ID}: create a BTCPay invoice, pay in BTC\n" "- GET /status/{TOKEN}: order status; one-time download link after settlement\n" "- GET /download/{PACK_ID}/{TOKEN}: one-time zip download, requires webhook-confirmed payment\n" "- GET /health: service health JSON\n\n" "## Rules\n" "- Public pages expose metadata/counts only; payloads require payment.\n" "- Every finding comes from a real scan; empty scans ship as empty. Zero fabricated data.\n", mimetype="text/plain", ) init_db() if __name__ == "__main__": app.run(host="127.0.0.1", port=8500)