{"record":{"id":"fc8745ff56be2a0a","repo":"crowdsecurity/crowdsec","slug":"leaf-rule-must-have-zones","errorCode":null,"errorMessage":"leaf rule must have zones","messagePattern":"leaf rule must have zones","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/appsec/appsec_rule/modsecurity.go","lineNumber":141,"sourceCode":"\treturn strings.Join(rules, \"\\n\"), m.ids, nil\n}\n\n// leafCopy returns a shallow copy of the rule with And/Or cleared.\nfunc leafCopy(rule *CustomRule) *CustomRule {\n\tcp := *rule\n\tcp.And = nil\n\tcp.Or = nil\n\n\treturn &cp\n}\n\n// flattenToDNF converts a CustomRule tree into Disjunctive Normal Form:\n// a list of AND-groups (conjunctions), where the outer list is OR.\nfunc flattenToDNF(rule *CustomRule) ([][]*CustomRule, error) {\n\t// Leaf node: has zones, no children\n\tif len(rule.And) == 0 && len(rule.Or) == 0 {\n\t\tif rule.Zones == nil {\n\t\t\treturn nil, errors.New(\"leaf rule must have zones\")\n\t\t}\n\n\t\treturn [][]*CustomRule{{rule}}, nil\n\t}\n\n\t// Collect DNF parts to be AND-combined via cross-product\n\tvar parts [][][]*CustomRule\n\n\t// If this node has zones alongside And/Or children, treat as implicit AND term\n\tif rule.Zones != nil {\n\t\tparts = append(parts, [][]*CustomRule{{leafCopy(rule)}})\n\t}\n\n\t// Each And child's DNF is cross-producted\n\tfor i := range rule.And {\n\t\tchildDNF, err := flattenToDNF(&rule.And[i])\n\t\tif err != nil {\n\t\t\treturn nil, err","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/appsec/appsec_rule/modsecurity.go#L123-L159","documentation":"When flattening a CustomRule tree into disjunctive normal form (DNF) for modsecurity/Coraza compilation, flattenToDNF treats a node with no `and`/`or` children as a leaf, and a leaf must carry a zones list. A childless node without zones is structurally invalid and cannot be rendered as a match condition, so the build of the whole rule fails.","triggerScenarios":"A leaf rule (no and/or) has `zones` nil — e.g. only `match:` present; flattenToDNF is reached from CustomRule Build via modsecurity.go during rule compilation.","commonSituations":"Rules constructed programmatically or by a generator that set Match but forgot Zones; YAML where the zones key was dropped; interacting with the DNF expansion path after refactor.","solutions":["Add a zones list to the leaf rule","If the node was meant to group children, add `and:` or `or:` entries instead","Re-check the generator/template producing rules so every leaf includes zones"],"exampleFix":"// before\nrule := &CustomRule{Match: Match{Type: \"contains\", Value: \"x\"}}\n\n// after\nrule := &CustomRule{Zones: []string{\"URI\"}, Match: Match{Type: \"contains\", Value: \"x\"}}","handlingStrategy":"validation","validationCode":"func isLeaf(r *CustomRule) bool { return len(r.And) == 0 && len(r.Or) == 0 }\nif isLeaf(rule) && rule.Zones == nil {\n    return fmt.Errorf(\"leaf rule %+v must define zones\", rule)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When building rules programmatically, always set Zones for childless nodes","Add unit tests constructing each rule shape you generate"],"tags":["appsec","rules","validation"],"backgroundTag":"missing-required-config-field","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}