Writing Sigma Rules: Turning an Attack into a Detection
Sigma rules give you one backend-agnostic detection format for log events. This guide turns a real attack into a tested rule, then converts it to Splunk and Elastic.
Sigma rules are the detection engineer's answer to a problem every blue team hits sooner or later: you work out how to spot an attack in your logs, write the query, and then discover it only runs on the one SIEM you happen to own. Sigma fixes that by describing the detection once, in plain YAML, in a format that is generic and backend-agnostic. You write the logic for a log event, and a converter turns it into Splunk SPL, an Elastic query, or whatever your stack speaks. This guide takes a single real attack, builds a rule for it from scratch, converts that rule to two backends, then tests and tunes it so it fires on the attack and stays quiet the rest of the time.
Read it top to bottom the first time to see the whole pipeline, from attack to shipped detection, then keep it open as a template when you write your own. Everything here is defensive work on logs from systems you run or are authorized to monitor.
These rules are for detecting activity in logs from a lab such as 10.10.10.0/24 or an environment you own or are authorized to defend. The attacker commands shown are illustrative context for the detection, not a how-to for use against systems you do not control.
01What Sigma is and why it beats per-SIEM rules
Sigma is a generic, backend-agnostic detection format for log events, written in YAML and maintained as an open standard. Think of it the way Snort rules describe network traffic or YARA rules describe files: Sigma describes a condition over log data without caring which product stores the logs. You express the match once, and tooling compiles it down to the query language of your SIEM.
The alternative is writing and maintaining a separate rule in every tool's native syntax. If you run Splunk and later add an Elastic cluster, every detection has to be rewritten and kept in sync by hand. Sigma collapses that into one source of truth. A rule you write today converts to SPL, to Elasticsearch Query String or ES|QL, to Microsoft Sentinel KQL, to QRadar AQL, and more, through a single converter. When the detection logic changes, you edit one file and recompile.
The second win is sharing. Because the format is vendor-neutral, security teams publish Sigma rules the same way they publish IOCs. A researcher who finds a new technique can ship a rule that any defender can convert and run, which is why the public rule set keeps pace with new tradecraft.
If you have ever written a SIEM query by hand and wished it ran everywhere, this is for you. You need basic familiarity with Windows event logs and a terminal with Python installed to follow the conversion steps.
02The anatomy of a Sigma rule
The anatomy of a Sigma rule is a small set of YAML keys, and once you have seen them once they recur in every rule. The file has metadata at the top, a log source, the detection body, and a few fields that tell a triage analyst what to do with an alert. Here is the shape before we fill it with a real attack.
title: Short human-readable name of the detection id: 0e3a1c66-6d2b-4f0a-9f8b-2b1d7c9e4a51 # a stable UUID status: test # experimental, test, stable, deprecated, unsupported description: What this rule detects and why it matters. references: - https://github.com/SigmaHQ/sigma author: 0xpriv.dev date: 2026-10-11 logsource: product: windows service: system detection: selection: EventID: 7045 condition: selection falsepositives: - Legitimate software that installs a service level: medium tags: - attack.persistence - attack.t1543.003
Each key earns its place. The list below is the contract every rule in the public repository follows, so learning it once lets you read any rule you find.
| Key | What it carries |
|---|---|
title | A short name an analyst reads in the alert queue. |
id | A UUID that stays constant for the life of the rule, so edits and tracking survive renames. |
status | Maturity: experimental, test, stable, deprecated, or unsupported. |
logsource | Which logs the rule reads, by category, product, and service. |
detection | One or more named search identifiers plus a condition that combines them. |
level | Severity: informational, low, medium, high, or critical. |
falsepositives | Known benign triggers, so triage starts with context. |
tags | Mostly MITRE ATT&CK mappings, such as a tactic and a technique ID. |
The two keys that do the real work are logsource and detection. The log source tells the converter which field names and which index or source type to target. The detection block holds named blocks of matches, called search identifiers, and a condition that stitches them together with boolean logic such as selection and not filter.
The tags field is where a rule connects to MITRE ATT&CK. A tag like attack.credential_access names the tactic and attack.t1558.003 names the exact technique, which lets a SOC map coverage to the ATT&CK matrix and see which techniques still have no detection.
03Field matching and value modifiers
Field matching is how a rule says what a value should look like, and value modifiers are the small suffixes that make a match flexible instead of exact. By default a field match is a case-insensitive equality: EventID: 4769 means the field equals that value. A modifier is appended to the field name with a pipe and changes how the comparison runs.
The brief ones you reach for constantly are below. A modifier chain reads left to right, so CommandLine|contains|all means every listed value must appear somewhere in the command line.
| Modifier | Meaning |
|---|---|
contains | The value appears anywhere inside the field. |
startswith | The field begins with the value (pair it with endswith for suffixes). |
all | Every value in the list must match, instead of the default any-of-list. |
re | The value is a regular expression matched against the field. |
Put together, a selection can be quite precise without being brittle. The block below matches an encoded PowerShell launch that writes into a temp directory, using three different modifiers at once.
detection:
selection:
Image|endswith: '\powershell.exe'
CommandLine|contains|all:
- '-enc'
- 'bypass'
TargetFilename|startswith: 'C:\Windows\Temp'
CommandLine|re: '(?i)downloadstring'
condition: selectionA list under a plain field (no all) is an OR: any one value matching is enough. Add all and it becomes an AND. That single switch is the difference between a noisy rule and a tight one, so it is worth being deliberate about which you mean.
04Writing your first rule for Windows service creation
Writing your first rule is easiest with a concrete attack, so we start with Windows service creation, event ID 7045. When a new service is installed, the Service Control Manager writes a 7045 event to the System log recording the service name and the image path it will run. Attackers install services for execution and persistence, and tools that drop a service often point the image path at a script interpreter or a file in a temp directory, which is exactly the signal a rule can read.
PS sc.exe create 0xpriv_svc binPath= "cmd.exe /c powershell -enc SQBFAFgA..." start= auto [SC] CreateService SUCCESS
Now build the rule step by step. The pattern is always the same: pin the log source, write a broad base match, add a tighter suspicious match, then combine them in the condition.
Set the log source
Event 7045 lives in the Windows System log under the Service Control Manager provider, so product: windows and service: system.
Match the event
A base selection on EventID: 7045 narrows to service installs, the population you want to reason about.
Add the suspicious signal
A second block matches image paths that contain an interpreter, an encoded flag, or a temp path, using contains with a list.
Combine and label
The condition requires both blocks, then tag the rule to ATT&CK technique T1543.003 and set a sensible level.
# 0xpriv.dev/defense title: Suspicious Windows Service Installation id: 9b0c1e74-3a5d-4c82-9f21-6b4e8a0d7c3f status: test description: Detects a new Windows service whose image path points at a script interpreter, an encoded command, or a temp directory, a common execution and persistence pattern. references: - https://github.com/SigmaHQ/sigma author: 0xpriv.dev date: 2026-10-11 logsource: product: windows service: system detection: selection: Provider_Name: 'Service Control Manager' EventID: 7045 suspicious: ImagePath|contains: - 'powershell' - 'cmd.exe /c' - '-enc' - '\Temp\' - '\Users\Public\' condition: selection and suspicious falsepositives: - Management agents or installers that legitimately register a service level: medium tags: - attack.persistence - attack.privilege_escalation - attack.t1543.003
That is a complete, valid rule. It reads like English, it names the technique it covers, and it already carries a note about the benign case a triage analyst will hit first.
05A second example: kerberoasting with RC4 tickets
Kerberoasting is a second example worth writing because it shows a rule built from exclusion rather than a suspicious-string match. In a kerberoasting attack, an authenticated user requests Kerberos service tickets for accounts that run services, then cracks the ticket offline to recover the service account password. The request is a normal event, 4769, so the signal is not the event itself but its shape: a ticket encrypted with RC4 (encryption type 0x17), for a real service account rather than a computer account, that succeeded.
The rule below pins those details, then uses two filter blocks and a condition that subtracts them. Machine accounts end in a dollar sign and the krbtgt account is expected, so both are excluded to cut the bulk of the noise.
title: Potential Kerberoasting via RC4 Service Ticket Request id: 4d7f2a19-8c63-4e05-b1a7-5f9c0e2d8b46 status: test description: Detects Kerberos TGS requests (event 4769) using RC4 encryption for service accounts, the core signal of a kerberoasting attack against Active Directory. references: - https://github.com/SigmaHQ/sigma - https://attack.mitre.org/techniques/T1558/003/ author: 0xpriv.dev date: 2026-10-11 logsource: product: windows service: security detection: selection: EventID: 4769 TicketEncryptionType: '0x17' TicketOptions: '0x40810000' Status: '0x0' filter_computer: ServiceName|endswith: '$' filter_krbtgt: ServiceName: 'krbtgt' condition: selection and not 1 of filter_* falsepositives: - Legacy services or applications that still negotiate RC4 encryption - Vulnerability scanners that enumerate service principal names level: high tags: - attack.credential_access - attack.t1558.003
Two things make this rule work. The encryption type 0x17 is RC4, which modern Active Directory rarely needs but kerberoasting tools request on purpose because RC4 tickets crack faster. And condition: selection and not 1 of filter_* uses a wildcard to fold in every block named filter_ something, so adding another exclusion later means adding a block, not editing the condition.
RC4 service tickets are a strong signal, but some environments still have legacy accounts negotiating RC4 for real work. Run this rule in audit mode first and list the service names it fires on, then exclude the known-good ones by name before it pages anyone.
06Converting a rule with sigma-cli and pySigma
Converting a rule is the step that turns YAML into something your SIEM runs, and the tooling for it is sigma-cli, a command-line front end for the pySigma library. pySigma is the engine that parses a rule, applies a processing pipeline that maps generic fields to a product's real field names, and emits the target query. sigma-cli is how you drive that engine without writing Python.
# install the CLI, then add the backends you need as plugins
pip install sigma-cli
sigma plugin install splunk
sigma plugin install elasticsearchBackends ship as plugins so you only install what you use. With the Splunk and Elasticsearch plugins in place, one rule compiles to either target by changing a flag. A processing pipeline, passed with -p, handles the field-name and log-source translation for Windows events.
# compile the service-creation rule to Splunk SPL sigma convert --target splunk --pipeline splunk_windows service_install.yml # compile the same rule to an Elastic query (Lucene / Query String) sigma convert --target elasticsearch --pipeline ecs_windows service_install.yml
$ sigma convert --target splunk --pipeline splunk_windows service_install.yml source="WinEventLog:System" EventCode=7045 Provider_Name="Service Control Manager" (ImagePath="*powershell*" OR ImagePath="*cmd.exe /c*" OR ImagePath="*-enc*" OR ImagePath="*\\Temp\\*" OR ImagePath="*\\Users\\Public\\*")
If you want the same logic inside Python, pySigma gives you direct control. The snippet below loads a rule, picks a backend, and prints the query, which is the whole pipeline in a few lines and the right place to script bulk conversion.
# convert one rule to Splunk with pySigma directly from sigma.collection import SigmaCollection from sigma.backends.splunk import SplunkBackend rules = SigmaCollection.load_ruleset(["service_install.yml"]) print(SplunkBackend().convert(rules)[0])
07Reading the Splunk and Elastic output
Reading the converted output matters as much as producing it, because a rule that compiles is not the same as a rule that runs cleanly in your environment. For Splunk, the converter targets a source type and emits SPL with the field names the pipeline mapped. For Elastic, the same rule becomes a query string that fits an alert or a saved search.
Notice that the field names in the output are not the ones you wrote. You wrote EventID, and the Splunk pipeline turned it into EventCode because that is what Splunk's Windows input calls it. The Elastic pipeline maps to Elastic Common Schema field names instead. That translation is the pipeline's job, and it is why picking the right pipeline for your data matters as much as the rule itself. If events are not matching after conversion, the pipeline mapping is the first thing to check.
08Testing a rule against real logs
Testing a rule means proving it fires on the attack and stays silent on everything else, before it ever reaches production. The fastest loop is to generate the event in a lab, confirm the raw log looks like you expect, then run the converted query against stored events and count the hits.
PS Get-WinEvent -LogName System -FilterXPath "*[System[EventID=7045]]" -MaxEvents 1 | Format-List ProviderName : Service Control Manager Id : 7045 Message : A new service was installed in the system. Service Name: 0xpriv_svc Service File Name: cmd.exe /c powershell -enc ...
With the event confirmed, run the converted query over a window that you know contains the test. A true positive proves the logic. The more useful number is what else the query returns over a normal day, because that is your false-positive rate in miniature.
$ source="WinEventLog:System" EventCode=7045 | stats count by ImagePath ImagePath count C:\Program Files\Agent\agent.exe 42 cmd.exe /c powershell -enc SQBFAFgA... 1
The one interesting row is the test. The 42 benign installs are the agent that runs in every environment, and they only survive because the base selection on 7045 is broad. The suspicious block filters them out in the real rule, which is the point of splitting the detection into two parts.
09Tuning to cut false positives
Tuning a rule is the work of pushing its false-positive rate down without blinding it to the attack, and it is where a detection either becomes something a SOC trusts or something they mute. The data from testing tells you exactly which benign events slip through, and each one gets handled with a named filter block rather than by weakening the core logic.
List what fired
Run the rule over a representative window and group the hits by the field that varies, such as image path or service name.
Separate benign from signal
Decide which rows are expected. These become exclusions; do not touch the part of the rule that catches the attack.
Add a filter block
Write the benign pattern as its own filter_ block and subtract it in the condition with and not.
Re-run and record
Confirm the attack still fires and the benign rows are gone, then note the exclusion in falsepositives so the next analyst has context.
# the service rule after tuning out the known-good management agent detection: selection: Provider_Name: 'Service Control Manager' EventID: 7045 suspicious: ImagePath|contains: - 'powershell' - 'cmd.exe /c' - '-enc' - '\Temp\' - '\Users\Public\' filter_agent: ImagePath|startswith: 'C:\Program Files\Agent\' condition: selection and suspicious and not filter_agent
Tune with exclusions, not by deleting values from the suspicious list. If you drop -enc because one tool uses it legitimately, you lose it against every attacker too. Excluding the specific benign path keeps the signal and removes only the noise, and it leaves an audit trail in the rule of why each exception exists.
10Where to get and contribute rules
Getting rules is mostly a matter of knowing about SigmaHQ, the project that maintains both the format and a large public rule set. The main repository holds thousands of community rules sorted by log source and technique, which means most common attacks already have a starting point you can convert and adapt rather than writing from zero.
# clone the public rule set and convert a folder of Windows rules at once git clone https://github.com/SigmaHQ/sigma.git sigma convert --target splunk --pipeline splunk_windows sigma/rules/windows/builtin/
Contributing back is the same workflow in reverse. When you write a rule that is not already covered, you can open a pull request against the SigmaHQ repository. The project runs automated checks on every submission: the rule must validate against the specification, use a unique UUID, and pass the format linter. Reading the specification repository before you contribute saves a round of review, because it documents every field and every modifier precisely.
The fastest way to write a good rule is to copy a published one for the same log source, keep its structure, and change the detection block. You inherit the correct field names and a sane set of tags for free.
11Detection engineering hygiene
Detection engineering hygiene is the set of habits that keep a rule set useful as it grows past a handful of files. The box below is the short version of what separates a maintained library from a pile of alerts nobody trusts.
12Frequently asked questions
Do I need to rewrite a Sigma rule for each SIEM?FAQ
No, that is the whole point. You write the detection once in YAML and use sigma-cli or pySigma to convert it to Splunk, Elastic, Sentinel, QRadar, and other backends. When the logic changes, you edit one file and recompile for every target.
What is the difference between sigma-cli and pySigma?FAQ
pySigma is the Python library that parses rules, applies field-mapping pipelines, and emits backend queries. sigma-cli is the command-line tool built on top of it, so you can convert rules with a single command instead of writing Python. Install backends and pipelines as plugins through the CLI.
Why does my converted query use different field names than my rule?FAQ
A processing pipeline maps the generic field names in your rule to the real field names your data uses. Splunk's Windows input calls the event id EventCode, while Elastic Common Schema uses its own names. If events are not matching after conversion, check that you selected the right pipeline for your data source.
How do the tags map a rule to MITRE ATT&CK?FAQ
Tags use a convention: a tactic such as attack.credential_access and a technique id such as attack.t1558.003. Tooling reads those tags to build a coverage matrix, so you can see at a glance which ATT&CK techniques your rule set detects and which are still blind spots.
What does status: test mean, and when should a rule be stable?FAQ
Status records maturity. Experimental is a first draft, test means it has run against real data but is still being tuned, and stable means it has proven itself with an acceptable false-positive rate. Promote a rule only once testing backs the claim, and deprecate rules that no longer fire on relevant activity.
13Wrapping up
Sigma rules turn a one-off SIEM query into a portable detection you write once and run anywhere. The pipeline is short to describe and worth doing in order: understand the attack in your logs, write the rule with the right log source and modifiers, map it to ATT&CK, convert it with sigma-cli, then test and tune until it fires on the attack and almost nothing else. Start from the public SigmaHQ rules, keep your own set in version control, and contribute back the detections that are missing. Do that and your detection logic stops being locked to one vendor and starts being an asset you can move, share, and trust.
references
Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.