#!/bin/sh
# ---------------------------------------------------------------------------
# check-php2shell.sh
#
# Checks a host for the "PHP2Shell" vulnerabilities:
#     CVE-2026-109618  (RCE via out-of-memory corruption)
#     CVE-2026-109619  (RCE via stale global state / use-after-free)
#
# Fixed upstream in PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12 (released 2026-10-21).
# PHP 8.1 and older get NO upstream version bump; distro/Remi/Webtatic
# backports keep the old version number and only the build date / rpm
# changelog proves whether the fix is present.
#
# Written to run on old /bin/sh too (CentOS 6 = bash 4.1, coreutils 8.4).
# POSIX sh only: no 'local', no arrays, no [[ ]], no ${var,,}.
#
# Exit codes:
#   0  all PHP instances found look patched (or no PHP found)
#   1  at least one instance is VULNERABLE
#   2  at least one instance is UNKNOWN (couldn't be determined) and none vuln
# ---------------------------------------------------------------------------

# Fixed branch floors. Format: "MAJOR.MINOR FIX_PATCH"
FIXED_BRANCHES="
8.2 35
8.3 36
8.4 27
8.5 12
"

# Branches with no upstream version bump: fix provable only via build date or
# rpm changelog. 2026-10-21 is the upstream release / backport date.
FIX_EPOCH_DATE="2026-10-21"

CVE1="CVE-2026-109618"
CVE2="CVE-2026-109619"

# ---- tiny helpers ---------------------------------------------------------

# colourless, works on dumb terminals
say()  { printf '%s\n' "$*"; }
hr()   { printf '%s\n' "----------------------------------------------------------------------"; }
head2(){ printf '\n=== %s ===\n' "$*"; }

have() { command -v "$1" >/dev/null 2>&1; }

# Compare two dotted versions A and B.
# echo  -1 if A<B, 0 if A==B, 1 if A>B   (numeric, field by field)
vercmp() {
    a="$1"; b="$2"
    # strip anything after first non [0-9.] (e.g. -dev, RC1, ~beta)
    a=`printf '%s' "$a" | sed 's/[^0-9.].*$//'`
    b=`printf '%s' "$b" | sed 's/[^0-9.].*$//'`
    IFS=.
    set -- $a; a1=${1:-0}; a2=${2:-0}; a3=${3:-0}
    set -- $b; b1=${1:-0}; b2=${2:-0}; b3=${3:-0}
    unset IFS
    for pair in "$a1 $b1" "$a2 $b2" "$a3 $b3"; do
        x=${pair%% *}; y=${pair##* }
        x=`printf '%s' "$x" | sed 's/[^0-9]//g'`; x=${x:-0}
        y=`printf '%s' "$y" | sed 's/[^0-9]//g'`; y=${y:-0}
        if [ "$x" -lt "$y" ]; then echo -1; return; fi
        if [ "$x" -gt "$y" ]; then echo 1;  return; fi
    done
    echo 0
}

# Convert a date string to a comparable YYYYMMDD integer, or empty on failure.
# Handles PHP's "built: Oct 21 2026 17:33:12" and plain "2026-10-21".
date_to_int() {
    d="$1"
    # already ISO?
    iso=`printf '%s' "$d" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -n1`
    if [ -n "$iso" ]; then
        printf '%s' "$iso" | tr -d '-'
        return
    fi
    # try GNU date (not available for "Mon DD YYYY" reliably on CentOS 6, so parse by hand)
    mon=`printf '%s' "$d" | grep -oE '[A-Z][a-z][a-z]' | head -n1`
    day=`printf '%s' "$d" | grep -oE ' [0-9]{1,2} ' | head -n1 | tr -d ' '`
    yr=`printf '%s' "$d" | grep -oE '[0-9]{4}' | head -n1`
    [ -n "$mon" ] && [ -n "$day" ] && [ -n "$yr" ] || return
    case "$mon" in
        Jan) m=01;; Feb) m=02;; Mar) m=03;; Apr) m=04;;
        May) m=05;; Jun) m=06;; Jul) m=07;; Aug) m=08;;
        Sep) m=09;; Oct) m=10;; Nov) m=11;; Dec) m=12;;
        *) return;;
    esac
    # zero-pad day
    if [ "`printf '%s' "$day" | wc -c`" -eq 1 ]; then day="0$day"; fi
    printf '%s%s%s' "$yr" "$m" "$day"
}

FIX_EPOCH_INT=`printf '%s' "$FIX_EPOCH_DATE" | tr -d '-'`

# Given MAJOR.MINOR, echo the fix patch floor, or empty if branch not in list.
branch_floor() {
    echo "$FIXED_BRANCHES" | while read bm bf; do
        [ -n "$bm" ] || continue
        if [ "$bm" = "$1" ]; then printf '%s' "$bf"; return; fi
    done
}

# ---------------------------------------------------------------------------
# Verdict for one PHP instance.
# Args: label  version  buildinfo(optional)
# Sets global VERDICT (VULNERABLE|PATCHED|UNKNOWN) and prints reasoning.
# ---------------------------------------------------------------------------
assess() {
    _label="$1"; _ver="$2"; _build="$3"
    VERDICT="UNKNOWN"

    mm=`printf '%s' "$_ver" | grep -oE '^[0-9]+\.[0-9]+' | head -n1`
    patch=`printf '%s' "$_ver" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' | head -n1 | sed 's/^[0-9]*\.[0-9]*\.//'`
    major=`printf '%s' "$mm" | cut -d. -f1`

    say "  Version string : $_ver"
    [ -n "$_build" ] && say "  Build info     : $_build"

    # PHP 5 / 7: no upstream fix at all. 8.0/8.1 EOL, source-backport only.
    if [ -z "$mm" ]; then
        say "  -> Could not parse a version number."
        VERDICT="UNKNOWN"; return
    fi

    floor=`branch_floor "$mm"`

    if [ -n "$floor" ]; then
        # Supported branch with a real version bump.
        cmp=`vercmp "0.0.$patch" "0.0.$floor"`
        if [ "$cmp" -ge 0 ]; then
            say "  -> Branch $mm, patch $patch >= $floor : looks PATCHED (upstream fix present)."
            VERDICT="PATCHED"
        else
            say "  -> Branch $mm, patch $patch < $floor : VULNERABLE. Fixed in $mm.$floor."
            VERDICT="VULNERABLE"
        fi
        return
    fi

    # Branch NOT in the supported-fix list: 8.1, 8.0, 7.x, 5.x.
    # Version number alone cannot tell us; rely on build date / changelog.
    case "$mm" in
        8.1|8.0|7.4)
            say "  -> Branch $mm is end-of-life. No upstream version bump exists;"
            say "     a fix can only come from a distro/Remi/Webtatic backport that keeps"
            say "     the version number. Checking build date against $FIX_EPOCH_DATE ..."
            bint=`date_to_int "$_build"`
            if [ -n "$bint" ]; then
                if [ "$bint" -ge "$FIX_EPOCH_INT" ]; then
                    say "     Build date $bint >= $FIX_EPOCH_INT : POSSIBLY PATCHED via backport."
                    say "     Confirm with the rpm changelog check below (must name $CVE1/$CVE2)."
                    VERDICT="UNKNOWN"
                else
                    say "     Build date $bint < $FIX_EPOCH_INT : almost certainly VULNERABLE"
                    say "     (built before the fix existed)."
                    VERDICT="VULNERABLE"
                fi
            else
                say "     No usable build date. UNKNOWN - rely on rpm changelog check."
                VERDICT="UNKNOWN"
            fi
            ;;
        *)
            # 7.0-7.3, 5.x, 6.x, or anything else with no fix path
            if [ "$major" = "5" ] || [ "$major" = "6" ] || [ "$mm" = "7.0" ] || [ "$mm" = "7.1" ] || [ "$mm" = "7.2" ] || [ "$mm" = "7.3" ]; then
                say "  -> Branch $mm is end-of-life with NO official PHP release or source backport."
                say "     The flaw reaches back to PHP 5.4.0 (and the stock CentOS 6 PHP 5.3 is"
                say "     almost certainly in the affected range too). Treat as VULNERABLE unless a"
                say "     commercial ELS vendor has backported the fix. For CentOS 6 / PHP 5.3 and"
                say "     PHP 5.x the realistic source is TuxCare ELS (formerly CloudLinux ELS);"
                say "     Remi/Webtatic do NOT rebuild branches this old. Build date and the rpm"
                say "     changelog (checked below, incl. TuxCare signatures) are the only proof."
                VERDICT="VULNERABLE"
                # If a build date is present and >= fix epoch, downgrade to UNKNOWN:
                # a TuxCare ELS rebuild would keep the 5.3.x version but carry a newer build.
                bint=`date_to_int "$_build"`
                if [ -n "$bint" ] && [ "$bint" -ge "$FIX_EPOCH_INT" ]; then
                    say "     NOTE: build date $bint >= $FIX_EPOCH_INT - could be a TuxCare ELS"
                    say "     rebuild. Downgrading to UNKNOWN; confirm via the changelog check."
                    VERDICT="UNKNOWN"
                fi
            else
                say "  -> Branch $mm is newer than anything this script knows about ($FIXED_BRANCHES)."
                say "     Update this script's FIXED_BRANCHES table. UNKNOWN."
                VERDICT="UNKNOWN"
            fi
            ;;
    esac
}

# ===========================================================================
# 0. Banner
# ===========================================================================
say "PHP2Shell vulnerability check ($CVE1 / $CVE2)"
say "Fixed in: PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12 (2026-10-21)."
say "Host: `hostname 2>/dev/null`   Date: `date 2>/dev/null`"
if [ -r /etc/redhat-release ]; then
    say "OS  : `cat /etc/redhat-release`"
elif [ -r /etc/os-release ]; then
    say "OS  : `. /etc/os-release 2>/dev/null; echo "$PRETTY_NAME"`"
fi
hr

# ===========================================================================
# 1. PHP binaries on PATH and in common locations (CLI, FPM, CGI, SCL)
# ===========================================================================
head2 "1. PHP binaries (CLI / FPM / CGI, including SCL and Remi collections)"

# Build a newline list of candidate binaries without relying on arrays.
# (Kept in a function rather than a `{ ...; } | sort` inside backticks, because
#  CentOS 6 /bin/sh and dash mis-parse that construct.)
list_candidates() {
    # things on PATH
    for n in php php-cli php-cgi php-fpm php7 php8 \
             php53 php54 php55 php56 php70 php71 php72 php73 php74 \
             php80 php81 php82 php83 php84 php85 \
             php-fpm7.4 php-fpm8.0 php-fpm8.1 php-fpm8.2 php-fpm8.3 php-fpm8.4 php-fpm8.5; do
        p=`command -v "$n" 2>/dev/null` && printf '%s\n' "$p"
    done
    # Remi / SCL collection paths (CentOS/RHEL)
    ls -1 /opt/remi/php*/root/usr/bin/php        2>/dev/null
    ls -1 /opt/remi/php*/root/usr/sbin/php-fpm   2>/dev/null
    ls -1 /opt/rh/php*/root/usr/bin/php          2>/dev/null
    ls -1 /usr/bin/php*                          2>/dev/null
    ls -1 /usr/sbin/php-fpm*                     2>/dev/null
    # Webtatic / IUS / CloudLinux alt-php sometimes drop php53, php54, php55 etc.
    ls -1 /usr/bin/php5* /usr/bin/php7* /usr/bin/php8* 2>/dev/null
    ls -1 /opt/alt/php5*/usr/bin/php /opt/alt/php7*/usr/bin/php 2>/dev/null
}
# Write the candidate list to a file so the loop runs in THIS shell (not a pipe
# subshell): that lets us accumulate per-instance records reliably and reconcile
# them against the rpm changelog later.
TMP=`mktemp 2>/dev/null || echo /tmp/php2shell.$$`
RECORDS="$TMP.rec"      # one line per engine instance: VERDICT|branch|path
CANDFILE="$TMP.cand"    # candidate binary paths
CONFIRMED="$TMP.conf"   # branches whose core-engine changelog names the CVE
: > "$RECORDS"; : > "$CANDFILE"; : > "$CONFIRMED"
list_candidates | sort -u > "$CANDFILE"

if [ ! -s "$CANDFILE" ]; then
    say "No PHP binaries found in PATH or common locations."
else
    while IFS= read -r bin; do
        [ -n "$bin" ] || continue
        # skip helper tools that aren't the interpreter
        case "$bin" in *php-config*|*phpize*|*php-dbg*) continue;; esac
        [ -x "$bin" ] || continue
        head2 "Binary: $bin"
        # -v works for cli and cgi; php-fpm uses -v too
        vout=`"$bin" -v 2>/dev/null | head -n1`
        if [ -z "$vout" ]; then
            say "  (could not run '$bin -v' - skipping, not counted)"
            continue
        fi
        ver=`printf '%s' "$vout" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1`
        build=`printf '%s' "$vout" | sed -n 's/.*(built: \([^)]*\)).*/\1/p'`
        assess "$bin" "$ver" "$build"
        br=`printf '%s' "$ver" | grep -oE '^[0-9]+\.[0-9]+' | head -n1`
        printf '%s|%s|%s\n' "$VERDICT" "$br" "$bin" >> "$RECORDS"
        printf '  RESULT: %s\n' "$VERDICT"
    done < "$CANDFILE"
fi

# ===========================================================================
# 2. RPM packages + changelog check (the authoritative check for backports)
# ===========================================================================
head2 "2. RPM packages and changelogs (backport-aware)"

if have rpm; then
    # All php packages across base, Remi (phpXX-php*), SCL, Webtatic/IUS.
    PKGS=`rpm -qa 2>/dev/null | grep -Ei '(^|-)php' | grep -Eiv 'phpmyadmin' | sort -u`
    if [ -z "$PKGS" ]; then
        say "No PHP-related RPMs installed."
    else
        say "Installed PHP-related RPMs:"
        printf '%s\n' "$PKGS" | sed 's/^/  /'
        hr
        # Only the CORE ENGINE packages carry the PHP2Shell fix, so only they are
        # worth asserting a CVE changelog entry on. Extensions, PECL modules and
        # PEAR packages are built from their own SRPMs and will never name a PHP
        # *core* CVE in their changelog - demanding it there is a false alarm.
        # Match engine names at the name->version boundary ("-<digit>"), covering
        # base (php, php-common/cli/fpm/cgi/embedded) and SCL/Remi (phpNN-php-*).
        CORE_RE='^(php|php-common|php-cli|php-fpm|php-cgi|php-embedded|php[0-9]+-php-common|php[0-9]+-php-cli|php[0-9]+-php-fpm|php[0-9]+-php-cgi|php[0-9]+-php-embedded)-[0-9]'
        CORE=`printf '%s\n' "$PKGS" | grep -E "$CORE_RE" | sort -u`
        EXTRAS=`printf '%s\n' "$PKGS" | grep -Ev "$CORE_RE" | sort -u`

        if [ -z "$CORE" ]; then
            say "No core PHP engine package (php-common / php-cli / php-fpm) identified"
            say "by name; falling back to checking every PHP package's changelog."
            CORE="$PKGS"; EXTRAS=""
        fi

        if [ -n "$EXTRAS" ]; then
            say "Extensions / PECL / PEAR (informational - these track the core engine,"
            say "they are NOT independent patch targets and need no CVE changelog entry):"
            printf '%s\n' "$EXTRAS" | sed 's/^/  /'
            hr
        fi

        for pkg in $CORE; do
            head2 "Changelog: $pkg"
            # vendor / build date / version
            rpm -q --qf 'Package : %{NAME}-%{VERSION}-%{RELEASE}\nVendor  : %{VENDOR}\nBuilt   : %{BUILDTIME:date}\n' "$pkg" 2>/dev/null
            pkgbr=`rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null | grep -oE '^[0-9]+\.[0-9]+' | head -n1`
            # Vendor string hints at a commercial ELS rebuild (TuxCare/CloudLinux).
            vendor=`rpm -q --qf '%{VENDOR}' "$pkg" 2>/dev/null`
            case "$vendor" in
                *TuxCare*|*CloudLinux*|*KernelCare*)
                    say "  Vendor looks like a commercial ELS provider: $vendor"
                    say "  -> On an EOL branch (e.g. PHP 5.3 on CentOS 6) this is the expected"
                    say "     source of a PHP2Shell backport. Trust the changelog match below." ;;
            esac
            # Does the changelog name either CVE? Also catch TuxCare's own markers.
            cl=`rpm -q --changelog "$pkg" 2>/dev/null`
            hit=`printf '%s' "$cl" | grep -Ei "$CVE1|$CVE2|109618|109619|php2shell|tuxcare|cloudlinux.*(109618|109619)" | head -n3`
            if [ -n "$hit" ]; then
                say "  CHANGELOG MENTIONS THE FIX:"
                printf '%s\n' "$hit" | sed 's/^/    /'
                say "  -> This package (branch ${pkgbr:-?}) carries a backport for PHP2Shell."
                # Record the confirmed branch so the summary can resolve a
                # version-based UNKNOWN for that same branch to PATCHED.
                [ -n "$pkgbr" ] && echo "$pkgbr" >> "$CONFIRMED"
            else
                say "  Changelog does not name $CVE1/$CVE2."
                say "  -> On a SUPPORTED branch (8.2-8.5) this is fine: the version number is"
                say "     the proof, and some vendors omit CVE IDs (e.g. a bare \"rebase to"
                say "     8.2.35\" entry). On an EOL branch (8.1/8.0/7.x/5.x) that relies on a"
                say "     backport, absence here means no backport evidence yet - see the"
                say "     per-binary verdict above, which drives the result."
                # Informational only; the binary pass owns VULNERABLE/UNKNOWN/PATCHED.
            fi
        done
    fi
elif have dpkg; then
    say "dpkg system detected (not RPM). Showing php packages for reference:"
    dpkg -l 'php*' 2>/dev/null | awk '/^ii/{print "  "$2"  "$3}'
    say "On Debian/Ubuntu the version string carries the fix; compare against your"
    say "distro's advisory (e.g. the +debNuM / ubuntuN.NN package revision)."
else
    say "Neither rpm nor dpkg found; skipping package changelog check."
fi

# ===========================================================================
# 2b. TuxCare / CloudLinux ELS detection (the only backport route for PHP 5.3)
# ===========================================================================
head2 "2b. TuxCare / CloudLinux Extended Lifecycle Support (CentOS 6 / PHP 5.3)"

ELS_FOUND=0
# TuxCare ELS ships a repo + the libcare/uchecker tooling; look for the signs.
for f in /etc/yum.repos.d/tuxcare-els.repo \
         /etc/yum.repos.d/els*.repo \
         /etc/yum.repos.d/cloudlinux-els.repo \
         /etc/sysconfig/l019e4e3b-els \
         /usr/bin/uchecker; do
    if [ -e "$f" ]; then say "  Found ELS marker: $f"; ELS_FOUND=1; fi
done
# Any TuxCare/CloudLinux-vendored RPM at all?
if have rpm; then
    elsrpm=`rpm -qa --qf '%{NAME} [%{VENDOR}]\n' 2>/dev/null | grep -Ei 'tuxcare|cloudlinux|kernelcare' | head -n5`
    if [ -n "$elsrpm" ]; then
        say "  ELS-vendored packages present:"
        printf '%s\n' "$elsrpm" | sed 's/^/    /'
        ELS_FOUND=1
    fi
fi
if [ "$ELS_FOUND" -eq 1 ]; then
    say "  -> This host appears to subscribe to TuxCare/CloudLinux ELS."
    say "     For PHP 5.3 on CentOS 6, run 'yum update php\\*' (or the TuxCare-documented"
    say "     command), then re-run this script: a fixed rebuild keeps the 5.3.x version"
    say "     but its rpm changelog should name $CVE1 / $CVE2."
    if have uchecker; then
        say "  'uchecker' is installed; you can also run it to check for in-memory vulnerable libs."
    fi
else
    say "  No TuxCare/CloudLinux ELS markers found."
    say "  -> If this is CentOS 6 with stock PHP 5.3 and no ELS subscription, there is NO"
    say "     backport available: the only fixes are to isolate/retire the host or move the"
    say "     app to a supported, patched PHP (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12)."
fi

# ===========================================================================
# 3. Running services actually serving requests (highest risk = mod_php/FPM)
# ===========================================================================
head2 "3. Running PHP-facing services (what an attacker can actually reach)"

# mod_php inside Apache/httpd
APACHE=`ps -eo comm= 2>/dev/null | grep -Ei '^(httpd|apache2)$' | head -n1`
if [ -n "$APACHE" ]; then
    say "Apache/httpd is running. Checking for mod_php (highest-risk SAPI)..."
    modphp=""
    for b in httpd apache2; do
        if have "$b"; then
            modphp=`"$b" -M 2>/dev/null | grep -i 'php[0-9_]*_module'`
            [ -n "$modphp" ] && break
        fi
    done
    if [ -n "$modphp" ]; then
        say "  mod_php LOADED: $modphp"
        say "  -> PHP runs inside Apache workers. PHP states this is the highest-risk"
        say "     configuration for PHP2Shell. Patch and restart httpd."
    else
        say "  No mod_php module detected (likely php-fpm behind Apache)."
    fi
else
    say "No Apache/httpd process running."
fi

# php-fpm masters
FPM=`ps -eo pid,comm,args 2>/dev/null | grep -i 'php-fpm' | grep -iv grep`
if [ -n "$FPM" ]; then
    say "php-fpm master(s) running:"
    printf '%s\n' "$FPM" | sed 's/^/  /'
    say "  -> Remember to restart every php-fpm master after patching so workers reload."
else
    say "No php-fpm process running."
fi

# ===========================================================================
# 4. Mitigation settings (reference only - PHP says none of these fully protect)
# ===========================================================================
head2 "4. Mitigation settings (stopgaps only - NOT a substitute for patching)"

say "PHP and its maintainers are explicit that NO combination of these settings"
say "fully blocks PHP2Shell; they only narrow some attack paths. Treat a 'good'"
say "value below as reduced exposure, never as protection. The real fix is a"
say "patched PHP + a restart of every worker."
say ""
# IMPORTANT: we do NOT read 'php -i' here. That reports the CLI SAPI, whose
# defaults (memory_limit=-1, post_max_size=8M) have nothing to do with what
# Apache/FPM actually serve with - reading them would be misleading. Instead we
# grep the web-SAPI config files on disk for directives that are EXPLICITLY set.
CFG_FILES=`{
    ls -1 /etc/php.ini /etc/php*/php.ini 2>/dev/null
    ls -1 /etc/php-fpm.conf /etc/php-fpm.d/*.conf 2>/dev/null
    ls -1 /etc/php/*/fpm/php.ini /etc/php/*/fpm/pool.d/*.conf 2>/dev/null
    ls -1 /opt/remi/php*/root/etc/php.ini /opt/remi/php*/root/etc/php-fpm.d/*.conf 2>/dev/null
    ls -1 /etc/httpd/conf.d/php.conf /etc/apache2/mods-enabled/php*.conf 2>/dev/null
} 2>/dev/null`

if [ -n "$CFG_FILES" ]; then
    say "Explicitly-set relevant directives found in web-SAPI config files"
    say "(this is a grep, not a full effective-config resolution):"
    found_any=0
    for f in $CFG_FILES; do
        [ -f "$f" ] || continue
        hits=`grep -EiHn '^[[:space:]]*(php_(admin_)?(value|flag)\[?[a-z_.]*\]?[[:space:]]*=?[[:space:]]*)?(session\.upload_progress\.enabled|post_max_size|memory_limit|fatal_error_backtraces)' "$f" 2>/dev/null | grep -Eiv '^\s*;'`
        if [ -n "$hits" ]; then
            printf '%s\n' "$hits" | sed 's/^/  /'
            found_any=1
        fi
    done
    [ "$found_any" -eq 0 ] && say "  (none of the relevant directives are explicitly set in these files)"
else
    say "No web-SAPI config files found in the usual locations; check your FPM pool"
    say "and Apache config by hand."
fi
say ""
say "PHP's interim stopgaps, for reference (lock them with php_admin_value in the"
say "FPM pool or Apache vhost so apps can't override them):"
say "  $CVE1 : memory_limit=-1, plus a low post_max_size (8M; 32M if uploads needed),"
say "          and a container/cgroup limit that kills only the affected worker."
say "  $CVE2 : session.upload_progress.enabled=0; on PHP 8.5 also fatal_error_backtraces=0."
say "  After ANY of these, restart every worker. They do not make an unpatched host safe."

# ===========================================================================
# 5. Final roll-up
# ===========================================================================
head2 "5. Summary"

# Reconcile each engine instance against the rpm changelog evidence. A core-
# engine changelog that names the CVE is direct proof the installed engine for
# that BRANCH is fixed, so it resolves a version-based UNKNOWN (and even an
# EOL build-date VULNERABLE guess) for the same branch to PATCHED. This is what
# fixes the "says UNKNOWN even though the backport changelog confirmed it" case.
ANY_VULN=0; ANY_UNKNOWN=0; N_INSTANCES=0; RECONCILED=0

if [ -s "$RECORDS" ]; then
    say "Per-instance verdicts:"
    while IFS='|' read -r verdict br path; do
        [ -n "$verdict" ] || continue
        N_INSTANCES=`expr $N_INSTANCES + 1`
        final="$verdict"
        if [ "$verdict" != "PATCHED" ] && [ -n "$br" ] && \
           grep -qx "$br" "$CONFIRMED" 2>/dev/null; then
            final="PATCHED"
            RECONCILED=1
            say "  $path  [$br]  $verdict -> PATCHED (rpm changelog for branch $br names the CVE)"
        else
            say "  $path  [$br]  $final"
        fi
        case "$final" in
            VULNERABLE) ANY_VULN=1;;
            UNKNOWN)    ANY_UNKNOWN=1;;
        esac
    done < "$RECORDS"
    say ""
fi

# A changelog can confirm a branch for which no runnable binary was found (e.g.
# php-fpm installed but no php CLI). Surface that as a patched signal too.
CONFIRMED_BRANCHES=`sort -u "$CONFIRMED" 2>/dev/null | tr '\n' ' '`
[ -n "$CONFIRMED_BRANCHES" ] && say "Branches confirmed patched via rpm changelog: $CONFIRMED_BRANCHES"

rm -f "$TMP" "$RECORDS" "$CANDFILE" "$CONFIRMED" 2>/dev/null

if [ "$ANY_VULN" -eq 1 ]; then
    say "RESULT: VULNERABLE - at least one PHP instance is below a fixed version or"
    say "        built before the fix with no backport evidence. Patch immediately:"
    say "        upgrade to 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, or apply your vendor's"
    say "        backport, then restart Apache/php-fpm."
    EXIT=1
elif [ "$ANY_UNKNOWN" -eq 1 ]; then
    say "RESULT: UNKNOWN - could not confirm patched state for every instance."
    say "        Review the per-binary and changelog sections above. For EOL branches"
    say "        (8.1/8.0/7.x/5.x) a matching rpm changelog entry naming $CVE1/$CVE2"
    say "        is the only reliable proof of a backport."
    EXIT=2
elif [ "$N_INSTANCES" -gt 0 ] || [ -n "$CONFIRMED_BRANCHES" ]; then
    say "RESULT: PATCHED - every PHP instance found is at or above a fixed version,"
    say "        or carries a backport changelog entry for PHP2Shell."
    [ "$RECONCILED" -eq 1 ] && say "        (one or more EOL instances confirmed via the rpm changelog, not the version.)"
    EXIT=0
else
    say "RESULT: No PHP found to assess."
    EXIT=0
fi

say ""
say "Reminder: this script reports likelihood from versions, build dates and rpm"
say "changelogs. The only definitive fix is running a patched PHP and restarting"
say "every worker (httpd / php-fpm). Mitigations alone are not sufficient."

exit $EXIT
