#!/usr/bin/env bash
# VulnRadar — Détection du stack technique (Linux)
# Usage : bash vulnradar-detect.sh [--send --url <url>] [--yes]
#   Sans options : affiche le JSON (à coller dans VulnRadar > Importer)
#   Avec --send   : envoie directement à VulnRadar
#   Avec --yes    : installe aussi une crontab quotidienne (heure nocturne aléatoire)
#
# ── Passage du jeton d'ingestion (ordre de priorité décroissant) ──────────────
#
#   1. --token-file <chemin>   Fichier de jeton explicite (chmod 600 recommandé)
#   2. $VULNRADAR_TOKEN        Variable d'environnement
#   3. ~/.vulnradar-token      Fichier par défaut (chmod 600 recommandé)
#   4. --token <valeur>        Argument en ligne de commande — DÉPRÉCIÉ :
#                              le jeton apparaît dans `ps`, `crontab -l` et les
#                              logs shell. Utiliser un fichier de jeton à la place.
#
# Le jeton ne doit JAMAIS apparaître dans la crontab, dans la sortie de `ps` ou
# dans les fichiers de log. Utiliser l'option 1, 2 ou 3 pour les installations
# automatisées (cron, CI/CD).
#
# Exemple d'installation sécurisée :
#   printf '%s\n' 'vri_xxxxxxxxxxxx' > ~/.vulnradar-token && chmod 600 ~/.vulnradar-token
#   bash detect.sh --send --url https://vulnradar.fr/api/ingest --yes
set -uo pipefail

SEND_TOKEN=""
SEND_URL=""
AUTO_CRON=0
_TOKEN_FILE_ARG=""          # --token-file explicite
_TOKEN_ARG_DEPRECATED=""    # --token (déprécié)

# ── Parse des arguments nommés ────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
    case "$1" in
        --send)        shift ;;
        --url)         SEND_URL="${2:-}";              shift 2 ;;
        --token)       _TOKEN_ARG_DEPRECATED="${2:-}"; shift 2 ;;
        --token-file)  _TOKEN_FILE_ARG="${2:-}";       shift 2 ;;
        --yes)         AUTO_CRON=1;                    shift ;;
        *)             shift ;;
    esac
done

# ── Résolution du jeton (priorité 1→4) ───────────────────────────────────────
_TOKEN_DEFAULT_FILE="$HOME/.vulnradar-token"

_resolve_token() {
    # Priorité 1 : --token-file explicite
    if [ -n "$_TOKEN_FILE_ARG" ]; then
        if [ ! -f "$_TOKEN_FILE_ARG" ]; then
            echo "Erreur : fichier de jeton introuvable : $_TOKEN_FILE_ARG" >&2; return 1
        fi
        if [ ! -r "$_TOKEN_FILE_ARG" ]; then
            echo "Erreur : fichier de jeton non lisible : $_TOKEN_FILE_ARG" >&2; return 1
        fi
        _check_token_perms "$_TOKEN_FILE_ARG"
        SEND_TOKEN=$(tr -d '[:space:]' < "$_TOKEN_FILE_ARG")
        return 0
    fi
    # Priorité 2 : variable d'environnement VULNRADAR_TOKEN
    if [ -n "${VULNRADAR_TOKEN:-}" ]; then
        SEND_TOKEN="$VULNRADAR_TOKEN"
        return 0
    fi
    # Priorité 3 : fichier par défaut ~/.vulnradar-token
    if [ -f "$_TOKEN_DEFAULT_FILE" ] && [ -r "$_TOKEN_DEFAULT_FILE" ]; then
        _check_token_perms "$_TOKEN_DEFAULT_FILE"
        SEND_TOKEN=$(tr -d '[:space:]' < "$_TOKEN_DEFAULT_FILE")
        return 0
    fi
    # Priorité 4 : --token (déprécié)
    if [ -n "$_TOKEN_ARG_DEPRECATED" ]; then
        echo "Avertissement : --token passe le jeton en clair (visible dans \`ps\`, \`crontab -l\`)." >&2
        echo "  Préférez : printf '%s\\n' '<jeton>' > ~/.vulnradar-token && chmod 600 ~/.vulnradar-token" >&2
        SEND_TOKEN="$_TOKEN_ARG_DEPRECATED"
        return 0
    fi
    return 1
}

# Avertit si les permissions du fichier de jeton sont trop ouvertes (> 600/400).
# N'échoue PAS (un exit casserait l'ingestion d'un client en 644) — avertit seulement.
_check_token_perms() {
    local f="$1"
    # stat -c '%a' (GNU/Linux) vs stat -f '%OLp' (macOS/BSD)
    local perms=""
    if stat --version 2>/dev/null | grep -q GNU; then
        perms=$(stat -c '%a' "$f" 2>/dev/null)
    else
        perms=$(stat -f '%OLp' "$f" 2>/dev/null)
    fi
    if [ -n "$perms" ]; then
        case "$perms" in
            600|400|0600|0400) ;;  # OK
            *)
                echo "Avertissement : le fichier de jeton $f a des permissions trop ouvertes ($perms)." >&2
                echo "  Recommandé : chmod 600 $f" >&2
                ;;
        esac
    fi
}

TMPFILE=$(mktemp /tmp/vulnradar-XXXXXX)
PAYLOAD=$(mktemp /tmp/vulnradar-pay-XXXXXX.json)
INGEST_LOG=$(mktemp /tmp/vulnradar-ingest-XXXXXX.log)
trap 'rm -f "$TMPFILE" "$PAYLOAD" "$INGEST_LOG"' EXIT

# ── Helpers ───────────────────────────────────────────────────────────────────
# Réduction des faux positifs « backport » : un correctif rétroporté par la distro
# est mentionné (par son CVE) dans le changelog LOCAL du paquet, sans changer le
# numéro de version amont. On lit ce changelog en local (aucun accès réseau,
# contrairement à `apt-get changelog`) et on renvoie un tableau JSON des CVE.
#
# La racine est une variable pour que les tests puissent exercer la VRAIE fonction sur
# un changelog forgé : beaucoup d'images (dont celles des conteneurs) sont strippées de
# leurs changelogs, donc sur ces machines l'extraction rend toujours [] et ne prouve rien.
_DOC_ROOT="${_DOC_ROOT:-/usr/share/doc}"
_changelog_cves() {
    # $@ = paquets candidats (le 1er dont le changelog existe est retenu)
    local pkg f csv=""
    if command -v dpkg-query &>/dev/null; then
        for pkg in "$@"; do
            for f in "$_DOC_ROOT/$pkg/changelog.Debian.gz" "$_DOC_ROOT/$pkg/changelog.gz"; do
                if [ -f "$f" ]; then
                    csv=$(zcat "$f" 2>/dev/null | grep -oiE 'CVE-[0-9]{4}-[0-9]+' \
                          | tr '[:lower:]' '[:upper:]' | sort -u | paste -sd, -)
                    break 2
                fi
            done
        done
    elif command -v rpm &>/dev/null; then
        for pkg in "$@"; do
            csv=$(rpm -q --changelog "$pkg" 2>/dev/null | grep -oiE 'CVE-[0-9]{4}-[0-9]+' \
                  | tr '[:lower:]' '[:upper:]' | sort -u | paste -sd, -)
            [ -n "$csv" ] && break
        done
    fi
    if [ -z "$csv" ]; then printf '[]'; return; fi
    local out="[" first=1 c IFS=,
    for c in $csv; do
        [ "$first" = 1 ] || out+=","
        out+="\"$c\""; first=0
    done
    printf '%s]' "$out"
}

# Paquet distro propriétaire d'un binaire. Vide si introuvable (ou installé hors gestionnaire
# de paquets : tarball, SDKMAN, nvm…), ce qui est le bon repli — pas de changelog, donc aucun
# rétroportage à prouver.
#
# Résout le paquet DEPUIS le binaire plutôt que de le nommer en dur : une liste statique doit
# épeler chaque variante (openjdk-21-jre-headless, openjdk-17-jre-headless, default-jre…) et
# vieillit à chaque version majeure. Le binaire, lui, est toujours celui qui vient d'être
# interrogé pour la version — le paquet trouvé décrit donc bien le logiciel mesuré.
_pkg_of() {
    local bin real p mode="" pkg=""
    bin=$(command -v "$1" 2>/dev/null) || return 0
    [ -n "$bin" ] || return 0
    # /usr/bin/java est un lien vers /etc/alternatives puis vers le JDK réel : c'est la cible
    # finale que dpkg connaît (openjdk-21-jre-headless).
    real=$(readlink -f "$bin" 2>/dev/null) || real="$bin"
    if command -v dpkg-query &>/dev/null; then mode=dpkg
    elif command -v rpm &>/dev/null;        then mode=rpm
    else return 0
    fi
    # Chemins tentés dans l'ordre : cible du lien, chemin d'origine, puis chacun décliné SANS
    # et AVEC le préfixe /usr. Cette déclinaison n'est pas défensive, elle est nécessaire : sur
    # un système « merged-usr » (Debian 12+, Ubuntu 24.04), /bin est un lien vers /usr/bin, donc
    # `readlink -f` rend toujours /usr/bin/x — alors que dpkg a enregistré /bin/x pour ~90
    # paquets de base. Sans elle, la recherche échoue en silence et le paquet passe pour absent,
    # ce qui rouvre exactement le faux positif de rétroportage qu'on cherche à fermer.
    for p in "$real" "$bin" "${real#/usr}" "${bin#/usr}" "/usr$real" "/usr$bin"; do
        [ -n "$p" ] || continue
        if [ "$mode" = dpkg ]; then
            # Sortie : "openjdk-21-jre-headless:amd64: /usr/lib/jvm/java-21-openjdk-amd64/bin/java"
            pkg=$(dpkg-query -S "$p" 2>/dev/null | head -1 | cut -d: -f1)
        else
            pkg=$(rpm -qf --qf '%{NAME}\n' "$p" 2>/dev/null | head -1)
        fi
        # Un message d'erreur ayant fui sur stdout contient toujours un espace ; un nom de
        # paquet jamais. Sans ce filtre, « no path found matching pattern » partirait comme
        # nom de paquet, et `_changelog_cves` chercherait un changelog à ce nom-là.
        case "$pkg" in *" "*|*"	"*) pkg="" ;; esac
        [ -n "$pkg" ] && break
    done
    printf '%s' "$pkg"
}

_add() {
    # _add <name> <version> <category> [source_type] [source_ref] [pkg_candidats...]
    local name="$1" ver="$2" cat="$3" stype="${4:-}" sref="${5:-}"
    [ -z "$ver" ] && return
    # Paquets candidats pour le changelog = arguments à partir du 6e (facultatif)
    local cvesjson="[]"
    [ "$#" -ge 6 ] && cvesjson=$(_changelog_cves "${@:6}")
    if [ -n "$stype" ] && [ "$stype" != "host" ]; then
        printf '{"name":"%s","version":"%s","category":"%s","source_type":"%s","source_ref":"%s","patched_cves":%s}\n' \
            "$name" "$ver" "$cat" "$stype" "$sref" "$cvesjson" >> "$TMPFILE"
    else
        printf '{"name":"%s","version":"%s","category":"%s","patched_cves":%s}\n' \
            "$name" "$ver" "$cat" "$cvesjson" >> "$TMPFILE"
    fi
}

detect() {
    # detect <name> <category> <cmd> [svc] [pkg_candidats...]
    local name="$1" category="$2" cmd="$3" svc="${4:-}"
    local version
    version=$(eval "$cmd" 2>/dev/null | head -1 \
        | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1) || true
    if [ -n "$version" ]; then
        # skip if service name given and service is not running
        if [ -n "$svc" ] && ! _svc_active "$svc"; then return; fi
        # ${@:5} = paquets candidats changelog (backport) pour les techs du catalogue NVD
        _add "$name" "$version" "$category" "" "" "${@:5}"
    fi
}

_py() {
    if command -v python3 &>/dev/null; then echo "python3"
    elif command -v python &>/dev/null; then echo "python"
    else echo ""; fi
}

# Vérifie si un service systemd est actif (0 = actif ou systemctl absent, 1 = inactif)
_svc_active() {
    command -v systemctl &>/dev/null || return 0
    systemctl is-active --quiet "$1" 2>/dev/null
}

# ── Web servers ───────────────────────────────────────────────────────────────
# 5e argument et suivants = paquets candidats pour le changelog local (suppression backport)
detect "nginx"    "webserver" "nginx -v 2>&1"                                 "nginx"   $(_pkg_of nginx) nginx-core nginx-full nginx-light nginx
detect "apache"   "webserver" "apache2 -v 2>&1 || httpd -v 2>&1"             "apache2" $(_pkg_of apache2) $(_pkg_of httpd) apache2 apache2-bin httpd
detect "caddy"    "webserver" "caddy version 2>&1"                            "caddy"    $(_pkg_of caddy) caddy
detect "lighttpd" "webserver" "lighttpd -v 2>&1"                              "lighttpd" $(_pkg_of lighttpd) lighttpd

# ── Languages / runtimes ──────────────────────────────────────────────────────
# Ces huit entrées n'avaient AUCUN paquet candidat : `_add` ne calculait donc pas
# `patched_cves`, et leur catégorie `language` n'a pas d'écosystème OSV (_CATEGORY_TO_ECOSYSTEM).
# Les deux leviers anti-rétroportage étaient morts pour elles — il ne restait que le
# rapprochement NVD/CPE sur le numéro amont nu. Un `openjdk-21-jre-headless` Ubuntu à jour,
# annoncé « 21.0.11 » par `java -version` alors que sa révision `+10-1~24.04.2` porte les
# correctifs, ressortait avec 7 CVE ouvertes (constaté chez un client le 2026-08-08).
detect "PHP"     "language" "php --version 2>&1"                                  "" $(_pkg_of php)
detect "Python"  "language" "python3 --version 2>&1 || python --version 2>&1"     "" $(_pkg_of python3) $(_pkg_of python)
detect "Node.js" "language" "node --version 2>&1"                                 "" $(_pkg_of node) nodejs
detect "Ruby"    "language" "ruby --version 2>&1"                                 "" $(_pkg_of ruby) ruby
detect "Go"      "language" "go version 2>&1"                                     "" $(_pkg_of go) golang-go
detect "Java"    "language" "java -version 2>&1"                                  "" $(_pkg_of java) default-jre-headless
detect ".NET"    "language" "dotnet --version 2>&1"                               "" $(_pkg_of dotnet)
detect "Rust"    "language" "rustc --version 2>&1"                                "" $(_pkg_of rustc) rustc

# ── Databases ─────────────────────────────────────────────────────────────────
detect "MySQL"         "database" "mysql --version 2>&1"                           "mysql"      $(_pkg_of mysql) mysql-server
detect "PostgreSQL"    "database" "psql --version 2>&1 || postgres --version 2>&1" "postgresql" $(_pkg_of psql) postgresql
detect "MariaDB"       "database" "mariadb --version 2>&1"                         "mariadb"    $(_pkg_of mariadb) mariadb-server
detect "MongoDB"       "database" "mongod --version 2>&1"                          "mongod"     $(_pkg_of mongod) mongodb-server
detect "Redis"         "database" "redis-server --version 2>&1"                    "redis"      $(_pkg_of redis-server) redis-server
# Elasticsearch : SANS paquet candidat, délibérément. La version est lue via une requête HTTP
# sur le port 9200, pas en exécutant un binaire — `$(_pkg_of curl)` rendrait le paquet de curl
# et son changelog ferait passer des CVE Elasticsearch pour rétroportées. Un faux négatif est
# ici pire que le faux positif qu'on cherche à supprimer. Elasticsearch s'installe de toute
# façon depuis le dépôt Elastic, sans changelog Debian.
detect "Elasticsearch" "database" "curl -s http://localhost:9200 2>/dev/null | grep -oE '\"number\":\"[^\"]+\"' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+'"
# SQL Server : produit Microsoft, aucun gestionnaire de paquets Debian/RPM en jeu.
detect "SQL Server"    "database" "sqlcmd -Q 'SELECT @@VERSION' 2>&1"              ""

# ── CMS ───────────────────────────────────────────────────────────────────────
# WordPress : wp-cli disponible. Drupal/Joomla : aucun CLI standard fiable (drush
# non universel, Joomla sans équivalent) → détection par scan de système de fichiers
# non implémentée (fragile, non portable, hors périmètre du helper detect()).
# Sans paquet candidat : WordPress s'installe par téléchargement, pas via dpkg/rpm — et
# `$(_pkg_of wp)` désignerait wp-cli, un OUTIL tiers dont le changelog n'a aucun rapport
# avec le CMS mesuré.
detect "WordPress" "framework" "wp core version 2>/dev/null"

# ── Containers ────────────────────────────────────────────────────────────────
detect "Docker"      "container" "docker --version 2>&1"                                     "docker"      $(_pkg_of docker) docker-ce docker.io
detect "Podman"      "container" "podman --version 2>&1"                                     ""            $(_pkg_of podman) podman
detect "containerd"  "container" "containerd --version 2>&1"                                 "containerd"  $(_pkg_of containerd) containerd containerd.io
detect "Kubernetes"  "container" "kubectl version --client --short 2>&1 || kubelet --version 2>&1" "" $(_pkg_of kubectl) $(_pkg_of kubelet) kubelet

# ── Security / TLS ────────────────────────────────────────────────────────────
detect "OpenSSL" "library" "openssl version 2>&1"                            ""     $(_pkg_of openssl) openssl libssl3 libssl1.1
detect "OpenSSH" "other"   "ssh -V 2>&1"                                     "sshd" $(_pkg_of ssh) openssh-server openssh-client openssh

# ── Mail servers ──────────────────────────────────────────────────────────────
detect "Postfix"  "other" "postconf -d mail_version 2>&1"                    "postfix"  $(_pkg_of postconf) postfix
detect "Dovecot"  "other" "dovecot --version 2>&1 || doveadm --version 2>&1" "dovecot"  $(_pkg_of dovecot) dovecot-core dovecot-imapd dovecot-pop3d

# ── CLI tools ─────────────────────────────────────────────────────────────────
# openjdk : la section « Languages / runtimes » émet déjà "Java" via `java -version`.
# Ajouter "OpenJDK" séparément générerait deux entrées pour la même JVM (doublon CVE).
# curl/git : pas de service à vérifier (4e arg vide = pas de filtre systemd).
detect "curl" "library" "curl --version 2>&1" ""  $(_pkg_of curl) curl libcurl4
detect "git"  "other"   "git --version 2>&1"  ""  $(_pkg_of git) git

# ── OS ────────────────────────────────────────────────────────────────────────
if [ -f /etc/os-release ]; then
    OS_NAME=$(grep -oP '(?<=^ID=)[^\n]+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]') || true
    OS_VERSION=$(grep -oP '(?<=^VERSION_ID=)[^\n]+' /etc/os-release | tr -d '"') || true
    if [ -n "$OS_NAME" ] && [ -n "$OS_VERSION" ]; then
        case "$OS_NAME" in
            ubuntu)  _add "Ubuntu"       "$OS_VERSION" "os" ;;
            debian)  _add "Debian"       "$OS_VERSION" "os" ;;
            centos)  _add "CentOS"       "$OS_VERSION" "os" ;;
            rhel)    _add "RHEL"         "$OS_VERSION" "os" ;;
            rocky)   _add "Rocky Linux"  "$OS_VERSION" "os" ;;
            alpine)  _add "Alpine Linux" "$OS_VERSION" "os" ;;
            *)       _add "$OS_NAME"     "$OS_VERSION" "os" ;;
        esac
    fi
fi

KERNEL=$(uname -r | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') || true
[ -n "$KERNEL" ] && _add "Linux Kernel" "$KERNEL" "os"

# ── Python packages (pip) ─────────────────────────────────────────────────────
_detect_pip() {
    local py; py=$(_py); [ -z "$py" ] && return
    "$py" -m pip list --format=json 2>/dev/null | "$py" -c "
import sys, json
try:
    for p in json.load(sys.stdin):
        n, v = p.get('name',''), p.get('version','')
        if n and v:
            print(json.dumps({'name':n,'version':v,'category':'python-package'}))
except Exception:
    pass
" 2>/dev/null >> "$TMPFILE" || true
}
_detect_pip

# ── Node.js packages (npm global) ─────────────────────────────────────────────
_detect_npm() {
    command -v npm &>/dev/null || return
    local py; py=$(_py); [ -z "$py" ] && return
    npm list -g --depth=0 --json 2>/dev/null | "$py" -c "
import sys, json
try:
    for name, info in json.load(sys.stdin).get('dependencies', {}).items():
        ver = info.get('version', '')
        if ver:
            print(json.dumps({'name':name,'version':ver,'category':'npm-package'}))
except Exception:
    pass
" 2>/dev/null >> "$TMPFILE" || true
}
_detect_npm

# ── Packages système Debian/Ubuntu (dpkg) — limité à 400 ─────────────────────
_detect_dpkg() {
    command -v dpkg-query &>/dev/null || return
    local py; py=$(_py); [ -z "$py" ] && return
    dpkg-query -W -f='${Package}\t${Version}\n' 2>/dev/null | "$py" -c "
import sys, json, re, gzip, os
CVE = re.compile(rb'CVE-[0-9]{4}-[0-9]+', re.I)
def changelog_cves(pkg):
    # Changelog LOCAL du paquet (aucun réseau) -> CVE rétroportés par la distro.
    for fn in ('changelog.Debian.gz', 'changelog.gz'):
        path = '/usr/share/doc/%s/%s' % (pkg, fn)
        if os.path.exists(path):
            try:
                with gzip.open(path, 'rb') as fh:
                    data = fh.read(1000000)  # plafond 1 Mo (changelogs récents en tête)
                return sorted({m.group(0).upper().decode() for m in CVE.finditer(data)})
            except Exception:
                return []
    return []
count = 0
for line in sys.stdin:
    if count >= 400: break
    p = line.strip().split('\t', 1)
    if len(p) == 2 and p[1] and p[1] != '<none>':
        full = p[1]
        # strip epoch (e.g. '1:9.6p1-3ubuntu13.16' -> '9.6p1-3ubuntu13.16')
        if ':' in full:
            pre, rest = full.split(':', 1)
            if pre.isdigit(): full = rest
        v = full.split('-')[0].split('+')[0]  # version amont (NVD/CPE, unicité)
        if v and v != '<none>':
            # full = version distro COMPLÈTE (révision incluse) pour le lookup OSV backport-aware
            print(json.dumps({'name':p[0],'version':v,'category':'system-package',
                              'osv_version':full,'patched_cves':changelog_cves(p[0])}))
            count += 1
" 2>/dev/null >> "$TMPFILE" || true
}
_detect_dpkg

# ── Packages système RHEL/CentOS (rpm) — limité à 400 ────────────────────────
_detect_rpm() {
    command -v rpm &>/dev/null || return
    command -v dpkg-query &>/dev/null && return
    local py; py=$(_py); [ -z "$py" ] && return
    rpm -qa --queryformat '%{NAME}\t%{VERSION}\n' 2>/dev/null | "$py" -c "
import sys, json, re, subprocess
CVE = re.compile(r'CVE-[0-9]{4}-[0-9]+', re.I)
def changelog_cves(pkg):
    # rpm -q --changelog lit la base RPM LOCALE -> CVE rétroportés par la distro.
    try:
        out = subprocess.run(['rpm', '-q', '--changelog', pkg],
                             capture_output=True, timeout=10, text=True).stdout
    except Exception:
        return []
    return sorted({m.group(0).upper() for m in CVE.finditer(out)})
count = 0
for line in sys.stdin:
    if count >= 400: break
    p = line.strip().split('\t', 1)
    if len(p) == 2 and p[1]:
        v = p[1]
        # strip epoch if present
        if ':' in v:
            pre, rest = v.split(':', 1)
            if pre.isdigit(): v = rest
        v = v.split('+')[0]  # strip build suffix
        if v:
            print(json.dumps({'name':p[0],'version':v,'category':'system-package',
                              'osv_version':v,'patched_cves':changelog_cves(p[0])}))
            count += 1
" 2>/dev/null >> "$TMPFILE" || true
}
_detect_rpm

# ── Packages dans les conteneurs Docker actifs ────────────────────────────────
_detect_docker_packages() {
    command -v docker &>/dev/null || return
    docker info &>/dev/null 2>&1 || return
    local py; py=$(_py); [ -z "$py" ] && return
    local container
    for container in $(docker ps --format '{{.Names}}' 2>/dev/null); do
        # pip
        docker exec "$container" python3 -m pip list --format=json 2>/dev/null \
            | "$py" -c "
import sys, json
cname = sys.argv[1]
try:
    for p in json.load(sys.stdin):
        n, v = p.get('name',''), p.get('version','')
        if n and v:
            print(json.dumps({'name':n,'version':v,'category':'python-package',
                              'source_type':'docker','source_ref':cname}))
except Exception:
    pass
" "$container" 2>/dev/null >> "$TMPFILE" || true
        # npm
        docker exec "$container" npm list -g --depth=0 --json 2>/dev/null \
            | "$py" -c "
import sys, json
cname = sys.argv[1]
try:
    for name, info in json.load(sys.stdin).get('dependencies',{}).items():
        ver = info.get('version','')
        if ver:
            print(json.dumps({'name':name,'version':ver,'category':'npm-package',
                              'source_type':'docker','source_ref':cname}))
except Exception:
    pass
" "$container" 2>/dev/null >> "$TMPFILE" || true
        # dpkg dans les conteneurs est omis : trop volumineux et redondant avec l'hôte
    done
}
_detect_docker_packages

# ── Construire le payload JSON dans un fichier (évite ARG_MAX sur -d "...") ──
_build_payload() {
    # $1 = "send" → {"technologies":[...]}  |  sinon → pretty array
    local mode="${1:-display}"
    local py; py=$(_py)
    if [ -n "$py" ]; then
        "$py" -c "
import json, sys
items = []
with open(sys.argv[1]) as f:
    for line in f:
        line = line.strip()
        if line:
            try: items.append(json.loads(line))
            except Exception: pass
with open(sys.argv[2], 'w') as out:
    if sys.argv[3] == 'send':
        json.dump({'technologies': items}, out)
    else:
        json.dump(items, out, indent=2)
" "$TMPFILE" "$PAYLOAD" "$mode"
    else
        # fallback bash pur
        local pref sfx
        [ "$mode" = "send" ] && pref='{"technologies":[' sfx=']}' || pref='[' sfx=']'
        printf '%s' "$pref" > "$PAYLOAD"
        local first=1
        while IFS= read -r line; do
            [ -z "$line" ] && continue
            [ "$first" = "1" ] || printf ',' >> "$PAYLOAD"
            printf '%s' "$line" >> "$PAYLOAD"; first=0
        done < "$TMPFILE"
        printf '%s\n' "$sfx" >> "$PAYLOAD"
    fi
}

# ── Installation crontab (--yes) ──────────────────────────────────────────────
if [ "$AUTO_CRON" = "1" ] && [ -n "$SEND_URL" ]; then
    # ⚠️ R25 (revue) : la crontab ne peut être autorisée que par une source de jeton
    # que la crontab pourra RELIRE chaque nuit. Un jeton présent uniquement dans
    # l'environnement du shell interactif ($VULNRADAR_TOKEN, non héritée par cron) ou
    # passé en argument (--token, volontairement jamais recopié dans la ligne de cron)
    # n'existe plus au moment où cron s'exécute : installer la crontab sur cette base
    # produit une ingestion qui échoue chaque nuit sans que personne le remarque.
    # Seul un FICHIER de jeton lisible est durable.
    _CRON_TOKEN_FILE=""
    if [ -n "$_TOKEN_FILE_ARG" ] && [ -r "$_TOKEN_FILE_ARG" ]; then
        _CRON_TOKEN_FILE="$_TOKEN_FILE_ARG"
    elif [ -r "$_TOKEN_DEFAULT_FILE" ]; then
        _CRON_TOKEN_FILE="$_TOKEN_DEFAULT_FILE"
    fi
    if [ -z "$_CRON_TOKEN_FILE" ]; then
        echo "Erreur : aucun fichier de jeton lisible — crontab NON installée." >&2
        if [ -n "$_TOKEN_FILE_ARG" ]; then
            echo "  --token-file \"$_TOKEN_FILE_ARG\" est introuvable ou non lisible." >&2
        elif [ -n "${VULNRADAR_TOKEN:-}" ]; then
            echo "  \$VULNRADAR_TOKEN n'est pas héritée par cron : elle ne suffit pas ici." >&2
        elif [ -n "$_TOKEN_ARG_DEPRECATED" ]; then
            echo "  --token n'est jamais recopié dans la crontab (jeton en clair)." >&2
        fi
        echo "  Créez le fichier de jeton, puis relancez avec --yes :" >&2
        echo "    printf '%s\\n' '<votre-jeton>' > ~/.vulnradar-token && chmod 600 ~/.vulnradar-token" >&2
    else
        SCRIPT_PATH="$HOME/vulnradar-detect.sh"
        chmod +x "$SCRIPT_PATH" 2>/dev/null || true
        # Heure nocturne tirée au hasard (minute 0-59, heure 1h-5h) pour éviter que toutes
        # les machines poussent à la même minute et saturent le serveur.
        CRON_MIN=$(( RANDOM % 60 ))
        CRON_HOUR=$(( 1 + RANDOM % 5 ))
        # La ligne de crontab ne contient PAS le jeton : le script le relit depuis son
        # fichier à chaque exécution. En revanche elle doit contenir le CHEMIN du
        # fichier dès qu'il n'est pas celui par défaut — un chemin n'est pas un secret,
        # et sans lui la crontab chercherait ~/.vulnradar-token, ne le trouverait pas,
        # et échouerait silencieusement chaque nuit.
        _CRON_TOKEN_OPT=""
        if [ "$_CRON_TOKEN_FILE" != "$_TOKEN_DEFAULT_FILE" ]; then
            _CRON_TOKEN_OPT=" --token-file \"$_CRON_TOKEN_FILE\""
        fi
        CRON_LINE="$CRON_MIN $CRON_HOUR * * * bash \"$SCRIPT_PATH\" --send --url \"$SEND_URL\"$_CRON_TOKEN_OPT >> ~/.vulnradar/detect.log 2>&1"
        # Créer le dossier de log si nécessaire
        mkdir -p "$HOME/.vulnradar" 2>/dev/null || true
        if ! crontab -l 2>/dev/null | grep -qF "vulnradar-detect.sh"; then
            (crontab -l 2>/dev/null; echo "$CRON_LINE") | crontab -
            printf 'Crontab installée (quotidien à %dh%02d — heure répartie aléatoirement).\n' "$CRON_HOUR" "$CRON_MIN"
            echo "  Le jeton sera lu depuis $_CRON_TOKEN_FILE à chaque exécution."
        else
            echo "Crontab déjà en place."
        fi
    fi
fi

# ── Envoi ou affichage ────────────────────────────────────────────────────────
# Résoudre le jeton si --send est demandé
if [ -n "$SEND_URL" ]; then
    _resolve_token || true
fi

if [ -n "$SEND_TOKEN" ] && [ -n "$SEND_URL" ]; then
    _build_payload "send"
    # Le jeton est passé via un fichier de configuration curl temporaire (chmod 600)
    # pour qu'il ne figure PAS dans la ligne de commande (invisible dans `ps aux`).
    # Limite résiduelle documentée : le contenu du fichier temporaire reste lisible
    # par root et par les outils d'audit système tant que le processus curl tourne,
    # mais il n'est PAS visible dans /proc/<pid>/cmdline ni via `ps`.
    CURL_CFG=$(mktemp /tmp/vulnradar-curl-XXXXXX)
    chmod 600 "$CURL_CFG"
    printf 'header = "X-Ingest-Token: %s"\n' "$SEND_TOKEN" > "$CURL_CFG"
    trap 'rm -f "$TMPFILE" "$PAYLOAD" "$INGEST_LOG" "$CURL_CFG"' EXIT
    HTTP_CODE=$(curl -s -o "$INGEST_LOG" -w "%{http_code}" \
        -X POST "$SEND_URL" \
        -H "Content-Type: application/json" \
        --config "$CURL_CFG" \
        --data-binary "@$PAYLOAD") || true
    rm -f "$CURL_CFG"
    case "$HTTP_CODE" in
        200|201|204)
            COUNT=$(grep -c '^{' "$TMPFILE" 2>/dev/null || echo "?")
            echo "Envoyé ($COUNT technologies, HTTP $HTTP_CODE)." ;;
        *)
            echo "Erreur HTTP ${HTTP_CODE:-?} lors de l'envoi." >&2
            cat "$INGEST_LOG" >&2 2>/dev/null || true ;;
    esac
else
    _build_payload "display"
    cat "$PAYLOAD"
fi
