learn-cyber · lesson 5 · detection engineering

How detection really works

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.

Your mission Every hotel you onboard has at least one weird log source — their specific PMS, their POS, a booking-engine integration nobody else runs. Monitoring those is exactly what they can't buy off the shelf. By the end of this lesson you can read a Wazuh rule, test a log line, and see how you'd write a custom detection for a quirky hotel app — a core billable skill.

The win for this lesson

Three concrete abilities:

Recap: the middle of the pipeline

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:

Raw lineThe log exactly as written
Pre-decodeHeader: time, host, program
DecodeBody fields: user, srcip, action
Rule matchBad? How bad? Which level?
AlertRaised for a human

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.

Step 1 — Pre-decoding (the free part)

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
Key idea Pre-decoding reads the envelope; decoding reads the letter. The header (who/when/which program) comes for free. The meaningful content inside the message — who tried to log in, from where — is the decoder's job.

Step 2 — Decoding (extracting the meaning)

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.

Step 3 — Rules (the decision)

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:

Matching on decoded fields

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>

Building on parent rules: if_sid

You 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).

Frequency + timeframe (this is correlation)

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.

ATT&CK tagging: mitre

The <mitre> tag stamps the rule with a MITRE ATT&CK technique ID (T1110 = Brute Force). More on why that matters at the end.

Key idea A rule is just: which decoded fields to look at + what counts as bad + how bad (level) + optionally how often (frequency/timeframe). Read any Wazuh rule by answering those four questions and you understand it.

Step 4 — Custom rules & decoders for a hotel's own app

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:

Never edit the built-in ruleset Built-in decoders and rules get overwritten on every Wazuh upgrade. Put your work only in /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

The custom decoder (in 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.

The custom rule (in 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.

Custom IDs start at 100000 Always give custom rules an ID of 100000 or higher. Lower numbers are reserved for Wazuh's built-in ruleset, and reusing one would clash on the next upgrade. 100100 here is just a free slot you picked.

Step 5 — Test with 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.

Step 6 — Why ATT&CK tags matter to the business

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.

Primary source to read next
Wazuh — Ruleset (decoders → rules → alerts). Read the overview, then open Custom rules & decoders and try writing one local rule yourself. Keep wazuh-logtest open beside it — that's the loop the pros actually work in.

Check yourself

Retrieval practice — answer from memory before peeking. The struggle is what makes it stick.

Question 1 of 3

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.

Question 2 of 3

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.

Question 3 of 3

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.

Sources

  1. Wazuh — Decoders (pre-decoding of the header; decoders identify the program and parse the body).
  2. Wazuh — Ruleset (rules inspect decoded fields, set levels 0–15, carry IDs; if_sid, if_matched_sid, frequency/timeframe).
  3. Wazuh — Custom rules & decoders (use local_rules.xml / local_decoder.xml; don't edit built-ins; custom IDs ≥ 100000).
  4. Wazuh — Testing your rules with wazuh-logtest.
  5. Wazuh — MITRE ATT&CK module (tagging rules with technique IDs).
I'm your teacher — ask me anything. Want to walk through writing a decoder for a real POS log format? Confused about 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