Gatekeeper inqaba-security/gatekeeper
v1.x

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_THRESHOLD to 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.