diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index 1d10342..3296047 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -95,7 +95,7 @@ Checks blocklist rules. On the first match, dispatches `BlocklistMatched`, sets For each fail2ban rule: -1. Checks if the key is already banned - if so, returns a blocked result immediately +1. Checks if the key is already banned - if so, dispatches `Fail2BanBlocked` and returns a blocked result immediately 2. If the filter matches, increments the failure counter and blocks the request (`403`). A match below the threshold sets `DecisionPath::Fail2BanMatched` and dispatches `Fail2BanMatched`; the Nth match additionally bans the key, sets `DecisionPath::Fail2BanBanned`, and dispatches `Fail2BanBanned` (never both events) The pre-handler path (during `decide()`) blocks on every match and bans at the threshold. The post-handler path (via `processRecordedSignal()`) shares the same `count >= threshold` ban comparison but never blocks the current request and never dispatches `Fail2BanMatched`. The ban fires on the Nth match, consistent with Allow2Ban. @@ -114,7 +114,7 @@ For each throttle rule: Unlike other evaluators, Allow2BanEvaluator **processes all rules before returning**. For each allow2ban rule: -1. If the key is already banned, records the block (regardless of the filter) +1. If the key is already banned, records the block (regardless of the filter); the rule that captures the block dispatches `Allow2BanBlocked` 2. Otherwise, if the rule has a filter that does not match, skips the rule (not counted) 3. Otherwise, increments the counter and bans if the threshold is reached (`count >= threshold`); the matching request itself passes until it is the one that reaches the threshold diff --git a/docs/advanced/observability.md b/docs/advanced/observability.md index ceaea7f..5d0ebfe 100644 --- a/docs/advanced/observability.md +++ b/docs/advanced/observability.md @@ -34,7 +34,9 @@ All events are dispatched **synchronously** during request processing. Every eve | `ThrottleExceeded` | Request exceeds a throttle limit | `rule`, `key`, `limit`, `period`, `count`, `retryAfter`, `serverRequest` | | `Fail2BanMatched` | Fail2Ban filter matches and blocks a request below the ban threshold | `rule`, `key`, `threshold`, `period`, `count`, `serverRequest` | | `Fail2BanBanned` | Key banned after reaching the failure threshold | `rule`, `key`, `threshold`, `period`, `banSeconds`, `count`, `serverRequest` | +| `Fail2BanBlocked` | Request blocked because its key is already banned by Fail2Ban | `rule`, `key`, `serverRequest` | | `Allow2BanBanned` | Key banned after exceeding request threshold | `rule`, `key`, `threshold`, `period`, `banSeconds`, `count`, `serverRequest` | +| `Allow2BanBlocked` | Request blocked because its key is already banned by Allow2Ban | `rule`, `key`, `serverRequest` | | `TrackHit` | Tracking rule filter matches | `rule`, `key`, `period`, `count`, `limit`, `thresholdReached`, `serverRequest` | | `FirewallError` | Error in fail-open mode (cache failure, etc.) | `exception`, `serverRequest` | | `PerformanceMeasured` | After every firewall decision | `decisionPath`, `durationMicros`, `ruleName` | @@ -110,6 +112,18 @@ $event->count; // int - Failure count that triggered the ban $event->serverRequest; // ServerRequestInterface ``` +### Fail2BanBlocked + +Dispatched when a request is blocked because its key is already banned by a Fail2Ban rule. The filter is not evaluated for banned keys, and the event fires on **every** blocked request, so a hammering client produces one event per request; aggregate in high-volume listeners. + +```php +use Flowd\Phirewall\Events\Fail2BanBlocked; + +$event->rule; // string - Rule name +$event->key; // string - Banned key (e.g., IP address) +$event->serverRequest; // ServerRequestInterface +``` + ### Allow2BanBanned Dispatched when an Allow2Ban rule bans a key after the counted-request threshold is reached. Allow2Ban counts every request for a key, or only the requests an optional filter matches, letting matching requests pass until the threshold. @@ -126,6 +140,18 @@ $event->count; // int - Request count that triggered the ban $event->serverRequest; // ServerRequestInterface ``` +### Allow2BanBlocked + +Dispatched when a request is blocked because its key is already banned by an Allow2Ban rule. Banned keys block every request regardless of the rule's filter, and the event fires on **every** blocked request; aggregate in high-volume listeners. + +```php +use Flowd\Phirewall\Events\Allow2BanBlocked; + +$event->rule; // string - Rule name +$event->key; // string - Banned key (e.g., IP address) +$event->serverRequest; // ServerRequestInterface +``` + ### TrackHit Track events fire on **every** matching request; they never block. When a `limit` is configured on the track rule, the `thresholdReached` flag becomes `true` once the counter reaches the threshold. This makes track rules ideal for alerting without blocking. @@ -243,6 +269,7 @@ Returns an array organized by category, each with a total and a per-rule breakdo 'fail2ban_matched' => ['total' => 4, 'by_rule' => ['scanner-probe' => 4]], 'fail2ban_banned' => ['total' => 1, 'by_rule' => ['scanner-probe' => 1]], 'allow2ban_banned' => ['total' => 2, 'by_rule' => ['high-volume' => 2]], + 'allow2ban_blocked' => ['total' => 1, 'by_rule' => ['high-volume' => 1]], 'track_hit' => ['total' => 50, 'by_rule' => ['api-calls' => 50]], 'passed' => ['total' => 1000, 'by_rule' => []], ] @@ -259,11 +286,12 @@ Returns an array organized by category, each with a total and a per-rule breakdo | `fail2ban_banned` | `Fail2BanBanned` | New Fail2Ban bans issued | | `fail2ban_blocked` | `PerformanceMeasured` | Requests blocked by existing Fail2Ban bans | | `allow2ban_banned` | `Allow2BanBanned` | New Allow2Ban bans issued | +| `allow2ban_blocked` | `PerformanceMeasured` | Requests blocked by existing Allow2Ban bans | | `track_hit` | `TrackHit` | Tracking rule matches | | `passed` | `PerformanceMeasured` | Requests that passed all checks | ::: tip -The `passed` and `fail2ban_blocked` categories are derived from the `PerformanceMeasured` event, which fires on every request. All other categories come from their dedicated events. +The `passed`, `fail2ban_blocked` and `allow2ban_blocked` categories are derived from the `PerformanceMeasured` event, which fires on every request. The dedicated `Fail2BanBlocked` and `Allow2BanBlocked` events are intentionally not counted so blocked requests are not double-counted. All other categories come from their dedicated events. ::: ### Resetting Counters diff --git a/docs/advanced/track-notifications.md b/docs/advanced/track-notifications.md index 6f5b23e..c91d9f3 100644 --- a/docs/advanced/track-notifications.md +++ b/docs/advanced/track-notifications.md @@ -181,7 +181,9 @@ Phirewall dispatches events for every significant decision. You can listen for a | `ThrottleExceeded` | A rate limit is exceeded | `rule`, `key`, `limit`, `period`, `count`, `retryAfter`, `serverRequest` | | `Fail2BanMatched` | A Fail2Ban filter match is blocked below the threshold | `rule`, `key`, `threshold`, `period`, `count`, `serverRequest` | | `Fail2BanBanned` | A client is banned by Fail2Ban | `rule`, `key`, `threshold`, `period`, `banSeconds`, `count`, `serverRequest` | +| `Fail2BanBlocked` | A request from a key already banned by Fail2Ban is blocked | `rule`, `key`, `serverRequest` | | `Allow2BanBanned` | A client is banned by Allow2Ban | `rule`, `key`, `threshold`, `period`, `banSeconds`, `count`, `serverRequest` | +| `Allow2BanBlocked` | A request from a key already banned by Allow2Ban is blocked | `rule`, `key`, `serverRequest` | | `PerformanceMeasured` | Every firewall decision (for metrics) | `decisionPath`, `durationMicros`, `ruleName` | | `FirewallError` | An exception occurs in fail-open mode | `exception`, `serverRequest` | diff --git a/docs/features/fail2ban.md b/docs/features/fail2ban.md index d4d2bc8..62f8c4a 100644 --- a/docs/features/fail2ban.md +++ b/docs/features/fail2ban.md @@ -17,7 +17,7 @@ Before 0.8 a filter match below the threshold passed through, so a Fail2Ban filt ### How It Works ```text -Request --> Is key already banned? --> Yes --> 403 Forbidden +Request --> Is key already banned? --> Yes --> 403 Forbidden (Fail2BanBlocked event) | No | @@ -42,7 +42,7 @@ Request --> Is key already banned? --> Yes --> 403 Forbidden 2. Every match is **blocked with `403`** and counted per **key** (e.g., IP address) within a time **period** 3. A match **below** the threshold blocks via `DecisionPath::Fail2BanMatched` and dispatches the [`Fail2BanMatched`](#fail2banmatched) event 4. When the count **reaches** the **threshold**, the key is additionally **banned** for the configured duration; that match blocks via `DecisionPath::Fail2BanBanned` and dispatches [`Fail2BanBanned`](#fail2banbanned) (never both events) -5. Banned keys then receive `403 Forbidden` immediately, without further rule evaluation +5. Banned keys then receive `403 Forbidden` immediately, without further rule evaluation; each of these blocks dispatches [`Fail2BanBlocked`](#fail2banblocked) ### Configuration @@ -309,7 +309,7 @@ Either way, an already-banned key is blocked on **every** request regardless of ### How It Works ```text -Request --> Is key already banned? --> Yes --> 403 Forbidden +Request --> Is key already banned? --> Yes --> 403 Forbidden (Allow2BanBlocked event) | No | @@ -442,7 +442,7 @@ $config->allow2ban->add( | **On a match** | **Blocks immediately** (`403`) and counts | **Lets the request pass** and counts, until the threshold | | **Trigger** | Any match (block); Nth match (ban) | Nth counted request (ban) | | **Use case** | Unambiguously malicious matches (scanner paths, invalid signatures), signal-only rules | Login brute-force counting, volume abuse, "count these, ban after N" | -| **Events** | `Fail2BanMatched` (sub-threshold block), `Fail2BanBanned` (ban) | `Allow2BanBanned` (ban) | +| **Events** | `Fail2BanMatched` (sub-threshold block), `Fail2BanBanned` (ban), `Fail2BanBlocked` (banned-key block) | `Allow2BanBanned` (ban), `Allow2BanBlocked` (banned-key block) | | **Ban parameter** | `$ban` | `$banSeconds` | ## Managing Bans @@ -487,7 +487,7 @@ Notes: ## Events -Fail2Ban and Allow2Ban dispatch events through your PSR-14 event dispatcher. Fail2Ban dispatches `Fail2BanMatched` for every match blocked below the threshold and `Fail2BanBanned` for the match that bans (never both for the same request); Allow2Ban dispatches `Allow2BanBanned` when a key is banned. +Fail2Ban and Allow2Ban dispatch events through your PSR-14 event dispatcher. Fail2Ban dispatches `Fail2BanMatched` for every match blocked below the threshold and `Fail2BanBanned` for the match that bans (never both for the same request); Allow2Ban dispatches `Allow2BanBanned` when a key is banned. A request blocked because its key is **already banned** dispatches `Fail2BanBlocked` or `Allow2BanBlocked`. ### Fail2BanMatched @@ -520,6 +520,19 @@ $event->count; // int - Failure count that triggered the ban $event->serverRequest; // ServerRequestInterface ``` +### Fail2BanBlocked + +Dispatched when a request is blocked because its key is already banned. The filter is not evaluated for banned keys, and the event fires on **every** blocked request, so a hammering client produces one event per request. + +```php +use Flowd\Phirewall\Events\Fail2BanBlocked; + +// Event properties +$event->rule; // string - Rule name +$event->key; // string - Banned key (e.g., IP address) +$event->serverRequest; // ServerRequestInterface +``` + ### Allow2BanBanned ```php @@ -535,6 +548,19 @@ $event->count; // int - Request count that triggered the ban $event->serverRequest; // ServerRequestInterface ``` +### Allow2BanBlocked + +Dispatched when a request is blocked because its key is already banned. Banned keys block every request regardless of the rule's filter, and the event fires on **every** blocked request. + +```php +use Flowd\Phirewall\Events\Allow2BanBlocked; + +// Event properties +$event->rule; // string - Rule name +$event->key; // string - Banned key +$event->serverRequest; // ServerRequestInterface +``` + ### Alerting on Bans ```php