You’ve been there. It’s 2:00 AM, and you’re staring at a wall of raw text in your archives.log. You know for a fact that a proprietary application is being hammered by a brute-force attack, but your Wazuh dashboard is silent. No alerts, no red flags, nothing. You’ve got the logs, but to Wazuh, they are just meaningless strings of characters. This is the “Silent SIEM” trap, and it’s where most security operations stall. You’ve installed the agents, you’re collecting the data, but you haven’t yet taught the brain of the system how to read.

The frustration is real because the stakes are high. An unparsed log is a blind spot, and a blind spot is an invitation. When you can’t extract a srcip or a user from a custom internal API log, you can’t correlate events, you can’t trigger active responses, and you certainly can’t map the activity to the MITRE ATT&CK framework. You’re flying blind with a very expensive flight recorder that no one knows how to read. We’re going to fix that today. We’re going to move past the “out of the box” limitations and turn you into a detection engineer who can make Wazuh dance to any log format imaginable.

Direct Practical Verdict:

To master Wazuh detection, you must implement a two-phase pipeline: first, use XML decoders with hierarchical parent-child relationships to extract raw data into structured fields via PCRE2 regex. Second, apply rules that utilize these fields to trigger alerts, using frequency and timeframe tags to filter noise and mapping every alert to specific MITRE ATT&CK techniques for actionable intelligence.

Why Are My Logs Disappearing Into the Void?

The most common misunderstanding I see among junior SOC analysts is the belief that “ingesting” a log is the same as “analyzing” it. When a Wazuh agent forwards a log—whether it’s a syslog entry, a JSON blob, or a Windows Event—it arrives at the Wazuh Manager as a raw string. The Manager’s analysis engine is a two-phase machine. If Phase 1 (Decoding) fails to identify the log format and extract key variables, Phase 2 (Rule Matching) never even stands a chance.

Think of the Decoder as a translator. If the translator doesn’t recognize the language, they can’t tell the commander (the Rules engine) what was said. Most people rely on the default decoders located in /var/ossec/ruleset/decoders/. These are great for SSH, Apache, and Windows, but they are useless for your company’s custom-built financial portal or that obscure IoT gateway you just deployed. If you don’t write a custom decoder, your logs are just taking up disk space without providing a shred of security value.

The Two-Phase Engine: How Wazuh Actually Thinks

To build effective detections, you have to understand the sequence of events inside the Manager. When a log hits the analysisd process, it goes through:

1. Pre-decoding: This is the hard-coded phase. Wazuh looks for common headers like timestamps, hostnames, and program names (e.g., sshd, sudo). If it finds them, it populates the basic fields automatically.
2. Decoding: This is where your custom XML comes in. Wazuh scans its decoder list to find a prematch. Once a parent decoder matches, it looks for “sibling” or “child” decoders to extract specific data like srcip, dstport, user, or status using regular expressions.
3. Rule Evaluation: Once the fields are extracted, the engine looks at the rules. It checks the decoded_as field or the program_name and then evaluates the logic (e.g., “If status is ‘failed’ and frequency is > 5 within 60 seconds, trigger a Level 10 alert”).

If you skip the decoder and try to write a rule that matches a raw string using <match>, you are making the engine work ten times harder than it needs to. Worse, you lose the ability to do advanced correlation.

The Parent-Child Trap: Why Your Custom Decoders Are Failing

The biggest mistake I see in production environments is the “Flat Decoder” approach. Engineers try to write one massive, complex regex that captures everything in a single go. This is a nightmare to maintain and a performance killer. Wazuh is designed for a hierarchical approach.

You should always define a Parent Decoder that identifies the “source” or “type” of the log using a simple <prematch>. Once that parent is triggered, Wazuh only looks at the children associated with that parent. This keeps the regex engine lean and fast. If you have 500 decoders and they are all flat, Wazuh has to test every single one against every single log. If you have 1 parent with 499 children, Wazuh tests the parent once, and then only dives into the children if the parent matches.

Feature Default Decoders Custom Decoders Correlation Rules Best Practice
Primary Role Standard OS/App parsing Proprietary/Internal log parsing Identifying patterns over time Use Custom for all non-standard apps
Regex Logic Pre-defined, static PCRE2 (Flexible) Field-based matching Keep regex simple and anchored
Performance High (Optimized) Variable (Depends on Regex) Moderate (Memory intensive) Use parent-child hierarchy
Alert Fatigue High (Generic alerts) Low (Specific to your env) Very Low (Filters noise) Combine custom rules with frequency

Building Your First Production-Grade Decoder From Scratch

Let’s get practical. Imagine you have a custom web application called “App-X” that generates logs like this:
2023-10-27 14:00:01 App-X: LOGIN_FAILED user=admin source=192.168.1.50

Wazuh won’t know what to do with this by default. Here is how you build the detection pipeline.

Step 1: The Parent Decoder

Create a new file in /var/ossec/etc/decoders/local_decoders.xml. We start with the parent to identify the application.


<decoder name="app-x">
<prematch>^App-X:</prematch>
</decoder>

The <prematch> tag is the anchor. If a log doesn’t start with “App-X:”, Wazuh ignores this decoder and moves on. This is the efficiency I was talking about.

Step 2: The Child Decoder

Now we extract the variables. We want the status, the username, and the source IP.


<decoder name="app-x-details">
<parent>app-x</parent>
<regex>(\S+) user=(\S+) source=(\S+)</regex>
<order>status, user, srcip</order>
</decoder>

The <parent> tag links this to our first decoder. The <regex> uses capture groups () to grab data, and the <order> tag maps those groups to Wazuh internal field names.

Writing Rules That Don’t Wake You Up at 3:00 AM

Now that we have structured data (status, user, srcip), we can write a rule. Don’t just alert on every failure; that’s how you get “alert fatigue” and start ignoring your dashboard. We want to alert on threats, not mistakes.

In /var/ossec/etc/rules/local_rules.xml:


<group name="app-x,security_event,">
<rule id="100001" level="3">
<decoded_as>app-x</decoded_as>
<description>App-X log received.</description>
</rule>

<rule id="100002" level="5">
<if_sid>100001</if_sid>
<field name="status">LOGIN_FAILED</field>
<description>App-X: Failed login attempt for user $(user).</description>
<mitre>
<id>T1110</id>
</mitre>
</rule>

<rule id="100003" level="10">
<if_sid>100002</if_sid>
<frequency>5</frequency>
<timeframe>60</timeframe>
<same_source_ip />
<description>App-X: Brute force attack detected from $(srcip).</description>
<mitre>
<id>T1110.001</id>
</mitre>
</rule>
</group>

Notice the hierarchy here. Rule 100001 just identifies the log. Rule 100002 identifies a single failure (Level 5). Rule 100003 is the “money” rule. It only triggers if Rule 100002 happens 5 times from the same IP within one minute. This is how you separate a user forgetting their password from a botnet trying to crack your gates.

Testing and Validating Your Logic Before Deployment

Never, and I mean never, restart your Wazuh Manager after writing a new rule without testing it first. A single syntax error in an XML file can prevent the Manager from starting, leaving your entire infrastructure unmonitored.

The tool you need is wazuh-logtest. It is the most powerful weapon in your arsenal. Run it from the command line on your Manager:

/var/ossec/bin/wazuh-logtest

Paste your raw log string into the prompt. The tool will show you exactly how Phase 1 and Phase 2 handle the log. It will tell you:
1. Which decoder matched.
2. Which fields were extracted.
3. Which rule triggered.

If the output says “Phase 3: Completed: No rules matched,” you know your logic is flawed. If it shows the fields you expected and the Level 10 alert you designed, you’re ready for production.

The Actionable Playbook: Implementing Custom Detections Today

Follow this checklist to clear your backlog of unparsed logs and harden your detection capabilities.

1. Audit Your Archives: Enable <logall>yes</logall> in your ossec.conf for 24 hours. Look at /var/ossec/logs/archives/archives.log to find logs that are currently being ignored (they won’t have a rule ID associated with them).
2. Identify the Anchor: For each unparsed log, find a unique string that identifies the application (e.g., “Database-Auth:”, “Firewall-Drop”).
3. Create the Parent Decoder: Write a simple decoder using <prematch> for that anchor in local_decoders.xml.
4. Map the Variables: Use wazuh-logtest to see the raw log and write your <regex> and <order> tags to extract srcip, user, and action.
5. Define the Baseline Rule: Write a Level 3 rule that simply matches the decoded_as name.
6. Layer the Intelligence: Write child rules (using <if_sid>) for specific security events like “Access Denied” or “Privilege Escalation.”
7. Apply Correlation: Use <frequency> and <timeframe> to escalate repetitive low-level events into high-level security alerts.
8. Map to MITRE: Add <mitre><id>TXXXX</id></mitre> tags to every alert rule. This allows you to use the MITRE ATT&CK dashboard in Wazuh to see where your environment is most vulnerable.
9. Validate and Restart: Run /var/ossec/bin/wazuh-logtest one last time, then restart the manager with systemctl restart wazuh-manager.

By following this process, you move from being a passive observer of logs to an active architect of security intelligence. You aren’t just collecting data; you’re creating a system that understands its environment. That is the difference between a tool and a solution.

Frequently Asked Questions

Why should I use custom decoders instead of just using the ‘match’ tag in rules?
Using the <match> tag in rules performs a simple string search on the entire log, which is computationally expensive and doesn’t extract variables. Custom decoders break the log into structured fields (like srcip), allowing you to use advanced rule features like <same_source_ip/> and <frequency>, which are impossible with simple string matching.

What is the difference between PCRE and the regex used in Wazuh?
Wazuh primarily uses a subset of POSIX regular expressions by default, but it also supports PCRE2 (Perl Compatible Regular Expressions) which is much more powerful. For most custom decoders, PCRE2 is preferred for its ability to handle complex patterns and non-greedy matching.

How do I handle JSON logs in Wazuh?
Wazuh has a built-in JSON decoder. If your logs are sent as valid JSON, Wazuh will automatically parse the key-value pairs into fields prefixed with json. (e.g., json.username). You don’t need a custom decoder for these; you can write rules directly targeting those json. fields.