Migrating from CRS 3.3 to CRS 4.25 LTS — Part 4: Anomaly Scoring and Reporting

This is Part 4 of the CRS 3.3 → 4.25 LTS migration series. 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.

Measuring and scoring every request

Measuring and scoring every request ThisIsEngineering on Pexels

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:

# 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:

# 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 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:

LevelBehaviour
0Reporting disabled
1Report when blocking anomaly score ≥ threshold
2Report when detection anomaly score ≥ threshold
3Report when blocking anomaly score > 0
4Report when detection anomaly score > 0 (default)
5Report 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:

ModeInbound evaluationOutbound evaluation
tx.early_blocking unset (default)End of phase 2End of phase 4
tx.early_blocking=1End of phase 1 and phase 2End 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 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 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:

Felipe Zipitria