#!/usr/bin/env bash
#
# php-cve-check.sh
# Verifica se un server Debian 12 (bookworm) / 13 (trixie) e' esposto a
# CVE-2026-109618 e CVE-2026-109619 (RCE nel core di PHP), controlla le
# mitigazioni globali e, su richiesta, le applica a tutti i vhost in una volta.
# Funziona con Apache (mod_php, FastCGI/suPHP, PHP-FPM) e con Nginx (PHP-FPM) e
# riconosce i server gestiti con ISPConfig (Apache o Nginx).
#
# Uso:   sudo ./php-cve-check.sh [opzioni]
#   --update      esegue "apt-get update" prima dei controlli (consigliato)
#   --logs        cerca indicatori sospetti (file .php recenti, ecc.)
#   --harden      applica la messa in sicurezza globale di PHP (vedi sotto)
#   --dry-run     con --harden: mostra cosa farebbe senza modificare nulla
#   --skip-disable-functions
#                 con --harden: non tocca disable_functions (se le app si rompono)
#   --rollback    rimuove la messa in sicurezza applicata da --harden
#   --no-color    disattiva i colori
#   -h, --help    questo aiuto
#
# Cosa fa --harden (per ogni versione PHP in /etc/php, SAPI fpm/apache2/cgi):
#   crea /etc/php/<ver>/mods-available/cve-hardening.ini e lo abilita con
#   phpenmod, quindi vale per TUTTI i vhost. I php.ini originali NON vengono
#   modificati e la CLI non viene toccata (composer, artisan, wp-cli continuano
#   a funzionare). Il file imposta:
#     session.upload_progress.enabled = Off
#     expose_php = Off
#     disable_functions = <valore gia' presente> + exec,passthru,shell_exec,
#                         system,proc_open,popen,pcntl_exec
#   Poi testa la configurazione e ricarica PHP-FPM/Apache. Backup in
#   /var/backups/php-cve-hardening/. ATTENZIONE: disable_functions puo' rompere
#   alcune applicazioni (WordPress con certi plugin, Magento, ecc.).
#   Le direttive php_admin_value[...] nei pool FPM hanno la precedenza sul file
#   globale: lo script le segnala ma non le modifica. Con ISPConfig i file in
#   pool.d sono rigenerati dal pannello: eventuali override vanno corretti da
#   ISPConfig (Sites > Website > Options > Custom php.ini settings), non a mano.
#   Le mitigazioni sono provvisorie e NON sostituiscono la patch di PHP.
#
# Exit code: 0 = ok, 1 = avvisi, 2 = sistema vulnerabile / azione richiesta
#
# Nota: Debian applica le correzioni come "backport" senza cambiare la versione
# upstream, quindi il confronto delle versioni non e' affidabile. Lo script
# verifica invece che il changelog del pacchetto installato citi le CVE.
# Per controllare altre CVE basta aggiungerle all'array CVES.

set -u
export LC_ALL=C

CVES=(CVE-2026-109618 CVE-2026-109619)

HARDEN_NAME="cve-hardening"
HARDEN_SAPIS=(fpm apache2 cgi)
DEFAULT_DF="exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec"
BACKUP_ROOT="/var/backups/php-cve-hardening"

DO_UPDATE=0
DO_LOGS=0
DO_HARDEN=0
DO_ROLLBACK=0
DRY_RUN=0
SKIP_DF=0
USE_COLOR=1
OKS=0
WARNS=0
FAILS=0

usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; }

for arg in "$@"; do
  case "$arg" in
    --update)   DO_UPDATE=1 ;;
    --logs)     DO_LOGS=1 ;;
    --harden)   DO_HARDEN=1 ;;
    --dry-run)  DRY_RUN=1 ;;
    --skip-disable-functions) SKIP_DF=1 ;;
    --rollback) DO_ROLLBACK=1 ;;
    --no-color) USE_COLOR=0 ;;
    -h|--help)  usage; exit 0 ;;
    *) echo "Opzione sconosciuta: $arg" >&2; exit 64 ;;
  esac
done

if [ "$DO_HARDEN" -eq 1 ] && [ "$DO_ROLLBACK" -eq 1 ]; then
  echo "--harden e --rollback non possono essere usati insieme" >&2; exit 64
fi
if [ "$DRY_RUN" -eq 1 ] && [ "$DO_HARDEN" -eq 0 ]; then
  echo "--dry-run ha senso solo con --harden" >&2; exit 64
fi
if { [ "$DO_HARDEN" -eq 1 ] || [ "$DO_ROLLBACK" -eq 1 ]; } && [ "$DRY_RUN" -eq 0 ] && [ "$(id -u)" -ne 0 ]; then
  echo "--harden e --rollback richiedono root (usa sudo)" >&2; exit 77
fi

if [ -t 1 ] && [ "$USE_COLOR" -eq 1 ]; then
  G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; B=$'\033[1m'; N=$'\033[0m'
else
  G=""; Y=""; R=""; B=""; N=""
fi

section() { printf '\n%s== %s ==%s\n' "$B" "$1" "$N"; }
ok()      { OKS=$((OKS+1));     printf '%s[ OK ]%s %s\n'  "$G" "$N" "$*"; }
warn()    { WARNS=$((WARNS+1)); printf '%s[WARN]%s %s\n'  "$Y" "$N" "$*"; }
fail()    { FAILS=$((FAILS+1)); printf '%s[FAIL]%s %s\n'  "$R" "$N" "$*"; }
info()    { printf '[info] %s\n' "$*"; }

minor_of() { # estrae X.Y dal nome pacchetto (php8.2-fpm -> 8.2)
  printf '%s' "$1" | sed -nE 's/^(libapache2-mod-)?php([0-9]+\.[0-9]+).*/\2/p'
}

changelog_has_cve() { # <pacchetto> <cve>
  local pkg=$1 cve=$2 v d f
  v=$(minor_of "$pkg")
  for d in "$pkg" "php${v}-common"; do
    for f in "/usr/share/doc/$d/changelog.Debian.gz" \
             "/usr/share/doc/$d/changelog.gz" \
             "/usr/share/doc/$d/changelog.Debian"; do
      [ -r "$f" ] || continue
      if zgrep -q "$cve" "$f" 2>/dev/null; then return 0; fi
    done
  done
  return 1
}

# Versioni PHP con configurazione in /etc/php (anche senza pacchetti "phpX.Y" visibili)
list_php_config_versions() {
  local d
  for d in /etc/php/[0-9]*.[0-9]*; do
    [ -d "$d" ] && basename "$d"
  done
}

# Legge il valore di una direttiva ESCLUDENDO il nostro drop-in (serve per unire
# disable_functions senza perdere quanto gia' configurato).
effective_ini_without_ours() { # <ver> <sapi> <chiave>
  local v=$1 s=$2 k=$3 d=/etc/php/$1/$2 bin tmp out f
  if bin=$(command -v "php$v" 2>/dev/null); then
    tmp=$(mktemp -d)
    for f in "$d"/conf.d/*; do
      [ -e "$f" ] || continue
      case "$(basename "$f")" in *"$HARDEN_NAME"*) continue ;; esac
      ln -s "$(readlink -f "$f")" "$tmp/$(basename "$f")"
    done
    out=$(PHP_INI_SCAN_DIR="$tmp" "$bin" -c "$d/php.ini" -r "echo ini_get('$k');" 2>/dev/null)
    rm -rf "$tmp"
    printf '%s' "$out"
  else
    # senza CLI: ultima occorrenza testuale in php.ini e conf.d (escluso il nostro file)
    grep -hE "^[[:space:]]*$k[[:space:]]*=" "$d/php.ini" \
      $(ls "$d"/conf.d/* 2>/dev/null | grep -v "$HARDEN_NAME") 2>/dev/null |
      tail -n1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^"//; s/"[[:space:]]*(;.*)?$//; s/[[:space:]]*;.*$//'
  fi
}

merge_csv() { # unisce liste separate da virgola, senza duplicati
  printf '%s\n' "$@" | tr ',' '\n' | sed 's/[[:space:]]//g' | awk 'NF && !seen[$0]++' | paste -sd, -
}

BACKUP_DIR=""
ensure_backup_dir() {
  if [ -z "$BACKUP_DIR" ]; then
    BACKUP_DIR="$BACKUP_ROOT/$(date +%Y%m%d-%H%M%S)"
    mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_ROOT" "$BACKUP_DIR"
  fi
}

CHANGED_VERSIONS=()

build_hardening_ini() { # <df>  -> stampa il contenuto (le righe ';' sono commenti)
  local df=$1
  printf '; priority=99\n'
  printf '; Generato da php-cve-check.sh il %s. Rimuovi con: php-cve-check.sh --rollback\n' "$(date '+%F %T')"
  printf '; Mitigazione provvisoria per CVE-2026-109618 / CVE-2026-109619. Non sostituisce la patch.\n'
  printf 'session.upload_progress.enabled = Off\n'
  printf 'expose_php = Off\n'
  [ -n "$df" ] && printf 'disable_functions = %s\n' "$df"
}

harden_version() { # <versione>
  local v=$1 mods="/etc/php/$1/mods-available/$HARDEN_NAME.ini" s present=() df="" cur new tmp
  for s in "${HARDEN_SAPIS[@]}"; do
    [ -f "/etc/php/$v/$s/php.ini" ] && present+=("$s")
  done
  if [ "${#present[@]}" -eq 0 ]; then
    info "PHP $v: nessun SAPI web (fpm/apache2/cgi) configurato, salto"
    return 0
  fi

  if [ "$SKIP_DF" -eq 0 ]; then
    df=$DEFAULT_DF
    for s in "${present[@]}"; do
      cur=$(effective_ini_without_ours "$v" "$s" disable_functions)
      df=$(merge_csv "$df" "$cur")
    done
  fi
  new=$(build_hardening_ini "$df")

  # gia' applicato e identico?
  if [ -f "$mods" ] && [ "$(grep -v '^;' "$mods")" = "$(printf '%s\n' "$new" | grep -v '^;')" ]; then
    local linked=1
    for s in "${present[@]}"; do
      ls /etc/php/$v/$s/conf.d/*"$HARDEN_NAME"* >/dev/null 2>&1 || linked=0
    done
    if [ "$linked" -eq 1 ]; then
      ok "PHP $v: messa in sicurezza gia' applicata (SAPI: ${present[*]})"
      return 0
    fi
  fi

  if [ "$DRY_RUN" -eq 1 ]; then
    info "[dry-run] PHP $v: scriverei $mods e lo abiliterei per: ${present[*]}"
    printf '%s\n' "$new" | sed 's/^/          /'
    return 0
  fi

  ensure_backup_dir
  if [ -f "$mods" ]; then
    cp -a "$mods" "$BACKUP_DIR/php$v-$HARDEN_NAME.ini.bak"
  fi
  tmp=$(mktemp "$mods.XXXXXX") || { fail "PHP $v: impossibile creare $mods"; return 1; }
  printf '%s\n' "$new" > "$tmp"
  chmod 644 "$tmp"
  mv -f "$tmp" "$mods"

  local failed=0
  for s in "${present[@]}"; do
    if command -v phpenmod >/dev/null 2>&1; then
      phpenmod -v "$v" -s "$s" "$HARDEN_NAME" >/dev/null 2>&1 || failed=1
    else
      ln -sf "$mods" "/etc/php/$v/$s/conf.d/99-$HARDEN_NAME.ini" || failed=1
    fi
  done
  if [ "$failed" -eq 1 ]; then
    fail "PHP $v: abilitazione di $HARDEN_NAME non riuscita per alcuni SAPI"
    return 1
  fi
  ok "PHP $v: messa in sicurezza applicata a ${present[*]} (disable_functions: $([ "$SKIP_DF" -eq 1 ] && echo non toccato || echo aggiornato))"
  CHANGED_VERSIONS+=("$v")
}

reload_php_services() { # [versioni...]
  local v any=0
  for v in "$@"; do
    # PHP-FPM
    if systemctl is-active --quiet "php$v-fpm" 2>/dev/null; then
      any=1
      if "php-fpm$v" -t >/dev/null 2>&1 || /usr/sbin/php-fpm$v -t >/dev/null 2>&1; then
        if systemctl reload "php$v-fpm" 2>/dev/null; then ok "php$v-fpm: configurazione valida, ricaricato"
        else fail "php$v-fpm: reload fallito"; fi
      else
        fail "php$v-fpm: il test della configurazione e' fallito, NON ricaricato (controlla: php-fpm$v -t)"
      fi
    fi
  done
  # Apache (mod_php o proxy_fcgi)
  if systemctl is-active --quiet apache2 2>/dev/null; then
    any=1
    if apache2ctl configtest >/dev/null 2>&1; then
      if systemctl reload apache2 2>/dev/null; then ok "apache2: configurazione valida, ricaricato"
      else fail "apache2: reload fallito"; fi
    else
      fail "apache2: configtest fallito, NON ricaricato"
    fi
  fi
  [ "$any" -eq 0 ] && info "Nessun servizio PHP-FPM/Apache attivo da ricaricare"
  return 0
}

do_rollback() {
  local v mods s any=0 vers=()
  for v in $(list_php_config_versions); do
    mods="/etc/php/$v/mods-available/$HARDEN_NAME.ini"
    [ -f "$mods" ] || continue
    any=1
    ensure_backup_dir
    cp -a "$mods" "$BACKUP_DIR/php$v-$HARDEN_NAME.ini.removed"
    if command -v phpdismod >/dev/null 2>&1; then
      phpdismod -v "$v" "$HARDEN_NAME" >/dev/null 2>&1
    fi
    for s in "${HARDEN_SAPIS[@]}" cli; do
      find "/etc/php/$v/$s/conf.d" -maxdepth 1 -name "*$HARDEN_NAME*" -type l -delete 2>/dev/null
    done
    rm -f "$mods"
    ok "PHP $v: messa in sicurezza rimossa (copia in $BACKUP_DIR)"
    vers+=("$v")
  done
  if [ "$any" -eq 0 ]; then
    info "Nessuna messa in sicurezza trovata da rimuovere"
  else
    reload_php_services "${vers[@]}"
  fi
}

# ---------------------------------------------------------------- 0. Contesto
section "Contesto"
if [ "$(id -u)" -ne 0 ]; then
  warn "Non sei root: alcuni controlli (processi, log, configurazioni) saranno incompleti. Usa sudo."
fi

# shellcheck disable=SC1091
. /etc/os-release 2>/dev/null || true
case "${ID:-}:${VERSION_ID:-}" in
  debian:12|debian:13) ok "Sistema supportato: ${PRETTY_NAME:-Debian ${VERSION_ID}}" ;;
  *) warn "Sistema non previsto (${PRETTY_NAME:-sconosciuto}): lo script e' pensato per Debian 12/13" ;;
esac
info "Host: $(hostname) - $(date '+%F %T %Z')"

if [ "$DO_ROLLBACK" -eq 1 ]; then
  section "Rollback della messa in sicurezza"
  do_rollback
  echo
  echo "Rollback completato. Rilancia lo script senza opzioni per verificare lo stato."
  [ "$FAILS" -gt 0 ] && exit 2
  exit 0
fi

if [ "$DO_UPDATE" -eq 1 ]; then
  if [ "$(id -u)" -eq 0 ]; then
    info "Aggiorno l'indice dei pacchetti (apt-get update)..."
    apt-get update -qq 2>/dev/null && ok "Indice pacchetti aggiornato" || warn "apt-get update fallito"
  else
    warn "--update richiede root, salto"
  fi
else
  stamp=/var/lib/apt/periodic/update-success-stamp
  if [ -e "$stamp" ]; then
    age=$(( ( $(date +%s) - $(stat -c %Y "$stamp") ) / 3600 ))
    [ "$age" -gt 12 ] && warn "Indice apt aggiornato ${age}h fa: usa --update per vedere le patch di oggi"
  else
    info "Eta' dell'indice apt non determinabile: usa --update per vedere le patch di oggi"
  fi
fi

# ------------------------------------------------------ 0a. Stack web rilevato
section "Stack web rilevato"
HAS_APACHE=0; HAS_NGINX=0; HAS_ISPC=0; POOL_OVERRIDE_FOUND=0
unit_state() { # <unit> <pacchetto>
  if systemctl is-active --quiet "$1" 2>/dev/null; then echo "attivo"
  elif dpkg -s "$2" >/dev/null 2>&1; then echo "installato ma non attivo"
  else echo "assente"; fi
}
ap_state=$(unit_state apache2 apache2)
ng_state=$(unit_state nginx nginx)
[ "$ap_state" != "assente" ] && HAS_APACHE=1
[ "$ng_state" != "assente" ] && HAS_NGINX=1
info "Apache: $ap_state - Nginx: $ng_state"

if [ "$ap_state" = "attivo" ]; then
  mods=$(apache2ctl -M 2>/dev/null)
  if [ -n "$mods" ]; then
    echo "$mods" | grep -qE 'php[0-9]*_module'  && info "Apache: PHP come modulo (mod_php), usa il php.ini di apache2"
    echo "$mods" | grep -qE 'proxy_fcgi_module'  && info "Apache: PHP tramite PHP-FPM (proxy_fcgi), usa il php.ini di fpm"
    echo "$mods" | grep -qE 'fcgid_module|suphp_module|cgi' && info "Apache: PHP tramite FastCGI/suPHP/CGI, usa il php.ini di cgi"
  else
    info "Apache: moduli non leggibili (servono privilegi root)"
  fi
fi
if [ "$ng_state" = "attivo" ]; then
  if [ "$(id -u)" -eq 0 ] && command -v nginx >/dev/null 2>&1; then
    fp=$(nginx -T 2>/dev/null | grep -oE 'fastcgi_pass[[:space:]]+[^;]+' | sort -u | wc -l)
    info "Nginx: PHP solo tramite PHP-FPM ($fp destinazioni fastcgi_pass distinte), usa il php.ini di fpm"
  else
    info "Nginx: PHP solo tramite PHP-FPM, usa il php.ini di fpm"
  fi
fi
if [ -d /usr/local/ispconfig ]; then
  HAS_ISPC=1
  isv=$(grep -hoE "ISPC_APP_VERSION'?[[:space:]]*,[[:space:]]*'[^']+" /usr/local/ispconfig/interface/lib/config.inc.php 2>/dev/null | grep -oE "[0-9][0-9.a-z-]*" | head -1)
  info "ISPConfig rilevato${isv:+ (versione $isv)}: i pool FPM e i vhost sono generati dal pannello"
  sitesini=$(find /var/www/conf /var/www/clients -xdev -maxdepth 4 -name php.ini 2>/dev/null | wc -l)
  [ "$sitesini" -gt 0 ] && warn "$sitesini php.ini per-sito (modalita' FastCGI/suPHP di ISPConfig): possono ridefinire disable_functions, controllali anche questi"
fi
opt=$(ls -d /opt/php* 2>/dev/null | tr '\n' ' ')
[ -n "$opt" ] && warn "PHP in /opt ($opt): versioni compilate a mano o aggiuntive di ISPConfig, NON gestite da questo script (aggiornale e controllane il php.ini a mano)"
[ "$HAS_APACHE" -eq 0 ] && [ "$HAS_NGINX" -eq 0 ] && info "Nessun web server Apache/Nginx rilevato"

# -------------------------------------------------- 0b. Messa in sicurezza globale
if [ "$DO_HARDEN" -eq 1 ]; then
  section "Messa in sicurezza globale di PHP$([ "$DRY_RUN" -eq 1 ] && echo ' (dry-run)')"
  mapfile -t CFG_VERS < <(list_php_config_versions)
  if [ "${#CFG_VERS[@]}" -eq 0 ]; then
    info "Nessuna configurazione PHP trovata in /etc/php (PHP non installato da pacchetti Debian/Sury?)"
  else
    for v in "${CFG_VERS[@]}"; do
      harden_version "$v"
    done
    if [ "$DRY_RUN" -eq 1 ]; then
      info "Dry-run: nessuna modifica effettuata. Per applicare: sudo $0 --harden"
    elif [ "${#CHANGED_VERSIONS[@]}" -gt 0 ]; then
      info "Backup in: ${BACKUP_DIR:-$BACKUP_ROOT}"
      if [ "$SKIP_DF" -eq 0 ]; then
        info "disable_functions e' ora attivo per fpm/apache2/cgi: se un'applicazione smette di funzionare, usa --rollback oppure rilancia con --harden --skip-disable-functions"
      fi
      reload_php_services "${CHANGED_VERSIONS[@]}"
      if [ "$HAS_ISPC" -eq 1 ]; then
        info "ISPConfig: prova ora il pannello e un paio di siti; se qualcosa si rompe usa --rollback o --skip-disable-functions"
      fi
    fi
  fi
fi

# ------------------------------------------------------- 1. Pacchetti PHP
section "Pacchetti PHP installati e stato patch"
mapfile -t PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' 2>/dev/null |
  awk '$1=="ii" && $2 ~ /^(libapache2-mod-)?php[0-9]+\.[0-9]+(-(cli|fpm|cgi|common))?$/ {print $2" "$3}')

if [ "${#PKGS[@]}" -eq 0 ]; then
  ok "Nessun pacchetto PHP di sistema trovato (se usi PHP compilato a mano o in container, controllalo a parte)"
  MINORS=()
else
  declare -A MINOR_SEEN=()
  declare -A MINOR_UNPATCHED=()
  for entry in "${PKGS[@]}"; do
    pkg=${entry%% *}; ver=${entry#* }
    v=$(minor_of "$pkg")
    MINOR_SEEN[$v]=1
    missing=()
    for cve in "${CVES[@]}"; do
      changelog_has_cve "$pkg" "$cve" || missing+=("$cve")
    done
    if [ "${#missing[@]}" -eq 0 ]; then
      ok "$pkg $ver: il changelog cita la correzione di tutte le CVE"
    else
      fail "$pkg $ver: nessuna correzione trovata per ${missing[*]}"
      MINOR_UNPATCHED[$v]=1
    fi
  done
  MINORS=("${!MINOR_SEEN[@]}")

  # Versioni a fine vita (8.2 e' l'ultima del gruppo ancora in sicurezza fino a fine 2026)
  for v in "${MINORS[@]}"; do
    major=${v%%.*}; minor=${v##*.}
    if [ "$major" -lt 8 ] || { [ "$major" -eq 8 ] && [ "$minor" -lt 2 ]; }; then
      fail "PHP $v e' a fine vita: nessuna patch attesa, migra a un ramo 8.x supportato"
    fi
  done

  # C'e' gia' una correzione nel repository?
  for v in "${!MINOR_UNPATCHED[@]}"; do
    inst=$(dpkg-query -W -f='${Version}' "php${v}-common" 2>/dev/null)
    cand=$(apt-cache policy "php${v}-common" 2>/dev/null | awk '/Candidate:/{print $2}')
    if [ -n "$cand" ] && [ "$cand" != "(none)" ] && [ "$cand" != "$inst" ]; then
      if timeout 40 apt-get changelog "php${v}-common" 2>/dev/null | grep -qE "$(IFS='|'; echo "${CVES[*]}")"; then
        warn "PHP $v: la correzione e' DISPONIBILE nel repository ($inst -> $cand). Esegui: apt install --only-upgrade 'php${v}*' 'libapache2-mod-php${v}'"
      else
        info "PHP $v: esiste un aggiornamento ($cand) ma il changelog non cita ancora le CVE (o apt changelog non e' raggiungibile)"
      fi
    else
      info "PHP $v: nessun pacchetto corretto nel repository per ora (patch upstream prevista il 21/10/2026 alle 14:00 UTC, poi servono le build Debian/Sury)"
    fi
  done
fi

# ----------------------------------------------------- 2. Processi da riavviare
section "Servizi gia' aggiornati ma non riavviati"
stale=0
for pid in $(pgrep -x 'php-fpm[0-9.]*|apache2|php-cgi[0-9.]*' 2>/dev/null); do
  [ -r "/proc/$pid/maps" ] || continue
  exe=$(readlink "/proc/$pid/exe" 2>/dev/null)
  if grep -qE '(php|libphp)[^ ]*\(deleted\)' "/proc/$pid/maps" 2>/dev/null || [[ "$exe" == *"(deleted)"* ]]; then
    stale=1
    warn "Processo $pid ($(cat /proc/$pid/comm 2>/dev/null)) usa un binario PHP sostituito: riavvialo"
  fi
done
if [ "$stale" -eq 0 ]; then
  if pgrep -x 'php-fpm[0-9.]*|apache2' >/dev/null 2>&1; then
    ok "Nessun processo web/PHP con binari obsoleti in memoria"
  else
    info "Nessun processo PHP-FPM/Apache in esecuzione"
  fi
fi
if command -v needrestart >/dev/null 2>&1 && [ "$(id -u)" -eq 0 ]; then
  nr=$(needrestart -b -r l 2>/dev/null | grep -E 'NEEDRESTART-SVC: .*(php|apache|nginx)')
  [ -n "$nr" ] && warn "needrestart segnala servizi da riavviare: $(echo "$nr" | sed 's/NEEDRESTART-SVC: //' | tr '\n' ' ')"
fi

# ------------------------------------------------------ 3. Mitigazioni globali
section "Mitigazioni a livello di server (provvisorie)"
info "Il meccanismo delle falle non e' ancora confermato ufficialmente: queste impostazioni riducono il rischio ma non sostituiscono la patch."
ini_get() { # <versione> <sapi> <chiave>
  local v=$1 s=$2 k=$3 d=/etc/php/$1/$2 bin
  bin=$(command -v "php$v") || return 1
  PHP_INI_SCAN_DIR="$d/conf.d" "$bin" -c "$d/php.ini" -r "echo ini_get('$k');" 2>/dev/null
}
mapfile -t CHECK_VERS < <(
  { printf '%s\n' "${MINORS[@]:-}"; list_php_config_versions; } | awk 'NF && !s[$0]++'
)
if [ "${#CHECK_VERS[@]}" -eq 0 ]; then
  info "Nessuna versione PHP di sistema da controllare"
else
  for v in "${CHECK_VERS[@]}"; do
    for s in fpm apache2 cgi; do
      [ -f "/etc/php/$v/$s/php.ini" ] || continue
      if ! command -v "php$v" >/dev/null 2>&1; then
        info "php$v-cli assente: salto la lettura delle impostazioni di $s"; continue
      fi
      up=$(ini_get "$v" "$s" session.upload_progress.enabled)
      df=$(ini_get "$v" "$s" disable_functions)
      ep=$(ini_get "$v" "$s" expose_php)
      case "$up" in ""|0) ok "php$v/$s: session.upload_progress.enabled = Off" ;;
                    *)    warn "php$v/$s: session.upload_progress.enabled = On" ;; esac
      if [ -n "$df" ]; then ok "php$v/$s: disable_functions impostato ($(echo "$df" | tr ',' '\n' | wc -l) funzioni)"
      else warn "php$v/$s: disable_functions vuoto"; fi
      case "$ep" in ""|0) ok "php$v/$s: expose_php = Off" ;;
                    *)    warn "php$v/$s: expose_php = On (l'header X-Powered-By facilita i bot)" ;; esac
    done
    if ls /etc/php/$v/fpm/pool.d/*.conf >/dev/null 2>&1; then
      npools=$(ls /etc/php/$v/fpm/pool.d/*.conf | wc -l)
      for key in disable_functions session.upload_progress.enabled expose_php; do
        esc=${key//./\\.}
        ov=$(grep -lE "^[[:space:]]*php_(admin_)?(value|flag)\[$esc\]" /etc/php/$v/fpm/pool.d/*.conf 2>/dev/null)
        if [ -n "$ov" ]; then
          POOL_OVERRIDE_FOUND=1
          n=$(echo "$ov" | wc -l)
          warn "php$v-fpm: $n pool su $npools sovrascrivono $key (l'impostazione globale NON vale per questi pool)"
          echo "$ov" | head -5 | sed 's/^/        /'
          [ "$n" -gt 5 ] && echo "        ... e altri $((n-5))"
        else
          ok "php$v-fpm: nessun pool sovrascrive $key ($npools pool)"
        fi
      done
    fi
  done
  if [ "$POOL_OVERRIDE_FOUND" -eq 1 ]; then
    if [ "$HAS_ISPC" -eq 1 ]; then
      info "ISPConfig rigenera i file in pool.d: non modificarli a mano. Imposta i valori da pannello (Sites > Website > Options > Custom php.ini settings) oppure verifica le impostazioni PHP del server in System > Server Config."
    else
      info "Per i pool elencati modifica le direttive php_admin_value/php_value nel file del pool, poi ricarica PHP-FPM."
    fi
  fi
fi

# --------------------------------------------------------------- 4. Esposizione
section "Esposizione di rete"
if command -v ss >/dev/null 2>&1; then
  web=$(ss -ltnH 2>/dev/null | awk '{print $4}' | grep -E ':(80|443|8080|8443)$' | sort -u | tr '\n' ' ')
  if [ -n "$web" ]; then warn "Porte web in ascolto: $web (se non puoi patchare subito, limita l'accesso o metti un WAF davanti)"
  else ok "Nessuna porta web standard in ascolto"; fi
  if [ "$HAS_ISPC" -eq 1 ] && ss -ltnH 2>/dev/null | awk '{print $4}' | grep -qE ':8080$'; then
    warn "Pannello ISPConfig in ascolto su :8080 (e' PHP e di solito aperto a tutti): limitalo agli IP di gestione con il firewall finche' non c'e' la patch"
  fi
else
  info "ss non disponibile"
fi

# ------------------------------------------------------ 5. Aggiornamenti pendenti
section "Aggiornamenti di sicurezza pendenti"
SIM=$(apt-get -s upgrade 2>/dev/null)
php_up=$(echo "$SIM" | awk '/^Inst /{print $2}' | grep -E '^(php|libapache2-mod-php)' | tr '\n' ' ')
sec_n=$(echo "$SIM" | grep -E '^Inst ' | grep -ciE 'security')
if [ -n "$php_up" ]; then warn "Pacchetti PHP aggiornabili: $php_up"; else ok "Nessun aggiornamento PHP in attesa"; fi
if [ "$sec_n" -gt 0 ]; then warn "$sec_n pacchetti con aggiornamento di sicurezza in attesa (apt list --upgradable)"; else ok "Nessun altro aggiornamento di sicurezza in attesa"; fi

# ------------------------------------------------------------ 6. Indicatori (opzionale)
if [ "$DO_LOGS" -eq 1 ]; then
  section "Ricerca di indicatori sospetti (euristica, non prova di compromissione)"
  hits=$(find /tmp /var/tmp /dev/shm -xdev -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) 2>/dev/null | head -50)
  [ -n "$hits" ] && { fail "File PHP in directory temporanee:"; echo "$hits" | sed 's/^/        /'; } || ok "Nessun file PHP in /tmp, /var/tmp, /dev/shm"

  recent=$(find /var/www /srv -xdev -type f -name '*.php' -mtime -3 \
           \( -ipath '*upload*' -o -ipath '*cache*' -o -ipath '*tmp*' -o -ipath '*media*' \) 2>/dev/null | head -50)
  [ -n "$recent" ] && { warn "File PHP modificati negli ultimi 3 giorni in directory di upload/cache:"; echo "$recent" | sed 's/^/        /'; } \
                   || ok "Nessun file PHP recente in directory di upload/cache"

  web_hits=$(find /var/www /srv -xdev -type f -name '*.php' -mtime -3 2>/dev/null | head -2000 |
             xargs -r grep -lE 'eval\(base64_decode|gzinflate\(base64_decode|assert\(\$_(POST|REQUEST)' 2>/dev/null | head -20)
  [ -n "$web_hits" ] && { fail "File recenti con pattern tipici di webshell:"; echo "$web_hits" | sed 's/^/        /'; } \
                     || ok "Nessun pattern da webshell nei file PHP recenti"

  tool=$(grep -rIl -i 'php2shell' /var/log/apache2 /var/log/nginx 2>/dev/null | head -5)
  [ -n "$tool" ] && { fail "Riferimenti a 'php2shell' nei log:"; echo "$tool" | sed 's/^/        /'; } || ok "Nessun riferimento a 'php2shell' nei log web"
fi

# ----------------------------------------------------------------- Riepilogo
section "Riepilogo"
printf 'OK: %s%d%s   Avvisi: %s%d%s   Problemi: %s%d%s\n' "$G" "$OKS" "$N" "$Y" "$WARNS" "$N" "$R" "$FAILS" "$N"
if [ "$FAILS" -gt 0 ]; then
  echo "Esito: sistema NON protetto o con problemi. Applica la patch appena disponibile e riavvia PHP-FPM/Apache."
  exit 2
elif [ "$WARNS" -gt 0 ]; then
  echo "Esito: nessun problema bloccante, ma ci sono avvisi da verificare."
  exit 1
fi
echo "Esito: tutto a posto."
exit 0
