DEV Community

Nguyen Dong
Nguyen Dong

Posted on

All 4052 if_sid anchors in the Wazuh ruleset resolve, so that's not why your rule is silent

I kept seeing the same guess from Wazuh users: my rule never fires, so the shipped ruleset must be broken. That's checkable, so I checked it instead of guessing.

At commit 3bf1912 (measured 23 Sep 2026) the shipped ruleset is 166 files and 4420 rules.

Every if_sid anchor resolves to a rule that exists: 4052 of 4052. Every if_matched_sid: 205 of 205. Every if_group: 129 of 129. No dangling parents anywhere.

That zero is only worth reading because the detector was checked in both directions. A SID that can't exist (99999999) is reported as dangling, and SIDs that do ship (1002, 5701) are reported as defined. XML comments are stripped before parsing; three rule definitions live inside comments and would otherwise have been counted as real.

The number that does explain silence is different: 635 of 4420 rules, 14.4 percent, carry level="0". They never alert by design, they exist to be matched on. A chain that terminates on one of them is silent and correct at the same time.

So when a rule doesn't fire, the shipped anchors aren't where the fault is. Your own local rules, your ordering, and the level 0 parents in the chain are.

Limits worth stating. This is ruleset/rules/*.xml at one commit and nothing else. It doesn't cover decoders, custom rules, or whether a rule that resolves is one that matches your logs. It proves the anchors are reachable, and it doesn't prove the detection is right.

Re-run it against any commit:

const defined = new Set(), refs = [];
for (const p of ruleFiles) {
  const src = fetchText(p).replace(/<!--[\s\S]*?-->/g, '');
  const re = /<rule\b[^>]*\bid\s*=\s*["'](\d+)["'][^>]*>([\s\S]*?)<\/rule>/g;
  let m;
  while ((m = re.exec(src)) !== null) {
    defined.add(m[1]);
    let k; const ifRe = /<if_sid>([\s\S]*?)<\/if_sid>/g;
    while ((k = ifRe.exec(m[2])) !== null)
      k[1].split(',').map(s => s.trim()).filter(Boolean).forEach(t => refs.push(t));
  }
}
refs.filter(r => !defined.has(r));
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
mihai_leanzero profile image
Mihai Perdum

Nguyen, checking both directions on the anchor graph is the part most people skip - a detector that can't flag a real dangling SID as dangling isn't proving anything. One edge case I didn't see covered: local_rules.xml collisions. Wazuh's own convention is custom rule IDs above 100000, specifically to avoid this. Someone who ignores it and reuses a shipped SID would still get anchor resolves=true, just pointing at the wrong definition. That's a second, quieter way "silent by design" and "silent by ID collision" end up looking identical from the outside.