#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
"""
SecuBox-Deb :: secubox-threatfeed  (Phase 1 — sovereign threat-intel #728)
CyberMind — https://cybermind.fr

Pulls FREE, no-enrollment public IP/CIDR/domain blocklists and writes them into
the shared `threat_intel` table (toolbox.db) that secubox-blacklist-sync already
drains into the nft blacklist. This REPLACES the CrowdSec CAPI community feed
with self-sourced lists — no central account, no paywall, no IP blocklisting.

Pure stdlib (urllib) — no extra Debian deps. Idempotent, safe on a timer.
"""
import os
import re
import sqlite3
import sys
import time
import urllib.request
from pathlib import Path

DB = Path(os.environ.get("SECUBOX_TI_DB", "/var/lib/secubox/toolbox/toolbox.db"))
TIMEOUT = int(os.environ.get("SECUBOX_TF_TIMEOUT", "30"))
UA = "SecuBox-ThreatFeed/1.0 (+https://secubox.in)"
TTL_DAYS = int(os.environ.get("SECUBOX_TF_TTL_DAYS", "10"))

# (name, url, ioc_type, weight). All free + no enrollment. CIDRs allowed for ip.
FEEDS = [
    ("feodo",          "https://feodotracker.abuse.ch/downloads/ipblocklist.txt",            "ip", 90),
    ("sslbl",          "https://sslbl.abuse.ch/blacklist/sslipblacklist.txt",                "ip", 85),
    ("firehol1",       "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset", "ip", 80),
    ("spamhaus-drop",  "https://www.spamhaus.org/drop/drop.txt",                             "ip", 85),
    ("blocklist-de",   "https://lists.blocklist.de/lists/all.txt",                           "ip", 60),
    ("cins-army",      "https://cinsscore.com/list/ci-badguys.txt",                          "ip", 70),
    ("et-compromised", "https://rules.emergingthreats.net/blockrules/compromised-ips.txt",   "ip", 75),
    ("dshield",        "https://feeds.dshield.org/block.txt",                                "ip", 65),
]
# Optional (set SECUBOX_TF_TOR=1 to also pull Tor exit nodes)
if os.environ.get("SECUBOX_TF_TOR") == "1":
    FEEDS.append(("tor-exit", "https://check.torproject.org/torbulkexitlist", "ip", 40))

_IPV4 = re.compile(r"^\s*(\d{1,3}(?:\.\d{1,3}){3}(?:/\d{1,2})?)")
_IPV6 = re.compile(r"^\s*([0-9A-Fa-f:]{2,}(?:/\d{1,3})?)\s*$")


def log(msg):
    print(f"[threatfeed] {msg}", file=sys.stderr, flush=True)


def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
        return r.read().decode("utf-8", "replace")


def parse_ips(text):
    out = []
    for line in text.splitlines():
        s = line.strip()
        if not s or s.startswith("#") or s.startswith(";"):
            continue
        # spamhaus DROP: "1.2.3.0/24 ; SBL..."  -> take the first field
        token = s.split(";")[0].split()[0].strip() if (";" in s or " " in s) else s
        m = _IPV4.match(token)
        if m:
            out.append((m.group(1), "ip"))
            continue
        m = _IPV6.match(token)
        if m and ":" in m.group(1):
            out.append((m.group(1), "ip"))
    return out


def ensure_schema(c):
    c.execute("""CREATE TABLE IF NOT EXISTS threat_intel (
        ioc TEXT NOT NULL, ioc_type TEXT NOT NULL, source TEXT NOT NULL,
        weight INTEGER NOT NULL DEFAULT 50, label TEXT,
        first_seen INTEGER, last_seen INTEGER,
        PRIMARY KEY (ioc, ioc_type, source))""")


def main():
    if not DB.exists():
        log(f"db missing: {DB}")
        return 1
    now = int(time.time())
    total = 0
    with sqlite3.connect(DB, timeout=20) as c:
        ensure_schema(c)
        for name, url, ioc_type, weight in FEEDS:
            src = f"feed:{name}"
            try:
                iocs = parse_ips(fetch(url))
            except Exception as e:
                log(f"{name}: fetch failed: {e}")
                continue
            n = 0
            for ioc, t in iocs:
                c.execute(
                    "INSERT INTO threat_intel(ioc,ioc_type,source,weight,label,first_seen,last_seen) "
                    "VALUES(?,?,?,?,?,?,?) "
                    "ON CONFLICT(ioc,ioc_type,source) DO UPDATE SET last_seen=excluded.last_seen, weight=excluded.weight",
                    (ioc, t, src, weight, name, now, now))
                n += 1
            log(f"{name}: {n} iocs")
            total += n
        # prune stale feed entries (older than TTL) — keeps the set fresh.
        cutoff = now - TTL_DAYS * 86400
        pruned = c.execute(
            "DELETE FROM threat_intel WHERE source LIKE 'feed:%' AND last_seen < ?",
            (cutoff,)).rowcount
        c.commit()
    log(f"done: {total} iocs across {len(FEEDS)} feeds, pruned {pruned} stale")
    return 0


if __name__ == "__main__":
    sys.exit(main())
