Project Name

Sigma Detection Lab: Authoring and Validating Sigma Rules Against Real Telemetry

One-line Summary

I wrote a Sigma rule for Linux account discovery, converted it into a backend query with sigma-cli, and followed it all the way through a home lab pipeline (auditd, Filebeat, Wazuh) until it actually fired on real telemetry.

Full Description

Writing Sigma YAML on its own doesn't prove much. What I wanted was the full loop. Pick an ATT&CK technique, produce the real behaviour on a Linux host, write the rule against the fields the logs actually contain, convert it with pySigma, and then confirm it in a SIEM. That last step is where detection engineering either holds up or falls apart, so it's the part I cared about most.

I used an Ubuntu VM as the monitored host and ran auditd on it for kernel level audit logging. For the SIEM I began with a local Elastic Stack (start-local) and Filebeat, then moved over to Wazuh once that first setup started fighting me. Wazuh was already built for auditd and came with MITRE ATT&CK mapping out of the box, so it fit the goal better.

Lab Architecture

The Detection-Engineering Loop

  1. Pick a technique from MITRE ATT&CK that fits a Linux host
  2. Produce the behaviour on the lab host and confirm auditd really records it
  3. Write the Sigma rule against the field structure the logs actually show
  4. Convert the rule into a backend query with sigma-cli
  5. Validate by running the query, watching it fire, and tuning out the false positives

Detection 1: Sensitive File Access on /etc/passwd

/etc/passwd holds every local user account: the name, UID, home directory, and shell. Once an attacker has a foothold, reading this file is a common way to list accounts and pick out privileged users before moving laterally or escalating. It maps to T1087.001, Account Discovery: Local Account.

An auditd watch rule tags any access to the file with the key passwd_watch:

sudo auditctl -w /etc/passwd -p rwa -k passwd_watch

A plain read is enough to trigger it:

sudo cat /etc/passwd

Detection Logic

detection:
    selection:
        auditd.log.key: 'passwd_watch'
    filter_config_change:
        auditd.log.record_type: 'CONFIG_CHANGE'
    condition: selection and not filter_config_change

The rule matches events tagged passwd_watch but throws out CONFIG_CHANGE events. That exclusion is where the interesting part of this project actually began.

Why the False-Positive Filter Exists

My first version of the rule used nothing but auditd.log.key: passwd_watch. It returned events in testing, which looked like success until I opened them up. They were CONFIG_CHANGE and op=add_rule records: auditd logging the moment the watch rule was created and removed, purely because the string passwd_watch lives inside the rule definition. None of them were file access.

That turned out to be the real lesson of the whole project. Getting results back from a search is not the same as detecting the behaviour you meant to catch. The filter_config_change clause strips that noise out so the rule reflects someone actually touching the file rather than me editing audit rules.

The false positives worth expecting in production:

Conversion

Converted with sigma-cli against the Lucene backend:

sigma convert -t lucene --without-pipeline rules/linux_sensitive_file_access_passwd.yml

The filter carries straight through into the final query:

auditd.log.key:passwd_watch AND (NOT auditd.log.record_type:CONFIG_CHANGE)

Detection Summary

Field Value
Technique T1087.001, Account Discovery
Log Source linux/auditd
Detection Key passwd_watch
Backend Lucene (Wazuh / Elasticsearch)
Status Experimental

Pipeline: Getting Telemetry to Actually Flow

Writing the rule was the quick part. The worst bug in this project had nothing to do with detection logic at all. It was a Filebeat version mismatch. The Wazuh indexer runs on OpenSearch and expects Filebeat OSS 7.10.2, but Filebeat had already been bumped to 9.x during the earlier Elastic Stack work. The newer Filebeat looked at the indexer, decided it wasn't a compatible Elasticsearch, and quietly refused to index anything, all while /var/ossec/logs/alerts/alerts.log kept filling up on the manager side. So the manager was clearly working, yet the dashboard stayed empty.

I found it by reading the shipper logs and following the _license and unexpected build flavor errors back to their cause. The fix was to remove Filebeat 9.x, install filebeat 7.10.2-1 from the Wazuh APT repo, and reapply the Wazuh config with the right host, TLS certs, and admin credentials. Alerts began flowing the moment it restarted, and the MITRE ATT&CK panel filled in with real hits (Sudo mapping to T1548, plus Valid Accounts).

Key Lessons

Known Limitations

On this test kernel (Ubuntu 26.04, kernel 7.x), auditd read auditing through -w was unreliable. Read syscalls often went unrecorded even though the watch rule's own CONFIG_CHANGE events came through fine. A later revision of this detection will watch the openat syscall on /etc/passwd directly instead, which should hold up better.

Tools & Technologies Used

Key Skills Demonstrated

The rule itself comes down to a handful of lines of YAML. The real work was making it mean something, so it matched genuine behaviour instead of my own rule editing, and getting it to travel cleanly through a pipeline that kept breaking in unglamorous places.

View on GitHub
Wazuh dashboard with the MITRE ATT&CK panel populated after fixing the Filebeat pipeline Detail view of a passwd_watch event showing the auditd fields passwd_watch events appearing in Discover after triggering the behaviour Terminal running sigma convert to turn the Sigma rule into a Lucene query