Endpoint Abuse Detection
The siem.abuse middleware watches response patterns per IP inside a sliding window and raises SIEM events when thresholds are crossed. It answers questions your access log can't answer in real time: is this IP brute-forcing? scanning? hammering rate limits?
Usage#
Apply broadly — it observes responses, so it belongs on everything you want visibility into:
// bootstrap/app.php (Laravel 11+)
$middleware->web(append: [\Inqaba\Gatekeeper\Http\Middleware\DetectEndpointAbuse::class]);
$middleware->api(append: [\Inqaba\Gatekeeper\Http\Middleware\DetectEndpointAbuse::class]);
// or per route group
Route::middleware('siem.abuse')->group(...);
It runs after the response is produced, adding no meaningful latency, and never changes the response itself.
The four detections#
| Detection | Watches | Default threshold | Emits |
|---|---|---|---|
| Brute force | 401 / 403 / 419 / 423 responses | 10 / minute | brute_force_detected (critical) |
| Scanning / forced browsing | 404 responses | 25 / minute | scanning_detected (high) |
| Rate-limit hammering | 429 responses | 5 / minute | rate_limit_abuse (medium) |
| Volumetric abuse | all requests | disabled (0) |
endpoint_abuse (high) |
Notes on what these catch in practice:
- Brute force counts every auth-shaped rejection, including CSRF 419s — so even bots that die at Laravel's CSRF layer surface here once they're persistent.
- Scanning is the wordlist crawler walking
/admin,/backup,/old, ... Each 404 is innocent; twenty-five in a minute from one IP is not. - Volumetric is off by default because a sensible threshold is very app-specific; set
SIEM_ABUSE_REQUEST_THRESHOLDto taste.
Noise control#
Each detection fires once per IP per window — a scanner sending 500 requests produces one scanning_detected event, not 500. The triggering event carries the count:
{
"event": { "action": "scanning_detected", "severity": 7 },
"source": { "ip": "203.0.113.9" },
"laravel": {
"context": { "count_in_window": 25, "window_minutes": 1, "auto_blocked": false }
}
}
Auto-blocking#
SIEM_ABUSE_AUTOBLOCK=true
When a detection fires, the source IP is added to the blocklist for abuse.block_minutes (default 60). IPs on the never_block list are exempt. Combine with the siem.block middleware to actually enforce it.
Configuration#
| Key | Env | Default |
|---|---|---|
abuse.window_minutes |
— | 1 |
abuse.auth_failures_threshold |
SIEM_ABUSE_AUTH_THRESHOLD |
10 |
abuse.not_found_threshold |
SIEM_ABUSE_404_THRESHOLD |
25 |
abuse.throttled_threshold |
SIEM_ABUSE_429_THRESHOLD |
5 |
abuse.request_threshold |
SIEM_ABUSE_REQUEST_THRESHOLD |
0 (disabled) |
abuse.auto_block |
SIEM_ABUSE_AUTOBLOCK |
false |
abuse.block_minutes |
— | 60 |
Relationship to Laravel's rate limiter#
They're complementary, not competing. Laravel's throttle middleware prevents excess requests; siem.abuse reports the pattern to your SOC. In fact they chain naturally: throttle produces 429s, and repeated 429s cross the throttled_threshold — so an attacker grinding against your rate limits becomes a classified SIEM event instead of an invisible nuisance.
Behind proxies#
All per-IP detection keys off $request->ip(). If you're behind a load balancer or CDN, configure Laravel's trusted proxies — otherwise every visitor appears as the proxy's IP and thresholds will fire on aggregate traffic.