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));
Top comments (1)
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.