Rules and decoders are the core of Wazuh’s detection engine. Decoders normalize raw log data; rules match patterns and generate alerts.
Architecture
Raw Log → Decoder (normalize) → Rule Engine (match) → Alert
↓ ↓
Field extraction If matched → alert + level
If not → discarded
Decoders — Parse raw logs into structured fields (source, destination, action, etc.)
Rules — Match decoded fields against patterns; assign level, group, and actions
Alerts — Generated when rules match; stored in the indexer for visualization
Decoder Structure
Basic Decoder Example
<!-- /var/ossec/etc/decoders/0000-custom-decoder.xml --><decoder name="custom-ssh"> <program_name>sshd</program_name> <prematch>^Accepted</prematch> <regex>^(\S+) (\S+) for (\S+) from (\S+) port (\d+) (\S+)</regex> <order>srcuser, user, srcip, srcport, protocol</order> <description>SSH login success</description></decoder>
Decoder Fields
Field
Description
program_name
Match logs by the program generating them
prematch
Fast regex check before full decode
regex
Full regex with capture groups for fields
order
Ordered list of captured fields
fts
First time seen — tracks new unique events
Decoder with Parent (Inheritance)
<!-- Base decoder for SSH --><decoder name="sshd-base"> <program_name>sshd</program_name> <prematch>^sshd</prematch></decoder><!-- Child decoder for specific SSH event --><decoder name="sshd-accepted" parent="sshd-base"> <prematch>^Accepted</prematch> <regex>^Accepted (\S+) for (\S+) from (\S+) port (\d+)</regex> <order>auth_method, user, srcip, srcport</order> <description>SSH authentication success</description></decoder>
Rule Structure
Basic Rule Example
<!-- /var/ossec/etc/rules/local_rules.xml --><group name="custom-ssh"> <rule id="100001" level="3"> <if_sid>SSH</if_sid> <!-- or decoder name --> <match>Accepted</match> <description>SSH login successful</description> <group>authentication_success,ssh</group> </rule> <rule id="100002" level="6"> <if_sid>100001</if_sid> <srcip>10.0.0.0/8</srcip> <!-- Internal IP - lower level --> <description>SSH login from internal network</description> <level>2</level> </rule> <rule id="100003" level="10"> <if_sid>100001</if_sid> <srcip>!10.0.0.0/8</srcip> <!-- Not internal --> <description>SSH login from external IP - alert</description> <group>authentication_fail,threat</group> </rule></group>
<!-- Alert only after 5 failed SSH attempts in 10 minutes --><rule id="100400" level="6"> <if_sid>100001</if_sid> <field name="action">failure</field> <same_field srcip>5</same_field> <!-- Same src IP, 5+ events --> <time_frame>10m</time_frame> <description>Multiple SSH login failures from same IP</description> <group>brute_force,ssh</group> <mitre> <id>T1110</id> </mitre></rule><!-- Block IP after 10 failures (for automation) --><rule id="100401" level="10"> <if_sid>100400</if_sid> <same_field srcip>10</same_field> <time_frame>5m</time_frame> <description>Brute force attack detected - 10+ failures</description> <group>brute_force,attack</group> <!-- Integrate with n8n for firewall block --></rule><!-- Disable alerting for known OK sources --><rule id="100402" level="0"> <if_sid>100001</if_sid> <srcip>10.0.1.100</srcip> <!-- Your jump host --> <description>SSH from trusted jump host - no alert</description> <noalert>yes</noalert></rule>
Dynamic Threshold (Statistical)
<!-- Above average failed logins for user --><rule id="100403" level="6"> <if_sid>sshd</if_sid> <field name="action>failure</field> <same_field user>10</same_field> <time_frame>1h</time_frame> <description>Unusual number of failed SSH attempts for user</description> <group>threat,user_anomaly</group></rule>
MITRE ATT&CK Mapping
Map your rules to MITRE ATT&CK for better threat context.
# Test configuration/var/ossec/bin/wazuh-logtest# Example input for SSH failed login:Wed Jan 15 10:30:00 server sshd[12345]: Failed password for invalid user admin from 203.0.113.50 port 54321 ssh2# Test JSON decoderecho '{"eventVersion":"1.08","userIdentity":{"type":"Root","arn":"arn:aws:iam::123456789012:root"}}' | /var/ossec/bin/wazuh-logtest -t# Reload rules without restarting/var/ossec/bin/wazuh-control reload# Check rules are loaded/var/ossec/bin/wazuh-rules --debug
Rule File Organization
# Wazuh rule directories/var/ossec/ruleset/rules/ # Built-in rules (read-only)var/ossec/etc/rules/ # Custom rules (your changes) local_rules.xml # Main custom rules file# Decoder directories/var/ossec/decoders/ # Built-in decoders/var/ossec/etc/decoders/ # Custom decoders# Best practice: Don't modify built-in files# Use local_rules.xml for custom rules# Create new decoder files in /var/ossec/etc/decoders/
Common Gotchas
Rules use regex not glob patterns (use .* not *)
if_sid must be decoder ID or rule ID
decoded_as matches decoder by name
match is case-insensitive; regex is case-sensitive
same_field requires time_frame
Levels 0-15; use noalert for silent rules
JSON decoder uses field names exactly as they appear in JSON
For nested JSON fields, use dot notation: requestParameters.policyName