#!/usr/bin/env python3
"""
SYLink PenTest VM — init au boot.
- Calcule le machine_fingerprint depuis DMI + machine-id + MAC primaire
- Demande au backend un token signé HMAC
- Génère le QR ASCII (qrencode) avec URL d'activation
- Écrit /etc/issue (banner console) + /etc/motd (login SSH) avec QR + code court
- Persiste config dans /opt/sylink-pentest/etc/

Service systemd : Type=oneshot, Before agent + setup-ui.
"""
from __future__ import annotations

import hashlib
import json
import os
import subprocess
import sys
import urllib.request
from pathlib import Path
from datetime import datetime, timezone

CONFIG_DIR = Path("/opt/sylink-pentest/etc")
LOG_DIR = Path("/opt/sylink-pentest/log")
PT_API_BASE = os.environ.get("PT_API_BASE", "https://pentest.unisoc.fr")
PORTAL_BASE = os.environ.get("PORTAL_BASE", "https://client.unisoc.fr")


def log(msg: str):
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    line = f"[{datetime.now(timezone.utc).isoformat()}] {msg}"
    print(line)
    with (LOG_DIR / "init.log").open("a") as f:
        f.write(line + "\n")


def read_first_line(path: str) -> str:
    try:
        with open(path) as f:
            return f.readline().strip()
    except Exception:
        return ""


def primary_mac() -> str:
    """1ère interface non-loopback, non-virtuelle."""
    try:
        out = subprocess.run(["ip", "-o", "link"], capture_output=True, text=True, timeout=5).stdout
        for line in out.splitlines():
            parts = line.split()
            if len(parts) > 1 and parts[1].rstrip(":") not in ("lo",) and "veth" not in parts[1]:
                # cherche link/ether
                for i, p in enumerate(parts):
                    if p == "link/ether" and i + 1 < len(parts):
                        return parts[i + 1]
    except Exception:
        pass
    return "00:00:00:00:00:00"


def compute_fingerprint() -> str:
    seeds = [
        read_first_line("/sys/class/dmi/id/product_uuid") or "no-dmi",
        read_first_line("/etc/machine-id") or "no-machine-id",
        primary_mac(),
        "pentest-scope",
    ]
    seed_str = "|".join(seeds)
    return hashlib.sha256(seed_str.encode()).hexdigest()


def detect_hypervisor() -> str:
    """Détecte l'hyperviseur. Override possible via /etc/sylink-pentest-hypervisor."""
    import subprocess as _sp
    for path in ("/etc/sylink-pentest-hypervisor", "/opt/sylink-pentest/etc/hypervisor"):
        if Path(path).exists():
            v = read_first_line(path).lower().strip()
            if v in ("vmware", "proxmox", "hyperv", "kvm", "virtualbox", "xen"):
                return v
    try:
        r = _sp.run(["systemd-detect-virt"], capture_output=True, text=True, timeout=4)
        v = r.stdout.strip().lower()
        if v == "vmware": return "vmware"
        if v == "microsoft": return "hyperv"
        if v == "xen": return "xen"
        if v == "oracle": return "virtualbox"
        if v in ("kvm", "qemu"):
            bv = (read_first_line("/sys/class/dmi/id/bios_version") + " " +
                  read_first_line("/sys/class/dmi/id/bios_vendor")).lower()
            if "proxmox" in bv or "pc-q35" in bv or "pc-i440fx" in bv or "seabios" in bv:
                return "proxmox"
            return "kvm"
    except Exception:
        pass
    vendor = read_first_line("/sys/class/dmi/id/sys_vendor").lower()
    if "vmware" in vendor: return "vmware"
    if "microsoft" in vendor: return "hyperv"
    if "innotek" in vendor or "virtualbox" in vendor: return "virtualbox"
    if "xen" in vendor: return "xen"
    if "qemu" in vendor: return "proxmox"
    return "unknown"


def collect_network() -> dict:
    """Snapshot réseau primaire pour affichage côté portail à l'activation."""
    import subprocess as _sp
    info: dict = {"interface": "ens192", "mode": "dhcp"}
    try:
        out = _sp.run(["ip", "-4", "route", "show", "default"], capture_output=True, text=True, timeout=4).stdout
        for line in out.splitlines():
            parts = line.split()
            if "dev" in parts:
                info["interface"] = parts[parts.index("dev") + 1]
            if "via" in parts:
                info["gateway"] = parts[parts.index("via") + 1]
            if "proto" in parts:
                info["mode"] = parts[parts.index("proto") + 1]
    except Exception:
        pass
    try:
        out = _sp.run(["ip", "-4", "-o", "addr", "show", "dev", info["interface"]],
                      capture_output=True, text=True, timeout=4).stdout
        for line in out.splitlines():
            parts = line.split()
            for i, p in enumerate(parts):
                if p == "inet" and i + 1 < len(parts):
                    info["ip_cidr"] = parts[i + 1]
                    info["ip"] = parts[i + 1].split("/")[0]
    except Exception:
        pass
    try:
        with open("/etc/resolv.conf") as f:
            info["dns"] = [l.split()[1] for l in f if l.startswith("nameserver") and len(l.split()) >= 2][:4]
    except Exception:
        info["dns"] = []
    info["mac"] = primary_mac()
    import socket as _s
    info["hostname"] = _s.gethostname()
    return info


def request_signed_token(machine_fp: str) -> dict:
    url = f"{PT_API_BASE}/api/pentest/vm/enroll/sign-token"
    payload = {
        "machine_fp": machine_fp,
        "network_current": collect_network(),
        "dmi_uuid": read_first_line("/sys/class/dmi/id/product_uuid") or "no-dmi",
        "hypervisor": detect_hypervisor(),
    }
    data = json.dumps(payload).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read())


def short_code(token: str) -> str:
    """Code human-friendly 8 chars dérivé du token (collision ~négligeable)."""
    h = hashlib.sha256(token.encode()).hexdigest()[:8].upper()
    return f"{h[:4]}-{h[4:]}"


def render_qr(url: str) -> str:
    """qrencode -t UTF8 produit le QR en ANSI compact."""
    try:
        out = subprocess.run(
            ["qrencode", "-t", "UTF8", "-m", "1", url],
            capture_output=True, text=True, timeout=5, check=True,
        )
        return out.stdout
    except Exception as e:
        return f"[QR generation failed: {e} — utilisez le code court ci-dessous]\n"


def write_banner(qr_text: str, code: str, url: str, fp_short: str):
    banner = f"""
═══════════════════════════════════════════════════════════════════
                       SYLink PenTest VM
        Bot pentest interne piloté par UniSOC SOC
═══════════════════════════════════════════════════════════════════

ÉTAT : EN ATTENTE D'ACTIVATION

Pour activer cette VM dans votre espace client :

▸ Option 1 — Scannez ce QR avec votre téléphone (recommandé)

{qr_text}

▸ Option 2 — Saisissez ce code dans votre portail UniSOC :

         Code d'activation : {code}

▸ Option 3 — Ouvrez l'URL ci-dessous dans votre navigateur :

         {url}

▸ Fingerprint VM : {fp_short}…

Une fois activée, cette VM apparaîtra dans
{PORTAL_BASE}/pentest

═══════════════════════════════════════════════════════════════════
"""
    # /etc/issue (console) — affiché à l'écran avant login
    Path("/etc/issue").write_text(banner)
    # /etc/motd (SSH/login banner)
    Path("/etc/motd").write_text(banner)
    log(f"banner écrit : /etc/issue, /etc/motd")


def write_config(machine_fp: str, token: str, code: str, url: str):
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    config = {
        "machine_fingerprint": machine_fp,
        "token": token,
        "qr_url": url,
        "short_code": code,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "pt_api_base": PT_API_BASE,
        "portal_base": PORTAL_BASE,
    }
    (CONFIG_DIR / "enroll.json").write_text(json.dumps(config, indent=2))
    os.chmod(CONFIG_DIR / "enroll.json", 0o600)
    log(f"config écrite : {CONFIG_DIR / 'enroll.json'}")


def is_already_activated() -> bool:
    license_file = CONFIG_DIR / "license.json"
    if not license_file.exists():
        return False
    try:
        data = json.loads(license_file.read_text())
        return bool(data.get("api_key"))
    except Exception:
        return False


def main() -> int:
    log("== sylink-pentest-init start ==")
    if is_already_activated():
        log("VM déjà activée — passe banner OK et exit.")
        # Affiche un banner activé (mais on garde simple)
        Path("/etc/issue").write_text(
            "═════════════════════════════════════════\n"
            "  SYLink PenTest VM — ✓ ACTIVÉE\n"
            "═════════════════════════════════════════\n"
        )
        return 0

    machine_fp = compute_fingerprint()
    log(f"fingerprint : {machine_fp[:16]}…")

    try:
        resp = request_signed_token(machine_fp)
        token = resp["token"]
        url = resp.get("qr_url") or f"{PORTAL_BASE}/pentest/activate?t={token}"
    except Exception as e:
        log(f"ERREUR appel /enroll/sign-token : {e}")
        # Mode dégradé : on génère un banner d'erreur
        Path("/etc/issue").write_text(
            "═══════════════════════════════════════════════\n"
            "  🐛 SYLink PenTest VM — ERREUR DE CONNEXION\n"
            "═══════════════════════════════════════════════\n"
            f"\n  Impossible de joindre {PT_API_BASE}\n"
            "  Vérifiez la connectivité réseau de la VM.\n"
            f"\n  Fingerprint : {machine_fp[:16]}…\n"
            f"\n  Erreur : {e}\n\n"
        )
        return 1

    code = short_code(token)
    qr = render_qr(url)
    write_config(machine_fp, token, code, url)
    write_banner(qr, code, url, machine_fp[:8])
    log("== sylink-pentest-init done ==")
    return 0


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