Files
threatmarket/app.py
2026-10-06 23:43:38 -07:00

255 lines
9.8 KiB
Python

#!/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 = """
<footer>
THREATMARKET &middot; real scans, real data, zero fabrication<br>
<a href="https://buymeacoffee.com/r26xrthzttg" style="color:#e3b341">&#9749; Buy Me a Coffee</a>
</footer>"""
def page(title, body):
return render_template_string(
"<!doctype html><html><head><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'>"
"<title>{{t}}</title><style>{{css}}</style></head><body><div class=wrap>{{body|safe}}</div>{{footer}}</body></html>",
t=title, css=BASE_CSS, body=body, footer=FOOTER,
)
@app.get("/")
def index():
rows = ""
for p in packs():
cats = "".join(f'<span class="badge">{c}</span>' for c in p["categories"])
counts = " &middot; ".join(f'{k}: <span class="count">{v}</span>' for k, v in p["finding_counts"].items())
rows += f"""<div class=card><div class=grid>
<div><a href=/buy/{p['pack_id']}><b>{p['domain']}</b></a>{cats}<br>
<span class=tag>{p['date']} &middot; {counts}</span><br>
<span class=sha>sha256 {p['sha256'][:32]}&hellip; &middot; {p['size_bytes']//1024} KB</span></div>
<div style=text-align:right><div class=price>${PACK_PRICE_USD}</div><a class=btn href=/buy/{p['pack_id']}>Buy pack</a></div>
</div></div>"""
body = f"""<h1>THREAT<span class=tm>MARKET</span></h1>
<p class=tag>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.</p>
{rows or '<p class=tag>No packs archived yet — the pipeline runs 2x daily.</p>'}"""
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/<pack_id>")
def buy_page(pack_id):
p = get_pack(pack_id)
if not p:
return page("404", "<h1>Pack not found</h1>"), 404
counts = " &middot; ".join(f"{k}: {v}" for k, v in p["finding_counts"].items())
body = f"""<h1>Pack: {p['domain']}</h1>
<div class=card><span class=tag>{p['date']} &middot; {counts}</span><br>
<span class=sha>sha256 {p['sha256']} &middot; {p['size_bytes']//1024} KB zip</span></div>
<p>Price <span class=price>${PACK_PRICE_USD}</span> &middot; paid in BTC over BTCPay.
After settlement you get a one-time download link for the pack zip.</p>
<form method=post action=/buy/{pack_id}><button class=btn type=submit>Pay ${PACK_PRICE_USD} with Bitcoin</button></form>"""
return page(f"Buy {pack_id}", body)
@app.post("/buy/<pack_id>")
def buy_go(pack_id):
p = get_pack(pack_id)
if not p:
return page("404", "<h1>Pack not found</h1>"), 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/<token>")
def status(token):
with db() as con:
row = con.execute("SELECT * FROM purchases WHERE token=?", (token,)).fetchone()
if not row:
return page("404", "<h1>Unknown order</h1>"), 404
if row["status"] == "paid" and not row["downloaded"]:
body = f"""<h1>Payment confirmed &#9989;</h1><p>Your one-time download is ready.
The link dies after the first successful download.</p>
<a class=btn href=/download/{row['pack_id']}/{token}>Download {row['pack_id']}.zip</a>"""
elif row["status"] == "paid":
body = "<h1>Already downloaded</h1><p>This one-time link has been consumed.</p>"
else:
body = f"""<h1>Awaiting payment&hellip;</h1><p>Invoice <code>{row['invoice_id']}</code> is
{row['status']}. This page refreshes every 15s.</p>
<meta http-equiv=refresh content=15>"""
return page("Order status", body)
@app.get("/download/<pack_id>/<token>")
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)