Host-Based Detection Part 7: How a Log Line Becomes an Alert
Everything in this series so far has referred to “an alert” without explaining where one actually comes from. It’s a two-stage pipeline, decode then match, and Wazuh’s own default ruleset does most of the work before you ever write a line of custom configuration.
Decoders: turning raw text into fields
A decoder’s job is purely extraction: take an unstructured log line and pull named fields out of it, a source IP, a username, a program name, using a prematch pattern to confirm the log is even the right shape, then one or more child decoders with regexes and an <order> tag naming which capture group becomes which field. A representative example, decoding a Fortigate-style log line:
<decoder name="fortigate-custom">
<prematch>^date=\d\d\d\d-\d\d-\d\d time=\d\d:\d\d:\d\d devname="\S+"</prematch>
</decoder>
<decoder name="fortigate-custom1">
<parent>fortigate-custom</parent>
<regex>^date=(\d\d\d\d-\d\d-\d\d) time=(\d\d:\d\d:\d\d) devname="(\S+)"</regex>
<order>date, time, devname</order>
</decoder>
The prematch confirms the line starts with the expected header, the child decoder’s regex captures date, time, and devname as named fields. Nothing here decides whether the line is interesting. That’s the rule’s job.
Rules: deciding what matters
A rule matches against decoded fields, not raw text, and assigns a severity level when it fires:
<rule id="222000" level="3">
<decoded_as>fortigate-custom</decoded_as>
<description>Fortigate messages grouped.</description>
</rule>
decoded_as ties the rule to a specific decoder’s output. if_sid builds composite rules that only evaluate once an earlier rule ID has already matched, which is how Wazuh chains a specific, narrow match, a single failed login, into a broader one, ten failed logins from the same source in two minutes, without re-parsing the raw log each time. if_level does the same thing against a severity level rather than a specific rule ID.
Severity itself runs 0 to 16, and the shape of that scale matters more than any individual number. 0 is explicitly “ignored, used to avoid false positives.” 3 covers successful, authorized, entirely unremarkable events like a normal login. 8 marks the first time something’s been seen at all, a new user, a new source. 10 is repeated failures suggesting a real attack or a genuinely broken credential. 12 to 14 climb through system-level and correlated security events, and 15 is reserved for cases the documentation describes as having “no chances of false positives, immediate attention necessary.” Level 16 exists but ships unused in the default ruleset entirely, reserved for custom rules a specific environment considers maximally severe.
Real evidence: it already works for the boring stuff
From Dropbox to Daily Driver Part 7 is the concrete demonstration this series keeps coming back to. A few ordinary SSH sessions against that Pi produced correctly classified alerts for sshd: authentication success (rule 5715), PAM: Login session opened and Login session closed (5501 and 5502), and Successful sudo to ROOT executed (5402), every one of them level 3, and every one of them matched without a single custom decoder or rule written. Wazuh’s default ruleset already ships decoders for sshd, PAM, and sudo, which is precisely why enrolling an agent, Part 5 of this series, was enough on its own to get real, correctly-classified visibility into every login and privilege escalation on that device.
Shipped rules aren’t infallible either
The same real run surfaced the other side of this. A shipped SCA policy check, a different kind of rule, but the same trust-then-verify lesson applies, had a genuine bug: one check double-escaped a regex pattern (\\s) where a sibling check used single-escaping (\s), so an identically correct banner passed under one check and failed under the other, a confirmed bug in the policy file rather than a real gap on the system. Part 6 of this series covers the SCA side of that finding in full. The lesson generalises past SCA specifically: a rule or decoder shipped by the vendor is a strong starting point, not ground truth, and the only way to tell the difference between a real finding and a broken check is to verify directly against the live system, exactly as this series’ source posts already did.
Sources:
- Creating decoders and rules from scratch (Wazuh)
- Rules classification (Wazuh documentation)
- Rules Syntax (Wazuh documentation)