Emergency server help: get in touch

WordPress Version Audit: Find Every WordPress Site and Its Version

Free bash script that finds every WordPress install on cPanel, DirectAdmin or plain servers and flags core versions below 7.0.3 (CVE-2026-64638).

Version
1.1.0
Last updated
October 7, 2026
Language
Bash
Tested on
1.1.0 on AlmaLinux 9.8 with cPanel & WHM 11.138 (lab test, 7 Oct 2026: three WordPress 7.1.2 sites MINIMUM-MET and BEHIND-LATEST with --current 7.1.3); 17 fixtures; Run on 6 Oct 2026 on AlmaLinux 9.8 with cPanel & WHM 11.138 (3 WordPress sites) and AlmaLinux 9.8 with DirectAdmin 1.712 (3 WordPress sites), plus a test folder with 7.0.2 and 6.9.6 copies; bash -n and ShellCheck 0.9.0 clean.
License
MIT
Pricing
Free

Short answer: wordpress-version-audit.sh finds every WordPress install on a cPanel, DirectAdmin or plain Linux server by reading wp-includes/version.php, and prints domain, path, core version, owner and a status. Installs below --min-version (default 7.0.3, the CVE-2026-64638 fix) are marked BELOW-MINIMUM, installs at or above it MINIMUM-MET, and patched older branches can be marked BRANCH-FIXED. MINIMUM-MET is not a full security assessment: add --current with the latest security release of each branch to see which sites are behind. --csv gives you a list to feed into an update loop. It is read-only.

Version 1.1.0 (7 October 2026): An external review found that the old OK status could be read as “up to date”, although it only meant “at or above 7.0.3” for one advisory. OK is now MINIMUM-MET and OUTDATED is now BELOW-MINIMUM, the report names the advisory, and the new --current option checks each site against the latest security release in a separate LATEST column.

We ran version 1.0.0 on our lab servers (AlmaLinux 9.8 with cPanel & WHM 11.138, and AlmaLinux 9.8 with DirectAdmin 1.712) on 6 October 2026, against three WordPress sites on each. On 7 October 2026 we ran version 1.1.0 on the cPanel lab again, with and without --current. It is bash -n and ShellCheck 0.9.0 clean.

What it does

  • Detects the panel: cPanel (/usr/local/cpanel/version and /etc/userdatadomains), DirectAdmin (/usr/local/directadmin), or neither.
  • Builds the list of folders to scan: every cPanel account home, every DirectAdmin user’s domains/ folder, or /var/www, /home and /srv on a plain server. --path overrides this.
  • Finds wp-includes/version.php and wp-load.php files and keeps only folders that have both wp-load.php and wp-includes/, so stray copies of the file are ignored. An install whose version.php is missing, unreadable or has no version is listed as UNKNOWN instead of being skipped.
  • Reads $wp_version from the file as text. It never runs PHP and never loads WordPress, so a broken or infected site cannot affect the scan.
  • Maps each folder to a domain: from /etc/userdatadomains on cPanel (main, addon and subdomains; parked domains are skipped) and from the domains/DOMAIN/public_html path on DirectAdmin.
  • Compares the version with --min-version and, with --branch-fix, treats patched releases on older branches as fixed for that advisory. With --current it also compares each install with the latest security release for its branch.
  • Skips virtfs, .trash, .cagefs, .snapshot and node_modules folders, plus anything you add with --exclude.

Exit codes make it easy to use from monitoring: 0 when every install is MINIMUM-MET or BRANCH-FIXED (and UP-TO-DATE when --current is given), 1 when at least one install is BELOW-MINIMUM or, with --current, BEHIND-LATEST, 2 for a usage or environment error, and 3 when nothing is below the minimum but at least one version is UNKNOWN.

Requirements

  • Bash 4 or newer (associative arrays) and GNU findutils, coreutils, grep and awk. Any current AlmaLinux, Rocky, CloudLinux, Debian or Ubuntu server has them.
  • Root, so it can read every account’s files. As a normal user it scans only what that user can read.
  • No PHP, wp-cli or database access needed.

Download and first run

Save the script from this page as /root/wordpress-version-audit.sh, then:

chmod 700 /root/wordpress-version-audit.sh
bash -n /root/wordpress-version-audit.sh && echo "syntax OK"
/root/wordpress-version-audit.sh

For the CVE-2026-64638 check, tell it about the patched older branches so 6.9.6 and 6.8.7 sites are not flagged. This only applies to that advisory:

/root/wordpress-version-audit.sh --branch-fix 6.9.6,6.8.7

To see which sites are behind the latest security release, pass the latest release of each branch from the official WordPress release and security announcements, for example --current 7.1.3,7.0.7. The script does not download release data, so update these numbers when a new release comes out.

Options

OptionDefaultWhat it does
--min-version X7.0.3Flag installs older than X as BELOW-MINIMUM
--advisory TEXTCVE-2026-64638 with the default minimumAdvisory the minimum belongs to, shown in the header and summary. If you change --min-version without it, the report says no advisory named
--branch-fix LISTnoneComma list of releases on older branches that fix the same advisory (e.g. 6.9.6,6.8.7); same major.minor at or above it is BRANCH-FIXED. It says nothing about other advisories
--current LISTnoneComma list of the latest security release per branch (e.g. 7.1.3,7.0.7). Fills the LATEST column with UP-TO-DATE or BEHIND-LATEST; an install whose branch is not listed is compared with the highest version listed
--path DIRautoScan this folder instead of account homes (repeatable)
--exclude PATTERNnoneSkip paths matching a find -path pattern, e.g. '*/backups/*' (repeatable)
--maxdepth N7How deep to search below each root (minimum 3)
--only-outdatedoffPrint only rows that need attention: BELOW-MINIMUM, UNKNOWN and BEHIND-LATEST (counts still cover everything)
--csvoffCSV output: domain,path,version,owner,status,latest. The “not a full security assessment” note goes to stderr, so the CSV stays clean
-h, -VHelp and script version

Statuses: MINIMUM-MET (at or above the minimum for the named advisory; this is not a full security assessment), BELOW-MINIMUM, BRANCH-FIXED (below the minimum but on a patched older branch for the same advisory), UNKNOWN (version.php missing, unreadable or without a version). The LATEST column shows UP-TO-DATE or BEHIND-LATEST with --current, and not-checked without it. In version 1.0.0 MINIMUM-MET was called OK and BELOW-MINIMUM was called OUTDATED, so update any alert that greps for those words.

Example output

Version 1.1.0 on our cPanel lab on 7 October 2026 (domains masked):

WordPress version audit - panel: cpanel - minimum: 7.0.3 (CVE-2026-64638) - latest: not checked
Note: MINIMUM-MET only means core >= 7.0.3 (CVE-2026-64638). It is not a full security assessment.
      Newer security releases are not checked; add --current with the latest release of each branch.

DOMAIN             PATH                     VERSION  OWNER  STATUS       LATEST
site1.example.com  /home/site1/public_html  7.1.2    site1  MINIMUM-MET  not-checked
site2.example.com  /home/site2/public_html  7.1.2    site2  MINIMUM-MET  not-checked
site3.example.com  /home/site3/public_html  7.1.2    site3  MINIMUM-MET  not-checked

Installs found: 3 - below minimum (< 7.0.3, CVE-2026-64638): 0 - unknown version: 0

With --current 7.1.3 on the same lab, the LATEST column showed BEHIND-LATEST for all three sites, the summary line ended with - behind latest (7.1.3): 3, and the exit code was 1. All three sites met the CVE-2026-64638 minimum but were one security release behind. Version 1.0.0 showed the same sites as OK.

On our DirectAdmin lab, where one user owns three domains (version 1.0.0 run of 6 October 2026, output format of 1.1.0):

WordPress version audit - panel: directadmin - minimum: 7.0.3 (CVE-2026-64638) - latest: not checked
Note: MINIMUM-MET only means core >= 7.0.3 (CVE-2026-64638). It is not a full security assessment.
      Newer security releases are not checked; add --current with the latest release of each branch.

DOMAIN             PATH                                               VERSION  OWNER  STATUS       LATEST
site1.example.com  /home/admin/domains/site1.example.com/public_html  7.1.2    admin  MINIMUM-MET  not-checked
site2.example.com  /home/admin/domains/site2.example.com/public_html  7.1.2    admin  MINIMUM-MET  not-checked
site3.example.com  /home/admin/domains/site3.example.com/public_html  7.1.2    admin  MINIMUM-MET  not-checked

Installs found: 3 - below minimum (< 7.0.3, CVE-2026-64638): 0 - unknown version: 0

No lab site was below the minimum, so to show flagging we created a test folder with a 7.0.2 copy and a 6.9.6 copy and scanned it with --path. The exit code was 1 (version 1.0.0 run of 6 October 2026, output format of 1.1.0):

./wordpress-version-audit.sh --path /root/srvs-lab/test-hostB/wp-fixture --branch-fix 6.9.6,6.8.7
WordPress version audit - panel: cpanel - minimum: 7.0.3 (CVE-2026-64638) - branch fixes for CVE-2026-64638 only: 6.9.6,6.8.7 - latest: not checked
Note: MINIMUM-MET only means core >= 7.0.3 (CVE-2026-64638). It is not a full security assessment.
      Newer security releases are not checked; add --current with the latest release of each branch.

DOMAIN  PATH                                                 VERSION  OWNER  STATUS         LATEST
-       /root/srvs-lab/test-hostB/wp-fixture/old-site        7.0.2    root   BELOW-MINIMUM  not-checked
-       /root/srvs-lab/test-hostB/wp-fixture/patched-branch  6.9.6    root   BRANCH-FIXED   not-checked

Installs found: 2 - below minimum (< 7.0.3, CVE-2026-64638): 1 - unknown version: 0

CSV output for the same cPanel lab sites (version 1.0.0 run, output format of 1.1.0):

domain,path,version,owner,status,latest
site1.example.com,/home/site1/public_html,7.1.2,site1,MINIMUM-MET,not-checked
site2.example.com,/home/site2/public_html,7.1.2,site2,MINIMUM-MET,not-checked
site3.example.com,/home/site3/public_html,7.1.2,site3,MINIMUM-MET,not-checked

Schedule it

Run it weekly and mail yourself the list of sites that need attention. The exit code is not 0 when any install is BELOW-MINIMUM, BEHIND-LATEST (with --current) or UNKNOWN, so cron only needs to act on failure:

# /etc/cron.d/wordpress-version-audit
MAILTO=admin@example.com
30 6 * * 1  root  /root/wordpress-version-audit.sh --branch-fix 6.9.6,6.8.7 --only-outdated > /root/wp-audit.txt || cat /root/wp-audit.txt

To be told about sites that are behind the latest security release as well, add --current with the latest release of each branch, and update that list whenever WordPress ships a security release, using the numbers from the official announcements. If you raise --min-version for a new advisory, add --advisory with its name so the report labels the check correctly.

How it works

  1. Parses and validates options; version arguments must look like 7.0.3.
  2. Detects the panel and loads the cPanel docroot-to-domain map from /etc/userdatadomains.
  3. Runs one find per root with prune rules and -print0, so odd folder names are handled.
  4. Extracts the version with grep and compares versions with sort -V: with the minimum (and branch fixes) for STATUS and, with --current, with the latest release for the install’s branch for LATEST.
  5. Collects rows, sorts them by status (BELOW-MINIMUM, BRANCH-FIXED, MINIMUM-MET, then UNKNOWN) and domain, and prints an aligned table (no dependency on column) or CSV.

Limitations

  • It reports the core version only, not plugin or theme versions. Use WP Toolkit or wp-cli for those. MINIMUM-MET plus UP-TO-DATE still says nothing about plugins, themes, PHP or configuration.
  • It does not download release data. The --current values must come from the official WordPress release and security announcements, and you have to update them yourself.
  • Pre-release versions (for example 7.1-RC1) compare as newer than the matching final release. --branch-fix and --current compare major.minor branches only.
  • On plain servers there is no domain map; the DOMAIN column shows -.
  • Installs deeper than --maxdepth below a root are missed; raise it if customers nest sites deeply.
  • Files on other users’ home directories are unreadable without root, and are silently skipped.

Official documentation: WordPress 7.0.3 release · WordPress: Configuring automatic background updates · WordPress: Upgrading WordPress

Related: Server hacked: incident response runbook for Linux and cPanel · cPanel PHP Version Audit · cPanel Account Inventory · Using WP Toolkit Security Risk scores, Smart Update and Vulnerable Components · DirectAdmin WordPress Manager and wp-cli: Site Control

See also: Clean a Hacked WordPress Site on cPanel: Step-by-Step · WordPress CVE-2026-64638 Patch: Find and Update Every Vulnerable Site · Imunify360 False Positives: Find the Rule ID and Fix It

The script

wordpress-version-audit.shDownload
#!/usr/bin/env bash
# WordPress Version Audit: Find Every WordPress Site and Its Version (v1.1.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/wordpress-version-audit/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
#
# wordpress-version-audit.sh
# Find every WordPress install on a cPanel, DirectAdmin or plain Linux server,
# print domain, path, core version and owner, and check each install against a
# minimum version for one advisory (default 7.0.3, the CVE-2026-64638 fix).
# Optionally (--current) also check against the latest security release of
# each branch. MINIMUM-MET is not a full security assessment.
#
# https://srvscripts.com/scripts/wordpress-version-audit/
# Version: 1.1.0
# License: MIT
#
# Read-only: the script only reads wp-includes/version.php and panel domain
# maps. It never runs PHP, never loads WordPress and never changes files.
#
# Exit codes: 0 = every install MINIMUM-MET/BRANCH-FIXED (and UP-TO-DATE when
#                 --current is given),
#             1 = at least one BELOW-MINIMUM (or BEHIND-LATEST with --current),
#             2 = usage or environment error,
#             3 = no install below, but at least one version is UNKNOWN.

set -euo pipefail

VERSION="1.1.0"
DEFAULT_MIN="7.0.3"
DEFAULT_ADVISORY="CVE-2026-64638"
MIN_VERSION="$DEFAULT_MIN"
ADVISORY=""
BRANCH_FIX=""
CURRENT=""
CSV=0
MAXDEPTH=7
ONLY_OUTDATED=0
declare -a ROOTS=()
declare -a EXCLUDES=()

usage() {
    cat <<'EOF'
Usage: wordpress-version-audit.sh [options]

Finds WordPress installs (wp-includes/version.php) and reports their core version.

Status (one advisory only):
  MINIMUM-MET     core is at or above --min-version (default 7.0.3, CVE-2026-64638).
                  This is NOT a full security assessment: it does not mean the site
                  is on the latest security release, and plugins, themes and
                  configuration are not checked.
  BRANCH-FIXED    below the minimum, but at or above a --branch-fix release for the
                  same advisory on its branch
  BELOW-MINIMUM   older than --min-version
  UNKNOWN         version.php missing, unreadable or without a version
Latest column (only with --current): UP-TO-DATE or BEHIND-LATEST; otherwise
"not-checked".

Options:
  --min-version X     Minimum version for the advisory (default: 7.0.3)
  --advisory TEXT     Advisory the minimum relates to, shown in the report
                      (default: CVE-2026-64638 when --min-version is the default)
  --branch-fix LIST   Comma list of releases on older branches that fix the SAME
                      advisory, e.g. 6.9.6,6.8.7. A site on that major.minor branch at
                      or above the release is BRANCH-FIXED instead of BELOW-MINIMUM.
                      It says nothing about other advisories.
  --current LIST      Comma list of the latest security release per branch, e.g.
                      7.1.3,7.0.7. Installs below the release for their branch (or
                      below the highest listed when their branch is not listed) are
                      BEHIND-LATEST. Take the numbers from the official WordPress
                      release/security announcements.
  --path DIR          Scan DIR instead of auto-detected account homes (repeatable)
  --exclude PATTERN   Skip paths matching this find -path pattern (repeatable),
                      e.g. '*/backups/*'
  --maxdepth N        How deep to search below each root (default: 7)
  --only-outdated     Print only rows needing attention (BELOW-MINIMUM, UNKNOWN,
                      BEHIND-LATEST)
  --csv               CSV output (domain,path,version,owner,status,latest)
  -h, --help          Show this help
  -V, --version       Show script version

Examples:
  wordpress-version-audit.sh
  wordpress-version-audit.sh --current 7.1.3,7.0.7
  wordpress-version-audit.sh --min-version 7.1.2 --advisory "CVE-XXXX-YYYY" --only-outdated
  wordpress-version-audit.sh --branch-fix 6.9.6,6.8.7 --csv > wp-audit.csv
EOF
}

die() { echo "Error: $*" >&2; exit 2; }

valid_version() { [[ "$1" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; }

while [[ $# -gt 0 ]]; do
    case "$1" in
        --min-version) [[ $# -ge 2 ]] || die "--min-version needs a value"
                       valid_version "$2" || die "invalid version '$2' (expected e.g. 7.0.3)"
                       MIN_VERSION="$2"; shift 2 ;;
        --advisory)    [[ $# -ge 2 && -n "$2" ]] || die "--advisory needs a value"
                       ADVISORY="$2"; shift 2 ;;
        --branch-fix)  [[ $# -ge 2 ]] || die "--branch-fix needs a value"
                       BRANCH_FIX="$2"; shift 2 ;;
        --current)     [[ $# -ge 2 ]] || die "--current needs a value"
                       CURRENT="$2"; shift 2 ;;
        --path)        [[ $# -ge 2 ]] || die "--path needs a directory"
                       [[ -d "$2" ]] || die "not a directory: $2"
                       ROOTS+=("$2"); shift 2 ;;
        --exclude)     [[ $# -ge 2 ]] || die "--exclude needs a pattern"
                       EXCLUDES+=("$2"); shift 2 ;;
        --maxdepth)    [[ $# -ge 2 && "$2" =~ ^[0-9]+$ && "$2" -ge 3 ]] || die "--maxdepth needs a number >= 3"
                       MAXDEPTH="$2"; shift 2 ;;
        --only-outdated) ONLY_OUTDATED=1; shift ;;
        --csv)         CSV=1; shift ;;
        -h|--help)     usage; exit 0 ;;
        -V|--version)  echo "wordpress-version-audit.sh $VERSION"; exit 0 ;;
        *)             usage >&2; die "unknown option: $1" ;;
    esac
done

if [[ -n "$BRANCH_FIX" ]]; then
    IFS=',' read -r -a _bf <<< "$BRANCH_FIX"
    for v in "${_bf[@]}"; do valid_version "$v" || die "invalid --branch-fix version '$v'"; done
fi
if [[ -n "$CURRENT" ]]; then
    IFS=',' read -r -a _cur <<< "$CURRENT"
    for v in "${_cur[@]}"; do valid_version "$v" || die "invalid --current version '$v'"; done
fi
if [[ -z "$ADVISORY" ]]; then
    if [[ "$MIN_VERSION" == "$DEFAULT_MIN" ]]; then ADVISORY="$DEFAULT_ADVISORY"; else ADVISORY="no advisory named"; fi
fi

if [[ $EUID -ne 0 && ${#ROOTS[@]} -eq 0 ]]; then
    echo "Warning: not running as root; other users' homes may be unreadable." >&2
fi

# ---- version helpers ---------------------------------------------------------
# ver_lt A B  -> true if A < B (natural version sort)
ver_lt() {
    [[ "$1" != "$2" ]] && [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]]
}
branch_of() { echo "$1" | cut -d. -f1,2; }

status_for() {
    local v="$1" fix
    [[ -z "$v" ]] && { echo "UNKNOWN"; return; }
    if ! ver_lt "$v" "$MIN_VERSION"; then echo "MINIMUM-MET"; return; fi
    if [[ -n "$BRANCH_FIX" ]]; then
        for fix in "${_bf[@]}"; do
            if [[ "$(branch_of "$v")" == "$(branch_of "$fix")" ]] && ! ver_lt "$v" "$fix"; then
                echo "BRANCH-FIXED"; return
            fi
        done
    fi
    echo "BELOW-MINIMUM"
}

# latest_for VERSION -> not-checked | UNKNOWN | UP-TO-DATE | BEHIND-LATEST
latest_for() {
    local v="$1" c target=""
    [[ -z "$CURRENT" ]] && { echo "not-checked"; return; }
    [[ -z "$v" ]] && { echo "UNKNOWN"; return; }
    for c in "${_cur[@]}"; do
        [[ "$(branch_of "$v")" == "$(branch_of "$c")" ]] && target="$c"
    done
    [[ -n "$target" ]] || target="$(printf '%s\n' "${_cur[@]}" | sort -V | tail -n1)"
    if ver_lt "$v" "$target"; then echo "BEHIND-LATEST"; else echo "UP-TO-DATE"; fi
}

# ---- panel detection and roots ----------------------------------------------
PANEL="plain"
declare -A DOCROOT_DOMAIN=()

if [[ -f /usr/local/cpanel/version && -r /etc/userdatadomains ]]; then
    PANEL="cpanel"
    # /etc/userdatadomains: domain: user==owner==type==main==docroot==ip:port==...
    while IFS= read -r line; do
        dom="${line%%:*}"
        rest="${line#*: }"
        IFS='|' read -r -a f <<< "${rest//==/|}"
        type="${f[2]:-}"; docroot="${f[4]:-}"
        [[ -z "$docroot" || "$type" == "parked" ]] && continue
        # keep the first non-parked domain per docroot (main/addon beat sub)
        if [[ -z "${DOCROOT_DOMAIN[$docroot]:-}" || "$type" == "main" || "$type" == "addon" ]]; then
            DOCROOT_DOMAIN[$docroot]="$dom"
        fi
    done < /etc/userdatadomains
    if [[ ${#ROOTS[@]} -eq 0 ]]; then
        for u in /var/cpanel/users/*; do
            [[ -f "$u" ]] || continue
            h="$(getent passwd "$(basename "$u")" | cut -d: -f6 || true)"
            [[ -n "$h" && -d "$h" ]] && ROOTS+=("$h")
        done
    fi
elif [[ -x /usr/local/directadmin/directadmin && -d /usr/local/directadmin/data/users ]]; then
    PANEL="directadmin"
    if [[ ${#ROOTS[@]} -eq 0 ]]; then
        for u in /usr/local/directadmin/data/users/*; do
            [[ -d "$u" ]] || continue
            h="$(getent passwd "$(basename "$u")" | cut -d: -f6 || true)"
            [[ -n "$h" && -d "$h/domains" ]] && ROOTS+=("$h/domains")
        done
    fi
fi

if [[ ${#ROOTS[@]} -eq 0 ]]; then
    for d in /var/www /home /srv; do [[ -d "$d" ]] && ROOTS+=("$d"); done
fi
[[ ${#ROOTS[@]} -gt 0 ]] || die "nothing to scan (no account homes found; use --path)"

domain_for() {
    local dir="$1" p
    case "$PANEL" in
        cpanel)
            p="$dir"
            while [[ -n "$p" && "$p" != "/" ]]; do
                if [[ -n "${DOCROOT_DOMAIN[$p]:-}" ]]; then
                    if [[ "$p" == "$dir" ]]; then echo "${DOCROOT_DOMAIN[$p]}"
                    else echo "${DOCROOT_DOMAIN[$p]}${dir#"$p"}"; fi
                    return
                fi
                p="$(dirname "$p")"
            done
            echo "-" ;;
        directadmin)
            # /home/USER/domains/DOMAIN/public_html[/sub]
            if [[ "$dir" =~ /domains/([^/]+)/(public_html|private_html)(/.*)?$ ]]; then
                echo "${BASH_REMATCH[1]}${BASH_REMATCH[3]:-}"
            else
                echo "-"
            fi ;;
        *) echo "-" ;;
    esac
}

# ---- scan --------------------------------------------------------------------
find_args=(-maxdepth "$MAXDEPTH")
for pat in '*/virtfs/*' '*/.trash/*' '*/.cagefs/*' '*/.snapshot/*' '*/node_modules/*' ${EXCLUDES[@]+"${EXCLUDES[@]}"}; do
    find_args+=(-path "$pat" -prune -o)
done
# match version.php and wp-load.php, so an install whose version.php is missing
# is still found (and reported as UNKNOWN)
find_args+=(-type f '(' -path '*/wp-includes/version.php' -o -name wp-load.php ')' -print0)

declare -a ROWS=()
declare -A SEEN=()
total=0; outdated=0; unknown=0; behind=0

for root in "${ROOTS[@]}"; do
    while IFS= read -r -d '' hit; do
        if [[ "$hit" == */wp-includes/version.php ]]; then wpdir="$(dirname "$(dirname "$hit")")"
        else wpdir="$(dirname "$hit")"; fi
        [[ -n "${SEEN[$wpdir]:-}" ]] && continue
        SEEN[$wpdir]=1
        # skip stray copies that are not a full core tree
        [[ -f "$wpdir/wp-load.php" && -d "$wpdir/wp-includes" ]] || continue
        vf="$wpdir/wp-includes/version.php"
        # missing or unreadable version.php -> empty version -> UNKNOWN
        ver="$(grep -m1 "\$wp_version[[:space:]]*=" "$vf" 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+(-[A-Za-z0-9]+)?' | head -n1 || true)"
        owner="$(stat -c '%U' "$vf" 2>/dev/null || stat -c '%U' "$wpdir/wp-load.php" 2>/dev/null || echo '?')"
        st="$(status_for "$ver")"
        lt="$(latest_for "$ver")"
        total=$((total + 1))
        [[ "$st" == "BELOW-MINIMUM" ]] && outdated=$((outdated + 1))
        [[ "$st" == "UNKNOWN" ]] && unknown=$((unknown + 1))
        [[ "$lt" == "BEHIND-LATEST" ]] && behind=$((behind + 1))
        [[ $ONLY_OUTDATED -eq 1 && "$st" != "BELOW-MINIMUM" && "$st" != "UNKNOWN" && "$lt" != "BEHIND-LATEST" ]] && continue
        ROWS+=("$(domain_for "$wpdir")"$'\t'"$wpdir"$'\t'"${ver:-?}"$'\t'"$owner"$'\t'"$st"$'\t'"$lt")
    done < <(find "$root" "${find_args[@]}" 2>/dev/null || true)
done

# align: tab-separated input -> padded columns (no dependency on column(1))
align() {
    awk -F'\t' '{ for (i = 1; i <= NF; i++) { c[NR, i] = $i; if (length($i) > w[i]) w[i] = length($i) } if (NF > n) n = NF }
        END { for (r = 1; r <= NR; r++) { line = ""; for (i = 1; i <= n; i++) line = line sprintf(i < n ? "%-" w[i] "s  " : "%s", c[r, i]); print line } }'
}

# ---- output ------------------------------------------------------------------
csv_field() { local s="${1//\"/\"\"}"; if [[ "$s" == *[,\"]* ]]; then printf '"%s"' "$s"; else printf '%s' "$s"; fi; }

NOTE="MINIMUM-MET only means core >= $MIN_VERSION ($ADVISORY). It is not a full security assessment."
if [[ $CSV -eq 1 ]]; then
    echo "domain,path,version,owner,status,latest"
    for r in "${ROWS[@]+"${ROWS[@]}"}"; do
        IFS=$'\t' read -r d p v o s l <<< "$r"
        printf '%s,%s,%s,%s,%s,%s\n' "$(csv_field "$d")" "$(csv_field "$p")" "$v" "$(csv_field "$o")" "$s" "$l"
    done
    echo "Note: $NOTE" >&2
else
    echo "WordPress version audit - panel: $PANEL - minimum: $MIN_VERSION ($ADVISORY)${BRANCH_FIX:+ - branch fixes for $ADVISORY only: $BRANCH_FIX} - latest: ${CURRENT:-not checked}"
    echo "Note: $NOTE"
    [[ -n "$CURRENT" ]] || echo "      Newer security releases are not checked; add --current with the latest release of each branch."
    echo
    {
        printf 'DOMAIN\tPATH\tVERSION\tOWNER\tSTATUS\tLATEST\n'
        for r in "${ROWS[@]+"${ROWS[@]}"}"; do printf '%s\n' "$r"; done | sort -t$'\t' -k5,5 -k6,6 -k1,1
    } | align
    echo
    echo "Installs found: $total - below minimum (< $MIN_VERSION, $ADVISORY): $outdated - unknown version: $unknown${CURRENT:+ - behind latest ($CURRENT): $behind}"
fi

[[ $outdated -eq 0 && $behind -eq 0 ]] || exit 1
[[ $unknown -eq 0 ]] || exit 3
exit 0
Version 1.1.0 · SHA-256 a2671c3b94a44f8f47d7f1bb52cbde3052168986dccf916c04914f9dda2a8b19
Download and verify on Linux or macOS
curl -fsSL -o wordpress-version-audit.sh https://scr.srvscripts.com/wordpress-version-audit/wordpress-version-audit.sh && curl -fsSL https://scr.srvscripts.com/wordpress-version-audit/wordpress-version-audit.sh.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/wordpress-version-audit/wordpress-version-audit.sh' -OutFile 'wordpress-version-audit.sh'; if ((Get-FileHash 'wordpress-version-audit.sh' -Algorithm SHA256).Hash -eq 'A2671C3B94A44F8F47D7F1BB52CBDE3052168986DCCF916C04914F9DDA2A8B19') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

Does the script change any files?

No. It only reads version.php files and the panel domain map. It never runs PHP or wp-cli.

Why is a 6.9.6 site shown as BELOW-MINIMUM?

Add –branch-fix 6.9.6,6.8.7 so patched releases on older branches are recognised as fixed.

Can it find WordPress outside public_html?

Yes. It scans the whole account home on cPanel and every domain folder on DirectAdmin, up to –maxdepth levels deep.

How do I update the sites it finds?

Use the CSV output in a loop that runs wp core update –minor as each site’s owner. Our CVE-2026-64638 guide shows the loop.

Does it work without a control panel?

Yes. It scans /var/www, /home and /srv, or the folders you pass with –path.

Does MINIMUM-MET mean the site is up to date?

No. It only means core is at or above the minimum for one advisory (7.0.3 for CVE-2026-64638 by default). Add –current with the latest security release of each branch to see which sites are BEHIND-LATEST.

Changelog

  • 1.1.0 — Status words now say what is checked: MINIMUM-MET / BELOW-MINIMUM (against 7.0.3 for CVE-2026-64638 by default) instead of OK / OUTDATED; the threshold and advisory are printed. New --current (latest release per branch, e.g. 7.1.3,7.0.7) adds an UP-TO-DATE / BEHIND-LATEST column. A missing or unreadable version.php is UNKNOWN (exit 3). Scripts that grep for OK or OUTDATED need updating. Found in an external review (PROD7-07).
  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.