Wazuh Decoders & Rules: The Missing Mental Model
A clear, beginner-friendly guide to how Wazuh decoders and rules work together — what fields are, where they come from, when you need a decoder, and how logs become alerts.
Tags: Wazuh · OSSEC · SIEM · Blue Team · Detection Engineering
Level: Beginner → Intermediate | Read time: 15 min
If you’ve ever looked at a Wazuh rule file and asked yourself:
- "Where does this field come from — the rule, the decoder, or the log itself?"
- "Do I even need a decoder for this rule to work?"
- "Why does my
<field>rule never fire even though the string is clearly in the log?"
…you are not alone. These are the exact questions most people hit when they start writing Wazuh rules. This post answers all of them, step by step, with diagrams at every stage.
1. The Big Picture: How Wazuh Processes Every Log
Before a single rule is evaluated, Wazuh passes every incoming log through a two-stage pipeline:
flowchart LR
A(["📄 Raw Log"]) --> B["Decoder Stage\nextract named fields"]
B --> C["Rules Engine\nmatch conditions"]
C --> D(["🚨 Alert or No Alert"])
style A fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style B fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style C fill:#2a1f10,stroke:#ff6b35,color:#c8cdd8
style D fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
Stage 1 — Decoder: Takes the raw log string and extracts structured, named fields from it. This stage is optional — if no decoder matches the log, the log passes through as a plain string.
Stage 2 — Rules Engine: Evaluates the log (and any decoded fields) against your rules. This stage always runs, decoder or not.
The key insight: what the rules engine sees depends entirely on whether a decoder ran first.
2. What Is a "Field" in Wazuh?
A field is a named piece of data extracted from a raw log by a decoder.
Take this raw SSH log:
Jan 10 12:00:00 webserver sshd[1234]: Failed password for root from 203.0.113.5 port 22
Out of the box, Wazuh’s built-in SSH decoder reads that line and extracts:
srcip = 203.0.113.5
user = root
action = Failed password
These become named fields that rules can match against precisely — instead of just scanning the raw string.
Without a decoder, the rules engine only sees the raw log text. With a decoder, it sees both the raw text and the extracted fields.
3. How Rules and Decoders Connect
This is where most people get confused. There are several rule tags, and they behave very differently depending on whether a decoder ran:
| Rule Tag | Requires Decoder? | What It Matches |
|---|---|---|
<match> |
❌ No | Substring search on the raw log line |
<field name="x"> |
✅ Yes | Value of a named field extracted by decoder |
<decoded_as> |
✅ Yes | Whether a specific decoder processed this log |
<if_sid> |
❌ No | Whether a parent rule previously fired (chaining) |
<same_field> |
✅ Yes | Field correlation across multiple events |
<list field="x"> |
✅ Yes | CDB / threat-intel lookup on a decoded field |
flowchart TD
A(["Raw Log"]) --> B{"Did a decoder\nrun on this log?"}
B -- "No" --> C["Only raw string\navailable to rules"]
B -- "Yes" --> D["Named fields\navailable to rules"]
C --> E["match ✅\nif_sid ✅\nfield ❌\ndecoded_as ❌"]
D --> F["match ✅\nif_sid ✅\nfield ✅\ndecoded_as ✅"]
style A fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style B fill:#2a1f10,stroke:#ff6b35,color:#ff6b35
style C fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style D fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style E fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style F fill:#0d2e1a,stroke:#00e5a0,color:#c8cdd8
<match> always works because it is a simple substring search on the raw log text — it does not care whether a decoder ran. <field> only works if a decoder already extracted that field by name.
4. Anatomy of a Wazuh Decoder: name, prematch, regex, order
Let’s build a decoder from scratch and explain every tag. Imagine you have a custom application that writes logs like this:
2024-01-10 12:00:00 app=myapp action=login user=alice srcip=203.0.113.5 status=failed
Here is a decoder that extracts the useful fields:
<decoder name="myapp">
<prematch>app=myapp</prematch>
<regex>action=(\w+) user=(\w+) srcip=(\d+\.\d+\.\d+\.\d+) status=(\w+)</regex>
<order>action, user, srcip, status</order>
</decoder>
flowchart LR
A(["Raw Log\napp=myapp action=login user=alice ..."])
A --> B{"prematch\napp=myapp found?"}
B -- "No" --> C(["Skip this decoder"])
B -- "Yes" --> D["Run regex\nextract capture groups"]
D --> E["Map via order\ngroup 1 to action\ngroup 2 to user\ngroup 3 to srcip\ngroup 4 to status"]
E --> F(["Fields ready\naction=login\nuser=alice\nsrcip=203.0.113.5\nstatus=failed"])
style A fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style B fill:#2a1f10,stroke:#ff6b35,color:#ff6b35
style C fill:#2e0d0d,stroke:#ff4444,color:#888
style D fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style E fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style F fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
name="myapp"
A label for this decoder. Rules can reference it with <decoded_as>myapp</decoded_as> to ensure they only fire on logs this decoder processed.
<prematch> — fast pre-filter
Before running the regex, Wazuh checks: does this string exist anywhere in the log? If not, the decoder is skipped entirely. This is a performance optimization — regex is expensive; prematch is cheap. Always include it.
Log: "2024-01-10 app=myapp action=login user=alice ..."
^^^^^^^^
prematch finds "app=myapp" → continue to regex
Log: "Jan 10 kernel: eth0 link up"
no "app=myapp" found → skip decoder entirely
<regex> — capture groups extract values
Each pair of () is a capture group. Whatever the regex matches inside the parentheses becomes a field value.
action=(\w+)
└──┘ group 1 — matches one or more word characters
Input: action=login
Result: login → group 1
user=(\w+)
└──┘ group 2
Input: user=alice
Result: alice → group 2
srcip=(\d+\.\d+\.\d+\.\d+)
└─────────────────┘ group 3 — IP address pattern
Input: srcip=203.0.113.5
Result: 203.0.113.5 → group 3
status=(\w+)
└──┘ group 4
Input: status=failed
Result: failed → group 4
<order> — names the capture groups
<order>action, user, srcip, status</order>
This maps: group 1 → action, group 2 → user, group 3 → srcip, group 4 → status.
⚠️ Critical rule: The number of names in
<order>must exactly match the number of()groups in<regex>. Four groups → four names. If they don’t match, the decoder silently fails and no fields are extracted.
5. Common Mistake: <prematch> Does NOT Extract Fields
This is the most common misconception among people new to Wazuh.
Q: My <prematch> checks for app=myapp — so does app become a field I can use in rules?
No. <prematch> is a filter only. It checks whether a string exists in the log to decide whether to continue. It extracts nothing.
flowchart TD
A["prematch: app=myapp"] --> B["Filter only ❌\napp NOT a field\ncannot use in rules"]
C["regex: app=group1 user=group2\norder: app, user"] --> D["app = myapp ✅\nuser = alice ✅\nboth usable in rules"]
style A fill:#2a1f10,stroke:#ff6b35,color:#c8cdd8
style B fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style C fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style D fill:#0d2e1a,stroke:#00e5a0,color:#c8cdd8
Fields only come from <regex> capture groups mapped by <order>. If you want app as a usable field, you must capture it in the regex:
<!-- app is NOT a field — only used as a pre-filter -->
<prematch>app=myapp</prematch>
<regex>action=(\w+) user=(\w+)</regex>
<order>action, user</order>
<!-- app IS a field — captured in regex -->
<prematch>app=</prematch>
<regex>app=(\w+) action=(\w+) user=(\w+)</regex>
<order>app, action, user</order>
6. Full Wazuh Flow: Raw Log → Decoder → Rule Chain → Alert
Let’s trace a realistic example end to end. A user fails to log in three times — we want to detect this with a chained rule.
Raw log:
2024-01-10 12:00:00 app=myapp action=login user=alice srcip=203.0.113.5 status=failed
flowchart TD
A(["Raw Log - app=myapp action=login user=alice status=failed"])
A --> B{"prematch - app=myapp found?"}
B -- "No" --> C(["No decoder runs - log stays as raw string"])
B -- "Yes" --> D["Regex extracts: action=login, user=alice, srcip=203.0.113.5, status=failed"]
C --> F
D --> F
F(["Rules Engine"])
F --> G
G{"Rule 1000 - match: app=myapp"}
G -- "string not found" --> Z(["No Alert"])
G -- "string found" --> H
H{"Rule 1001 - if_sid: 1000 - field: status=failed"}
H -- "field missing - no decoder ran" --> Z
H -- "field exists" --> I
I{"Rule 1002 - if_sid: 1001 - same_field: user - frequency 3 in 60s"}
I -- "not yet 3 times" --> Z
I -- "3 failures same user" --> J
J(["ALERT level 10 - Brute force: alice failed 3 times"])
style A fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style B fill:#2a1f10,stroke:#ff6b35,color:#ff6b35
style C fill:#1a1f2e,stroke:#444,color:#888
style D fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style F fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style G fill:#2a1f10,stroke:#ff6b35,color:#c8cdd8
style H fill:#2a1f10,stroke:#ff6b35,color:#c8cdd8
style I fill:#2a1f10,stroke:#ff6b35,color:#c8cdd8
style J fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
style Z fill:#2e0d0d,stroke:#ff4444,color:#ff4444
Notice the branch at Rule 1001: if no decoder ran, the status field was never extracted. The <field name="status"> check silently fails — even though the raw log clearly contains status=failed. This is the most common reason rules appear to not work.
7. What Happens in Wazuh Without a Decoder?
Your rules still partially work — but only the tags that do not need extracted fields.
| Rule Tag | Without Decoder | Why |
|---|---|---|
<match>app=myapp</match> |
✅ Works | Raw substring search — no fields needed |
<if_sid>1000</if_sid> |
✅ Works | Rule chaining — no fields needed |
<field name="status">failed</field> |
❌ Silently fails | Field status was never extracted |
<same_field>user</same_field> |
❌ Silently fails | Field user was never extracted |
<list field="srcip">blocklist</list> |
❌ Silently fails | Field srcip was never extracted |
flowchart LR
A(["No Decoder"]) --> B["match\nraw string search"]
A --> C["if_sid\nrule chaining"]
A --> D["field\nextracted field"]
A --> E["same_field\nfield correlation"]
B --> F(["✅ Always works"])
C --> G(["✅ Always works"])
D --> H(["❌ Always fails"])
E --> I(["❌ Always fails"])
style F fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
style G fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
style H fill:#2e0d0d,stroke:#ff4444,color:#ff4444
style I fill:#2e0d0d,stroke:#ff4444,color:#ff4444
⚠️ Wazuh does not warn you when
<field>fails because no decoder ran. It simply does not match. This is why a rule that looks correct can silently never fire.
8. When Do You Actually Need a Wazuh Decoder?
Use this decision tree any time you are writing a new rule:
flowchart TD
Q(["What does your rule need to do?"])
Q --> A["Detect that a\nstring exists in the log"]
Q --> B["Match a specific\nfield value precisely"]
Q --> C["Correlate events\nacross multiple logs"]
Q --> D["Look up a value\nin a threat-intel list"]
Q --> E["Parse a structured format\nFortiGate or JSON or CEF"]
A --> A1(["❌ No decoder needed\nuse match"])
B --> B1(["✅ Decoder needed\nuse field"])
C --> C1(["✅ Decoder needed\nuse same_field\nor different_field"])
D --> D1(["✅ Decoder needed\nuse list field"])
E --> E1(["✅ Decoder needed\nuse decoded_as"])
style A1 fill:#0d2e1a,stroke:#00e5a0,color:#c8cdd8
style B1 fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style C1 fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style D1 fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
style E1 fill:#2e0d0d,stroke:#ff4444,color:#c8cdd8
Practical examples:
| What you want to detect | Needs decoder? |
|---|---|
Log line contains the word error |
❌ No — use <match>error</match> |
| Failed SSH login from a specific IP range | ✅ Yes — need srcip field |
| Same username fails login from two different IPs | ✅ Yes — need user and srcip fields |
| Source IP matches a known malicious IP list | ✅ Yes — need srcip field for <list> |
| FortiGate firewall blocked a connection | ✅ Yes — need decoder for FortiGate log format |
| Windows EventID 4625 (failed logon) fired | ❌ No — Wazuh’s built-in decoder already handles this |
9. Decoder Priority: When Built-in Decoders Take Over
Wazuh ships with built-in decoders for common log sources — SSH, Windows, FortiGate, Apache, and more. These run before your custom decoders.
This matters when you write a custom rule for a log source that Wazuh already knows about:
flowchart TD
A(["Log from a known source\ne.g. FortiGate with date= logid="])
A --> B{"Does a built-in\ndecoder match?"}
B -- "Yes" --> C["Built-in decoder fires\ne.g. fortigate-firewall-v6"]
B -- "No" --> D["Custom decoder checked"]
C --> E{"Does your custom rule\nchain off the right parent?"}
E -- "chains off built-in group\nif_group: fortigate" --> F(["✅ Rule fires correctly"])
E -- "chains off custom rule\nif_sid: your-base-rule" --> G(["❌ Parent never fired\nrule never matches"])
D --> H(["Custom decoder fires\nnormal flow"])
style A fill:#1a1f2e,stroke:#5b8fff,color:#c8cdd8
style B fill:#2a1f10,stroke:#ff6b35,color:#ff6b35
style C fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
style F fill:#0d2e1a,stroke:#00e5a0,color:#00e5a0
style G fill:#2e0d0d,stroke:#ff4444,color:#ff4444
style H fill:#1a2e1f,stroke:#00e5a0,color:#c8cdd8
The fix is to use <if_group> to chain off the built-in decoder’s rule group instead of your own base rule:
<!-- ❌ Wrong — your base rule never fires for FortiGate logs -->
<rule id="200">
<if_sid>100</if_sid> <!-- your custom base rule -->
<field name="action">deny</field>
</rule>
<!-- ✅ Correct — chains off the built-in fortigate group -->
<rule id="200">
<if_group>fortigate</if_group>
<field name="action">deny</field>
</rule>
10. Wazuh Decoder & Rule Tags: Quick Reference Cheatsheet
| Tag | Stage | Extracts Fields? | Requires Decoder? | Use For |
|---|---|---|---|---|
<prematch> |
Decoder | ❌ No | — | Fast pre-filter before regex |
<regex> + <order> |
Decoder | ✅ Yes | — | Extract named fields from log |
<match> |
Rule | — | ❌ No | Raw substring match |
<field> |
Rule | — | ✅ Yes | Match a specific extracted field value |
<decoded_as> |
Rule | — | ✅ Yes | Only match if a specific decoder ran |
<if_sid> |
Rule | — | ❌ No | Chain off a parent rule by ID |
<if_group> |
Rule | — | ❌ No | Chain off a rule group (e.g. built-in fortigate) |
<same_field> |
Rule | — | ✅ Yes | Correlate repeated events on same field value |
<different_field> |
Rule | — | ✅ Yes | Detect same user, different IP (impossible travel) |
<list field> |
Rule | — | ✅ Yes | Threat-intel CDB lookup on a decoded field |
Conclusion
The Wazuh pipeline is simple once you internalize the two-stage model:
- Decoders run first — they turn raw log text into named fields using
<prematch>(filter),<regex>(extract), and<order>(name). Only<regex>capture groups become fields.<prematch>extracts nothing. - Rules run second —
<match>works on raw text with no decoder needed;<field>,<same_field>, and<list>only work if a decoder already extracted those fields.
When a rule silently never fires, the cause is almost always one of these:
- You used
<field>but no decoder extracted that field - A built-in decoder fired before your custom one, breaking your
<if_sid>chain - Your
<order>count does not match your<regex>group count
Start with <match>-only rules to confirm your log is reaching Wazuh and your base rules are firing. Then add a decoder and <field> rules once you need precise field-level matching.
Frequently Asked Questions
Q: Does <prematch> create a named field I can use in rules?
No. <prematch> is a performance filter only — it checks whether a string exists in the log before running the regex. It extracts nothing. Fields are only created by <regex> capture groups mapped with <order>.
Q: Can a Wazuh rule fire without any decoder?
Yes, as long as it only uses <match> or <if_sid>. These operate on the raw log string and do not require decoded fields. Tags like <field>, <decoded_as>, <same_field>, and <list field> will silently fail without a decoder.
Q: What is <if_sid> used for in Wazuh?
<if_sid> chains a child rule to a parent rule by rule ID. The child only fires if the parent already matched on the same event. This is how Wazuh builds cascading detection — base rule matches the log source, child rules match specific conditions within it.
Q: What is the difference between <match> and <field> in a Wazuh rule?
<match> performs a substring search on the entire raw log line — no decoder needed. <field name="x"> matches against a specific named field that a decoder must have already extracted. Use <match> for broad detection; use <field> when you need precision on a particular value.
Q: How many capture groups can a Wazuh decoder have?
There is no hard limit documented, but the count of () groups in <regex> must exactly match the count of comma-separated names in <order>. A mismatch causes the decoder to silently fail with no error message.
Q: Why does my rule not fire even though the string is in the log?
The most likely causes: (1) you used <field> but no decoder extracted that field, (2) a built-in decoder fired first and broke your <if_sid> chain, or (3) your <prematch> string does not appear in the log. Use wazuh-logtest to trace the exact decoder and rule path for any log line.
Q: What is wazuh-logtest and how do I use it?
wazuh-logtest is a built-in Wazuh tool that lets you paste a raw log line and see exactly which decoder matched, which fields were extracted, and which rules fired. It is the fastest way to debug a decoder or rule that is not behaving as expected. Run it from the Wazuh manager CLI or from the Wazuh web UI under Tools → Logtest.
Part of the Detection Engineering series. Found an error or want to suggest an example? Open an issue or reach out.
Get in Touch with us
Related Posts
- Wazuh 解码器与规则:缺失的思维模型
- 为制造工厂构建实时OEE追踪系统
- Building a Real-Time OEE Tracking System for Manufacturing Plants
- The $1M Enterprise Software Myth: How Open‑Source + AI Are Replacing Expensive Corporate Platforms
- 电商数据缓存实战:如何避免展示过期价格与库存
- How to Cache Ecommerce Data Without Serving Stale Prices or Stock
- AI驱动的遗留系统现代化:将机器智能集成到ERP、SCADA和本地化部署系统中
- AI-Driven Legacy Modernization: Integrating Machine Intelligence into ERP, SCADA, and On-Premise Systems
- The Price of Intelligence: What AI Really Costs
- 为什么你的 RAG 应用在生产环境中会失败(以及如何修复)
- Why Your RAG App Fails in Production (And How to Fix It)
- AI 时代的 AI-Assisted Programming:从《The Elements of Style》看如何写出更高质量的代码
- AI-Assisted Programming in the Age of AI: What *The Elements of Style* Teaches About Writing Better Code with Copilots
- AI取代人类的迷思:为什么2026年的企业仍然需要工程师与真正的软件系统
- The AI Replacement Myth: Why Enterprises Still Need Human Engineers and Real Software in 2026
- NSM vs AV vs IPS vs IDS vs EDR:你的企业安全体系还缺少什么?
- NSM vs AV vs IPS vs IDS vs EDR: What Your Security Architecture Is Probably Missing
- AI驱动的 Network Security Monitoring(NSM)
- AI-Powered Network Security Monitoring (NSM)
- 使用开源 + AI 构建企业级系统













