jeffersongoncalves/laravel-scanner-guard
Laravel Scanner Guard detects and blocks vulnerability scanners probing your app for exposed files, admin panels, and known exploit paths. It logs suspicious requests and lets you deny abusive IPs with middleware, helping cut noise and harden your Laravel site with minimal configuration.
The first decision on this release is no longer "install and migrate" — it is which driver you run. Look at config/scanner-guard.php and the new driver key (env SCANNER_GUARD_DRIVER) before touching anything else:
database — the pre-1.3 behaviour. You still need php artisan migrate for the audit and daily-stats tables.cache — no migrations, no tables. Bans live only in the configured cache store for ban_duration.http — bans live in the local cache and are also forwarded to a central app after the response.Minimal start for the common case (single app, full audit trail):
composer require the package, then publish the config and run migrations.SCANNER_GUARD_DRIVER unset (defaults to database) and confirm your cache/session setup is normal Laravel.ScannerGuard::ban() returned a ScannerGuardBan model.If you already publish the config, add the new driver and http keys manually — the package won't merge them for you, and a missing key means the new code paths fall back or error depending on how you read config.
First use case to wire up: the guard on your login, contact and search endpoints, then a single ScannerGuard::ban($request) call at the detection point. From there the driver determines what "ban" actually costs you.
Pick the driver per environment, not per project. A very natural split: database locally and on your primary app, cache in CI and ephemeral review apps (no migrations to run, no tables to seed), http on satellites that should not own a database. Set SCANNER_GUARD_DRIVER in .env / .env.testing rather than editing the config, so the same image runs everywhere.
Treat ban() as fire-and-forget. It now returns ?ScannerGuardBan: a model for database, null for cache and http. Any call site that used the return value — to log an ID, redirect to an admin page, or assert in a test — needs a null-safe path. The idiomatic shape is:
$ban = ScannerGuard::ban($request);
// don't branch on $ban unless you're on the database driver
Central aggregation topology (http driver). Satellites forward a HMAC-signed POST; the central app opts in with SCANNER_GUARD_HTTP_SERVER_ENABLED=true, which registers POST scanner-guard/bans. On the central side you get the same artefacts as a local ban: its own salted ip_hash, daily stats, and a raw IP held only in cache — which means the central app's scanner-guard:export-denylist covers every satellite in one nginx file. That is the real payoff: one denylist to distribute, many edges reporting into it.
Signing details you'll actually implement. The signature is hash_hmac('sha256', X-Timestamp.body, secret), sent as X-Signature. So the secret is shared out-of-band between satellite and central, and the timestamp must be inside the central app's freshness window. Build a small helper or config value for the secret rather than inlining env() calls (which return null once config is cached).
Testing. Under cache the driver is fast and stateless — ideal for feature tests that assert "this request gets banned" without touching a database. Assert on the cache key/TTL rather than on a model. Keep a separate database-driver test for anything that depends on stats or the export command.
scanner-guard:aggregate-and-prune and scanner-guard:export-denylist are not scheduled by the package, and they skip with a notice unless the driver is database. If you switch a production app to cache or http, your nginx denylist quietly stops updating. Watch for the skip notice in scheduler output, and remember that aggregation/pruning only ever happens on a database node — for an http fleet that node is the central app.X-Timestamp will 409 forever — treat it as "already recorded" and move on.cache:clear, and deploy-time flushes all drop active bans. Use a dedicated store for scanner-guard if bans must survive deploys, and keep ban_duration short enough that the TTL semantics are obvious to whoever reads the config.driver and the http block, set the shared secret and central URL on satellites, share the same secret on the central app, and run config:cache fresh. If ban() returning null breaks a type hint somewhere, the failure shows up only on non-database drivers — grep for ScannerGuardBan before you flip the env var.timestamp + '.' + body, you can construct and verify forward requests yourself for custom transports (queues, log shippers) without forking the package — just keep the header names and signing string identical.How can I help you explore Laravel packages today?