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.
Table of Contents
What it does
- Detects the panel: cPanel (
/usr/local/cpanel/versionand/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,/homeand/srvon a plain server.--pathoverrides this. - Finds
wp-includes/version.phpandwp-load.phpfiles and keeps only folders that have bothwp-load.phpandwp-includes/, so stray copies of the file are ignored. An install whoseversion.phpis missing, unreadable or has no version is listed as UNKNOWN instead of being skipped. - Reads
$wp_versionfrom 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/userdatadomainson cPanel (main, addon and subdomains; parked domains are skipped) and from thedomains/DOMAIN/public_htmlpath on DirectAdmin. - Compares the version with
--min-versionand, with--branch-fix, treats patched releases on older branches as fixed for that advisory. With--currentit also compares each install with the latest security release for its branch. - Skips
virtfs,.trash,.cagefs,.snapshotandnode_modulesfolders, 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
| Option | Default | What it does |
|---|---|---|
--min-version X | 7.0.3 | Flag installs older than X as BELOW-MINIMUM |
--advisory TEXT | CVE-2026-64638 with the default minimum | Advisory 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 LIST | none | Comma 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 LIST | none | Comma 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 DIR | auto | Scan this folder instead of account homes (repeatable) |
--exclude PATTERN | none | Skip paths matching a find -path pattern, e.g. '*/backups/*' (repeatable) |
--maxdepth N | 7 | How deep to search below each root (minimum 3) |
--only-outdated | off | Print only rows that need attention: BELOW-MINIMUM, UNKNOWN and BEHIND-LATEST (counts still cover everything) |
--csv | off | CSV output: domain,path,version,owner,status,latest. The “not a full security assessment” note goes to stderr, so the CSV stays clean |
-h, -V | Help 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
- Parses and validates options; version arguments must look like
7.0.3. - Detects the panel and loads the cPanel docroot-to-domain map from
/etc/userdatadomains. - Runs one
findper root with prune rules and-print0, so odd folder names are handled. - Extracts the version with
grepand compares versions withsort -V: with the minimum (and branch fixes) for STATUS and, with--current, with the latest release for the install’s branch for LATEST. - 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
--currentvalues 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-fixand--currentcompare major.minor branches only. - On plain servers there is no domain map; the DOMAIN column shows
-. - Installs deeper than
--maxdepthbelow 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
#!/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
a2671c3b94a44f8f47d7f1bb52cbde3052168986dccf916c04914f9dda2a8b19curl -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 -cInvoke-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.