learn-cyber · lesson 5 · detection engineering
Inside the manager's brain — how a raw log line becomes a field, a field becomes a rule match, and a rule match becomes the alert you get paid to read.
Three concrete abilities:
wazuh-logtest — your REPL for
detection — and watch which decoder and rule it hits.In Lesson 1 you learned the shape: event → decode → match → alert. Now we zoom into the decode and match steps, because that's where detection actually lives. A single raw line takes this journey on the manager:
Memorize this sub-pipeline. When a detection misbehaves, your first question is always "did it fail to decode, or did it decode fine but no rule matched?" Those are two completely different fixes.
Before any of your logic runs, Wazuh automatically pulls the standard fields out of the log header: the timestamp, the hostname, and the program name. You don't write anything for this — it just happens.1 Take a typical line from a hotel's front-desk Linux box:
May 28 14:02:11 frontdesk-01 sshd[4123]: Failed password for admin from 203.0.113.9 port 22 ssh2
Pre-decoding extracts, with zero configuration from you:
timestamp: May 28 14:02:11
hostname: frontdesk-01
program: sshd
A decoder does two things: it
identifies which program produced the line (so it knows the
format), then it parses the message body into named fields.1 For the line above, the sshd decoder pulls:
srcuser: admin
srcip: 203.0.113.9
That's the whole point of decoding: it turns an unstructured sentence
(Failed password for admin from 203.0.113.9) into structured fields
a rule can reason about. Before decoding, a rule can only do crude text matching.
After decoding, a rule can say "alert if srcip is on my threat list"
or "count distinct srcuser values" — real logic.
Wazuh ships decoders for hundreds of common products (SSH, sudo, web servers, Windows events, many firewalls). Your hotel's off-the-shelf systems are mostly covered. The gap — and your opportunity — is the hotel's custom app log, which we'll handle below.
A rule is XML that inspects decoded fields and decides two things: do we alert, and at what level.2 Severity runs from 0 to 15: level 0 means "ignore, don't alert," and higher numbers mean more serious. Every rule has a numeric ID. Wazuh ships thousands of built-in rules, so a lot of detection works the moment you install it.
Here's a simplified built-in-style rule, the kind that fires on that single failed SSH password. Read it like a sentence:
<rule id="5710" level="5">
<decoded_as>sshd</decoded_as>
<match>Failed password</match>
<description>sshd: authentication failed</description>
</rule>
"For anything decoded as sshd, if the text contains
Failed password, raise a level-5 alert called authentication
failed." Useful, but one failed login is barely worth a glance. The
real power is in four features you'll lean on constantly:
Instead of crude text matching, target the named fields the decoder produced. This rule cares only about the admin account:
<field name="srcuser">^admin$</field>
if_sidYou rarely start from scratch. <if_sid> says "only consider
this rule if rule N already matched," letting you refine a generic
built-in into something hotel-specific. <if_matched_sid> is its
correlation cousin — "fire only if rule N matched recently"
(used with a timeframe, below).
This is how "5 failed logins in 120 seconds → brute force" actually works. You build a rule on top of the single-failure rule and tell it to count:
<rule id="5712" level="10" frequency="5" timeframe="120">
<if_matched_sid>5710</if_matched_sid>
<description>sshd: possible brute-force (5 failures in 120s)</description>
<mitre><id>T1110</id></mitre>
</rule>
One failed login stays a quiet level-5. Five within two minutes escalates to a loud level-10 brute-force alert. Same events, far more meaning — that's correlation compressed into two attributes.
mitreThe <mitre> tag stamps the rule with a
MITRE ATT&CK technique ID
(T1110 = Brute Force). More on why that matters at the end.
This is the billable skill. Suppose a boutique chain runs a homegrown PMS whose audit log writes lines like:
2026-05-28T14:30:02 PMS-AUDIT user=jdoe action=EXPORT records=5000 module=guest_profiles
No built-in decoder knows this format, so Wazuh can't extract
action or records yet. You teach it. There is one
non-negotiable rule:
/var/ossec/etc/decoders/local_decoder.xml and
/var/ossec/etc/rules/local_rules.xml — those survive upgrades and
are the supported place for customizations.3
local_decoder.xml)First, identify the program and pull the fields you care about:
<decoder name="hotel-pms-audit">
<prematch>PMS-AUDIT </prematch>
</decoder>
<decoder name="hotel-pms-audit-fields">
<parent>hotel-pms-audit</parent>
<regex>user=(\S+) action=(\S+) records=(\d+)</regex>
<order>srcuser,action,records</order>
</decoder>
Now Wazuh extracts srcuser=jdoe, action=EXPORT,
records=5000 from that line.
local_rules.xml)A bulk export of guest profiles by a single user is exactly the kind of data-theft signal a hotel needs caught — it threatens guest PII and the PCI DSS obligations they're paying you to help with:
<group name="hotel,pms,">
<rule id="100100" level="12">
<decoded_as>hotel-pms-audit</decoded_as>
<field name="action">^EXPORT$</field>
<field name="records">^[1-9]\d{3,}$</field>
<description>PMS: bulk guest record export by single user</description>
<mitre><id>T1530</id></mitre>
</rule>
</group>
"If a line decoded as our PMS audit log shows action=EXPORT with
1,000+ records, raise a level-12 alert and tag it
T1530 (Data from Cloud/Local Storage)." You could just as easily
write a sibling rule that elevates severity when a known POS process is
tampered with — same pattern, different fields.
100100 here is just a free slot you picked.
wazuh-logtest (your REPL)You don't deploy a detection and hope. Wazuh gives you a tight feedback loop: paste a raw log line and it shows you the decoder that matched, the fields it extracted, and the rule that fired at what level.4
sudo /var/ossec/bin/wazuh-logtest
Then paste your PMS line and read the output:
**Phase 1: Completed pre-decoding.
timestamp: '2026-05-28T14:30:02'
**Phase 2: Completed decoding.
name: 'hotel-pms-audit'
srcuser: 'jdoe'
action: 'EXPORT'
records: '5000'
**Phase 3: Completed filtering (rules).
id: '100100'
level: '12'
description: 'PMS: bulk guest record export by single user'
mitre.id: 'T1530'
Notice the three phases map exactly onto the sub-pipeline at the top: pre-decode, decode, rule match. If Phase 2 shows no fields, your decoder is wrong. If Phase 2 is fine but Phase 3 fires nothing, your rule is wrong. That's the diagnosis the tool hands you for free — treat it as the REPL of detection engineering: edit, paste, observe, repeat.
Editing the local_*.xml files on disk does not reload
them by itself. After you're happy with a change, apply it with
sudo systemctl restart wazuh-manager. wazuh-logtest
reads the current ruleset, so test first, restart once.
That <mitre> tag isn't decoration. When a rule fires, the
alert carries the technique ID, so instead of "weird PMS export thing" you can
tell the hotel — or another analyst — "we detected T1530, theft of stored
data."5 ATT&CK is the common language
defenders share, and speaking it makes your reports credible and your handoffs
clean. For a hotel owner, "mapped to MITRE ATT&CK" is also a phrase that
sounds like the professional service they're buying.
wazuh-logtest
open beside it — that's the loop the pros actually work in.
Retrieval practice — answer from memory before peeking. The struggle is what makes it stick.
A hotel's custom PMS line decodes fine in Phase 2 but
wazuh-logtest fires no rule in Phase 3. What is broken?
Phase 2 showing fields means the decoder worked. No alert in Phase 3 means no rule matched those fields — so the fix is in your rule (wrong field name, wrong pattern, or no rule written yet), not the decoder.
You want one failed PMS login to stay quiet but five within two minutes to alert loudly. Which rule attributes do that?
frequency="5" timeframe="120" tells the rule to
fire only after five matching events inside 120 seconds — that's correlation,
exactly how brute-force detection escalates many small events into one signal.
Where do you put a brand-new rule for a hotel's custom app, and what ID range should it use?
Custom rules live in
/var/ossec/etc/rules/local_rules.xml (built-in files get
overwritten on upgrade) and use IDs ≥ 100000 so they never clash with Wazuh's
reserved built-in numbering.
if_sid, if_matched_sid, frequency/timeframe).local_rules.xml /
local_decoder.xml; don't edit built-ins; custom IDs ≥ 100000).wazuh-logtest.prematch vs
regex, or when to use if_sid vs
if_matched_sid? Want me to invent a tricky hotel log line and have
you write the rule? Ask in the chat — building and testing detections together
is where this skill actually forms.
You just earned: the ability to read a Wazuh rule, trace a
log line through pre-decode → decode → rule match with
wazuh-logtest, and sketch a safe custom decoder + rule (in the
local files, IDs ≥ 100000, ATT&CK-tagged) for a hotel's own log source.
Up next (Lesson 6): you can write detections — now organize them per hotel. Multi-tenancy with agent groups: one Wazuh deployment cleanly watching many hotels without mixing their data, configs, or alerts.
← Previous: Lesson 4 — Lab: your first agent & detection
Reference: Glossary · All resources · Mission