# 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

WPSniffer is published on PyPI. Install it into a virtual environment so its dependencies stay separate from the system Python. The commands below create one in your home directory:

$ python3 -m venv ~/wpsniffer
$ ~/wpsniffer/bin/pip install wpsniffer

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:

$ ~/wpsniffer/bin/pip install --upgrade wpsniffer

Every release is also published as a wheel and source archive on the GitHub releases page. On a server without access to PyPI, download the wheel there and install it with pip install ./wpsniffer-*.whl.

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 --progress /var/www/html

Add --progress for a live display of files scanned, matches and throughput. 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.

By default only files with code-like extensions are scanned; add -a to scan every file regardless of extension, or --images to include images. 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.
--yara-forge-release TAGPin a YARA Forge release such as 20261018 instead of latest, for reproducible scans. See pinning a release.
-e RULE, --exclude-signaturesIgnore rules by name. Shell-style wildcards work (-e 'SIGNATURE_BASE_SUSP_*'), values can be comma-separated, and the flag can repeat. Rules from your own files are named namespace:RULE. See handling false positives.
-i RULE, --include-signaturesReport only the named rules, with the same wildcard and comma syntax.
--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. With -E the file is attached to the email report.
-m FORMAT, --output-formatcsv (default), tsv, null-delimited, line-delimited or human. Add --output-headers for a header row.
--output-columns a,b,cChoose which columns appear in the report. See reports and columns.
-w N, --workersNumber of worker processes, default 1. Raise it on machines with spare cores, keep it low on busy shared hosts.
--progressLive progress display in the terminal.
--scan-timeout SECONDSMaximum time per file, default 10 seconds. A file that exceeds it is reported as timed out instead of stalling the run.
-M SIZE, --scanned-content-limitHow much of each file is scanned, default 50m. Files are scanned whole up to this limit, so YARA filesize conditions work and matches are never split across chunk boundaries.
-a, --imagesBy default only files with code-like extensions are scanned. -a scans every file; --images adds image files.
-N REGEX, -X REGEXOnly scan, or never scan, filenames matching a Python regular expression, for example -N '.*\.php$'. -n and -x do the same for exact filenames.
-E ADDRESS, --emailEmail the report summary, attaching the output file when --output-path is set. Sends through sendmail by default, or SMTP with --smtp-host and related options. See email reports.
--no-read-stdinDo not read paths from standard input. Required under cron and CI, where stdin is not a terminal and the scan would otherwise wait for input.
--pre-compileDownload and compile the rules into the cache without scanning, for example right after an upgrade. --refresh-rules forces a new download; --purge-cache clears the cache.
-c FILE, --configurationRead options from an INI file. Without it, ~/.config/wpsniffer/wpsniffer.ini or /etc/wpsniffer/wpsniffer.ini is used, which is the right place for SMTP passwords. wpsniffer configure writes one interactively.

Reports and columns

Choose the format with -m: csv (the default), tsv, line-delimited or null-delimited text for scripts, or human for reading in the terminal. Pick the columns with --output-columns; the default set is filename,signature_id,signature_description,rule_score,matched_text. The columns available are:

ColumnContents
filenameFull path of the matched file.
signature_idName of the YARA rule that matched.
signature_nameThe rule's display name.
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 first matched string, up to 256 bytes, so you can see what was flagged without opening the file.
rule_tagsThe rule's YARA tags.

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

Pass -E with one or more addresses to get a summary by email at the end of a scan: the suspicious files, the number of files scanned, bytes processed and elapsed time. When --output-path is set, the report file is attached. Mail goes through the local sendmail by default; set --smtp-host, --smtp-port, --smtp-user and --smtp-tls-mode to use an SMTP server instead, and keep --smtp-password in the INI file rather than on the command line.

$ wpsniffer malware-scan -E ops@example.com --email-from scanner@example.com \
    --output-path /var/log/wpsniffer/report.csv /var/www

Scanning paths from stdin

When standard input is not a terminal, malware-scan reads file paths from it, in addition to any paths on the command line. That lets you combine it with find or any other tool that produces a file list. Use -print0, because the default separator is the null byte (change it with -s). Under cron or CI, where stdin is open but empty, pass --no-read-stdin so the scan does not wait.

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 --no-read-stdin -w 2 \
    --output-path /var/log/wpsniffer/$(date +\%F).csv /var/www

Notes for cron:

  • Pass --no-read-stdin. Under cron, standard input is not a terminal, and without the flag the scan waits for a file list that never comes.
  • 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 -E you@example.com to receive the summary by email with the CSV attached, 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 wpsniffer
$ .venv/bin/wpsniffer malware-scan --no-read-stdin --output-path report.csv .
$ test ! -s report.csv

Pass --no-read-stdin, since CI runners leave standard input open. For reproducible results across runs, pin the YARA Forge release with --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 -w on multi-core machines (the default is a single worker), restrict the scan with -N '.*\.php$', 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 scan hangs under cron or CI

It is waiting for file paths on standard input. Add --no-read-stdin, or redirect < /dev/null.

The same rule matches on every site

That is a false positive for your environment. Exclude the rule with -e (wildcards such as -e 'SIGNATURE_BASE_SUSP_*' work), 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.