Writing a Sigma rule from scratch, field by field
HuntRule Team · · 8 min read

On this page
The three keys that must be there
A Sigma rule has exactly three mandatory keys: title, logsource and detection. Everything else is optional in the specification, which is why so many first drafts parse cleanly and still get rejected in review. The current spec is version 2.1.0, released 2025-08-02 by SigmaHQ. Here is the minimal example the specification itself ships, with the reference URL trimmed.
title: Suspicious Whoami Execution Detected (via process_creation)
description: Detects a whoami.exe execution
author: HuntRule Team
logsource:
category: process_creation
product: windows
detection:
selection:
Image: 'C:\Windows\System32\whoami.exe'
condition: selection
level: highFile conventions matter because tooling and repositories enforce them. UTF-8 encoding, LF line breaks, four space indentation, lowercase keys, single quotes for strings, no quotes for numbers. Filenames run 10 to 70 characters, lowercase letters and digits only, underscores instead of spaces, and end in .yml. A name like proc_creation_win_powershell_download_cradle.yml passes.
Choosing a log source
The logsource block has three evaluated attributes: category, product and service. Category selects a logical group of logs such as process_creation, firewall or webserver. Product selects everything from an operating system or software package, for example windows. Service narrows to a single channel, for example sshd on Linux or the Security event log on Windows.
Values are lowercase with spaces replaced by underscores. The spec gives the mapping rules directly: the Windows channel System becomes service: system, Process Creation becomes category: process_creation, and Microsoft-Windows-Windows Firewall With Advanced Security becomes service: firewall-as. The point of this abstraction is that one rule covers Sysmon, Microsoft-Windows-Security-Auditing and Microsoft-Windows-Kernel-Process without three separate copies.
Do not invent logsource values. The accepted set lives in the SigmaHQ Taxonomy Appendix, and a value you made up will silently convert to nothing. Use the optional definition key to tell the reader what has to be switched on.
Selections are maps, lists are ORs
Inside detection you define search identifiers. A map joins its items with AND. A list joins its items with OR. That single rule explains most of the logic you will ever write.
detection:
selection:
EventLog: Security
EventID:
- 517
- 1102
condition: selectionThat reads as Security AND (517 OR 1102). A list of maps flips the grouping, so each map in the list is OR-linked with the others. All values are treated as case-insensitive strings. Regular expressions, by contrast, are case-sensitive by default.
Null is its own type and cannot sit in a list of values. To express not null, make a second search identifier and negate it in the condition.
detection:
selection:
EventID: 4738
filter:
PasswordLastSet: null
condition: selection and not filterIf you only need to know whether a field is present, use the exists modifier instead: PasswordLastSet|exists: true.
Wildcards, modifiers and the escape rules
Two wildcards exist. ? replaces one mandatory character, * replaces an unbounded run. The modifiers startswith, endswith and contains are the readable equivalents of a leading or trailing star, and they chain with a pipe after the field name.
Backslash escaping trips up beginners. A plain backslash not followed by a wildcard can be written single or double, and single is recommended. To match a literal star you write \*. To match a backslash followed by a wildcard you write \\*. Four backslashes produce one literal double backslash, which is what you want in UNC paths.
Modifiers come in two flavours. Transformation modifiers such as base64 change the value and work with any backend. Type modifiers such as re or cidr tell the backend to treat the value differently, and the backend has to support them. If it does not, conversion fails rather than degrading quietly.
The condition line
Condition supports and, or, not, brackets, and the quantifier forms 1 of them, all of them and 1 of selection*. Precedence runs from least binding to most: or, and, not, x of, then parentheses.
The spec discourages all of them. It prevents downstream users from adding their own filter identifiers without breaking your logic. Prefer the pattern form.
1 of selection* and not 1 of filter*Search identifiers beginning with an underscore are excluded from them by convention. Use that when you need a helper block that should not join the quantifier.
A worked rule on process creation
We are targeting in-memory download cradles under T1059.001. Sysmon event ID 1 gives us Image, CommandLine, ParentImage, ParentCommandLine, User, IntegrityLevel and Hashes. Windows 4688 gives command line too if you enabled it, but it carries no file hash, so the same abstract logsource covers both with different fidelity.
title: PowerShell Download Cradle In Command Line
id: b3f1c2ad-4e6e-4b1a-9f0c-2d7a5c1e8b44
status: experimental
description: Detects powershell.exe or pwsh.exe process creation where the command line contains a common in-memory download cradle
author: huntrule
logsource:
category: process_creation
product: windows
detection:
selection_img:
- Image|endswith: '\powershell.exe'
- Image|endswith: '\pwsh.exe'
selection_cradle:
CommandLine|contains:
- 'DownloadString'
- 'DownloadFile'
- 'Net.WebClient'
- 'Invoke-WebRequest'
- 'BitsTransfer'
filter_main_automation:
ParentImage|startswith: 'C:\ProgramData\YourDeploymentTool\'
condition: selection_img and selection_cradle and not 1 of filter_*
fields:
- ParentImage
- ParentCommandLine
- User
- IntegrityLevel
falsepositives:
- Software deployment and packaging scripts
- Vulnerability scanners and EDR test content
- Administrator one-liners pulling internal artifacts
level: medium
tags:
- attack.execution
- attack.t1059.001The filter identifier is a placeholder. Replace the path with whatever your deployment tooling actually is, and keep it as its own block so the not 1 of filter_* construction still holds when you add a second exclusion.
What it catches and what it misses
It catches the cradle written in plain text on the command line. It misses everything that never appears there. An operator running powershell.exe -file obfuscated_script.ps1 produces a command line with no cradle string at all, as TrustedSec demonstrates in their PowerShell logging write-up. It also misses execution through the System.Management.Automation assembly, which MITRE documents under T1059.001 as PowerShell without powershell.exe.
That gap is why script block logging exists. Event ID 4104 records script content at compilation time, and each Invoke-Expression of a decoded string produces its own 4104 event. A second rule closes part of the gap.
detection:
selection:
EventID: 4104
ScriptBlockText|contains:
- 'FromBase64String'
- 'DownloadString'
- '[adsisearcher]'
condition: selectionTwo caveats we cannot hedge away. Obfuscated code is logged in its obfuscated form, so keyword matching on 4104 catches the second script block more often than the first. And Windows PowerShell 5.1 writes to Microsoft-Windows-PowerShell/Operational while PowerShell 7 writes the same 4103 and 4104 IDs to PowerShellCore/Operational. Query both or an operator switching to pwsh.exe splits your coverage in half.
Metadata that decides whether anyone runs it
id is a random UUID version 4. Write a new id when you change the logic materially, and use the related key with a type of derived, obsolete, merged, renamed or similar to keep the lineage readable.
status tells the consumer how much to trust it: stable, test, experimental, deprecated or unsupported. level is the five-value scale, and the definitions are stricter than most people assume. informational means enrichment only with no alerting. critical is reserved for cases where probability borders certainty. Our cradle rule is medium, meaning review on a frequent basis rather than page someone.
date and modified use ISO 8601 with separators, YYYY-MM-DD. falsepositives is optional in the schema and mandatory in practice, because it is the only place you can warn the next analyst before they mute your rule permanently.
Browse how other authors set these fields in the catalog at /rules?domain=windows before you settle on a level.
Converting and testing
pySigma backends turn the condition tree into a target query language. Backends should not handle field naming or value representation, that belongs in processing pipelines, which is why you pass a pipeline as well as a backend.
sigma convert -t splunk -p sysmon proc_creation_win_powershell_download_cradle.ymlBackend names and pipeline names depend on which pySigma plugins you installed. Backend-specific behaviour is controlled with -O options on the CLI, and a repeated option is passed to the backend as an array. Read the generated query before you deploy it. Conversion is where an unsupported type modifier or a bad field mapping shows up.
Summary
Three keys are mandatory, the rest is discipline. Maps are AND, lists are OR, and the condition should use 1 of selection* patterns rather than all of them so downstream teams can filter without forking your rule. Every rule you publish should carry a level, a falsepositives list and a logsource definition explaining what logging must be enabled, otherwise it is untestable in someone else's environment. We have not benchmarked the volume of either rule above on a production fleet, so treat the medium level as a starting point and tune on your own data. If you want more PowerShell detection content to compare against, start at /rules?q=powershell.
Telemetry to collect before deploying these rules
Sysmon 1 process creation (Image, CommandLine, ParentImage, Hashes, User)
Sysmon 3 network connection (disabled by default in Sysmon)
Sysmon 11 file create
4688 Windows process creation, command line, no file hash
4103 PowerShell module logging
4104 PowerShell script block logging
4624 logon events, correlate to 4103/4104 through the matching 4688 or Sysmon 1 event via LogonId
Channels Microsoft-Windows-Sysmon/Operational
Microsoft-Windows-PowerShell/Operational (5.1)
PowerShellCore/Operational (PowerShell 7)Related articles

Baselines in detection engineering: what to actually measure
Microsoft counts more than 3,000 group policy settings in Windows 10, plus over 1,800 more for Internet Explorer 11, and says only some of those roughly 4,800 settings are security relevant. That is…
2026-08-019 min read

What Is a Data Source in Detection Engineering?
Windows event ID 4688 fires on every process creation. The Process Command Line field inside it is empty by default. Microsoft documents this plainly: the field arrived in event version 1 on Windows…
2026-03-187 min read

What Is a Detection Signal vs Noise?
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -EncodedCommand SQBu... That is a malicious command line in one estate and an Ansible task in the next one over. Ansible's…
2026-03-078 min read