How to scan all WordPress sites on a server
Updated October 2026
On a hosting or agency server one compromised site is usually not alone: the same outdated plugin, the same stolen password or a cross-site write gives the attacker all of them. Scanning every site in a single run is the only way to be sure, and with a file-based scanner it is one command.
Count the sites
Start with an inventory. count-sites walks a directory tree and counts WordPress installations, which tells you what a full scan will cover and catches forgotten staging copies:
$ wpsniffer count-sites /var/www/
To list them by path, search for wp-config.php. Excluding backup and node_modules directories keeps the list honest:
$ find /var/www /home -maxdepth 6 -name wp-config.php -not -path '*/backup*' 2>/dev/null | sort
Scan everything in one run
Point malware-scan at the parent directory. WPSniffer walks it recursively and scans every file against the YARA Forge core package with several worker processes:
$ wpsniffer malware-scan -j 8 --output-path /var/log/wpsniffer/all-sites.csv /var/www
Choose -j to match the cores you can spare; on a busy production host leave half of them for the web server. One CSV for the whole server is convenient for triage because the filename column tells you which site each hit belongs to.
Run it as a user that can read every site. On servers where each site runs as its own user, that is typically root or a dedicated backup user with read access.
One report per site
If you hand results to customers or ticket them per site, loop over the document roots and write a report each. The loop below derives one report per directory under /var/www and skips sites with an empty report:
#!/bin/sh
WPS=/home/deploy/wpsniffer/bin/wpsniffer
OUT=/var/log/wpsniffer/$(date +%F)
mkdir -p "$OUT"
for site in /var/www/*/; do
name=$(basename "$site")
"$WPS" malware-scan --match-all --output-path "$OUT/$name.csv" \
--output-columns filename,signature_id,signature_description,rule_reference,matched_text \
"$site"
[ -s "$OUT/$name.csv" ] || rm -f "$OUT/$name.csv"
done
ls "$OUT"
Because the rule package and compiled rules are cached after the first site, the per-site loop costs little more than the single run.
Keep it fast
A server with hundreds of sites can hold millions of files, most of them images. Three habits keep a full scan to minutes:
- Scan PHP only for routine runs. Feed WPSniffer a
findlist limited to code files:$ find /var/www -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.inc' \) -print0 | wpsniffer malware-scan -j 8 - Scan only what changed since the last run, using
-cminas in the recent-files guide. Combine with a full weekly scan. - Stay on
corefor scheduled runs; reserveextendedfor the sites that already showed hits, so noise does not scale with the number of sites.
Rule names that fire on the same plugin across many clean sites are false positives for your environment; exclude them with -e RULE_NAME so they stop drowning the real hits.
Schedule it and act on results
Turn the per-site script into a nightly cron job with an email summary, as shown in the scheduling guide. When a site comes up with matches, isolate it (disable it or restrict it in the web server) before cleaning, so it cannot re-infect its neighbours, then work through the cleanup guide. Check whether the compromised sites share a plugin or theme version; that is usually the entry point for all of them.