# docs

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 pip and the venv module. Most Linux distributions ship these; on Debian and Ubuntu install python3-venv if 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 none and 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.

Install as the user that owns the web files, or as a user that can read them. Running the scanner as root works but is not required.

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.

OptionWhat it does
--yara-forge core|extended|full|noneWhich 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 PATHAdd 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 RULEExclude a rule by name. Use it to silence a rule that keeps matching legitimate files on your sites. See handling false positives.
--match-allReport 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 FILEWrite the report to a file instead of standard output.
--output-columns a,b,cChoose which columns appear in the report. See reports and columns.
-j NNumber of worker processes. The scanner is multi-process; raise this on machines with spare cores, lower it on busy shared hosts.
Per-file timeoutA 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 sizeFiles 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 filtersRestrict 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:

ColumnContents
filenameFull path of the matched file.
signature_idName of the YARA rule that matched.
signature_descriptionThe rule's description, from its meta section.
rule_scoreThe rule's score or quality rating, where the rule provides one. Higher means a more specific, higher-confidence rule.
rule_authorWho wrote the rule. WPSniffer credits every rule author in its output.
rule_referenceA URL or reference the author supplied, often a write-up of the malware family.
matched_textThe 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 wpsniffer binary 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 --help to 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):

  1. Do not delete anything yet. Open the matched_text and the rule's rule_reference to confirm the match is real. Some broad rules fire on legitimate minified or obfuscated plugin code.
  2. 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.
  3. Look for siblings. Attackers rarely drop one file. Re-run the scan with --yara-forge extended and --match-all, and list recently changed files with find -cmin as shown above.
  4. 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.
  5. 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.