diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index 3296047..fc558f0 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -66,8 +66,8 @@ The `EvaluationContext` is a mutable transport object that carries shared config | `normalize` | `Closure(string): string` | Discriminator key normalizer | | `responseHeadersEnabled` | `bool` | Whether `X-Phirewall` headers are active | | `rateLimitHeadersEnabled` | `bool` | Whether `X-RateLimit-*` headers are active | -| `owaspDiagnosticsHeaderEnabled` | `bool` | Whether `X-Phirewall-Owasp-Rule` header is active | -| `counter` | `FixedWindowCounter` | Shared counter for fail2ban, allow2ban, and track rules | +| `diagnosticsHeadersEnabled` | `bool` | Whether matcher-provided diagnostic headers are copied onto blocked responses | +| `counter` | `FixedWindowCounter` | Shared counter for Fail2Ban, Allow2Ban, and track rules | | `decisionPath` | `DecisionPath` | Updated by evaluators to record which stage decided | | `decisionRule` | `?string` | Updated by evaluators to record the matching rule name | | `pendingRateLimitHeaders` | `?array` | Rate-limit headers captured by `ThrottleEvaluator` for pass-through responses | @@ -89,11 +89,11 @@ Checks safelist rules. On the first match, dispatches `SafelistMatched`, sets th ### BlocklistEvaluator -Checks blocklist rules. On the first match, dispatches `BlocklistMatched`, sets the decision path to `Blocklisted`, and returns `FirewallResult::blocked()`. For OWASP-sourced rules, includes the `X-Phirewall-Owasp-Rule` diagnostic header when enabled. +Checks blocklist rules. On the first match, dispatches `BlocklistMatched`, sets the decision path to `Blocklisted`, and returns `FirewallResult::blocked()`. Diagnostic headers the matcher declared in its `MatchResult` metadata (`diagnostic_headers`) are copied onto the response when `enableDiagnosticsHeaders()` is active. ### Fail2BanEvaluator -For each fail2ban rule: +For each Fail2Ban rule: 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) @@ -112,7 +112,7 @@ For each throttle rule: ### Allow2BanEvaluator -Unlike other evaluators, Allow2BanEvaluator **processes all rules before returning**. For each allow2ban 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); 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) diff --git a/docs/advanced/observability.md b/docs/advanced/observability.md index 5d0ebfe..c8aadcd 100644 --- a/docs/advanced/observability.md +++ b/docs/advanced/observability.md @@ -43,6 +43,8 @@ All events are dispatched **synchronously** during request processing. Every eve All event classes live in the `Flowd\Phirewall\Events` namespace and are `readonly`. +Every matcher-carried event (`SafelistMatched`, `BlocklistMatched`, `ThrottleExceeded`, `TrackHit`, `Fail2BanMatched`, `Fail2BanBanned`, `Allow2BanBanned`) additionally exposes a `MatchResult $matchResult` property with the match (or filter/scope match) that triggered it. It is nullable only on `ThrottleExceeded`, `Fail2BanBanned` and `Allow2BanBanned`, where no matcher may have run (unscoped throttles, post-handler `recordFailure()`/`recordHit()` signals, filterless Allow2Ban rules). Listeners can read matcher metadata such as `diagnostic_headers` from it without enabling the attacker-visible response headers. + ### SafelistMatched Dispatched when a request matches a safelist rule. The request bypasses all remaining checks. diff --git a/docs/advanced/portable-config.md b/docs/advanced/portable-config.md index f4ae4ab..87c49c5 100644 --- a/docs/advanced/portable-config.md +++ b/docs/advanced/portable-config.md @@ -72,7 +72,7 @@ Everything `PortableConfig` can express today. | Factory | Matches when … | |---------|----------------| | `filterAll()` | always | -| `filterNone()` | never: a filter that never matches; use it for a rule that must not be assertable from any request property (e.g. a fail2ban driven solely by `RequestContext::recordFailure`) | +| `filterNone()` | never: a filter that never matches; use it for a rule that must not be assertable from any request property (e.g. a Fail2Ban rule driven solely by `RequestContext::recordFailure`) | | `filterPathEquals(path)` | the path equals `path` | | `filterPathPrefix(prefix)` | the path starts with `prefix` | | `filterPathRegex(pattern)` | the path matches the PCRE `pattern` (delimiters included) | @@ -88,7 +88,7 @@ Everything `PortableConfig` can express today. `filterIp`, `filterKnownScanners`, and `filterSuspiciousHeaders` compile to the dedicated matcher classes (so you get their diagnostics and CIDR handling); the remaining filters compile to a request-predicate closure. ::: warning -`filterHeaderEquals`, `filterHeaderPresent`, and `filterHeaderRegex` are rejected on `safelist()` (and on `fromArray()` deserialize): a client-controlled header value would be a forgeable bypass token (anyone presenting it skips every downstream rule). They remain valid on blocklists, throttles, fail2ban, allow2ban, and track rules. +`filterHeaderEquals`, `filterHeaderPresent`, and `filterHeaderRegex` are rejected on `safelist()` (and on `fromArray()` deserialize): a client-controlled header value would be a forgeable bypass token (anyone presenting it skips every downstream rule). They remain valid on blocklists, throttles, Fail2Ban, Allow2Ban, and track rules. ::: ### Key extractors @@ -132,7 +132,7 @@ Pattern backends carry a list of entries; each entry has a `PatternKind`: |---------|------------------------------| | `enableRateLimitHeaders()` | emit `X-RateLimit-*` headers | | `enableResponseHeaders()` | emit `X-Phirewall-*` headers | -| `enableOwaspDiagnosticsHeader()` | emit the OWASP diagnostics header | +| `enableDiagnosticsHeaders()` | copy matcher-provided diagnostic headers onto blocked responses (`enableOwaspDiagnosticsHeader()` is a deprecated alias) | | `setFailOpen(bool)` | fail-open (default) vs fail-closed on backend errors | | `setKeyPrefix(prefix)` | cache-key prefix | diff --git a/docs/advanced/track-notifications.md b/docs/advanced/track-notifications.md index c91d9f3..fde002a 100644 --- a/docs/advanced/track-notifications.md +++ b/docs/advanced/track-notifications.md @@ -187,7 +187,7 @@ Phirewall dispatches events for every significant decision. You can listen for a | `PerformanceMeasured` | Every firewall decision (for metrics) | `decisionPath`, `durationMicros`, `ruleName` | | `FirewallError` | An exception occurs in fail-open mode | `exception`, `serverRequest` | -All event classes live in the `Flowd\Phirewall\Events` namespace and are `readonly`. +All event classes live in the `Flowd\Phirewall\Events` namespace and are `readonly`. The matcher-carried events also expose a `MatchResult $matchResult` with the match that triggered them; it is nullable only where no matcher may have run (see [Observability](/advanced/observability#event-summary)). ## Notification Examples diff --git a/docs/faq.md b/docs/faq.md index 38a06e8..c28bf79 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -185,7 +185,7 @@ $context?->recordFailure('login-failures'); The second argument to `recordFailure()` is optional; when omitted, the firewall extracts the discriminator key from the rule's own `keyExtractor`. The matching Fail2Ban rule should use `filter: fn($request): bool => false` so it only counts failures signaled programmatically. -For allow2ban rules, use `$context->recordHit('rule-name')`, same shape, routed through the allow2ban evaluator instead. +For Allow2Ban rules, use `$context->recordHit('rule-name')`, same shape, routed through the Allow2Ban evaluator instead. ## Rate Limiting @@ -322,13 +322,13 @@ $crs->enable(942100); // Re-enable it later ### How do I debug which OWASP rule is blocking a request? -Enable the diagnostics header: +Enable diagnostic headers: ```php -$config->enableOwaspDiagnosticsHeader(); +$config->enableDiagnosticsHeaders(); ``` -This adds an `X-Phirewall-Owasp-Rule` header to blocked responses containing the matched rule ID. +The CRS matcher attaches an `X-Phirewall-Owasp-Rule` header with the matched rule ID to blocked responses. This works wherever the matcher decides the block: as a blocklist rule and as a Fail2Ban filter. Any matcher can ship such headers via the `diagnostic_headers` metadata key on its `MatchResult`. ::: warning Only enable this in development or staging. In production, it reveals information about your security rules to potential attackers. @@ -372,7 +372,7 @@ Common causes: Debug steps: 1. Enable `$config->enableResponseHeaders()` and check the `X-Phirewall` and `X-Phirewall-Matched` response headers to identify the blocking rule 2. Temporarily disable suspect rules and re-enable them one by one -3. If OWASP rules are involved, enable `$config->enableOwaspDiagnosticsHeader()` to see which rule ID matched +3. If OWASP rules are involved, enable `$config->enableDiagnosticsHeaders()` to see which rule ID matched ### Rate limits are not working in PHP-FPM @@ -386,7 +386,7 @@ Enable `$config->enableResponseHeaders()` and check the response headers on bloc |--------|-------| | `X-Phirewall` | Block type: `blocklist`, `throttle`, `fail2ban`, or `allow2ban` | | `X-Phirewall-Matched` | Name of the rule that triggered the block | -| `X-Phirewall-Owasp-Rule` | OWASP rule ID (only if diagnostics are enabled) | +| `X-Phirewall-Owasp-Rule` | OWASP rule ID (only if `enableDiagnosticsHeaders()` is active) | ::: info These headers are disabled by default. Call `$config->enableResponseHeaders()` to enable them for debugging. diff --git a/docs/features/owasp-crs.md b/docs/features/owasp-crs.md index cdb1af5..2a79711 100644 --- a/docs/features/owasp-crs.md +++ b/docs/features/owasp-crs.md @@ -42,9 +42,9 @@ $config = new Config(new InMemoryCache()); $config = $config->with(Presets::blocklist(ParanoiaLevel::Level1)); ``` -Want to also ban repeat offenders? Use the fail2ban preset instead. A CRS +Want to also ban repeat offenders? Use the Fail2Ban preset instead. A CRS match is malicious by definition, so from 0.8 both presets block every match -with `403`; the difference is that the fail2ban preset additionally **bans** +with `403`; the difference is that the Fail2Ban preset additionally **bans** the key after the threshold. A banned attacker is then blocked by a cheap ban lookup (the CRS engine no longer runs for them), and the ban is observable via `Fail2BanBanned` and mirrorable to your web server: @@ -282,11 +282,11 @@ $rule = $rules->getRule(942100); ## OWASP Diagnostics Header -Enable the diagnostics header to see which OWASP rule matched: +Enable response and diagnostic headers to see which OWASP rule matched: ```php $config->enableResponseHeaders(); -$config->enableOwaspDiagnosticsHeader(); +$config->enableDiagnosticsHeaders(); ``` When an OWASP rule blocks a request, the response includes: @@ -298,7 +298,7 @@ X-Phirewall-Owasp-Rule: 942100 ``` ::: info -`X-Phirewall` and `X-Phirewall-Matched` require `enableResponseHeaders()`. The `X-Phirewall-Owasp-Rule` header is controlled independently by `enableOwaspDiagnosticsHeader()`. +`X-Phirewall` and `X-Phirewall-Matched` require `enableResponseHeaders()`. The `X-Phirewall-Owasp-Rule` header is controlled independently by `enableDiagnosticsHeaders()` (`enableOwaspDiagnosticsHeader()` is a deprecated alias): the CRS matcher declares it via the generic `diagnostic_headers` metadata key on its `MatchResult`, so it also appears when the matcher is used as a Fail2Ban filter. ::: ::: info