@@ -280,14 +280,14 @@ rule 901320 (phase: request) {
280280
281281Use a ** layered approach** :
282282
283- 1 . ** Phase 3a ** — implement Option A (side-effects in ` then ` only). This handles the
283+ 1 . ** Phase 2a ** — implement Option A (side-effects in ` then ` only). This handles the
284284 vast majority of CRS rules. Chain links without intermediate side-effects are already
285285 flattened by the existing normalizer, so this is the natural starting point.
286286
287- 2 . ** Phase 3b ** — add ` let ` bindings (Option C) for data-flow dependencies. This
287+ 2 . ** Phase 2b ** — add ` let ` bindings (Option C) for data-flow dependencies. This
288288 cleanly handles Category 3 without complicating the boolean expression model.
289289
290- 3 . ** Phase 3c ** — if Category 2 (` ctl ` on intermediate links) proves common enough to
290+ 3 . ** Phase 2c ** — if Category 2 (` ctl ` on intermediate links) proves common enough to
291291 warrant language support, add conditional side-effects (Option B) as an extension.
292292 Before doing so, audit whether these ` ctl ` patterns can be restructured as separate
293293 rules instead.
@@ -359,6 +359,56 @@ when count(tx.enable) |> eq(1) ...
359359then pass { init_collection(ip: client.ip + "_" + ua_hash) }
360360```
361361
362+ ### Collection Quantifier: ` each() `
363+
364+ SecLang's ` multiMatch ` action changes how a collection-targeting condition evaluates —
365+ instead of stopping at the first match, it iterates all values and fires side-effects
366+ per match. This is currently modeled as a non-disruptive action, but it is semantically
367+ a condition quantifier.
368+
369+ ** Recommendation: ` each() ` as a condition-level quantifier (Option A).**
370+
371+ ```
372+ # Without each(): first match wins, effects fire once
373+ when request.args |> detect_sqli()
374+
375+ # With each(): all values tested, effects fire per match
376+ when each(request.args) |> detect_sqli()
377+ then block {
378+ tx.sqli_score += 5 # incremented per matching argument
379+ log(data: matched.var) # logged per matching argument
380+ }
381+ ```
382+
383+ ` each() ` wraps a map-typed field and signals "iterate all values." Without it, the
384+ default is first-match semantics.
385+
386+ ** Alternatives documented:**
387+
388+ - ** Option B: Effect-level modifier** — ` then block (per_match: true) { ... } ` . Simpler
389+ to parse but misleading: the reader assumes first-match from the condition until
390+ they notice the modifier.
391+ - ** Option C: Separate iteration block** — `for each match { per-match effects } then
392+ block { once-only effects }`. Most expressive (supports both per-match and once-only
393+ effects) but adds a new block type.
394+ - ** Option D: Drop it** — if scoring becomes first-class (ADR-0011), per-match scoring
395+ may be handled at the scoring level rather than as a language construct.
396+
397+ ` multiMatch ` is rarely used in CRS, so Option A is sufficient for the foreseeable
398+ future. Options B/C can be revisited if use cases emerge.
399+
400+ ### String Interpolation
401+
402+ SecLang uses ` %{TX:score} ` and ` %{MATCHED_VAR} ` for string interpolation in actions
403+ (` logdata ` , ` msg ` , ` setvar ` ). Most of these cases become direct field references or
404+ expressions in CRSLang (e.g., ` log(data: matched.var) ` ).
405+
406+ For cases that require composed strings (log messages, dynamic values), CRSLang needs
407+ a string construction mechanism. The exact form — string interpolation
408+ (` "Score: ${tx.anomaly_score}" ` ), concatenation (` "Score: " + string(tx.score) ` ), or
409+ a format function (` format("Score: %d", tx.score) ` ) — is deferred to the effects model
410+ design in Phase 3. The IR must support composed string values in effect arguments.
411+
362412### New Capabilities
363413
364414Boolean algebra enables patterns that are impossible or awkward in SecLang:
@@ -464,10 +514,10 @@ match request {
464514
465515### Negative
466516
467- - Three categories of intermediate side-effects require a layered migration (Phase 3a /b/c)
517+ - Three categories of intermediate side-effects require a layered migration (Phase 2a /b/c)
468518 rather than a single clean cutover
469- - `let` bindings (Phase 3b ) add a new language concept not present in SecLang
470- - Conditional side-effects (Phase 3c , if adopted) complicate the expression model and
519+ - `let` bindings (Phase 2b ) add a new language concept not present in SecLang
520+ - Conditional side-effects (Phase 2c , if adopted) complicate the expression model and
471521 require precise execution semantics
472522- Some deeply chained rules may become long single expressions (mitigated by
473523 line breaks and formatting conventions)
@@ -485,7 +535,7 @@ match request {
485535 (found primarily in initialization and internal-traffic rules like 905111). If they
486536 prove confined to a small set of rules, they can be handled by restructuring those
487537 rules rather than adding Option B to the language. A full CRS audit should quantify
488- this before committing to Phase 3c .
538+ this before committing to Phase 2c .
489539- **Category 3 data flow** — `let` bindings change CRSLang from a purely declarative
490540 rule language to one with local variable scoping. This is a significant conceptual
491541 shift. The alternative is to require these patterns to be split into multiple rules
0 commit comments