With the pipeline understood, it was time to write the first custom rules. The process for every rule is the same: generate a real event, confirm the field names from actual data, write the rule, test it. Never assume a field name. Always verify from a real alert.
The rule writing workflow
- Identify the gap in default coverage
- Generate a real event to inspect
- Confirm exact field names from the actual alert in the dashboard
- Write the rule targeting those confirmed fields
- Deploy, test, and validate
Parent and child rule structure
<group name="local,windows,authentication,">
<rule id="100002" level="5">
<if_sid>60122</if_sid>
<description>Windows: Failed logon attempt</description>
<group>windows_failed_logon,authentication_failed,</group>
<mitre><id>T1110</id></mitre>
</rule>
<rule id="100003" level="10" frequency="5" timeframe="120">
<if_matched_sid>100002</if_matched_sid>
<same_field>win.eventdata.targetUserName</same_field>
<description>Windows: Brute force attack detected</description>
<group>windows_brute_force,</group>
<mitre><id>T1110</id></mitre>
</rule>
</group>
Rule 100002 is a parent that chains from Wazuh's built-in rule 60122. By owning an intermediate rule, the correlation logic is controlled without depending on built-in rules that might change on updates. Custom rule IDs must be 100000 or above.
How the correlation rule works
Rule 100003 uses three attributes together:
frequency="5"— the parent must fire 5 timestimeframe="120"— within 120 secondssame_field>win.eventdata.targetUserName— all 5 must target the same username
Five failed logins against the same account in two minutes is a brute force attack. One failed login is a typo.
Critical: field names in rules vs dashboard
The dashboard shows field names with a data. prefix. Inside rule XML that prefix does not exist:
# Dashboard search
data.win.eventdata.targetUserName
# Rule XML — drop the data. prefix
<same_field>win.eventdata.targetUserName</same_field>
The data. prefix is added by the indexer when storing alerts. It does not exist in the rules engine. This is the single most common early mistake.
same_field vs same_srcuser
Wazuh has shortcut tags like <same_srcuser /> that are supposed to handle username correlation. For Windows eventchannel logs these silently fail — they map to internal fields that the Windows decoder does not populate. Always use <same_field>win.eventdata.fieldname</same_field> for Windows events.
Deploying and testing
# Deploy
sudo systemctl reload wazuh-manager
# Watch alerts in real time
sudo tail -f /var/ossec/logs/alerts/alerts.log
Simulate the attack — five failed logins quickly against the same account. The brute force rule fired at level 10. Alert appeared in the dashboard within seconds and was automatically mapped to MITRE ATT&CK T1110 in the framework view.
Rules written this session
| Rule ID | Description | Level |
|---|---|---|
100002 | Failed logon parent — fires on every Event ID 4625 | 5 |
100003 | Brute force — same account fails 5× in 120 seconds | 10 |
100004 | Privilege escalation parent — Event ID 4672 | 7 |
100005 | Repeated privilege escalation — same user 3× in 300 seconds | 10 |