Documentation
WPSniffer is a command-line malware scanner for WordPress and PHP sites. It runs on your own server, compares every file against community YARA rules from YARA Forge using the YARA-X engine, and writes a report. This guide takes you from a bare server to a scheduled scan with an email summary.
Requirements
- Python 3.8 or newer with
pipand thevenvmodule. Most Linux distributions ship these; on Debian and Ubuntu installpython3-venvif the virtualenv step fails. - Read access to the files you want to scan. WPSniffer only reads files. It never writes to, moves or deletes anything in the scanned directory.
- Outbound HTTPS to GitHub for the first run, so the YARA Forge rule package can be downloaded. Air-gapped servers can skip this with
--yara-forge noneand local rules (see Rules). - Shell access such as SSH. WPSniffer is not a WordPress plugin and does not need WordPress, PHP or a database to be running.
Installation
Install into a virtual environment so WPSniffer's dependencies stay separate from the system Python. The commands below create one in your home directory and install straight from the Git repository.
$ python3 -m venv ~/wpsniffer
$ ~/wpsniffer/bin/pip install git+https://github.com/CommanderWaterford/wpbastion
The wpsniffer command now lives at ~/wpsniffer/bin/wpsniffer. Either call it by that path, add the bin directory to your PATH, or activate the environment first:
$ source ~/wpsniffer/bin/activate
$ wpsniffer --help
To upgrade later, run the same pip install command again with --upgrade.
Your first scan
Point malware-scan at the document root of a site. On the first run WPSniffer downloads the YARA Forge core package, compiles the rules and caches both, so later runs start faster.
$ wpsniffer malware-scan /var/www/html
While it runs you get a live progress display. Each suspicious file is reported with the path and the YARA rule it matched, for example:
File at /var/www/html/wp-content/uploads/2026/10/cache.php matched YARA rule SIGNATURE_BASE_WEBSHELL_PHP_GENERIC
File at /var/www/html/wp-includes/images/wp-img.php matched YARA rule SIGNATURE_BASE_WEBSHELL_PHP_OBFUSC
Rule names come from YARA Forge and encode the source collection and the rule author, so you can look up what each detection means. Paths and rule names above are illustrative.
A good first target on a shared server is everything under /var/www; a good first target on a single-site server is the WordPress root. Scanning wp-content/uploads on its own is a fast way to find dropped webshells, because that directory should never contain PHP at all. The guides walk through each of these scenarios step by step.
Commands
wpsniffer malware-scan
Scans one or more paths, or paths read from standard input, against the selected rules and prints or writes a report. This is the command you will use most. Directories are walked recursively.
$ wpsniffer malware-scan [options] PATH [PATH ...]
wpsniffer count-sites
Walks a directory tree and counts the WordPress installations it finds. Useful on hosting servers to see how many sites live under a path before you scan them all, or to feed an inventory into monitoring.
$ wpsniffer count-sites /var/www/
Every command accepts --help, which prints the complete and authoritative option list for the version you have installed.
Options reference
The options below are the ones you will reach for most often. Run wpsniffer malware-scan --help for the full list and the exact defaults of your installed version.
| Option | What it does |
|---|---|
--yara-forge core|extended|full|none | Which YARA Forge package to download and use. core is the default and has the fewest false positives. none skips the download so you can scan with your own rules only. |
--rules PATH | Add your own .yar or .yara file, or a directory of them. Can be combined with a YARA Forge package. Each file gets its own namespace, so rule names never collide. See custom rules. |
-e RULE | Exclude a rule by name. Use it to silence a rule that keeps matching legitimate files on your sites. See handling false positives. |
--match-all | Report every rule a file matches instead of stopping at the first match. Useful when you want the full picture for a report, at the cost of a slightly slower scan. |
--output-path FILE | Write the report to a file instead of standard output. |
--output-columns a,b,c | Choose which columns appear in the report. See reports and columns. |
-j N | Number of worker processes. The scanner is multi-process; raise this on machines with spare cores, lower it on busy shared hosts. |
| Per-file timeout | A file that takes too long to scan (very large or pathological input) is skipped and reported, instead of stalling the whole run. The limit is configurable; see --help. |
| Maximum file size | Files are scanned whole, up to 50 MiB by default, so YARA filesize conditions work and matches are never split across chunk boundaries. Larger files are skipped unless you raise the limit. |
| Filename and regex filters | Restrict the scan to files whose name matches a pattern, for example only *.php, to cut scan time on sites with large media libraries. |
Reports and columns
WPSniffer can write the report as CSV, TSV, line-delimited or null-delimited text for scripts, or as a human-readable listing for the terminal. Pick the columns you want with --output-columns. The columns available are:
| Column | Contents |
|---|---|
filename | Full path of the matched file. |
signature_id | Name of the YARA rule that matched. |
signature_description | The rule's description, from its meta section. |
rule_score | The rule's score or quality rating, where the rule provides one. Higher means a more specific, higher-confidence rule. |
rule_author | Who wrote the rule. WPSniffer credits every rule author in its output. |
rule_reference | A URL or reference the author supplied, often a write-up of the malware family. |
matched_text | The bytes in the file that triggered the match, so you can see what was flagged without opening the file. |
A full CSV report with every column looks like this:
$ wpsniffer malware-scan --match-all --output-path results.csv \
--output-columns filename,signature_id,signature_description,rule_score,rule_author,rule_reference,matched_text \
/var/www/html
Email reports
WPSniffer can also send a summary by email at the end of a scan. The summary lists the suspicious files, the number of files scanned, bytes processed and elapsed time, which makes it a good fit for a nightly cron job. Email delivery options are listed under wpsniffer malware-scan --help.
Scanning paths from stdin
When no path is given on the command line, malware-scan reads file paths from standard input. That lets you combine it with find or any other tool that produces a file list. Use -print0 so paths with spaces or unusual characters are handled correctly.
Scan only files changed in the last hour, which is the fastest way to check a site right after you suspect a break-in:
$ find /var/www -cmin -60 -type f -print0 | wpsniffer malware-scan
Scan only PHP files under uploads, where no PHP should ever exist:
$ find /var/www/html/wp-content/uploads -type f -name '*.php' -print0 | wpsniffer malware-scan
Scheduled scans with cron
Because WPSniffer has no daemon and no account, a scheduled scan is one crontab line. This example scans all sites under /var/www every night at 03:00, writes a dated CSV, and keeps the terminal output quiet.
0 3 * * * /home/deploy/wpsniffer/bin/wpsniffer malware-scan \
--output-path /var/log/wpsniffer/$(date +\%F).csv /var/www
Notes for cron:
- Use the full path to the
wpsnifferbinary inside the virtualenv, because cron does not load your shell profile. - Escape
%as\%in crontab, as shown above. - The rule package cache is refreshed daily, so a nightly job always scans with current rules without any extra step.
- Add the email options from
--helpto receive a summary instead of reading log files.
Running in CI
WPSniffer runs cleanly in a pipeline: it needs only Python, prints machine-readable output, and can scan a checked-out repository or a build artifact before it is deployed. A minimal job installs the tool, scans the workspace and fails the build when a report is non-empty.
$ python3 -m venv .venv && .venv/bin/pip install git+https://github.com/CommanderWaterford/wpbastion
$ .venv/bin/wpsniffer malware-scan --output-path report.csv .
$ test ! -s report.csv
For reproducible results across runs, pin the YARA Forge release rather than taking the daily refresh (see pinning a release), or ship your own rules with --rules and --yara-forge none.
After a scan finds matches
WPSniffer detects; it does not clean. When a scan reports files (the cleanup guide covers each step in depth):
- Do not delete anything yet. Open the
matched_textand the rule'srule_referenceto confirm the match is real. Some broad rules fire on legitimate minified or obfuscated plugin code. - Check the file's origin. A PHP file inside
wp-content/uploads, a core file whose checksum differs from the official release, or a file with a recent change time on an otherwise static site is very likely malicious. - Look for siblings. Attackers rarely drop one file. Re-run the scan with
--yara-forge extendedand--match-all, and list recently changed files withfind -cminas shown above. - Clean and close the door. Remove or restore the files from a known-good backup, update WordPress, themes and plugins, rotate passwords and salts, and check for unknown admin users and scheduled tasks.
- Re-scan. A clean second scan is your confirmation. Then put the scan on a schedule.
Troubleshooting
The rule download fails
The only network request WPSniffer makes is fetching the YARA Forge package from GitHub. Check that the server can reach github.com over HTTPS. If it cannot, download the package on another machine, copy the rules over and use --rules with --yara-forge none.
A scan is slow
Raise -j on multi-core machines, restrict the scan to PHP files with a filename filter, or feed it a narrower file list from find. Scanning a media library full of large images and videos adds time without adding detections.
The same rule matches on every site
That is a false positive for your environment. Exclude the rule with -e, and consider reporting it upstream so the rule can be improved. See handling false positives.
Python is too old
WPSniffer needs Python 3.8 or newer. On older distributions install a newer interpreter from your distribution's backports or a tool such as pyenv, then create the virtualenv with it.