#!/usr/bin/env python3
"""
SYLink HoneyBot — cred-sync.

Polle /api/honeypot/credentials/leaked avec le license_token de la VM.
Récupère les plaintext credentials du tenant (issus de Telegram / DeHashed / etc.
côté UniSOC) et les injecte dans :
  - /opt/cowrie/etc/userdb.txt   (Cowrie SSH/Telnet)
  - userlist Veeam fake          (à venir)
  - Samba users                  (à venir)

Au prochain hit attaquant qui tente une de ces creds → cowrie.login.success +
event marqué leak_source=<source>. Indicateur fort qu'on a affaire à un attaquant
qui a consulté la base de leaks.
"""
from __future__ import annotations

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

CONFIG_DIR = Path("/opt/sylink-honeypot/etc")
LOG_DIR = Path("/opt/sylink-honeypot/log")
COWRIE_USERDB = Path("/opt/cowrie/etc/userdb.txt")
PT_API_BASE = os.environ.get("HP_API_BASE", "https://api.unisoc.fr")

USERDB_HEADER = "# SYLink HoneyBot userdb -- combine creds statiques + decoy leak creds tenant."  # ASCII-only (Cowrie ouvre en encoding=ascii)
LEAK_MARKER_START = "# --- BEGIN AUTO LEAK CREDS (synced from UniSOC) ---"
LEAK_MARKER_END = "# --- END AUTO LEAK CREDS ---"


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


def read_license_token() -> str | None:
    f = CONFIG_DIR / "license.json"
    if not f.exists():
        return None
    try:
        return json.loads(f.read_text()).get("license_token")
    except Exception:
        return None


def fetch_leaked(license_token: str) -> list[dict]:
    url = f"{PT_API_BASE}/api/honeypot/credentials/leaked"
    req = urllib.request.Request(
        url,
        headers={"Authorization": f"Bearer {license_token}", "Accept": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            data = json.loads(resp.read())
        return data.get("creds") or []
    except urllib.error.HTTPError as e:
        log(f"fetch HTTP {e.code} : {e.reason}")
        return []
    except Exception as e:
        log(f"fetch erreur : {e}")
        return []


def update_cowrie_userdb(creds: list[dict]) -> int:
    """Met à jour userdb.txt en remplaçant uniquement le bloc auto-géré (entre markers).
    Garde les creds statiques pré-existantes (admin/admin, root/toor, etc.)."""
    existing = COWRIE_USERDB.read_text() if COWRIE_USERDB.exists() else ""
    lines = existing.splitlines()
    # Retire l'ancien bloc auto si présent
    out_lines = []
    in_auto = False
    for ln in lines:
        if ln.strip() == LEAK_MARKER_START:
            in_auto = True
            continue
        if ln.strip() == LEAK_MARKER_END:
            in_auto = False
            continue
        if not in_auto:
            out_lines.append(ln)

    # Construit le nouveau bloc.
    # ⚠ Cowrie n'accepte PAS les commentaires inline (le password contiendrait les chars du commentaire).
    # Sidecar metadata persistées dans leak_creds.json pour permettre l'enrichissement d'alerte.
    auto_block = [LEAK_MARKER_START]
    sidecar = {}
    seen = set()
    for c in creds:
        u = (c.get("username") or "").strip()
        p = (c.get("password") or "").strip()
        if not u or not p:
            continue
        key = (u, p)
        if key in seen:
            continue
        seen.add(key)
        if ":" in u or ":" in p or "\n" in u or "\n" in p:
            continue
        auto_block.append(f"{u}:x:{p}")
        sidecar[f"{u}|{p}"] = {
            "source": c.get("source"),
            "leak_date": c.get("leak_date"),
            "malware_family": c.get("malware_family"),
        }
    auto_block.append(LEAK_MARKER_END)

    # Persist sidecar pour permettre la corrélation login.success → leak metadata
    try:
        (CONFIG_DIR / "leak_creds.json").write_text(json.dumps(sidecar, indent=2))
        os.chmod(CONFIG_DIR / "leak_creds.json", 0o600)
    except Exception:
        pass

    # Assemble : header + lignes existantes + bloc auto
    # ⚠ Cowrie ouvre userdb.txt en encoding=ascii. Tout contenu non-ASCII fait crash le load
    # silencieux et Cowrie tombe sur le _USERDB_DEFAULTS interne. Donc on force ASCII partout.
    header_added = any("SYLink HoneyBot" in l for l in out_lines[:3])
    final = []
    if not header_added:
        final.append(USERDB_HEADER)
    # Strip non-ASCII des lignes existantes (sécurité)
    out_lines = [l.encode("ascii", errors="ignore").decode("ascii") for l in out_lines]
    final.extend(out_lines)
    if final and final[-1].strip():
        final.append("")
    final.extend(auto_block)
    new_content = "\n".join(final) + "\n"

    COWRIE_USERDB.parent.mkdir(parents=True, exist_ok=True)
    COWRIE_USERDB.write_text(new_content)
    try:
        os.chown(COWRIE_USERDB, *_cowrie_uid_gid())
    except Exception:
        pass
    return len(auto_block) - 2  # nb creds réellement ajoutées


def _cowrie_uid_gid():
    import pwd
    p = pwd.getpwnam("cowrie")
    return p.pw_uid, p.pw_gid


def reload_cowrie():
    """Cowrie relit userdb.txt à la volée pour les login attempts. Pas besoin de restart."""
    # Cowrie lit userdb au moment de chaque tentative, donc un simple touch suffit.
    # Si on veut forcer un reload de config plus large, on peut restart, mais ça coupe
    # les sessions attaquant en cours = perte de capture commandes.
    log("userdb.txt mis à jour — Cowrie le relira au prochain login attempt")


def main() -> int:
    log("== cred-sync start ==")
    token = read_license_token()
    if not token:
        log("license.json absent — VM pas activée, rien à faire")
        return 0

    creds = fetch_leaked(token)
    if not creds:
        log("aucune cred decoy reçue (ou erreur fetch)")
        return 0

    nb = update_cowrie_userdb(creds)
    log(f"userdb.txt synchronisé : {nb} creds leak injectées")
    reload_cowrie()
    log("== cred-sync done ==")
    return 0


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