Wazuh fix note · rule precedence

Wazuh rule shadowed by a sibling: another child of the same parent matches first

When several children of one parent can match an event, one of them takes it. In our measurements the higher level went first, and at equal level the rule loaded first went first. If a sibling with a higher level matches your event, your rule never fires, and neither the config check nor a one-line logtest shows it.

Dong Nguyen, ATK New Technology · 28 September 2026 · measured on Wazuh 4.14.7 unless stated otherwise


How to check in one minute

Feed wazuh-logtest the real sequence of events, not one line, and read which rule took the last one:

/var/ossec/bin/wazuh-logtest < sequence.log 2>&1 | grep -E "^[[:space:]]+(id|level|description): " | tail -3

If the ID is a sibling's rather than yours, your rule is shadowed on that sequence. Keep the 2>&1: logtest on 4.14 prints its results to stderr.

To find the sequence, look at alerts from siblings of your rule, meaning other children of the same parent. Their full_log is the event that sibling took. If you do not know whether your rule would have matched those events, see "Confirm before you fix" below.

Why it happens

We placed one rule in local_rules.xml and changed only its parent and level between runs:

<group name="local,syslog,sshd,">
  <rule id="100080" level="$LEVEL">
    <if_sid>$PARENT</if_sid>
    <match>Accepted password</match>
    <description>probe</description>
  </rule>
</group>

Two inputs through wazuh-logtest: S1, one Accepted password line; S2, 8 Failed password lines from the same IP within 8 seconds, then the same Accepted password line.

caseparentlevel-tS1 lands onS2 lands on
A57159exit 0, no warning100080 (ours)40112 level 12, ours loses
B571513exit 0, no warning100080100080 level 13, ours wins
C57009exit 0, no warning100080100080
D4011212exit 0, no warning5715 level 3, ours does not fire100080
controlno custom rule5715 level 340112 level 12

We ran the matrix twice; all 8 verdicts matched.

Fix

Each of these was measured on the same pair of rules (the table above). Which one is right depends on what you want the rule to mean:

In B and C, 40112 no longer fires on S2: your rule takes the event instead. If you rely on 40112 as a brute-force alert, use D.

After the change, feed the same sequence through logtest again and confirm the last line lands on your rule ID.

Confirm before you fix

A sibling that fired proves only that the sibling matched something. It does not prove your rule would have matched the same event. On a test ruleset we built three candidates this way; one of them, a rule on a user name that none of the events had, looked shadowed and was not.

The check that separated them: on a throwaway manager with a copy of the rules, raise your rule's level to 15, replay the full_log of the sibling's alerts through wazuh-logtest, and see whether any of them flips to your rule. If some flip, the rule is shadowed on those events. If none flip, your rule does not match them, and the problem is the rule's own conditions. In our test, the two real cases flipped (2 of 2 and 1 of 2 samples) and the false one flipped 0 times.

Limits of what we measured

One release (4.14.7), one single-node manager, one parent (5715) and one higher-level sibling (40112) for the level test, replayed through wazuh-logtest. The equal-level case and the replay check ran on test rules we wrote, on a throwaway 4.14.7 manager. We have not measured if_matched_sid or if_group chains, clusters, or live traffic. Our own lab showed no shadowed rules, but it had almost no traffic, so that says nothing about a busy manager. The replay check needs the full_log of the sibling's alerts; we have not measured it on Windows eventchannel or JSON events.

Free check

Check your whole manager for this and other silent rules: Rule Doctor Lite (free, read-only, one Python file).

Lite reports SHADOW-CANDIDATE when a sibling that ranks ahead of your rule has fired. That proves the sibling matched something, not that your rule would have matched; the replay step above is how you confirm it.

curl -sO https://raw.githubusercontent.com/xuxu298/rule-doctor-lite/main/rule-doctor-lite.py
sudo python3 rule-doctor-lite.py

Or have it fixed for you.