Where custom rules load from
Custom rules go in /var/ossec/etc/rules/. Those files are not loaded after the stock ruleset: they are merged with /var/ossec/ruleset/rules/ and loaded in order of the full file name. On 4.14.7 the image has 168 stock rule files, all starting with a digit, the last being 0999-malicious-ioc-rules.xml, so local_rules.xml loads after all of them. We measured the same child rule of stock rule 5715 in five file names: 0094-test.xml and 0095-aaa.xml were ignored; 0095-zzz.xml, 0096-test.xml and local_rules.xml fired. Full measurement.
How to check in one minute
/var/ossec/bin/wazuh-analysisd -t 2>&1 | grep -E 'WARNING|ERROR|CRITICAL'
The exit code is 0 even when rules are ignored, so read the lines. Each will be ignored line names a rule that is not running.
Six ways, measured
1. The parent rule is in a file that loads later
A child rule in a file that sorts before its if_sid parent's file is read before the parent exists. Wazuh logs 7617 and 7619, ignores the rule, and the config check still exits 0:
WARNING: (7617): Signature ID '5715' was not found and will be ignored in the 'if_sid' option of rule '100080'.
WARNING: (7619): Empty 'if_sid' value. Rule '100080' will be ignored.
Rule dropped at load: 7617 / 7619
2. A static field written as <field>
action is a static field with its own <action> tag. Writing <field name="action"> instead logged ERROR: Failure to read rule 100210. Field 'action' is static. and the manager did not start: no alerts at all, not only for FortiGate.
FortiGate note, with the measurement
3. More than 50 load warnings
wazuh-analysisd -t and ossec.log keep at most 50 load messages; anything beyond that is dropped, oldest first, with no line anywhere. In our test 60 rules were ignored: 50 lines were printed, naming the last 25, and the first 35 were never mentioned. A public community ruleset printed 52 warnings where a static check found 55.
The 50-warning limit, measured
4. An if_group the load check cannot match
At load time, 4.14.7 accepted an upper-case, partial group name and a | list, and rejected a name starting with ^ that does not begin any group:
<rule id="100715" level="5"><if_group>AUTHENTICATION_FAI</if_group>...</rule>
<rule id="100716" level="5"><if_group>zzz_nope|authentication_success</if_group>...</rule>
<rule id="100717" level="5"><if_group>^uthentication</if_group>...</rule>
WARNING: (7610): Group '^uthentication' was not found. Invalid 'if_group'. Rule '100717' will be ignored.
Rules 100715 and 100716 loaded with no warning. This is the load check only; we did not test which events these rules then match.
5. JSON logs behind a syslog header
A line that is only JSON decodes on its own. The same JSON behind a standard syslog header matched no decoder, and behind a plain date it was claimed by the stock windows-date-format decoder with no fields, so rules on the JSON keys never see them.
6. A CDB list rule under load
A rule with lookup="not_match_key" fed only values that were in the list gave no false alerts at 1,000 events per second, 10 at 5,000, and 24 and 27 in two 50,000-event bursts; with a single rule-matching thread, or a negated <field> in place of the list, the same bursts gave none.
CDB list not_match_key false positives
Limits of what we measured
Each item measured on 4.14.7; details on the linked page. Item 4 was measured for this page with wazuh-analysisd -t only. Other versions, clusters and the Wazuh 5.0 engine (which does not load XML rules) were not covered here.