7.2 KiB
Custom Decoders Reference
Detailed reference for every custom decoder in local_decoder.xml. Documents what each decoder matches, what fields it extracts, and why it exists.
Table of contents
- pfsense-wrapped and children
- mailcow-journald-unwrap and child
- gitea and gitea-auth-fail
- mailcow-dovecot-success
- mailcow-postfix-sasl-fail
pfsense-wrapped and children
Purpose. Parse pfSense filter log entries that arrive wrapped in additional syslog headers from the pfSense box.
Structure. One parent decoder plus four children that extract different sections of the log line via chained offset="after_regex" extractions.
pfsense-wrapped (parent)
<decoder name="pfsense-wrapped">
<prematch>filterlog</prematch>
</decoder>
Matches any log line containing filterlog (the pfSense filter daemon).
pfsense-wrapped-fields (first child)
<decoder name="pfsense-wrapped-fields">
<parent>pfsense-wrapped</parent>
<regex>filterlog\S* \S*,\S*,\S*,(\S*),\S*,\S*,(\S*),</regex>
<order>id,action</order>
</decoder>
Extracts rule ID and action (block/pass) from the CSV-like pfSense filter log.
pfsense-wrapped-fields (second child)
<regex offset="after_regex">\S*,\S*,\S*,\S*,\S*,\S*,\S*,\S*,\S*,(\S*),\S*,(\S*),(\S*),</regex>
<order>protocol,srcip,dstip</order>
Continues from where the first child left off. Extracts protocol, source IP, destination IP.
pfsense-wrapped-fields (third child)
<regex offset="after_regex">(\d*),(\d*),\S*</regex>
<order>srcport,dstport</order>
Extracts source and destination ports.
pfsense-wrapped-fields (fourth child)
<regex offset="after_regex">datalength=(\S*)|(\d*)</regex>
<order>length</order>
Extracts packet length.
Note. All four children share the same name deliberately. Wazuh's chained extraction pattern supports multiple children with identical names when each uses offset="after_regex" to continue from the previous child's position.
Referenced by. Rules 87699, 87761, 87762 (pfSense correlation chain).
mailcow-journald-unwrap and child
Purpose. Strip mailcow's outer journald syslog envelope to expose the inner Postfix log for further decoding.
<decoder name="mailcow-journald-unwrap">
<prematch>postfix\(\d+\): \w+ \d+ \d+:\d+:\d+ \w+ \.+\(\d+\):</prematch>
</decoder>
<decoder name="mailcow-journald-unwrap-child">
<parent>mailcow-journald-unwrap</parent>
<regex offset="after_parent">\.+</regex>
<order>extra_data</order>
</decoder>
Status. Present but not actively referenced by current rules. Reserved for future use if additional Postfix log types need parsing beyond what mailcow-postfix-sasl-fail handles.
gitea and gitea-auth-fail
Purpose. Parse Gitea log lines for authentication and account activity.
gitea (parent)
<decoder name="gitea">
<program_name>gitea</program_name>
</decoder>
Matches on syslog program name gitea. Sets up the child decoder chain for Gitea-specific extraction.
gitea-auth-fail (child)
<decoder name="gitea-auth-fail">
<parent>gitea</parent>
<prematch>Failed authentication attempt</prematch>
<regex>Failed authentication attempt for (\S+) from (\d+.\d+.\d+.\d+)</regex>
<order>user, srcip</order>
</decoder>
Fires on Gitea failed login attempts. Extracts the attempted username and the source IP.
Sample log this matches:
2026/05/29 14:19:59 routers/web/auth/auth.go:309:SignInPost() [W] Failed authentication attempt for admin from 192.0.2.100
Referenced by. Rules 100400 through 100470 (all Gitea rules).
mailcow-dovecot-success
Purpose. Extract source IP from any Dovecot event containing rip=IP,. Universally useful because it captures the source IP from successful logins, disconnects, port scan probes, and reconnaissance activity - all of which include the rip= field.
<decoder name="mailcow-dovecot-success">
<parent>dovecot</parent>
<regex>rip=(\S+),</regex>
<order>srcip</order>
</decoder>
Design note. Despite the name suggesting it only handles successful logins, this decoder actually populates srcip on ALL Dovecot events that include rip=. This was the result of iterative debugging - simpler patterns worked better than more specific ones. Consider renaming to mailcow-dovecot-rip in a future refactor.
Field extracted:
srcip- source IP from therip=field
Referenced by. Rules 100200-100206 (Dovecot suppression and recon), 100311 (compromise correlation, indirectly via rule 100204).
mailcow-postfix-sasl-fail
Purpose. Extract source IP, subnet, and target username from mailcow-wrapped Postfix SASL authentication failure logs. Enables correlation on all three fields.
<decoder name="mailcow-postfix-sasl-fail">
<parent>postfix</parent>
<prematch type="pcre2">postfix/\S+smtpd\S*: warning: \S+: SASL </prematch>
<regex type="pcre2">unknown\[((\d+\.\d+\.\d+)\.\d+)\]: SASL \S+ authentication failed.*sasl_username=(\S+)</regex>
<order>srcip, src_subnet, dstuser</order>
</decoder>
Fields extracted:
| Field | Example | Notes |
|---|---|---|
srcip |
192.0.2.100 |
Full IPv4 source address |
src_subnet |
192.0.2 |
First three octets, for /24 correlation |
dstuser |
admin |
The sasl_username the attacker attempted |
Why PCRE2. OSSEC regex lacks the .* quantifier needed to match the variable content between "authentication failed" and "sasl_username=" in Postfix logs. PCRE2 mode handles it cleanly.
Why not shipped decoder alone. Wazuh's shipped postfix-sasl decoder assumes standard Postfix log format like postfix/smtpd[PID]: .... Mailcow wraps every log in an outer journald envelope: Jul 31 02:00:03 mailcow postfix[142]: Jul 30 21:00:03 e239f49ad459 postfix/submission/smtpd: .... The shipped decoder's anchored regex fails on the wrapped format, so srcip never gets extracted, and correlation on same_srcip never fires.
IPv4-only. This decoder requires dotted-decimal IPv4 addresses. IPv6 attackers (rare against SMTP submission but possible) would fall through to the shipped postfix decoder alone with no field extraction. Add a separate IPv6 decoder if that becomes an issue.
Referenced by. Rules 3332 (shipped Postfix SASL failure alert), 100310-100316 (all mail correlation rules).
Testing decoders
For any decoder change, verify with wazuh-logtest:
sudo /var/ossec/bin/wazuh-logtest
Paste a real log line at the prompt. Look at Phase 2:
- Verify the
name:matches the decoder you expect - Verify each field in
<order>shows up asfield_name: 'value'
If Phase 2 doesn't show the expected fields, the regex isn't matching. Common causes:
- Regex has
^anchor but log doesn't start at that position - XML entities not escaped (
<should be<,>should be>) - OSSEC regex used where PCRE2 syntax is required
- Prematch too strict, not matching the log line
- Parent decoder didn't match, so children never evaluated
See README.md for full troubleshooting notes.
Last updated: 2026-07-31