The shellproof command line
Find and fix PHP security exposure from your own shell. One file, one dependency-free Python standard library, no account. It inspects the machine it runs on and the paths you give it, and sends nothing about them anywhere.
Every command and sample on this page was produced by running shellproof 1.1.0. Vulnerability data comes from php.net.
Install
Shellproof needs Python 3.8 or newer and nothing else — no pip packages, no compiler, no network for installation. It is tested on CPython 3.8 through 3.13. Pick whichever path fits the box you are triaging.
Zero install (recommended for incident response)
Download the single-file application and run it with the Python already on the host. The .pyz is a standard zipapp that bundles the whole package and its vulnerability database.
curl -LO https://shellproof.dev/downloads/shellproof.pyz python3 shellproof.pyz scan
The file is also linked directly: shellproof.pyz. Each version is attached to its GitHub release too. Every subcommand works the same way — python3 shellproof.pyz version 8.3.12, python3 shellproof.pyz fix, and so on. Throughout these docs the command is written as patched; substitute python3 shellproof.pyz if you are running the bundle.
One-liner triage with quickcheck.sh
For a 3am first look, quickcheck.sh is POSIX shell with no dependencies. It only reads php -v for every PHP binary on PATH and tells you whether each is affected by PHP2Shell. It does not inspect configuration, dependencies, code or running workers — use the full scan for that.
curl -LsS https://shellproof.dev/downloads/quickcheck.sh | sh
sh runs it sightunseen.
Prefer curl -LO then read it before running — it is short and readable. The same caution applies to any install one-liner, ours included.
Real output on a host with no PHP on PATH (exit code 0; a web server may still bundle its own PHP via FPM or mod_php):
Shellproof quickcheck — PHP2Shell (CVE-2026-109618 / CVE-2026-109619) Scanning PHP binaries on PATH... No PHP binary found on PATH. A web server may still bundle its own PHP (FPM, mod_php, LiteSpeed lsphp). Check the version your site reports and run the full tool to inspect it: python3 shellproof.pyz scan
When it finds an affected binary it exits 1; an end-of-life branch exits 1 with a migration note.
pip / pipx from source
To get a real shellproof command on PATH, install the package. pyproject.toml declares the shellproof entry point, so pipx gives you an isolated executable:
# isolated executable (recommended) pipx install git+https://github.com/nicolascoculuzzi/shellproof # or into the current environment pip install git+https://github.com/nicolascoculuzzi/shellproof # or from a checkout git clone https://github.com/nicolascoculuzzi/shellproof pip install ./shellproof
After either, shellproof --version prints shellproof 1.1.0. The distribution name on an index is shellproof; the command it installs is shellproof.
Quickstart
Three commands cover most of a response: check one version string, scan a tree, and get the fix steps for this host.
1. Check a single version
shellproof version takes any version string — 8.3.12, the first line of php -v, or an X-Powered-By header value — and prints a verdict:
$ shellproof version 8.3.12 PHP 8.3.12 CRITICAL PHP 8.3.12 is affected by a critical, actively exploited vulnerability. Upgrade to 8.3.36 now. Advisories: CVE-2026-109618 PHP2Shell critical [fixed in 8.3.36] [exploited in the wild] CVE-2026-109619 PHP2Shell critical [fixed in 8.3.36] [exploited in the wild] Missing security releases on branch 8.3: 8.3.14, 8.3.19, 8.3.23, 8.3.29, 8.3.31, 8.3.32, 8.3.33, 8.3.35, 8.3.36 Upgrade to: 8.3.36 Branches in active support: 8.5 -> 8.5.12 8.4 -> 8.4.27 data: php.net · generated 2026-10-22T04:58:35Z
2. Scan a webroot
Point scan at a path (it defaults to the current directory). With no flags it also inspects the host's PHP binaries, /etc configuration and running workers; add --no-system to scan only the files you name. Output is grouped by category and coloured on a TTY. Trimmed real run against a small sample tree (--no-system --offline):
shellproof 1.1.0 (data: php.net, generated 2026-10-22T06:22:24Z)
scanned: ./example.com
findings: 2 critical, 2 high, 2 low, 1 info
== Application code (1) ==
[HIGH] CODE-EVAL Use of eval()
example.com/public/up.php:1
php.net: the eval() construct 'is very dangerous because it allows
execution of arbitrary PHP code. Its use thus is discouraged.' ...
evidence: <?php @eval($_POST["x"]);
fix:
Remove eval(). Replace dynamic dispatch with an explicit allowlist ...
ref: https://www.php.net/manual/en/function.eval.php
== Indicators of compromise (3) ==
[CRITICAL] COMPROMISE-EXEC-SUPERGLOBAL Command or code executed directly from request input
example.com/public/up.php:1
A command/code-execution sink (system, exec, shell_exec, passthru,
popen, proc_open, pcntl_exec, eval or assert) receives a value taken
straight from $_GET/$_POST/$_REQUEST/$_COOKIE/$_SERVER ...
evidence: <?php @eval($_POST["x"]);
fix:
Treat this host as compromised. Isolate it, preserve the file for
forensics, remove it or restore from a known-good backup ...
== File integrity (2) ==
[CRITICAL] INT-ENV-EXPOSED Environment file served from a public web root
example.com/public/.env
public/.env sits inside the public web root 'public'. If the web server
serves it, every secret it holds ... is downloadable.
... (3 more findings: INT-APP-DEBUG, 2x COMPROMISE-RECENT-MODIFICATION, DEP-OFFLINE-SKIPPED)
RESULT: findings at or above high. (exit 1)
The header line gives the counts by severity; each finding carries a stable rule id, a location, masked evidence, a concrete fix and sources. The final RESULT line and the exit code reflect --fail-on (default high): 0 = nothing at or above the threshold, 1 = findings at or above it, 2 = a usage or runtime error.
3. Get the fix steps
shellproof fix detects this host's platform, PHP branch and SAPI and prints the exact upgrade, restart and verify commands, the interim mitigations and the after-patch checklist. The output never runs anything; --format sh -o fix.sh writes a script for you to review and run yourself.
shellproof fix # detect this host and print the playbook shellproof fix --platform ubuntu-ondrej --branch 8.3 --sapi fpm-fcgi shellproof fix --format sh -o fix.sh # a reviewable script; NOT executed
shellproof scan
The full scan runs six checks in order: runtime (PHP binaries, versions, stale workers), config (php.ini, FPM pools, web-server config), deps (composer.lock against Packagist advisories), code (risky code and webshell signatures), crypto (exposed keys, seed phrases, drainers) and integrity (WordPress core checksums, exposed .env and backups).
shellproof scan [PATH ...] # PATH defaults to the current directory
Flags
| Flag | Effect |
|---|---|
| --php BIN | Inspect this PHP binary instead of auto-discovery. Repeatable. |
| --ini FILE | Audit this extra php.ini / FPM pool file. Repeatable. |
| --no-system | Do not inspect host binaries, /etc configs or processes — a files-only scan of the paths you give. |
| --skip CHECK | Skip a check: runtime, config, deps, code, crypto or integrity. Repeatable. |
| --offline | No network: skip the Packagist advisory API and WordPress checksums. |
| --since YYYY-MM-DD | Flag files modified on or after this date. Default 2026-10-20. |
| --exclude GLOB | Exclude paths matching GLOB. Repeatable. |
| --max-file-size MB | Skip content checks on files larger than MB. Default 2. |
| --format FMT | text, json, sarif, html or md. Default text; inferred from the -o extension when omitted. |
| -o, --output FILE | Write the report to FILE instead of stdout. |
| --fail-on SEV | Exit 1 if any finding is at or above SEV. One of info, low, medium, high, critical, or never. Default high. |
| -q, --quiet | Print only the summary header and the result line. |
| --no-color | Disable ANSI colour (also honours NO_COLOR). |
| -v, --verbose | Log progress to stderr. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Nothing at or above --fail-on (or --fail-on never). |
| 1 | At least one finding at or above --fail-on. |
| 2 | Usage error, or the scan could not run. |
So --fail-on is what makes scan useful as a gate. --fail-on never always exits 0, which is handy when you want the report but not a failing step:
shellproof 1.1.0 (data: php.net, generated 2026-10-22T04:58:35Z) scanned: ./example.com findings: 2 critical, 2 high, 2 low, 1 info RESULT: nothing at or above never.
All commands
Run shellproof <command> --help for the exact flags of any subcommand.
version
shellproof version VERSION [--windows] [--json] [--no-color]
Verdict for one PHP version string. --windows evaluates the host as Windows (so the php-cgi Best-Fit advisory applies). --json emits the full verdict — status, missing_security_releases, applicable cves and advisories, upgrade_to, recommended_branches — for scripting:
{
"input": "8.3.12",
"version": "8.3.12",
"branch": "8.3",
"known": true,
"branch_status": "security",
"branch_security_until": "2027-12-31",
"latest_in_branch": "8.3.36",
"is_latest": false,
"missing_security_releases": ["8.3.14", "8.3.19", "...", "8.3.36"],
"advisories": [
{"id": "php2shell-oom", "cve": "CVE-2026-109618", "name": "PHP2Shell",
"severity": "critical", "exploited_in_wild": true,
"fixed_in": "8.3.36", "status": "vulnerable"}
],
"status": "critical",
"headline": "PHP 8.3.12 is affected by a critical, actively exploited vulnerability. Upgrade to 8.3.36 now.",
"upgrade_to": "8.3.36",
"recommended_branches": ["8.5", "8.4"]
}
fix
shellproof fix [--platform ID] [--branch X.Y] [--sapi SAPI] [--format text|md|sh] [-o FILE]
With no flags it detects the platform, branch and SAPI on this host. It prints the upgrade, restart and verify commands with versions filled in from the live release data, the interim mitigations and the after-patch checklist. --format md is ticket-ready; --format sh -o fix.sh writes a reviewable script that Shellproof does not execute. Real excerpt (detected a Homebrew/macOS host):
shellproof fix — Homebrew (macOS)
(auto-detected platform/branch/SAPI on this host; override with --platform/--branch/--sapi)
Target: PHP 8.4.27 · Homebrew (macOS) · CLI / cron
== Upgrade ==
1. Update and upgrade the PHP formula
$ brew update
$ brew upgrade php@8.4
$ brew link --overwrite --force php@8.4
== Verify ==
$ php -v
$ php@8.4 -v || true
The version a browser sees can differ from the CLI; confirm every SAPI
reports 8.4.27 or newer.
== After the patch ==
* Restart every PHP worker and web server
* Rotate secrets that may have leaked
* Scan for signs of compromise
$ shellproof scan --since 2026-10-20
...
See the interactive fix generator for every platform.
monitor
shellproof monitor [PATH ...] [scan flags] [--interval DURATION] [--once] [--state FILE]
[--webhook URL] [--webhook-format json|slack|discord]Re-scans on an interval and reports what changed. Covered in Monitoring below.
wallet
shellproof wallet ADDRESS [--rpc URL] [--since YYYY-MM-DD] [--limit N] [--json]
shellproof wallet ADDRESS --watch [--interval 60s] [--once] [--state FILE]
[--webhook URL] [--webhook-format json|slack|discord]Read-only Solana activity for one address, for checking whether a hot wallet moved funds after a key exposure. It validates the address (base58, 32 bytes) before calling, and only ever reads — it issues a getSignaturesForAddress JSON-RPC request and never signs or sends anything. The default endpoint is https://api.mainnet-beta.solana.com; override it with --rpc.
Solana activity for So11111111111111111111111111111111111111112
endpoint: https://api.mainnet-beta.solana.com
2 transaction(s), newest first:
2026-10-22 06:22:16Z ok 3nAYqr2rzxjeQs2uukjyk1SJBTmKG66riPVcuMaK887Ce…
2026-10-22 06:22:16Z FAILED QF5BefFWCerA2CxAj3nTJwGqiC7v6AG1R1SL4syafX8C5s…
An invalid address is rejected before any network call (exit 2): shellproof: address is not valid base58. With --watch it keeps checking and flags new transactions the wallet signed; see Watching a wallet.
delegates
shellproof delegates ADDRESS [--rpc URL] [--json]
Lists every token account owned by ADDRESS that still has a delegate (an approval) set. Covered in Token delegations below.
update
shellproof update
Refreshes the vulnerability data from php.net into ~/.cache/shellproof. Every later command prefers this cache over the copy bundled in the tool, so you stay current between releases.
shellproof: fetching php.net release index for PHP 5 shellproof: fetching php.net ChangeLog-5 shellproof: fetching php.net release index for PHP 7 shellproof: fetching php.net ChangeLog-7 shellproof: fetching php.net release index for PHP 8 shellproof: fetching php.net ChangeLog-8 shellproof: refreshed vulnerability data from php.net wrote /Users/you/.cache/shellproof/php-releases.json generated_at: 2026-10-22T06:22:24Z 530 releases, 18 branches
Solana wallets
If a vulnerable PHP worker could read a wallet key, two on-chain questions matter: can anyone still spend from your token accounts without that key, and has the wallet signed anything you did not? shellproof delegates answers the first and shellproof wallet --watch the second. Both are read-only JSON-RPC lookups against an endpoint you choose: Shellproof never builds, signs or sends a transaction, and never asks for a key.
Only the scheme and host of --rpc and --webhook are ever printed or saved, because provider RPC URLs usually carry an API key and a Slack or Discord webhook URL is itself a secret.
Token delegations: shellproof delegates
shellproof delegates ADDRESS [--rpc URL] [--json]
A delegate can move up to its approved amount out of a token account without the owner signing. Card and DeFi apps set them on purpose; a delegate you do not recognise is a standing spend permission someone else holds. The command validates ADDRESS (base58, 32 bytes), then calls getTokenAccountsByOwner with jsonParsed encoding once for the spl-token program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA) and once for Token-2022 (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb), and lists the accounts whose parsed data has a delegate.
| Flag | Effect |
|---|---|
| --rpc URL | JSON-RPC endpoint. Default https://api.mainnet-beta.solana.com, which answers this call from a server or terminal. |
| --json | Print {"owner", "rpc", "count", "delegations": [...]}. Each delegation has account, mint, program (spl-token or token-2022), amount, decimals, delegate and delegated_amount; amounts are the RPC's uiAmountString. |
| Code | Meaning |
|---|---|
| 0 | No token account has a delegate. |
| 1 | At least one token account has a delegate. |
| 2 | Invalid address, or the RPC lookup failed. |
Real run against a mainnet wallet with two card-spend delegates on spl-token accounts and one on Token-2022, with its addresses replaced by placeholders (exit 1):
Token delegations for OWNER_WALLET
endpoint: https://api.mainnet-beta.solana.com (spl-token and Token-2022 accounts)
3 token account(s) with an active delegate:
mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (spl-token)
delegate DELEGATE_1
delegated amount 4 (balance 0.983234)
token account TOKEN_ACCOUNT_1
revoke spl-token revoke TOKEN_ACCOUNT_1
mint Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB (spl-token)
delegate DELEGATE_2
delegated amount 1 (balance 1.000564)
token account TOKEN_ACCOUNT_2
revoke spl-token revoke TOKEN_ACCOUNT_2
mint 2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo (token-2022)
delegate DELEGATE_1
delegated amount 5 (balance 0.773118)
token account TOKEN_ACCOUNT_3
revoke spl-token revoke TOKEN_ACCOUNT_3
A delegate can move up to the delegated amount out of that token account
without the owner's signature. If you did not set it up on purpose, revoke
it from the owner wallet: run `spl-token revoke <TOKEN_ACCOUNT_ADDRESS>`
with the owner keypair, or use your wallet's revoke / token approvals
feature. Shellproof only reads; it never builds, signs or sends a
transaction.
Token delegations for Vote111111111111111111111111111111111111111 endpoint: https://api.mainnet-beta.solana.com (spl-token and Token-2022 accounts) no token account has an active delegate. Nothing to revoke.
Revoking is your action, in your wallet. It is a transaction the owner signs, so Shellproof only prints the command. With the Solana CLI, spl-token revoke <TOKEN_ACCOUNT_ADDRESS> clears the delegate on one token account, signed by your default keypair (pass --owner KEYPAIR otherwise); it detects whether the account belongs to spl-token or Token-2022. Most wallets also have a revoke or token-approvals screen. If the owner key itself may have leaked, revoking is not enough: move the tokens to a freshly generated wallet.
Watching a wallet: shellproof wallet --watch
--watch turns shellproof wallet into a watcher. The first cycle only records the newest signature as the baseline and alerts on nothing. Every later cycle asks getSignaturesForAddress for signatures newer than the baseline, reads each one with getTransaction (jsonParsed, maxSupportedTransactionVersion 0, asking again with the version the node names for newer message versions), and prints them newest first with whether the wallet signed, its SOL balance change and the status. A transaction the wallet signed is the alert that matters: it needs the private key, so one you did not make means someone else has the key.
| Flag | Effect |
|---|---|
| --watch | Keep checking and report new transactions. |
| --interval DURATION | Time between checks: 60s, 5m, 1h. Default 60s, minimum 10s. |
| --once | Run one check and exit. Use this under cron; the state file carries the baseline between runs. |
| --state FILE | Where the baseline is kept. Default ~/.cache/shellproof/wallet-ADDRESS.json. It holds the address, the newest signature and slot, and a timestamp — no RPC URL and nothing secret. |
| --webhook URL | POST the new transactions to URL whenever there is anything new. |
| --webhook-format | json ({"tool", "address", "new": [...]}), slack ({"text"}) or discord ({"content"}). Default json. |
| --limit N | Most signatures read per cycle. Default 100; a note is printed when a cycle hits it. |
| --json | One JSON object per cycle on stdout, for piping into other tools. |
| --rpc URL | JSON-RPC endpoint. Default https://api.mainnet-beta.solana.com. |
Exit codes: Ctrl-C stops a running watch with 0. With --once it exits 1 when the cycle found a new transaction signed by the wallet, 0 otherwise, and 2 on a bad flag or when the signature lookup fails (the baseline is left as it was). A transaction whose details cannot be read is reported with the signer unknown and is not counted as signed. A running watch logs a failed cycle to stderr and tries again at the next interval.
Real runs against a busy trading wallet on mainnet, 35 seconds apart, with its address and signatures replaced by placeholders. The first records the baseline (exit 0); the second finds two new transactions the wallet signed and posts them to the webhook (exit 1):
[2026-10-22 11:45:07] shellproof wallet watch — OWNER_WALLET baseline: newest transaction is SIGNATURE_A (2026-10-22 15:44:56Z) nothing is reported on the first run; later runs report anything newer
[2026-10-22 11:45:42] shellproof wallet watch — 2 new transaction(s), 2 SIGNED BY THIS WALLET for OWNER_WALLET SIGNED BY WALLET 2026-10-22 15:45:14Z ok SOL +0.0015648 SIGNATURE_B SIGNED BY WALLET 2026-10-22 15:45:13Z ok SOL +0.0015648 SIGNATURE_C A transaction signed by this wallet needs its private key. If you did not make it, assume the key is compromised and move what is left to a new wallet. webhook: delivered (json)
The json webhook body that run delivered (pretty-printed here):
{
"tool": "shellproof",
"address": "OWNER_WALLET",
"new": [
{"signature": "SIGNATURE_B",
"slot": 460090954, "block_time": 1792683914, "signer": true,
"sol_change_lamports": 1564800, "err": null},
{"signature": "SIGNATURE_C",
"slot": 460090949, "block_time": 1792683913, "signer": true,
"sol_change_lamports": 1564800, "err": null}
]
}
Each entry is the transaction's involvement for this address: signer is true when the wallet signed, sol_change_lamports is its post-minus-pre SOL balance (null if it is not in the transaction), and err is the transaction error (null on success). If a transaction cannot be read, the entry still appears with signer: null and a lookup_error.
Left running, it checks on the interval until you press Ctrl-C. This run used --interval 20s and a Slack webhook:
[2026-10-22 11:45:52] shellproof wallet watch — OWNER_WALLET baseline: newest transaction is SIGNATURE_D (2026-10-22 15:45:45Z) nothing is reported on the first run; later runs report anything newer shellproof wallet watch: next check in 20s (Ctrl-C to stop) [2026-10-22 11:46:13] shellproof wallet watch — 2 new transaction(s), 2 SIGNED BY THIS WALLET for OWNER_WALLET SIGNED BY WALLET 2026-10-22 15:46:05Z ok SOL -0.002512213 SIGNATURE_E SIGNED BY WALLET 2026-10-22 15:46:04Z ok SOL +0.00026384 SIGNATURE_F A transaction signed by this wallet needs its private key. If you did not make it, assume the key is compromised and move what is left to a new wallet. webhook: delivered (slack) shellproof wallet watch: next check in 20s (Ctrl-C to stop) ... ^C shellproof wallet watch: stopped
shellproof wallet watch — 2 new transaction(s) for OWNER_WALLET, 2 signed by this wallet • SIGNED BY WALLET 2026-10-22 15:46:05Z ok SOL -0.002512213 SIGNATURE_E https://explorer.solana.com/tx/SIGNATURE_E • SIGNED BY WALLET 2026-10-22 15:46:04Z ok SOL +0.00026384 SIGNATURE_F https://explorer.solana.com/tx/SIGNATURE_F Transactions signed by this wallet need its private key. If you did not make them, assume the key is compromised and move what is left to a new wallet.
Under cron, pair --once with a webhook. Nothing is posted when nothing is new, so a quiet wallet stays quiet:
# every 5 minutes: alert Slack when the hot wallet has new activity */5 * * * * /usr/bin/python3 /opt/shellproof.pyz wallet HOT_WALLET_ADDRESS --watch --once \ --webhook "$SLACK_WEBHOOK_URL" --webhook-format slack
The Crypto & Solana page has the same two checks in the browser. Its watcher polls every 60 seconds while the tab is open and can raise a browser notification. Browsers get a narrower choice of public endpoints: api.mainnet-beta.solana.com refuses every request that carries a browser Origin header, and PublicNode (the page's default) answers activity lookups but refuses getTokenAccountsByOwner and keeps only a few days of history, so the delegation check there needs your own RPC URL. The CLI has neither limit.
Report formats
scan renders the same findings in five shapes. Choose with --format, or let -o report.html infer it from the extension.
| Format | For |
|---|---|
| text | Reading in a terminal. Grouped by category, coloured on a TTY. |
| json | Scripting and pipelines. The canonical envelope below. |
| sarif | GitHub code scanning. SARIF 2.1.0, one rule per finding id. |
| html | A single self-contained file styled like this site, for sharing. |
| md | Pasting into a ticket or pull request. |
JSON envelope
Every JSON report has the same top-level shape. summary, findings, errors and installs are always present; this is the structure the website reads too.
{
"tool": "shellproof",
"version": "1.0.0",
"generated_at": "2026-10-22T06:21:46Z",
"data_generated_at": "2026-10-22T04:58:35Z",
"target": ["./example.com"],
"summary": {
"total": 7,
"by_severity": {"info": 1, "low": 2, "medium": 0, "high": 2, "critical": 2},
"by_category": {"compromise": 3, "integrity": 2, "code": 1, "dependency": 1},
"worst": "critical",
"fail_on": "high"
},
"findings": [
{
"id": "COMPROMISE-EXEC-SUPERGLOBAL",
"severity": "critical",
"title": "Command or code executed directly from request input",
"category": "compromise",
"location": "example.com/public/up.php",
"line": 1,
"evidence": "<?php @eval($_POST[\"x\"]);",
"fix": "Treat this host as compromised. Isolate it, preserve the file …",
"references": ["https://www.php.net/manual/en/function.exec.php"],
"confidence": "high",
"fingerprint": "ca22005545e240ac"
}
],
"errors": [],
"installs": []
}
fingerprint is stable across runs for the same finding, which is how monitor tells NEW from RESOLVED and how SARIF results dedupe in code scanning.
Continuous integration
The repository ships a composite GitHub Action (action.yml) that runs a scan, writes SARIF, uploads it to GitHub code scanning and fails the job on findings at or above your threshold. Findings then appear in the repository's Security → Code scanning tab, annotated on the pull request.
name: Shellproof
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 7 * * 1" # weekly re-scan catches newly disclosed CVEs
permissions:
contents: read
security-events: write # required to upload SARIF
jobs:
shellproof:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nicolascoculuzzi/shellproof@v1
with:
path: .
fail-on: high # info|low|medium|high|critical|never
# offline: "true" # skip Packagist + WordPress checks
# args: "--skip crypto --exclude 'tests/*'"
The action inputs map to scan flags: path, fail-on, offline, output (the SARIF file, default shellproof.sarif), args (anything extra passed straight through to shellproof scan) and upload-sarif (set false to keep the SARIF as a build artifact only). It always uploads the SARIF before enforcing fail-on, so results reach the Security tab even on a failing run.
Without the action
On any other CI, call the tool directly and hand the SARIF to the official uploader:
python3 shellproof.pyz scan . --format sarif -o shellproof.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shellproof.sarif
category: shellproof
Exit 1 fails the step when something is at or above --fail-on; exit 2 means the scan itself could not run.
Monitoring
shellproof monitor re-runs the same scan on an interval, diffs the findings against the previous run by fingerprint, and reports which are NEW and which are RESOLVED. It also refreshes php.net data each cycle and flags security releases it had not seen before — so a new PHP CVE shows up even if your files never changed.
It accepts every scan flag, plus:
| Flag | Effect |
|---|---|
| --interval DURATION | Time between cycles: 30m, 6h, 1d. Default 6h. |
| --once | Run a single cycle and exit. Use this under cron. |
| --state FILE | Where the baseline is stored. Default ~/.cache/shellproof/monitor-state.json. |
| --webhook URL | POST the delta to URL when anything changed. |
| --webhook-format | json (raw delta), slack or discord. Default json. |
First run against a tree establishes the baseline; everything is NEW and it exits per --fail-on. A later run with no changes reports nothing and exits 0:
[2026-10-22 02:22:01] shellproof monitor — 7 finding(s): 7 new, 0 resolved
NEW:
• [CRITICAL] COMPROMISE-EXEC-SUPERGLOBAL: Command or code executed directly from request input — example.com/public/up.php:1
• [CRITICAL] INT-ENV-EXPOSED: Environment file served from a public web root — example.com/public/.env
• [HIGH] CODE-EVAL: Use of eval() — example.com/public/up.php:1
• [HIGH] INT-APP-DEBUG: APP_DEBUG is enabled in a production environment — example.com/public/.env
...
[2026-10-22 02:22:01] shellproof monitor — 7 finding(s): 0 new, 0 resolved no change since last run
Under cron
Pair --once with your scheduler. A webhook is sent only when there is a delta, so a quiet host is quiet:
# every 6 hours: scan /var/www, notify Slack on any change 0 */6 * * * /usr/bin/python3 /opt/shellproof.pyz monitor /var/www --once \ --webhook "$SLACK_WEBHOOK_URL" --webhook-format slack --fail-on never
Use --fail-on never under cron if you do not want a non-zero exit to be logged as a job failure; drop it if you do.
Privacy & network
Shellproof runs entirely on your machine. It reads files and runs local probes (php -v, ps, package-manager queries) to build findings, and nothing about the scanned host — no file contents, paths, versions, findings or identifiers — is ever uploaded anywhere. Detected secrets are masked before they reach a finding, a report, the console or any output.
The only outbound requests are read-only lookups of public vulnerability data, and they happen only for the commands that need them. All network I/O goes through one small module so it is easy to audit. --offline disables every one of them.
| Endpoint | Used by | Why |
|---|---|---|
| www.php.net/releases/index.php www.php.net/ChangeLog-N.php | update, monitor | Fetch the PHP release index and changelogs to refresh the vulnerability database. Read-only; sends only the request. |
| packagist.org/api/ | scan (deps), monitor | Look up advisories for the package names found in composer.lock. Only package names are sent — never your code, versions or paths. |
| api.wordpress.org/core/ | scan (integrity), monitor | Fetch official core checksums for a detected WordPress version to find modified core files. Only the version and locale are sent. |
| api.mainnet-beta.solana.com (or your --rpc) | wallet, delegates | Read-only getSignaturesForAddress, getTransaction and getTokenAccountsByOwner for a Solana address you supply. Never signs or sends transactions. |
your --webhook URL | monitor, wallet --watch | Only if you configure it. monitor sends the finding delta (ids, severities, titles, locations); wallet --watch sends the new transaction signatures for the address you watch. |
Shellproof contacts no telemetry, analytics or update server of its own. Run with --offline and it uses only the data bundled in the tool — no connection leaves the host. You can read the one module that makes every request, shellproof/net.py, in the source browser.
Shellproof is defensive tooling: it helps you find and fix exposure on systems you operate. It is not a scanner for other people's hosts, it ships no exploit code, and it is independent and not affiliated with the PHP Group.