#!/usr/bin/env python3
"""UniSOC Honeypot — vérification licence VM au boot.

Au démarrage de la VM, AVANT que les services honeypot ne démarrent :

  1. Calcule fingerprint = sha256(product_uuid + board_serial + primary_mac + first_boot_iso).
     `first_boot_iso` est persisté à l'install et ne change pas, même après reboot.
  2. Si /etc/unisoc-honeypot/license.json existe et signature HMAC-SHA256 valide
     ET que le fingerprint dedans correspond → mode ACTIF, services autorisés.
  3. Sinon → mode LOCK :
        - stop + disable les 8 services honeypot
        - laisse actifs : ssh (port 22222 admin) + unisoc-honeypot-agent
        - enroll auprès du SOC avec la pre-enrollment key
        - écrit l'état pour que l'agent push le fingerprint en heartbeat

Layout :
    /etc/unisoc-honeypot/agent.env           (UNISOC_API + ENROLL_KEY + …)
    /etc/unisoc-honeypot/license.json        (license_token + payload + applied_at)
    /var/lib/unisoc-honeypot-agent/first_boot.iso     (timestamp 1er install, fixe)
    /var/lib/unisoc-honeypot-agent/fingerprint.txt    (cache du fingerprint courant)
    /var/lib/unisoc-honeypot-agent/license_state.json (mode + détails pour heartbeat)
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import logging
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

LOG_FILE = "/var/log/unisoc-honeypot-license.log"
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()],
)
log = logging.getLogger("unisoc-honeypot-license-check")

CONF_DIR = Path("/etc/unisoc-honeypot-agent")
LICENSE_FILE = CONF_DIR / "license.json"
STATE_DIR = Path("/var/lib/unisoc-honeypot-agent")
FIRST_BOOT_FILE = STATE_DIR / "first_boot.iso"
FINGERPRINT_FILE = STATE_DIR / "fingerprint.txt"
LICENSE_STATE_FILE = STATE_DIR / "license_state.json"

CONF_PATH = CONF_DIR / "agent.conf"

# Services honeypot à mettre en LOCK (stoppés tant que la licence n'est pas active)
LOCKED_SERVICES = [
    "opencanary", "cowrie", "veeam-fake", "ssh-tarpit",
    "rdp-recorder", "smbd", "proftpd-fake", "file-watcher",
    "xrdp", "http-honeytrap",
]
# Restent UP en lock : ssh (port 22222 admin) + unisoc-honeypot-agent (qui parle au SOC)


def _read_conf(path: Path) -> dict:
    cfg = {}
    if not path.exists():
        return cfg
    for line in path.read_text(errors="replace").splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" in line:
            k, v = line.split("=", 1)
            cfg[k.strip()] = v.strip().strip('"').strip("'")
    return cfg


CFG = _read_conf(CONF_PATH)
API = (os.environ.get("UNISOC_API") or CFG.get("UNISOC_API") or "https://api.unisoc.fr").rstrip("/")
ENROLLMENT_KEY = (
    os.environ.get("UNISOC_HONEYPOT_ENROLL_KEY")
    or CFG.get("UNISOC_HONEYPOT_ENROLL_KEY")
    or "unisoc_enroll_template_v1_change_me_in_prod"
)
LICENSE_SIGNING_KEY = (
    os.environ.get("UNISOC_HONEYPOT_LICENSE_KEY")
    or CFG.get("UNISOC_HONEYPOT_LICENSE_KEY")
    or "unisoc_honeypot_signing_secret_v1_change_me_in_prod"
).encode()

STATE_DIR.mkdir(parents=True, exist_ok=True)
CONF_DIR.mkdir(parents=True, exist_ok=True)


# ─────────────────────────────────────────────────────────────────────────────
# Fingerprint VM
# ─────────────────────────────────────────────────────────────────────────────


def _read_dmi(field: str) -> str:
    try:
        return Path(f"/sys/class/dmi/id/{field}").read_text().strip()
    except Exception:
        return ""


def _primary_mac() -> str:
    """MAC de la première interface non-loopback."""
    try:
        for iface_dir in sorted(Path("/sys/class/net").iterdir()):
            name = iface_dir.name
            if name == "lo":
                continue
            mac = (iface_dir / "address").read_text().strip()
            if mac and mac != "00:00:00:00:00:00":
                return mac
    except Exception:
        pass
    return ""


def _ensure_first_boot_iso() -> str:
    """Persiste le timestamp du tout premier boot. Ne change jamais après."""
    if FIRST_BOOT_FILE.exists():
        return FIRST_BOOT_FILE.read_text().strip()
    iso = datetime.now(timezone.utc).isoformat()
    FIRST_BOOT_FILE.write_text(iso)
    return iso


def collect_dmi() -> dict[str, str]:
    return {
        "product_uuid":  _read_dmi("product_uuid"),
        "product_serial": _read_dmi("product_serial"),
        "board_serial":  _read_dmi("board_serial"),
        "board_vendor":  _read_dmi("board_vendor"),
        "product_name":  _read_dmi("product_name"),
        "bios_version":  _read_dmi("bios_version"),
        "sys_vendor":    _read_dmi("sys_vendor"),
    }


def compute_fingerprint() -> tuple[str, dict]:
    """SHA256 stable des attributs VM. Réécrit le cache si on a un fingerprint stable."""
    dmi = collect_dmi()
    mac = _primary_mac()
    first_boot = _ensure_first_boot_iso()
    payload = "|".join([
        dmi.get("product_uuid", ""),
        dmi.get("board_serial", ""),
        dmi.get("product_serial", ""),
        mac,
        first_boot,
    ])
    fp = "sha256:" + hashlib.sha256(payload.encode()).hexdigest()
    FINGERPRINT_FILE.write_text(fp)
    info = {
        "fingerprint": fp,
        "first_boot_iso": first_boot,
        "primary_mac": mac,
        "dmi": dmi,
    }
    return fp, info


# ─────────────────────────────────────────────────────────────────────────────
# License token (HMAC-SHA256)
# ─────────────────────────────────────────────────────────────────────────────


def _b64u_decode(s: str) -> bytes:
    pad = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)


def verify_license_token(token: str, fingerprint: str) -> dict | None:
    """Retourne le payload si signature OK ET fingerprint matche."""
    try:
        p_b64, s_b64 = token.split(".", 1)
        payload_bytes = _b64u_decode(p_b64)
        sig = _b64u_decode(s_b64)
        expected = hmac.new(LICENSE_SIGNING_KEY, payload_bytes, hashlib.sha256).digest()
        if not hmac.compare_digest(sig, expected):
            log.warning("license signature MISMATCH")
            return None
        payload = json.loads(payload_bytes)
        if payload.get("fingerprint") != fingerprint:
            log.warning("fingerprint mismatch licence=%s vm=%s",
                        payload.get("fingerprint"), fingerprint)
            return None
        return payload
    except Exception as e:
        log.warning("license parse error: %r", e)
        return None


# ─────────────────────────────────────────────────────────────────────────────
# Lock / Unlock services
# ─────────────────────────────────────────────────────────────────────────────


def systemctl(*args: str) -> int:
    try:
        r = subprocess.run(["systemctl", *args], capture_output=True, timeout=15, text=True)
        return r.returncode
    except Exception as e:
        log.warning("systemctl %s err: %r", args, e)
        return 1


def lock_services() -> None:
    """Stop + disable tous les services honeypot. Laisse ssh + agent UP."""
    log.warning("LOCK MODE — stopping honeypot services until activation")
    for svc in LOCKED_SERVICES:
        systemctl("--no-block", "stop", svc)
        systemctl("disable", svc)


def unlock_services() -> None:
    """Enable + start tous les services honeypot.

    IMPORTANT : on utilise `--no-block` car license_check.service a une directive
    `Before=opencanary.service cowrie.service …` qui crée un cycle sinon :
    systemctl start <svc> attend que <svc> finisse de démarrer, mais <svc>
    a After=license-check.service qui ne peut pas finir tant qu'il `wait` →
    deadlock 15s timeout × N services. Avec --no-block, on enqueue les démarrages
    et license-check peut se terminer."""
    log.info("UNLOCK MODE — enabling honeypot services (--no-block)")
    for svc in LOCKED_SERVICES:
        systemctl("enable", svc)
        systemctl("--no-block", "start", svc)


# ─────────────────────────────────────────────────────────────────────────────
# Enrollment auprès du SOC
# ─────────────────────────────────────────────────────────────────────────────


def enroll_with_soc(fingerprint: str, info: dict) -> bool:
    body = {
        "fingerprint": fingerprint,
        "hostname": os.uname().nodename,
        "agent_version": "license_check/0.1",
        "dmi": info.get("dmi"),
        "macs": [info.get("primary_mac")] if info.get("primary_mac") else [],
    }
    headers = {
        "X-Enrollment-Key": ENROLLMENT_KEY,
        "Content-Type": "application/json",
        "User-Agent": "unisoc-honeypot-license-check/0.1",
    }
    req = urllib.request.Request(
        f"{API}/api/honeypot/enroll",
        data=json.dumps(body).encode(),
        headers=headers,
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            log.info("enroll OK fp=%s status=%d", fingerprint[:24], r.status)
            return True
    except urllib.error.HTTPError as e:
        log.warning("enroll HTTPError %d: %s", e.code, e.read()[:200])
    except Exception as e:
        log.warning("enroll error: %r", e)
    return False


# ─────────────────────────────────────────────────────────────────────────────
# Boucle principale
# ─────────────────────────────────────────────────────────────────────────────


def write_state(state: dict) -> None:
    LICENSE_STATE_FILE.write_text(json.dumps(state, indent=2))


def main() -> int:
    fingerprint, info = compute_fingerprint()
    log.info("fingerprint=%s mac=%s first_boot=%s",
             fingerprint[:24], info.get("primary_mac"), info.get("first_boot_iso"))

    # 1. Tente de valider une licence existante
    if LICENSE_FILE.exists():
        try:
            doc = json.loads(LICENSE_FILE.read_text())
            token = doc.get("license_token") or ""
            payload = verify_license_token(token, fingerprint)
            if payload:
                log.info("LICENSE OK tenant=%s honeypot_id=%s issued_at=%s",
                         payload.get("tenant_id"), payload.get("honeypot_id"),
                         payload.get("issued_at"))
                unlock_services()
                write_state({
                    "mode": "active",
                    "fingerprint": fingerprint,
                    "tenant_id": payload.get("tenant_id"),
                    "honeypot_id": payload.get("honeypot_id"),
                    "issued_at": payload.get("issued_at"),
                    "checked_at": datetime.now(timezone.utc).isoformat(),
                    "first_boot_iso": info.get("first_boot_iso"),
                    "primary_mac": info.get("primary_mac"),
                })
                return 0
            else:
                log.warning("invalid license — falling back to LOCK")
        except Exception as e:
            log.warning("license file unreadable: %r — falling back to LOCK", e)

    # 2. Pas de licence valide → enroll + LOCK
    enrolled = enroll_with_soc(fingerprint, info)
    lock_services()
    write_state({
        "mode": "locked",
        "fingerprint": fingerprint,
        "first_boot_iso": info.get("first_boot_iso"),
        "primary_mac": info.get("primary_mac"),
        "dmi": info.get("dmi"),
        "enrolled": enrolled,
        "checked_at": datetime.now(timezone.utc).isoformat(),
        "message": (
            "VM en attente d'activation. Allez sur https://client.unisoc.fr/ → "
            "Mon Réseau → Honeypot → activer cette VM (fingerprint ci-dessus)."
        ),
    })
    print(f"\n[UNISOC HONEYPOT] LOCK MODE", file=sys.stderr)
    print(f"[UNISOC HONEYPOT] Fingerprint: {fingerprint}", file=sys.stderr)
    print(f"[UNISOC HONEYPOT] Activez la VM sur https://client.unisoc.fr/ (Mon Réseau → Honeypot)", file=sys.stderr)
    return 0


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