#!/usr/bin/env python3
"""
SYLink HoneyBot — init au boot.

Calcule le machine_fingerprint + demande un token signé HMAC au backend
(scope=honeypot) + génère le QR ASCII pour activation client portail.

Si la VM est déjà activée (license.json présent), pose un banner « de couverture »
cohérent avec le rôle leurré (BackupServer Corp) — pas de mention "SYLink"
pour ne pas griller le piège auprès d'un attaquant qui ouvre la console.

Service systemd : Type=oneshot, Before sylink-honeypot-setup-ui.service.
"""
from __future__ import annotations

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

CONFIG_DIR = Path("/opt/sylink-honeypot/etc")
LOG_DIR = Path("/opt/sylink-honeypot/log")
PT_API_BASE = os.environ.get("HP_API_BASE", "https://api.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_iface() -> str:
    """Renvoie le nom de l'interface réseau primaire (celle qui a la route default)."""
    try:
        out = subprocess.run(["ip", "-4", "route", "show", "default"], capture_output=True, text=True, timeout=5).stdout
        # ex: "default via 192.168.1.1 dev ens192 proto dhcp src 192.168.1.222 metric 100"
        for line in out.splitlines():
            parts = line.split()
            if "dev" in parts:
                return parts[parts.index("dev") + 1]
    except Exception:
        pass
    # fallback : 1ère interface non-loopback
    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]:
                return parts[1].rstrip(":")
    except Exception:
        pass
    return "eth0"


def primary_mac() -> str:
    iface = primary_iface()
    try:
        out = subprocess.run(["ip", "-o", "link", "show", iface], capture_output=True, text=True, timeout=5).stdout
        parts = out.split()
        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 collect_network() -> dict:
    """Snapshot complet du réseau primaire — sera affiché côté portail à l'activation."""
    iface = primary_iface()
    info: dict = {"interface": iface, "mac": primary_mac(), "hostname": socket.gethostname(), "mode": "unknown"}

    # IP + CIDR + broadcast
    try:
        out = subprocess.run(["ip", "-4", "-o", "addr", "show", "dev", iface], capture_output=True, text=True, timeout=5).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]   # ex: 192.168.1.222/24
                    info["ip"] = parts[i + 1].split("/")[0]
                    info["prefix"] = int(parts[i + 1].split("/")[1])
    except Exception:
        pass

    # Gateway via route default
    try:
        out = subprocess.run(["ip", "-4", "route", "show", "default"], capture_output=True, text=True, timeout=5).stdout
        for line in out.splitlines():
            parts = line.split()
            if "via" in parts:
                info["gateway"] = parts[parts.index("via") + 1]
            if "proto" in parts:
                # ip route met proto dhcp si l'IP vient du DHCP, proto static sinon
                info["mode"] = parts[parts.index("proto") + 1]
    except Exception:
        pass

    # DNS via resolv.conf
    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"] = []

    # Test heuristique mode dhcp : présence d'un lease dhclient ou systemd-networkd
    if info["mode"] not in ("dhcp", "static"):
        if Path("/var/lib/dhcp").exists() or any(Path("/run/systemd/netif/leases").glob("*")) if Path("/run/systemd/netif/leases").exists() else False:
            info["mode"] = "dhcp"
        else:
            info["mode"] = "dhcp"  # défaut prudent

    return info


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(),
        "honeypot-scope",  # différent du fingerprint pentest
    ]
    return hashlib.sha256("|".join(seeds).encode()).hexdigest()


def request_signed_token(machine_fp: str, network_current: dict, dmi_uuid: str) -> dict:
    url = f"{PT_API_BASE}/api/honeypot/enroll/sign-token"
    payload = {
        "machine_fp": machine_fp,
        "network_current": network_current,
        "dmi_uuid": dmi_uuid,
    }
    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 render_qr(url: str) -> str:
    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}]\n"


def write_banner(qr_text: str, code: str, url: str, fp_short: str, network: dict | None = None):
    # QR retiré (URL trop longue pour rester scannable en pratique sur console VMware) —
    # le user retrouve l'URL courte du portail + saisit juste le code à 8 chars.
    net = network or {}
    net_block = (
        f"  IP courante (DHCP) : {net.get('ip_cidr', '—')}    Interface : {net.get('interface', '—')}\n"
        f"  Passerelle         : {net.get('gateway', '—')}    DNS : {', '.join(net.get('dns', []) or ['—'])}\n"
    ) if net else ""
    portal_short = f"{PORTAL_BASE}/honeypot/activate"
    vm_ip = (net.get("ip") if net else None) or "<ip-vm>"

    # Box ASCII : largeur intérieure fixe = 70 chars (entre les deux │).
    BW = 70

    def line(content: str = "") -> str:
        # rstrip pour éliminer espaces piégés ; ljust pour aligner ; on coupe à BW si dépasse.
        s = content.rstrip()
        if len(s) > BW:
            s = s[:BW]
        return "│" + s.ljust(BW) + "│"

    top    = "┌" + "─" * BW + "┐"
    bottom = "└" + "─" * BW + "┘"
    # Mini-box code : top/middle/bottom = 19 chars de large
    inner_top    = " " * 22 + "┌─────────────────┐"
    inner_code   = " " * 22 + "│ " + code.center(15) + " │"
    inner_bot    = " " * 22 + "└─────────────────┘"

    box_lines = [
        top,
        line("                       ACTIVATION"),
        line(),
        line("   Option 1 — Saisissez ce code sur le portail :"),
        line(),
        line("        " + portal_short),
        line(),
        line(inner_top),
        line(inner_code),
        line(inner_bot),
        line(),
        line("   Fingerprint VM : " + fp_short),
        bottom,
    ]
    box_block = "\n  ".join([""] + box_lines)  # 2 espaces de marge gauche

    banner = f"""
═══════════════════════════════════════════════════════════════════════════════
                          SYLink HoneyBot
              VM honeypot interne pilotée par UniSOC AI
═══════════════════════════════════════════════════════════════════════════════

  ÉTAT : EN ATTENTE D'ACTIVATION

{net_block}{box_block}

  Option 2 — Lien complet à coller dans votre navigateur (1 clic, sans saisie) :

  {url}

  💡 Astuce : si vous ne pouvez pas copier-coller depuis cette console, ouvrez
     http://{vm_ip}:8080 depuis n'importe quel navigateur de votre LAN
     (lien d'activation cliquable + config IP statique disponibles).

  Une fois activée, la VM redémarre automatiquement et passe en mode actif.
  Elle apparaîtra dans : {PORTAL_BASE}/network

  ⚠  Cette VM exposera 14 services LEURRES — toute interaction = alerte SOC.

═══════════════════════════════════════════════════════════════════════════════
"""
    Path("/etc/issue").write_text(banner)
    Path("/etc/motd").write_text(banner)
    log("banner écrit /etc/issue + /etc/motd")


def write_config(machine_fp: str, token: str, code: str, url: str, network: dict):
    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(),
        "hp_api_base": PT_API_BASE,
        "portal_base": PORTAL_BASE,
        "network_current": network,
    }
    (CONFIG_DIR / "enroll.json").write_text(json.dumps(config, indent=2))
    os.chmod(CONFIG_DIR / "enroll.json", 0o600)


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


def main() -> int:
    log("== sylink-honeypot-init start ==")

    if is_already_activated():
        log("VM déjà activée — banner couverture (Windows Server BackupServer) + setup-ui à stopper.")
        hostname = socket.gethostname() or "srv01-backup"

        # ⚡ Faux "last logon" : déterministe per-VM (même valeur à chaque boot pour cohérence)
        # ~300-450 jours dans le passé → effet "serveur oublié, admin absent" → attaquant
        # plus à l'aise pour exécuter des commandes.
        # Faux "boot time" et "uptime" du même registre : la VM est censée tourner depuis longtemps.
        fp = compute_fingerprint()
        seed = int(hashlib.sha256(fp.encode()).hexdigest()[:8], 16)
        days_logon = 300 + (seed % 150)              # 300-450 jours (~10-15 mois)
        hour_logon = (seed >> 8) % 24
        min_logon  = (seed >> 16) % 60
        fake_logon = datetime.now(timezone.utc) - timedelta(days=days_logon, hours=hour_logon, minutes=min_logon)
        days_boot  = 180 + (seed % 90)                # boot ~6-9 mois plus vieux
        fake_boot  = datetime.now(timezone.utc) - timedelta(days=days_boot)
        fake_install = datetime.now(timezone.utc) - timedelta(days=820 + (seed % 200))  # ~27-33 mois

        cover_banner = (
            "\n"
            "===================================================================\n"
            "             Corp Backup Infrastructure\n"
            f"                       {hostname.upper()}\n"
            "             Backup Cluster Node - Production\n"
            "===================================================================\n"
            "\n"
            "  AUTHORIZED PERSONNEL ONLY\n"
            "  All access and commands are logged and monitored.\n"
            "  Unauthorized access is prohibited and will be prosecuted.\n"
            "\n"
            "  System    : Microsoft Windows Server 2019 Standard - Build 17763\n"
            "  Roles     : Backup Server, Storage Services, IIS Web Server\n"
            "  Storage   : 4x RAID6 SAS - Veeam Backup & Replication 10.0 cluster\n"
            "  Domain    : CORP\\\\BACKUPADMINS\n"
            f"  Installed : {fake_install.strftime('%Y-%m-%d')}\n"
            f"  Boot time : {fake_boot.strftime('%Y-%m-%d')}\n"
            f"  Last logon: {fake_logon.strftime('%Y-%m-%d %H:%M')}  (Administrator)\n"
            "\n"
        )
        Path("/etc/issue").write_text(cover_banner)
        Path("/etc/issue.net").write_text(cover_banner)
        Path("/etc/motd").write_text(cover_banner)
        # Le setup-ui sera disabled par le watch de license.json côté setup_ui
        return 0

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

    dmi_uuid = read_first_line("/sys/class/dmi/id/product_uuid") or "no-dmi"
    log(f"DMI UUID (carte mère) : {dmi_uuid}")

    network = collect_network()
    log(f"network : iface={network.get('interface')} ip={network.get('ip_cidr')} gw={network.get('gateway')} mode={network.get('mode')}")

    try:
        resp = request_signed_token(machine_fp, network, dmi_uuid)
        token = resp["token"]
        url = resp.get("qr_url") or f"{PORTAL_BASE}/honeypot/activate?t={token}"
        code = resp.get("short_code", "????-????")
    except Exception as e:
        log(f"ERREUR /honeypot/enroll/sign-token : {e}")
        Path("/etc/issue").write_text(
            "═══════════════════════════════════════════════\n"
            "  SYLink HoneyBot — ERREUR DE CONNEXION\n"
            "═══════════════════════════════════════════════\n"
            f"\n  Impossible de joindre {PT_API_BASE}\n  Vérifiez la connectivité réseau.\n"
            f"\n  Fingerprint : {machine_fp[:16]}…\n  Erreur : {e}\n\n"
        )
        return 1

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


if __name__ == "__main__":
    sys.exit(main())
# v1.0.1 — patch test OTA
