Wazuh ships excellent built-in detections, but your environment is unique, your applications, your business logic, your insider-threat model. That is where custom rules earn their keep. This guide walks the full path: log line --> decoder --> rule --> test.
Anatomy of a Wazuh rule
<rule id="100100" level="10">
<if_sid>5710</if_sid>
<match>Failed password</match>
<description>SSH brute force candidate</description>
<group>authentication_failed,</group>
</rule>- id, must be 100000+ for custom rules.
- level, 0 (silent) to 15 (max severity).
- if_sid, chain off a parent rule (massively reduces CPU).
- match / regex, text the rule looks for inside the decoded log.
- group, tags used for filtering + correlation.
Decoders come first
A rule only fires if a decoder has extracted fields from the raw log. For custom apps, write a custom decoder in /var/ossec/etc/decoders/local_decoder.xml first:
<decoder name="myapp">
<prematch>^MYAPP </prematch>
</decoder>
<decoder name="myapp-fields">
<parent>myapp</parent>
<regex>user=(\S+) action=(\S+) status=(\S+)</regex>
<order>user,action,status</order>
</decoder>Frequency + timeframe = correlation
For brute-force-style detections combine frequency and timeframe with same_source_ip:
<rule id="100110" level="12" frequency="6" timeframe="120">
<if_matched_sid>100100</if_matched_sid>
<same_source_ip/>
<description>SSH brute force: 6 failed logins in 2 minutes</description>
</rule>5 worked examples
a. Detect new sudo binary
<rule id="100200" level="12">
<if_sid>550</if_sid>
<field name="file">/usr/bin/sudo</field>
<description>sudo binary changed on disk</description>
</rule>b. Detect cleartext password in app log
<rule id="100210" level="13">
<decoded_as>myapp</decoded_as>
<regex>password=\w+</regex>
<description>Application logged a cleartext password</description>
</rule>c. Off-hours admin login
<rule id="100220" level="10">
<if_sid>5715</if_sid>
<time>10:00 pm - 6:00 am</time>
<description>Admin SSH login outside business hours</description>
</rule>d. New process from /tmp
<rule id="100230" level="12">
<if_sid>92301</if_sid>
<field name="image">^/tmp/</field>
<description>Process executed from /tmp -- possible dropper</description>
</rule>e. Multiple distinct accounts failing on one host
<rule id="100240" level="13" frequency="5" timeframe="180">
<if_matched_sid>5710</if_matched_sid>
<different_user/>
<description>Password spraying: 5 different users failing on same host</description>
</rule>The right workflow
- Capture a raw log sample.
- Run wazuh-logtest, confirm a decoder matches.
- Write the rule in local_rules.xml.
- Run wazuh-logtest again, confirm rule fires at correct level.
- configtest, then restart wazuh-manager.
- Watch /var/ossec/logs/alerts/alerts.json to confirm production traffic triggers as expected.
Official reference
Commands and configuration in this guide are based on the official Wazuh documentation. For the canonical source, see: