Checking a Request¶
bin/firewall-check answers one question from the terminal: would this request be blocked, and by what?
It runs the real evaluation path against a config you name, without needing a running site and without touching your production data.
BLOCKED GET /wp-admin/
client 203.0.113.5
blocked by IP Address
status 400
reason blocked
storage throwaway (in-memory)
Options¶
| Option | Purpose |
|---|---|
--config=FILE | Config file. Repeatable, merged in order exactly as Firewall::create() merges them |
--ip=ADDRESS | Client IP, IPv4 or IPv6. Default 127.0.0.1 |
--url=URL | Path with optional query string. Default / |
--method=VERB | HTTP method. Default GET, or POST when --body is given |
--header=NAME:VAL | Request header. Repeatable |
--body=STRING | Request body |
--explain | Show every plugin that evaluated, plus the ones that never ran |
--json | Machine-readable output |
--live-storage | Use the configured storage instead of a throwaway — see Safety |
Exit codes¶
Designed to compose in scripts and CI, so the verdict is the exit status:
| Code | Meaning |
|---|---|
0 | allowed |
1 | blocked |
2 | challenged |
64 | usage error |
70 | internal error |
if vendor/bin/firewall-check --config=firewall.yml --url=/wp-admin/ >/dev/null; then
echo "Not blocked — the WordPress preset is not doing its job."
exit 1
fi
Understanding why¶
--explain lists every plugin that ran, with its result and timing, and then the plugins that never ran because something earlier matched. That second list is usually the answer to "why wasn't this caught?":
$ vendor/bin/firewall-check --config=firewall.yml --ip=203.0.113.5 --url=/wp-admin/ --explain
BLOCKED GET /wp-admin/
client 203.0.113.5
blocked by IP Address
...
Plugins evaluated, in order:
MATCH IP Address 0.06 ms
Configured but not reached:
block Kanopi\Firewall\Plugins\Url weight -10
block Kanopi\Firewall\Plugins\UserAgent weight 0
The IP list matched first, so nothing else was consulted. If you were testing whether your URL rules catch /wp-admin/, this tells you the test never reached them.
The timings are real and occasionally revealing — matomo/device-detector is markedly more expensive on a cold call than the pattern-matching plugins.
Safety¶
Storage is replaced with a throwaway by default, and this matters. A block is not a read-only event: it writes to storage, records an offense, and applies blocking_escalation. A checker wired straight to a production config would ban the address it was asked about.
So by default the durable blocklist is not consulted and not written. The output says which mode you are in on the storage line.
--live-storage restores the configured backend when you genuinely need the repeat-offender state considered — for instance to confirm an address is currently banned rather than merely matching a rule. It prints a warning to stderr, and a blocked verdict will be recorded:
If that records a block you did not want, clearing it means removing the entry from your storage backend directly — which is the reason the throwaway is the default.
Scripting¶
--json writes a single object to stdout; warnings go to stderr, so the output stays parseable even with --live-storage.
The tool redirects its own diagnostics to stderr so stdout carries nothing but the JSON document. One case is outside its control: PHP's CLI SAPI prints startup warnings to stdout before any script runs, so a duplicate extension= line in your php.ini would land ahead of the JSON and break the pipe. If you hit that, run it as:
{
"verdict": "blocked",
"plugin": "IP Address",
"status": 400,
"reason": "blocked",
"request": { "ip": "203.0.113.5", "method": "GET", "path": "/", "query": null },
"storage": "throwaway (in-memory)"
}
Notes¶
- Mode is forced to
exceptionregardless of what the config says.Firewall::evaluate()returns early in CLI under any other mode, which would report every request as allowed — so this is not optional, and it is why hand-rolled checking scripts so often appear to work while telling you nothing. - Plugins that reach the network still do so.
AbuseIpdbwill consult its cache and, on a miss, call the API and spend quota. Nothing here suppresses that. - Attribution comes from the firewall's own decision log, not from re-running each plugin — a second pass would repeat every side effect.