ADR-0030: A negated operator applies per value
- Status: proposed
- Date: 2026-10-09
- Deciders: @fzipi
- Category: Clarification
Context
spec/02-grammar.md#operator said that a rule with a negated operator (!@streq abc,
!abc) matches when the operator is false for every selected value. The three engines
apply the negation to each value and let the rule match as soon as one value matches:
- ModSecurity v2 (
apache2/re.c,execute_operator): per target value,rc == 1withop_negatedis “no match” andrc == 0withop_negatedis a match; the rule loop stops at the first matching value unlessmultiMatch. - libmodsecurity v3.0.16 (
src/operators/operator.cc,Operator::evaluateInternalreturns!resunderm_negation;src/rule_with_operator.ccmatches on the first value whoseexecuteOperatorAtis true). - Coraza v3.8.1 (
internal/corazawaf/rule.go,if r.operator.Negation { match = !match }per value).
The differential run of the Lean model against Coraza (adapters/coraza/cmd/differential)
found the gap: with ARGS_GET:a holding abc and zzz, !@streq abc matches on every
engine and did not in the model, which had implemented the text. No Core profile had a
negated operator over more than one value.
Decision
A leading ! negates the operator’s result for each selected value; the rule matches
when the operator is false for at least one selected value. A rule that selects no value
never matches (06-operators.md#unconditionalmatch). Normative text:
spec/02-grammar.md#operator.
Options considered
- Keep “false for every value”: no engine does it, and it would turn
!@streqon a multi-valued collection into an all-quantifier no rule author expects; rejected. - Per-value negation (chosen): what every engine does and what the reference manual describes.
Consequences
- For the engines: none.
- For rule authors:
ARGS "!@rx ^[a-z]+$"fires when any argument fails the pattern, the usual intent of an allow-list rule. - For this repository: the Lean model negates per value;
operator-forms.yamlgains a two-valued stage.
Tests
tests/engine/grammar/operator-forms.yaml(two values, one failing the operator)