#!/usr/bin/env bash
# HZone PHP2Shell Security Audit v4.0
# Read-only inventory / evidence audit for CVE-2026-109618 and CVE-2026-109619.
# Does not install, update, restart, or test exploitation.
set -u
# Optional modes: default is fast/local, --deep inspects configuration files,
# --updates queries enabled package repositories (read-only; may refresh DNF cache).
DEEP=0
UPDATES=0
for opt in "$@"; do
  case "$opt" in
    --deep) DEEP=1 ;;
    --updates) UPDATES=1 ;;
    --help|-h) printf 'Usage: bash %s [--deep] [--updates]\nAll modes are read-only; --updates may refresh package metadata cache.\n' "$0"; exit 0 ;;
    *) printf 'Unknown option: %s\n' "$opt" >&2; exit 2 ;;
  esac
done
export LC_ALL=C
umask 077
CVE1=CVE-2026-109618
CVE2=CVE-2026-109619
STAMP=$(date +%Y%m%d-%H%M%S)
HOST=$(hostname -s 2>/dev/null | tr -cd '[:alnum:]._-')
[ -n "$HOST" ] || HOST=unknown
OUTDIR=${HZ_AUDIT_DIR:-${HOME:-/tmp}}
[ -d "$OUTDIR" ] && [ -w "$OUTDIR" ] || OUTDIR=/tmp
REPORT="$OUTDIR/hzone-php2shell-${HOST}-${STAMP}-$$.txt"
exec 3>&1
if ! exec >"$REPORT" 2>&1; then echo 'Cannot write report' >&3; exit 2; fi
say(){ printf '%s\n' "$*"; }
heading(){ say; say "========== $* =========="; case "$*" in 'PHP RUNTIME INVENTORY'|'CPANEL MULTIPHP VHOST EXPOSURE'|'WEB SAPI RPM PATCH EVIDENCE (READ-ONLY)'|'LIVE PHP WORKER EXECUTABLES') printf '  [SCAN] %s\n' "$*" >&3;; esac; }
console(){ :; }
console_warn(){ :; }

exists(){ command -v "$1" >/dev/null 2>&1; }
version_fixed(){
  local v=$1 base
  case "$v" in
    8.2.*) base=8.2.35;; 8.3.*) base=8.3.36;; 8.4.*) base=8.4.27;; 8.5.*) base=8.5.12;; *) return 1;;
  esac
  [ "$(printf '%s\n' "$base" "$v" | sort -V | head -n1)" = "$base" ]
}
php_version(){
  local s
  s=$(timeout 4 "$1" -n -r 'echo PHP_VERSION;' 2>/dev/null | head -n1) || true
  [[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && { printf '%s' "$s"; return; }
  s=$(timeout 4 "$1" -n -v 2>/dev/null | head -n1) || true
  if [[ "$s" =~ PHP[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]]; then printf '%s' "${BASH_REMATCH[1]}"; fi
}
rpm_evidence(){
  local p=$1 log
  log=$(rpm -q --changelog "$p" 2>/dev/null | head -c 250000) || true
  [[ "$log" == *"$CVE1"* && "$log" == *"$CVE2"* ]]
}
dpkg_evidence(){
  local p=$1 f
  for f in "/usr/share/doc/$p/changelog.Debian.gz" "/usr/share/doc/$p/changelog.gz"; do
    [ -r "$f" ] || continue
    if gzip -cd "$f" 2>/dev/null | head -c 250000 | grep -q "$CVE1" && gzip -cd "$f" 2>/dev/null | head -c 250000 | grep -q "$CVE2"; then return 0; fi
  done
  return 1
}
status_for(){
  local pkg=$1 ver=$2 owner=$3
  if { [ "$owner" = rpm ] || [ "$owner" = cpanel ]; } && rpm_evidence "$pkg"; then echo 'PATCH_EVIDENCE_FOUND_CLI'; return; fi
  if [ "$owner" = dpkg ] && dpkg_evidence "$pkg"; then echo 'PATCH_EVIDENCE_FOUND_CLI'; return; fi
  # Only apply upstream version floors to conventional upstream/cPanel PHP builds.
  # Vendor backports and vendor package numbering require independent evidence.
  if [ "$owner" = cpanel ] && version_fixed "$ver"; then echo 'UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD'; return; fi
  if [ "$owner" = none ] && version_fixed "$ver"; then echo 'UPSTREAM_VERSION_FIXED_VERIFY_BUILD'; return; fi
  if [ "$owner" = rpm ] || [ "$owner" = dpkg ]; then echo 'UNCONFIRMED_VENDOR'; return; fi
  case "$ver" in
    8.2.*|8.3.*|8.4.*|8.5.*) echo 'POSSIBLY_VULNERABLE_NO_VENDOR_EVIDENCE';;
    *) echo 'UNCONFIRMED_UNSUPPORTED_BRANCH';;
  esac
}
heading 'HZONE PHP2SHELL AUDIT v4.0'
say "Date: $(date -Is)"
say "Host: $(hostname -f 2>/dev/null || hostname)"
say "Kernel: $(uname -sr)"
[ -r /etc/os-release ] && grep -E '^(NAME|VERSION|ID)=' /etc/os-release || true
say 'Scope: AlmaLinux/CloudLinux + cPanel, PHP packages, process hints and configuration.'
if ! exists rpm || [ ! -d /usr/local/cpanel ]; then
  say 'WARNING: unsupported host (requires cPanel and RPM-based AlmaLinux/CloudLinux). Results may be incomplete.'
fi
say 'Read-only. No exploit probes. No package updates. No service restarts.'
say "Modes: deep=$DEEP updates=$UPDATES (repository metadata may be refreshed)"
printf '\n  HZONE | PHP2SHELL SECURITY AUDIT v4.0\n  Host: %s\n  CVE-2026-109618 / CVE-2026-109619\n  Mod: READ-ONLY | fara modificari, exploituri sau restarturi\n\n' "$(hostname -f 2>/dev/null || hostname)" >&3
heading 'PHP RUNTIME INVENTORY'
printf '%-56s %-11s %-38s %s\n' 'BINARY' 'PHP' 'PACKAGE' 'RESULT'
declare -A seen=()
count=0
declare -A status_count=()
declare -A runtime_status=()
declare -A runtime_version=()
declare -A review_domains=()
review_count=0
declare -a review_domain_lines=()
declare -a review_php_lines=()
declare -a review_sapi_lines=()
legacy_os=0
declare -a aliases=()
inspect(){
  local bin=$1 resolved ver pkg owner=none result up ini realbin rpkg
  [ -x "$bin" ] || return
  resolved=$(readlink -f "$bin" 2>/dev/null || echo "$bin")
  [ -n "${seen[$resolved]+x}" ] && return
  seen[$resolved]=1
  ver=$(php_version "$bin")
  [ -n "$ver" ] || return
  pkg='unowned'
  if exists rpm; then
    pkg=$(rpm -qf --qf '%{NAME}' "$resolved" 2>/dev/null) && owner=rpm || pkg=unowned
  fi
  # cPanel's ea-php-cli is a version selector, not the PHP runtime package.
  # Map it to its selected EA-PHP build and do not count the selector as a second runtime.
  if [ "$pkg" = 'ea-php-cli' ] || [ "$pkg" = 'ea-php-cli-lsphp' ]; then
    local shortver candidate
    shortver=${ver%.*}; shortver=${shortver//./}
    candidate="/opt/cpanel/ea-php${shortver}/root/usr/bin/php"
    if [ -x "$candidate" ]; then
      aliases+=("$bin -> $candidate (PHP $ver; wrapper, not a separate runtime)")
      return
    fi
    # If the selected version is not installed at the expected path, do not
    # mistake the wrapper's RPM for proof of a PHP security patch.
    result='UNCONFIRMED_WRAPPER_TARGET'
  else
    case "$pkg" in cpanel-php[0-9]*) owner=cpanel;; esac
    result=$(status_for "$pkg" "$ver" "$owner")
  fi
  printf '%-56s %-11s %-38s %s\n' "$bin" "$ver" "$pkg" "$result"
  count=$((count+1))
  status_count[$result]=$(( ${status_count[$result]:-0} + 1 ))
  case "$result" in PATCH_EVIDENCE_FOUND_CLI|UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD|UPSTREAM_VERSION_FIXED_VERIFY_BUILD) ;; *) review_php_lines+=("$ver | $pkg | $result"); console_warn "  REVIEW PHP: $bin | $ver | $result";; esac
  # Map actual PHP runtime to cPanel MultiPHP version identifiers.
  local multi_id=''
  if [[ "$bin" =~ /opt/cpanel/(ea-php[0-9]+)/root/usr/bin/php ]]; then
    multi_id="${BASH_REMATCH[1]}"
  elif [[ "$bin" =~ /opt/alt/(php[0-9]+)/usr/bin/php ]]; then
    multi_id="alt-${BASH_REMATCH[1]}"
  fi
  if [ -n "$multi_id" ]; then
    runtime_status[$multi_id]="$result"
    runtime_version[$multi_id]="$ver"
  fi
  # Execute only known PHP runtime binaries; CGI wrappers may print usage text
  # instead of interpreting -r/--ini. Limit output and validate INI values.
  up=$(timeout 4 "$bin" -n -r 'echo ini_get("session.upload_progress.enabled") ? "On" : "Off";' 2>/dev/null | head -n1 || true)
  case "$up" in On|Off) ;; *) up='UNKNOWN (not a working CLI -r interface)' ;; esac
  ini=$(timeout 4 "$bin" -i 2>/dev/null | grep -m1 '^Loaded Configuration File => ' || true)
  say "    session.upload_progress.enabled (CLI defaults, ignores php.ini): $up [NOT effective web config]"
  [ -n "$ini" ] && say "    $ini"
  # Real effective CLI config; do not infer web/FPM behavior from it.
  up=$(timeout 4 "$bin" -r 'echo ini_get("session.upload_progress.enabled") ? "On" : "Off";' 2>/dev/null | head -n1 || true)
  case "$up" in On|Off) say "    session.upload_progress.enabled (CLI effective): $up [web/FPM may differ]";;
    *) say '    session.upload_progress.enabled (CLI effective): UNKNOWN';;
  esac
}

for b in /usr/bin/php /usr/local/bin/php /usr/local/cpanel/3rdparty/bin/php /usr/local/cpanel/3rdparty/php/*/bin/php /opt/cpanel/ea-php*/root/usr/bin/php /opt/alt/php*/usr/bin/php /usr/local/php*/bin/php /usr/local/lsws/lsphp*/bin/php /usr/bin/php[0-9]* /usr/local/bin/php[0-9]*; do inspect "$b"; done
say "PHP runtimes found: $count"
if [ ${#aliases[@]} -gt 0 ]; then
  heading 'PHP SELECTOR / WRAPPER ALIASES (NOT SEPARATE RUNTIMES)'
  printf '%s\n' "${aliases[@]}"
fi
declare -A domain_counts=()
heading 'CPANEL MULTIPHP VHOST EXPOSURE'
say 'Domain assignments are configuration evidence, not proof of active requests or exploitability.'
say 'CLI patch evidence is provisional. Check installed web SAPI RPMs below.'
say 'session.upload_progress.enabled shown earlier is CLI-only, NOT verified for suPHP/FPM/LSAPI.'
if [ -x /usr/local/cpanel/bin/rebuild_phpconf ]; then
  say '--- PHP HANDLERS (cPanel) ---'
  timeout 12 /usr/local/cpanel/bin/rebuild_phpconf --current 2>&1 | head -35 || true
fi
WHMAPI=''
if [ -x /usr/local/cpanel/bin/whmapi1 ]; then
  WHMAPI=/usr/local/cpanel/bin/whmapi1
elif exists whmapi1; then
  WHMAPI=$(command -v whmapi1)
fi
if [ -n "$WHMAPI" ]; then
  api_json=$(timeout 25 "$WHMAPI" php_get_vhost_versions --output=json 2>/dev/null || true)
  if [ -z "$api_json" ]; then
    say 'WARNING: php_get_vhost_versions returned no data (permissions/API/timeout).'
  elif exists python3; then
    # JSON parser: never execute data returned by WHM API.
    parsed=$(printf '%s' "$api_json" | python3 -c '
import sys,json
try:
    data=json.load(sys.stdin)
    if data.get("metadata",{}).get("result") != 1:
        print("ERROR\tWHM API result != 1")
        sys.exit(0)
    vhosts=data.get("data",{}).get("versions",[])
    for v in vhosts:
        values=[str(v.get(k,"")) for k in ("vhost","version","php_fpm","account")]
        print("\t".join(x.replace("\t"," ").replace("\n"," ") for x in values))
except Exception as e:
    print("ERROR\tCannot parse WHM API JSON: "+str(e))
' 2>/dev/null)
    domain_total=0
    printf '%-42s %-12s %-7s %-22s %s\n' 'DOMAIN' 'MULTIPHP' 'FPM' 'PHP' 'PATCH STATUS'
    while IFS=$'\t' read -r domain multi fpm account; do
      if [ "$domain" = ERROR ]; then say "WARNING: $multi"; continue; fi
      [ -n "$domain" ] || continue
      domain_total=$((domain_total+1))
      domain_counts[$multi]=$(( ${domain_counts[$multi]:-0} + 1 ))
      case "${runtime_status[$multi]:-}" in
        '') state='UNKNOWN_RUNTIME_NOT_IN_INVENTORY';;
        *) state=${runtime_status[$multi]};;
      esac
      printf '%-42s %-12s %-7s %-22s %s\n' "$domain" "$multi" "$fpm" "${runtime_version[$multi]:-UNKNOWN}" "$state"
      case "$state" in
        UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD|UPSTREAM_VERSION_FIXED_VERIFY_BUILD) ;;
        PATCH_EVIDENCE_FOUND_CLI) ;; # CLI evidence; web SAPI audited separately below.
        *) say "    REVIEW_REQUIRED: $domain uses $multi; patch evidence is not confirmed."
           review_count=$((review_count+1))
           review_domains[$multi]=$(( ${review_domains[$multi]:-0} + 1 ))
           review_domain_lines+=("$domain | $multi | ${runtime_version[$multi]:-UNKNOWN}")
           console_warn "  REVIEW DOMAIN: $domain | $multi | ${runtime_version[$multi]:-UNKNOWN} | $state";;
      esac
    done <<< "$parsed"
    say "Configured vhosts found: $domain_total"
    console "  Vhosts configured: $domain_total | unconfirmed CLI evidence: $review_count"
    say '--- VHOST COUNT BY MULTIPHP VERSION ---'
    for multi in "${!domain_counts[@]}"; do
      say "$multi: ${domain_counts[$multi]} vhost(s); patch=${runtime_status[$multi]:-UNKNOWN_RUNTIME_NOT_IN_INVENTORY}"
    done
  else
    say 'WARNING: python3 unavailable; cannot safely parse WHM API JSON. Vhost mapping skipped.'
  fi
else
  say 'WARNING: whmapi1 unavailable; cPanel vhost mapping skipped.'
fi

heading 'WEB SAPI RPM PATCH EVIDENCE (READ-ONLY)'
say 'Inspects installed runtime-related RPM packages. Changelog evidence is not cryptographic verification of patched code.'
say 'If a web SAPI is absent, it is not proof of exposure; confirm actual handler and package mapping.'
web_pkg_total=0
web_pkg_unconfirmed=0
if exists rpm; then
  while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    web_pkg_total=$((web_pkg_total+1))
    if rpm_evidence "$pkg"; then
      verdict=PATCH_EVIDENCE_FOUND
    else
      verdict=UNCONFIRMED
      web_pkg_unconfirmed=$((web_pkg_unconfirmed+1))
      review_sapi_lines+=("$pkg")
      console_warn "  REVIEW SAPI RPM: $pkg | $verdict"
    fi
    say "SAPI_RPM $pkg $verdict"
  done < <(rpm -qa --qf '%{NAME}\n' 2>/dev/null | grep -E '^(ea-php[0-9]+-php-(cgi|fpm|litespeed|common)|alt-php[0-9]+-(cgi|fpm|lsphp|common)|php-(cgi|fpm|common)|lsphp[0-9]*(-common)?)$' | sort -u)
  say "Web/core SAPI RPM packages inspected: $web_pkg_total; without both CVE references: $web_pkg_unconfirmed"
  console "  Web/core RPMs: $web_pkg_total | unconfirmed: $web_pkg_unconfirmed"
else
  say 'WARNING: RPM unavailable; cannot inspect web SAPI packages.'
fi

heading 'LIVE PHP WORKER EXECUTABLES'
say 'Reads /proc/<pid>/exe only. Processes may exit during inspection. No commands executed inside workers.'
worker_count=0
stale_count=0
unmapped_count=0
declare -A active_bins=()
if [ -d /proc ]; then
  for exe in /proc/[0-9]*/exe; do
    [ -L "$exe" ] || continue
    target=$(readlink "$exe" 2>/dev/null || true)
    case "$target" in
      *php*|*lsphp*|*httpd*|*apache*) ;;
      *) continue;;
    esac
    case "$target" in
      *php*|*lsphp*) ;;
      *) continue;;
    esac
    worker_count=$((worker_count+1))
    if [[ "$target" == *' (deleted)'* ]]; then
      stale_count=$((stale_count+1))
      say "STALE_PHP_WORKER ${exe%/exe} -> $target"
      console_warn "  STALE PHP WORKER: ${exe%/exe} | $target"
      continue
    fi
    [ -n "${active_bins[$target]+x}" ] && continue
    active_bins[$target]=1
    pkg=$(rpm -qf --qf '%{NAME} %{VERSION}-%{RELEASE}' "$target" 2>/dev/null || true)
    if [ -z "$pkg" ]; then
      pkg='UNOWNED_OR_UNRESOLVED'
      unmapped_count=$((unmapped_count+1))
      console_warn "  REVIEW LIVE BIN: $target | $pkg"
    fi
    say "ACTIVE_PHP_EXECUTABLE $target | $pkg"
  done
fi
say "Live PHP worker processes observed: $worker_count; stale: $stale_count; unowned unique binaries: $unmapped_count"
console "  Live PHP workers: $worker_count | stale: $stale_count | unowned binaries: $unmapped_count"

heading 'OPERATING SYSTEM SUPPORT HINTS'
if [ -r /etc/os-release ]; then
  . /etc/os-release
  case "${ID:-}:${VERSION_ID:-}" in
    centos:7*|cloudlinux:7*)
      say "REVIEW_REQUIRED: legacy OS family ${ID:-unknown} ${VERSION_ID:-unknown}; verify current vendor lifecycle/support contract."
      legacy_os=1
      console_warn "  REVIEW OS: ${ID:-unknown} ${VERSION_ID:-unknown} (legacy family)";;
    *) say "OS: ${ID:-unknown} ${VERSION_ID:-unknown}; lifecycle not independently verified.";;
  esac
fi

heading 'PHP PACKAGE INVENTORY (COMPACT CORE PACKAGES)'
if exists rpm; then
  rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE}\n' | grep -Ei '^(php|php[0-9]|ea-php|alt-php|cpanel-php|lsphp)' | grep -Ei '(^|[- ])(cli|common|fpm|cgi|litespeed|lsphp)( |$)|^(ea-php[0-9]+|alt-php[0-9]+|cpanel-php[0-9]+) ' | sort || true
else say 'No supported package manager found'; fi
heading 'WEB / PHP SERVICES'
if exists systemctl; then
  systemctl list-units --type=service --all --no-legend --no-pager 2>/dev/null | grep -Ei 'php.*fpm|apache|httpd|lsws|litespeed|nginx|cpanel|plesk' | head -80 || true
fi
heading 'PHP WORKER PROCESSES AND DELETED BINARIES'
if exists ps; then ps -eo pid,comm,args --no-headers 2>/dev/null | grep -Ei '[p]hp-fpm|[l]sphp|[p]hp-cgi|[a]pache2|[h]ttpd|[f]rankenphp' | head -80 || true; fi
if [ -d /proc ]; then
  n=0
  for exe in /proc/[0-9]*/exe; do
    link=$(readlink "$exe" 2>/dev/null || true)
    case "$link" in *'(deleted)'*) case "$link" in *php*|*lsphp*|*httpd*|*apache*) say "STALE_EXECUTABLE $exe -> $link"; n=$((n+1));; esac;; esac
  done
  say "Deleted PHP/web executable mappings found: $n"
fi
heading 'SESSION UPLOAD PROGRESS CONFIGURATION HINTS'
say 'Review session.upload_progress.enabled according to the current PHP/vendor security advisory.'
say 'CLI values above are NOT necessarily effective web configuration; review pool, vhost and user ini overrides.'
say 'This is a static configuration search, not an exhaustive effective web SAPI audit.'
for root in /etc/php.ini /etc/php /etc/php-fpm.d /opt/cpanel /opt/alt /usr/local/lsws/conf /var/cpanel/userdata; do
  [ -e "$root" ] || continue
  if [ -f "$root" ]; then grep -nH -E '^[[:space:]]*session\.upload_progress\.enabled[[:space:]]*=' "$root" 2>/dev/null || true
  elif exists find; then
    find "$root" -maxdepth 5 -type f \( -name '*.ini' -o -name '*.conf' \) -size -1M -print0 2>/dev/null | xargs -0 -r grep -nH -E '^[[:space:]]*session\.upload_progress\.enabled[[:space:]]*=' 2>/dev/null | head -100 || true
  fi
done
heading 'SUMMARY BY RUNTIME STATUS'
for status in PATCH_EVIDENCE_FOUND_CLI UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD UPSTREAM_VERSION_FIXED_VERIFY_BUILD UNCONFIRMED_VENDOR UNCONFIRMED_WRAPPER_TARGET POSSIBLY_VULNERABLE_NO_VENDOR_EVIDENCE UNCONFIRMED_UNSUPPORTED_BRANCH; do
  [ "${status_count[$status]:-0}" -gt 0 ] && say "$status: ${status_count[$status]}"
done
say "TOTAL PHP RUNTIMES: $count"
console "  Total PHP runtimes: $count"
console "  Domains with unconfirmed CLI evidence: $review_count"
console "  Web/core RPMs without both CVE references: $web_pkg_unconfirmed"
console "  Stale PHP workers: $stale_count"
for status in PATCH_EVIDENCE_FOUND_CLI UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD UPSTREAM_VERSION_FIXED_VERIFY_BUILD UNCONFIRMED_VENDOR UNCONFIRMED_WRAPPER_TARGET POSSIBLY_VULNERABLE_NO_VENDOR_EVIDENCE UNCONFIRMED_UNSUPPORTED_BRANCH; do
  [ "${status_count[$status]:-0}" -gt 0 ] && console "  $status: ${status_count[$status]}"
done
say 'Configuration review is separate from PHP patch evidence; no automatic all-clear is issued.'
say 'Vhost-to-PHP mapping is cPanel configuration only; check live web SAPI, .user.ini and handler overrides separately.'
heading 'INTERPRETATION' 
say 'PATCH_EVIDENCE_FOUND_CLI: both CVEs present in CLI RPM changelog; NOT proof that web SAPI is patched.'
say 'UPSTREAM_VERSION_FIXED_VERIFY_VENDOR_BUILD: cPanel CLI version meets upstream threshold; web handler remains separate.'
say 'UPSTREAM_VERSION_FIXED_VERIFY_BUILD: unowned binary reports fixed upstream version; build provenance unknown.'
say 'UNCONFIRMED_VENDOR: vendor package lacks explicit changelog evidence; may include a backport.'
say 'UNCONFIRMED_WRAPPER_TARGET: wrapper target could not be verified; inspect manually.'
say 'POSSIBLY_VULNERABLE_NO_VENDOR_EVIDENCE: no patch evidence; investigate promptly.'
say 'UNCONFIRMED_UNSUPPORTED_BRANCH: PHP <=8.1 needs vendor backport evidence or upgrade.'
say 'Checks cannot prove live web workers use patched code, all configuration overrides, or absence of compromise.'
say 'PHP 5.6-8.1 may be EOL even if vendor backport evidence exists; assess separately.'
say 'References: https://www.php.net/archive/2026.php#2026-10-21-5'
say '            https://www.php.net/'
say "Report: $REPORT"
console "[HZONE] Completed: $HOST"
console "Full report: $REPORT"

# v4.0: v2.3-style readable RPM evidence table in SSH.
# The full audit is still captured in the TXT report.
ui_table_color(){
  local code="$1"; shift
  if [ -t 3 ] && [ -z "${NO_COLOR:-}" ]; then
    printf '\033[%sm%s\033[0m\n' "$code" "$*" >&3
  else
    printf '%s\n' "$*" >&3
  fi
}
if exists rpm; then
  printf '\n==============================================================\n' >&3
  ui_table_color '1;36' " HZONE SECURITY AUDIT v4.0  |  $HOST"
  printf ' PHP2Shell - installed RPM changelog evidence\n' >&3
  printf ' CVE-2026-109618 / CVE-2026-109619\n' >&3
  printf '%-38s %-27s %s\n' 'PACKAGE' 'VERSION' 'RESULT' >&3
  table_ok=0; table_review=0
  while IFS=$'\t' read -r pkg ver; do
    [ -n "$pkg" ] || continue
    if rpm_evidence "$pkg"; then
      verdict=DOCUMENTED; color='0;32'; table_ok=$((table_ok+1))
    else
      verdict=UNCONFIRMED; color='1;33'; table_review=$((table_review+1))
    fi
    printf -v table_line '%-38.38s %-27.27s %s' "$pkg" "$ver" "$verdict"
    ui_table_color "$color" "$table_line"
  done < <(rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' 2>/dev/null | grep -E '^(ea-php[0-9]+-php-(cli|common|cgi|fpm|litespeed)|alt-php[0-9]+-(cli|common|cgi|fpm|lsphp)|alt-php-internal-(cli|common)|cpanel-php[0-9]+|php-(cli|common|cgi|fpm))([[:space:]]|$)' | sort -u)
  printf '\n' >&3
  printf ' DOCUMENTED  = both CVEs appear in installed RPM changelog.\n' >&3
  printf ' UNCONFIRMED = missing changelog evidence; NOT proof of vulnerability.\n' >&3
  printf ' Packages: %s documented / %s unconfirmed.\n' "$table_ok" "$table_review" >&3
fi

# Concise human-readable SSH dashboard. Technical detail remains in the report.
ui_color(){
  local code="$1"; shift
  if [ -t 3 ] && [ -z "${NO_COLOR:-}" ]; then
    printf '\033[%sm%s\033[0m\n' "$code" "$*" >&3
  else
    printf '%s\n' "$*" >&3
  fi
}
ui_line(){ printf '  %-26s %s\n' "$1" "$2" >&3; }
printf '\n==============================================================\n' >&3
ui_color '1;36' '  HZONE | REZULTAT AUDIT PHP2SHELL'
printf '==============================================================\n' >&3
ui_line 'Server:' "$(hostname -f 2>/dev/null || hostname)"
ui_line 'Vulnerabilitati:' 'CVE-2026-109618 / CVE-2026-109619'
ui_line 'PHP instalate:' "$count"
ui_line 'Domenii cPanel:' "${domain_total:-NEVERIFICATE}"
ui_line 'Domenii de verificat:' "$review_count"
ui_line 'Pachete web verificate:' "$web_pkg_total"
ui_line 'Pachete fara dovada:' "$web_pkg_unconfirmed"
ui_line 'Procese PHP active:' "$worker_count"
ui_line 'Procese PHP vechi:' "$stale_count"
printf '\n' >&3
if [ "$review_count" -gt 0 ] || [ "$web_pkg_unconfirmed" -gt 0 ] || [ "$stale_count" -gt 0 ] || [ "$unmapped_count" -gt 0 ]; then
  ui_color '1;33' '  [ATENTIE] Sunt necesare verificari suplimentare.'
  printf '  Lipsa dovezii in changelog NU confirma exploatarea sau vulnerabilitatea.\n' >&3
else
  ui_color '1;32' '  [OK CU REZERVE] Nu s-au gasit pachete web fara dovezi de patch.'
  printf '  Changelog-ul nu confirma singur protectia efectiva a tuturor site-urilor.\n' >&3
fi
if [ "${legacy_os:-0}" -eq 1 ]; then
  ui_color '1;33' '  [ATENTIE] Sistem de operare vechi: verificati suportul furnizorului.'
fi
if [ "${#review_php_lines[@]}" -gt 0 ]; then
  printf '\n  PHP CU PATCH NECONFIRMAT:\n' >&3
  for line in "${review_php_lines[@]}"; do printf '    - %s\n' "$line" >&3; done
fi
if [ "${#review_sapi_lines[@]}" -gt 0 ]; then
  printf '\n  PACHETE WEB FARA REFERINTE LA AMBELE CVE-URI:\n' >&3
  for line in "${review_sapi_lines[@]}"; do printf '    - %s\n' "$line" >&3; done
  printf '  Nota: unele pachete pot fi instalate fara a fi handlerul activ.\n' >&3
fi
if [ "${#review_domain_lines[@]}" -gt 0 ]; then
  printf '\n  DOMENII CARE NECESITA VERIFICARE (%s):\n' "$review_count" >&3
  max_show=15
  for ((i=0; i<${#review_domain_lines[@]} && i<max_show; i++)); do
    printf '    - %s\n' "${review_domain_lines[$i]}" >&3
  done
  if [ "${#review_domain_lines[@]}" -gt "$max_show" ]; then
    printf '    ... inca %s domenii; vezi raportul complet.\n' "$((${#review_domain_lines[@]}-max_show))" >&3
  fi
fi
printf '\n  CE INSEAMNA REZULTATUL:\n' >&3
printf '    PATCH_EVIDENCE_FOUND = ambele CVE-uri apar in changelog-ul RPM.\n' >&3
printf '    UNCONFIRMED = nu avem dovada suficienta; nu inseamna automat vulnerabil.\n' >&3
printf '    CLI si web SAPI pot avea configuratii si pachete diferite.\n' >&3
printf '\n  RAPORT COMPLET: %s\n' "$REPORT" >&3
printf '==============================================================\n' >&3

# v4.0: informational remediation playbook. No command below is executed.
heading 'RECOMMENDED ACTIONS (MANUAL ONLY)'
say 'Commands below are suggestions, NOT executed by this audit.'
say 'Before changes: backup/snapshot, maintenance window, review package dependencies and vendor advisories.'
say 'Do not run a full OS upgrade solely to fix a single PHP runtime.'
say
say '1) cPanel maintenance (not proof of PHP CVE remediation):'
say '   /usr/local/cpanel/cpanel -V'
say '   /usr/local/cpanel/scripts/upcp --force'
say '   Note: upcp --force makes changes; it does NOT guarantee a PHP 7.2 security backport.'
say
say '2) Check EA4 PHP updates without installing anything:'
if exists dnf; then
  say "   dnf list --showduplicates 'ea-php*-php-common' 'ea-php*-php-litespeed'"
  say "   dnf upgrade --assumeno 'ea-php*'"
  say '   If a supported, newer package is available, review the transaction before approving.'
elif exists yum; then
  say "   yum list --showduplicates 'ea-php*-php-common' 'ea-php*-php-litespeed'"
  say "   yum update --assumeno 'ea-php*'"
  say '   Check compatibility and vendor support before applying updates.'
else
  say '   Package manager not recognized: check your OS vendor instructions.'
fi
say
say '3) Verify PHP assignment and effective runtime before removing old versions:'
say '   whmapi1 php_get_vhost_versions'
say "   ps -eo pid,args | grep -E '[e]a-php(56|70|71|72|73|74)'"
say "   grep -R -nE 'ea-php(56|70|71|72|73|74)' /var/spool/cron /etc/cron* 2>/dev/null"
say '   Also inspect custom LiteSpeed handlers, user scripts, .htaccess and .user.ini.'
say
say '4) Check changelog evidence after a package update:'
say "   rpm -q --changelog ea-php74-php-common | grep -E 'CVE-2026-109618|CVE-2026-109619'"
say '   Replace ea-php74 with the actual PHP branch. Check BOTH CVEs and the web SAPI package.'
say
if [ "${web_pkg_unconfirmed:-0}" -gt 0 ] || [ "${review_count:-0}" -gt 0 ]; then
  say "PRIORITY: ${review_count:-0} vhost(s) and ${web_pkg_unconfirmed:-0} web package(s) require manual review."
  say 'An installed but unused PHP version has lower exposure, but cron/CLI/custom handlers may still use it.'
else
  say 'No unconfirmed web RPMs or vhost assignments found by this audit; not a security guarantee.'
fi
if [ "${stale_count:-0}" -gt 0 ]; then
  say "WARNING: ${stale_count} stale process(es) detected. Identify owning service and plan targeted restart."
fi
printf '\n  RECOMMENDED ACTIONS (commands are NOT executed):\n' >&3
printf '    cPanel version: /usr/local/cpanel/cpanel -V\n' >&3
printf '    cPanel update:  /usr/local/cpanel/scripts/upcp --force  [CHANGES SYSTEM]\n' >&3
if exists dnf; then
  printf "    PHP preview:   dnf upgrade --assumeno 'ea-php*'  [NO CHANGES]\n" >&3
elif exists yum; then
  printf "    PHP preview:   yum update --assumeno 'ea-php*'  [NO CHANGES]\n" >&3
fi
printf '    PHP vhosts:    whmapi1 php_get_vhost_versions\n' >&3
printf '    Read the full report for checks, limitations and migration notes.\n' >&3


# v4.0 extensions: inventory, lifecycle, provenance, configuration, optional repo queries.
heading 'HOSTING INVENTORY (CPANEL ACCOUNTS AND DOMAINS)'
say 'Counts distinguish cPanel accounts from MultiPHP vhosts. Aliases, parked domains, and subdomains are not necessarily separate websites.'
if [ -d /var/cpanel/users ]; then
  accounts=0
  for userfile in /var/cpanel/users/*; do
    [ -f "$userfile" ] || continue
    accounts=$((accounts+1))
  done
  say "CPANEL_ACCOUNTS_LOCAL $accounts"
else
  say 'CPANEL_ACCOUNTS_LOCAL UNKNOWN (/var/cpanel/users unavailable)'
fi
say "MULTIPHP_VHOSTS ${domain_total:-UNKNOWN}"
if [ -n "${WHMAPI:-}" ] && exists python3; then
  # cPanel's domainuserdata API is not assumed; domain categorization is optional.
  account_json=$(timeout 30 "$WHMAPI" listaccts --output=json 2>/dev/null || true)
  if [ -n "$account_json" ]; then
    printf '%s' "$account_json" | python3 -c '
import sys,json
try:
 d=json.load(sys.stdin)
 if d.get("metadata",{}).get("result")!=1: raise ValueError("API result not successful")
 accounts=d.get("data",{}).get("acct",[])
 if not isinstance(accounts,list): raise ValueError("Unexpected acct type")
 print("CPANEL_ACCOUNTS_API",len(accounts))
 print("PRIMARY_DOMAINS_API",len(set(str(a.get("domain","")) for a in accounts if a.get("domain"))))
except Exception as e: print("CPANEL_ACCOUNTS_API UNKNOWN ("+str(e)+")")
' || true
  else
    say 'CPANEL_ACCOUNTS_API UNKNOWN (timeout/permissions)'
  fi
fi
say 'Addon/subdomain/alias counts not asserted without a verified domain classification API.'

heading 'PHP LIFECYCLE AND POSSIBLE UNUSED RUNTIMES'
say 'EOL is a lifecycle warning independent of CVE changelog evidence.'
if [ "${#domain_counts[@]}" -eq 0 ] 2>/dev/null; then
  say 'WARNING: domain mapping unavailable; unused-runtime classification is incomplete.'
fi
if exists rpm; then
  while IFS= read -r meta; do
    [ -n "$meta" ] || continue
    branch=${meta#ea-php}; branch=${branch%% *}
    case "$branch" in
      56|70|71|72|73|74|80|81|82|83) lifecycle=EOL_UPSTREAM ;;
      *) lifecycle=CHECK_SUPPORT_POLICY ;;
    esac
    uses=${domain_counts[ea-php$branch]:-0}
    say "PHP_BRANCH ea-php$branch lifecycle=$lifecycle mapped_vhosts=$uses"
    if [ "$uses" -eq 0 ]; then
      say "  CANDIDATE_UNUSED: ea-php$branch (NOT proven unused; check cron, CLI, overrides, custom handlers)"
    fi
  done < <(rpm -qa --qf '%{NAME}\n' 2>/dev/null | grep -E '^ea-php[0-9]{2}$' | sort -u)
fi

heading 'RPM PROVENANCE (NOT PROOF OF PATCH)'
if exists rpm; then
  printf '%-35s %-33s %-23s %s\n' 'PACKAGE' 'VERSION-RELEASE' 'VENDOR' 'INSTALL TID'
  while IFS= read -r pkg; do
    [ -n "$pkg" ] || continue
    rpm -q --qf '%-35{NAME} %-33{VERSION}-%{RELEASE} %-23{VENDOR} %{INSTALLTID}\n' "$pkg" 2>/dev/null || \
      rpm -q --qf '%{NAME} %{VERSION}-%{RELEASE} vendor=%{VENDOR} (source unavailable)\n' "$pkg" 2>/dev/null || true
  done < <(rpm -qa --qf '%{NAME}\n' 2>/dev/null | grep -E '^(ea-php[0-9]+-php-(cli|common|litespeed|fpm|cgi)|cpanel-php[0-9]+)$' | sort -u)
fi
say 'RPM vendor/release and changelog are evidence; they do not establish a verified patched binary.'

heading 'PHP CONFIGURATION / WEB OVERRIDE HINTS'
say 'CLI php.ini is NOT the effective LSAPI/FPM configuration. No HTTP requests are performed.'
if [ "$DEEP" -eq 0 ]; then
  say 'Deep configuration inspection skipped; use --deep for bounded local file checks.'
else
  say 'Configuration paths (metadata only; no file content from customer websites is printed):'
  for root in /opt/cpanel/ea-php*/root/etc /usr/local/lsws/conf /etc/apache2/conf.d; do
    [ -d "$root" ] || continue
    say "CONFIG_ROOT $root"
  done
  for ini in /opt/cpanel/ea-php*/root/etc/php.ini; do
    [ -f "$ini" ] || continue
    say "PHP_INI $ini"
    for setting in session.upload_progress.enabled allow_url_include expose_php display_errors; do
      value=$(awk -v key="$setting" '
        /^[[:space:]]*[;#]/ {next}
        index($0,"=") {split($0,a,"="); k=a[1]; gsub(/[[:space:]]/,"",k); if(k==key) {v=substr($0,index($0,"=")+1); sub(/[[:space:]]*[;#].*$/,"",v); gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); last=v}}
        END {if(last!="") print last; else print "NOT_EXPLICITLY_SET"}
      ' "$ini" 2>/dev/null)
      say "  $setting=${value:-UNKNOWN} (global file only)"
    done
  done
  say 'Custom LiteSpeed external app/handler settings and .htaccess/.user.ini may override these values.'
  say 'Per-site overrides are not exhaustively evaluated; no effective web SAPI verification is claimed.'
fi

heading 'SYSTEM HEALTH AND PATCH READINESS'
if exists systemctl; then
  failed=$(timeout 8 systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | head -30 || true)
  if [ -n "$failed" ]; then
    while IFS= read -r unit; do [ -n "$unit" ] && say "FAILED_SERVICE $unit"; done <<< "$failed"
  else
    say 'FAILED_SERVICES none observed (or systemctl unavailable/timeout)'
  fi
fi
if exists rpm; then
  held=$(grep -hE '^[[:space:]]*(exclude|excludepkgs|includepkgs)[[:space:]]*=' /etc/dnf/dnf.conf /etc/yum.conf /etc/yum.repos.d/*.repo 2>/dev/null | head -30 || true)
  [ -z "$held" ] || { say 'PACKAGE_FILTERS_PRESENT (may affect update visibility):'; printf '%s\n' "$held"; }
fi
if [ "$UPDATES" -eq 1 ]; then
  say 'UPDATE CHECK REQUESTED: package manager may refresh its metadata cache; no RPM transaction is performed.'
  if exists dnf; then
    say 'Command: dnf -q --setopt=metadata_expire=never check-update (PHP packages only in displayed output)'
    update_text=$(timeout 75 dnf -q --setopt=metadata_expire=never check-update 2>&1)
    update_rc=$?
    say "DNF_CHECK_UPDATE_EXIT $update_rc (100 means updates available; 0 none; other may indicate error)"
    if [ "$update_rc" -eq 0 ] || [ "$update_rc" -eq 100 ]; then
      matched=$(printf '%s\n' "$update_text" | grep -E '^(ea-php[0-9]+|alt-php[0-9]+|cpanel-php[0-9]+)[.-]' | head -100 || true)
      if [ -n "$matched" ]; then printf '%s\n' "$matched"; else say 'NO_PHP_UPDATES_LISTED'; fi
    else
      say 'UPDATE_CHECK_INCOMPLETE: repository error, timeout or metadata issue.'
      printf '%s\n' "$update_text" | head -12
    fi
  elif exists yum; then
    say 'Command: yum -q check-update (PHP packages only in displayed output)'
    update_text=$(timeout 75 yum -q check-update 2>&1)
    update_rc=$?
    say "YUM_CHECK_UPDATE_EXIT $update_rc"
    if [ "$update_rc" -eq 0 ] || [ "$update_rc" -eq 100 ]; then
      matched=$(printf '%s\n' "$update_text" | grep -E '^(ea-php[0-9]+|alt-php[0-9]+|cpanel-php[0-9]+)[.-]' | head -100 || true)
      if [ -n "$matched" ]; then printf '%s\n' "$matched"; else say 'NO_PHP_UPDATES_LISTED'; fi
    else
      say 'UPDATE_CHECK_INCOMPLETE: yum error or timeout.'
      printf '%s\n' "$update_text" | head -12
    fi
  else
    say 'UPDATE_CHECK_UNAVAILABLE: no supported package manager.'
  fi
else
  say 'Repository query skipped (default). Use --updates to check available updates.'
fi
say 'Caution: DNF check-update only sees enabled repos; an EOL PHP branch may have no patched candidate.'

heading 'V4.0 TRIAGE NOTES'
say 'Priority 1: unconfirmed web SAPI RPMs serving vhosts; verify vendor advisory and effective web handler.'
say 'Priority 2: EOL PHP serving public sites; plan compatible migration even if CVE evidence is found.'
say 'Priority 3: installed unused PHP; review cron/CLI/custom handler before optional removal.'
say 'No automatic update/restart. cPanel upcp --force and package upgrades are manual change operations.'
printf '\n  V4.0 ADDITIONAL CHECKS: inventory, lifecycle, RPM provenance, health, config, repo mode.\n' >&3
printf '  --deep = local configuration hints; --updates = read-only package query.\n' >&3
printf '  Full report: %s\n' "$REPORT" >&3
