diff --git a/content/blog/2026-04-20-migrating-from-crs-3-to-crs-4-part-4-scoring.md b/content/blog/2026-04-20-migrating-from-crs-3-to-crs-4-part-4-scoring.md new file mode 100644 index 00000000..fbc9f9cf --- /dev/null +++ b/content/blog/2026-04-20-migrating-from-crs-3-to-crs-4-part-4-scoring.md @@ -0,0 +1,138 @@ +--- +author: fzipi +categories: + - Blog +date: '2026-04-20T09:00:00-03:00' +tags: + - CRS-News + - Migration + - CRS-v4 +images: + - /images/2026/04/pexels-thisisengineering-3861957.jpg +title: 'Migrating from CRS 3.3 to CRS 4.25 LTS — Part 4: Anomaly Scoring and Reporting' +slug: 'migrating-crs-3-to-4-part-4-scoring' +--- + +This is Part 4 of the [CRS 3.3 → 4.25 LTS migration series]({{< ref "blog/2026-03-30-migrating-from-crs-3-to-crs-4-part-1-overview.md" >}}). Part 3 covered the plugin architecture. This post covers anomaly scoring, the reporting model, and paranoia level changes — the areas most likely to affect your baseline after a migration. + +{{< figure src="/images/2026/04/pexels-thisisengineering-3861957.jpg" caption="Measuring and scoring every request" attr="ThisIsEngineering on Pexels" attrlink="https://www.pexels.com" >}} + +## How Anomaly Scoring Changed + +### The CRS 3 Model + +In CRS 3, every rule that fires adds to a single transaction variable `tx.anomaly_score`. At the end of phase 2 (for inbound) and phase 4 (for outbound), the total accumulated score is compared against `tx.inbound_anomaly_score_threshold` and `tx.outbound_anomaly_score_threshold`. If the score exceeds the threshold, the request is blocked. + +This model is simple but has one significant weakness: you cannot tell from the final score alone which paranoia levels contributed to it. A score of `15` at PL2 might come from three PL1 rules or one PL2 rule, and the log entry for the blocking action does not distinguish between them. + +### The CRS 4 Model + +CRS 4 refactored how anomaly scores are accumulated and reported. The variables you configure in `crs-setup.conf` are unchanged — you still set `tx.inbound_anomaly_score_threshold` and `tx.outbound_anomaly_score_threshold` exactly as in CRS 3. + +The per-severity scoring variables are unchanged from CRS 3: + +```apache +# These existed in CRS 3 and carry over to CRS 4 unchanged: +tx.critical_anomaly_score = 5 +tx.error_anomaly_score = 4 +tx.warning_anomaly_score = 3 +tx.notice_anomaly_score = 2 +``` + +What changed is the internal score accumulation. In CRS 3, the running total lived in `tx.anomaly_score`. In CRS 4 the score is tracked per paranoia level, and a set of new aggregate variables is computed at evaluation time: + +```apache +# New per-PL accumulators (inbound; outbound has the same shape): +tx.inbound_anomaly_score_pl1 +tx.inbound_anomaly_score_pl2 +tx.inbound_anomaly_score_pl3 +tx.inbound_anomaly_score_pl4 + +# New per-direction aggregates used by the blocking and reporting logic: +tx.blocking_inbound_anomaly_score # sum of per-PL scores up to tx.blocking_paranoia_level +tx.detection_inbound_anomaly_score # sum of per-PL scores up to tx.detection_paranoia_level + +# New combined inbound+outbound aggregates, set in phase 5: +tx.blocking_anomaly_score # tx.blocking_inbound_anomaly_score + tx.blocking_outbound_anomaly_score +tx.detection_anomaly_score # tx.detection_inbound_anomaly_score + tx.detection_outbound_anomaly_score +``` + +The equivalent `tx.outbound_anomaly_score_pl1..pl4`, `tx.blocking_outbound_anomaly_score`, and `tx.detection_outbound_anomaly_score` variables exist for the response side. `tx.anomaly_score` still exists but is now a derived combined value set by the correlation rule — it is no longer the accumulator. + +The visible change is in what gets reported. CRS 4 reporting rules (see [The Reporting Model](#the-reporting-model) below) include more structured context about which paranoia level and rule category contributed to the score, making it significantly easier to understand what drove a blocking action. + +### Impact on Custom Rules + +If you have custom rules or Lua scripts that read `tx.anomaly_score` directly — for example, to make a routing decision mid-request — those rules need to be verified against CRS 4. Check your WAF configuration for any `@eq`/`@gt` checks against `tx.anomaly_score` and test that they behave as expected after upgrading. + +## The Reporting Model + +### CRS 3 Reporting: 980xxx Rules + +CRS 3 had a set of `980xxx` reporting rules that fired when a request exceeded the anomaly threshold. These rules were redundant — one for each combination of inbound/outbound and paranoia level — and produced noisy, repetitive log entries. The reporting model was widely criticised as difficult to parse and easy to misconfigure. + +### CRS 4 Reporting: Granular Control + +CRS 4 restructures the `980xxx` reporting rules into a consolidated reporting system controlled by `tx.reporting_level`. A single reporting action (`980170`, phase 5) emits one combined message covering both inbound and outbound scores, gated by rules that decide *whether* it fires based on the level you configure. The result is cleaner logs and operator control over verbosity. + +The six reporting levels (configured via rule `900115`) are: + +| Level | Behaviour | +|---|---| +| `0` | Reporting disabled | +| `1` | Report when blocking anomaly score ≥ threshold | +| `2` | Report when detection anomaly score ≥ threshold | +| `3` | Report when blocking anomaly score > 0 | +| `4` | Report when detection anomaly score > 0 (default) | +| `5` | Report all requests | + +The default is `4`, which is more verbose than CRS 3. This is intentional — the extra log output at level 4 is the mechanism that shows you near-miss requests (requests that scored above zero but did not hit the blocking threshold), which is essential for tuning. + +The practical migration impact: if you have SIEM rules, alerting logic, or log parsers that match on `980xxx` rule IDs, update them to the new CRS 4 reporting rule IDs. Also, the log message format changed — run your log parser against a sample of CRS 4 output before cutting over. + +## Early Blocking + +The `tx.early_blocking` option (covered in detail in Part 2) changes the phase at which anomaly score evaluation can occur: + +| Mode | Inbound evaluation | Outbound evaluation | +|---|---|---| +| `tx.early_blocking` unset (default) | End of phase 2 | End of phase 4 | +| `tx.early_blocking=1` | End of phase 1 *and* phase 2 | End of phase 3 *and* phase 4 | + +With early blocking enabled, a request that trips a phase-1 rule (primarily header-based rules) can be blocked before the WAF processes the request body. This reduces latency for clearly malicious requests and reduces WAF load for attack traffic that signals itself early in the connection. + +The trade-off: if a request's score does not exceed the threshold based on headers alone but would have exceeded it after body inspection, early blocking will not block it in phase 1 — it will still be blocked in phase 2 as usual. Early blocking is additive, not a replacement. + +For migration, leave `tx.early_blocking` commented out (disabled). This matches CRS 3 behaviour exactly. After your initial migration is stable and your false positive rate is under control, consider enabling it. + +## Paranoia Level Redistribution + +CRS 4 made a broad effort to better distribute rules across paranoia levels. In CRS 3, PL1 carried a disproportionately large fraction of the total rule count. Many rules that were quite specialised or had higher false positive rates were at PL1 simply because PL2–PL4 were underused. + +In CRS 4, a significant number of rules were moved from lower to higher paranoia levels. The direction was almost always toward higher PLs — rules moved up, not down. + +### What This Means for You + +**If you run at PL1:** Your anomaly score baseline will likely *decrease* after migration. Rules that previously fired at PL1 in CRS 3 may now only fire at PL2 or higher. This is generally good — fewer false positives at PL1 — but it also means some attacks you were detecting at PL1 in CRS 3 may now only be detected at PL2 in CRS 4. Review your threat model. + +**If you run at PL2 or higher:** Your baseline should remain stable or decrease. A rule that moved from PL1 in CRS 3 to PL2 in CRS 4 still fires for you at PL2 — it was already part of your coverage. Shifting a rule to a higher PL does not add detection at levels that already included it. Any baseline changes you observe at PL2+ come from genuinely new rules, removed rules, or revised detection logic, not from the PL redistribution itself. + +**If you have PL-specific exclusions:** Some of your exclusions may no longer be necessary if the rules they targeted moved to a higher PL than the one you run at. Conversely, new rules may fire at your PL that were not present in CRS 3. After the migration, run in detection mode for at least a week before enabling blocking to establish a new baseline. + +## Verifying Your Scoring Setup + +After installing CRS 4 with your migrated configuration, verify that scoring is working as expected by sending a test request that should trigger detection. The CRS documentation and the [go-ftw](https://github.com/coreruleset/go-ftw) testing framework provide test cases for this purpose. + +A simple check: send a request containing a known attack pattern at your configured paranoia level and confirm that: + +1. The rule fires (visible in access or audit logs) +2. The anomaly score variables are populated correctly +3. The reporting rule fires and logs the expected block action + +If you have an anomaly score below the threshold but a rule fired, the per-PL breakdown in the CRS 4 logs will show you exactly which paranoia level bucket the score landed in. + +## What's Next + +[Part 5]({{< ref "blog/2026-04-27-migrating-from-crs-3-to-crs-4-part-5-rule-changes.md" >}}) covers the rule-level changes — new detection categories, removed and reorganized rules, RE2/Hyperscan compatibility, and how to audit your existing `SecRuleRemoveById` exclusions against the CRS 4 rule set. + +{{< related-pages "Migration" "CRS-v4" >}} diff --git a/static/images/2026/04/pexels-thisisengineering-3861957.jpg b/static/images/2026/04/pexels-thisisengineering-3861957.jpg new file mode 100644 index 00000000..e64682dd Binary files /dev/null and b/static/images/2026/04/pexels-thisisengineering-3861957.jpg differ