What Is a Detection Rule? From Alerts to Analytics
HuntRule Team · · 9 min read

On this page
ParentImage ends with \WINWORD.EXE and Image ends with \cmd.exe. That pair, plus a statement about which log it reads and what happens when it fires, is a complete detection rule. Everything else is packaging.
Rules get described by the tool that runs them. A Splunk search, a Sentinel analytic, an EDR custom detection, a Sigma YAML file. The packaging changes, the anatomy does not. This post takes a rule apart into the parts it actually has, then walks four rungs of a ladder from a hash match to a multi-event correlation, and says which rung breaks first when the attacker changes one thing.
The three parts every rule has
A rule reads something. In Sigma that is the logsource block, and it is the part people skip.
logsource:
category: process_creation
product: windowsTwo lines, and both are load-bearing. process_creation on Windows resolves to Sysmon Event ID 1 or Security Event ID 4688, and those are not the same telemetry. Sysmon Event ID 1 carries CommandLine, ParentImage, OriginalFileName and a Hashes field. Security 4688 carries NewProcessName and ParentProcessName, and it only carries the command line if the "Include command line in process creation events" audit policy is enabled. A rule that keys on OriginalFileName reads as valid YAML on a 4688-only estate and matches nothing, forever, silently.
A rule matches something. This is the part everyone means when they say "rule": a boolean over fields.
A rule owes a response. In Sigma this is the level field plus the falsepositives list. In your SIEM it is the routing decision: does this page someone, open a case, or land in a hunting queue. A rule with no answer to "then what" is not finished.
The fourth part, and most rules skip it
A rule should state a hypothesis about attacker behaviour. Not a restatement of the logic. Most description fields read like this:
Detects winword.exe spawning cmd.exeThat is the condition written in prose. A hypothesis says what the attacker is forced to do and why:
Macro and exploit-based document delivery needs a child process to fetch
or run the next stage. Office products do not launch interpreters during
normal document use, so the parent-child edge is the constrained step.The difference is testable. The second version tells you the rule dies the moment the payload runs in-process instead of spawning, which is exactly what shellcode injection from a macro does. The first version tells you nothing, so nobody notices when it goes blind. If you are new to writing these, detection engineering is largely the practice of making that hypothesis explicit before the YAML.
Rung one: the atomic IOC match
We hashed the EICAR anti-malware test file and wrote the narrowest possible rule against it.
logsource:
category: process_creation
product: windows
detection:
selection:
Hashes|contains: 'SHA256=275A021BBFB6489E54D471899F7DB9D1663FC695EC2FE2A2C4538AABF651FD0F'
condition: selectionSysmon writes the Hashes field uppercase and comma-delimited, which is why the match is a contains against a prefixed value rather than an equality test. This rule has a false positive rate of approximately zero and a shelf life of approximately one recompile. Change a single byte of padding and the SHA-256 changes. Change the packer and the IMPHASH changes too.
Atomic indicators are still worth running. They cost nothing to evaluate and they catch the operator who reused infrastructure. Just do not confuse coverage of a hash with coverage of a technique.
Rung two: the behavioural rule
Here is a rule for the Office child process case, written for this post and deliberately narrower than the SigmaHQ original (438025f9-5856-4663-83f7-52f878a70a50), which covers a much longer child list.
title: Office Application Spawning A Command Interpreter
id: 7c2b0d5e-3f41-4a8a-9d2e-6b1f0c4a8e77
status: experimental
description: Macro and exploit-based document delivery needs a child process to
fetch or run the next stage, and Office products do not launch interpreters
during normal document use.
author: HuntRule
date: 2026-07-31
references:
- https://github.com/SigmaHQ/sigma/blob/master/rules/windows/process_creation/proc_creation_win_office_susp_child_processes.yml
tags:
- attack.execution
- attack.t1204.002
- attack.t1059.001
- attack.t1059.003
logsource:
category: process_creation
product: windows
detection:
selection_parent:
ParentImage|endswith:
- '\EXCEL.EXE'
- '\MSACCESS.EXE'
- '\ONENOTE.EXE'
- '\POWERPNT.EXE'
- '\WINWORD.EXE'
selection_child:
OriginalFileName:
- 'Cmd.Exe'
- 'PowerShell.EXE'
- 'pwsh.dll'
- 'cscript.exe'
- 'wscript.exe'
condition: selection_parent and selection_child
falsepositives:
- Line-of-business workbooks that shell out to a reporting script on open
- Vendor add-ins and templates that call wscript.exe during initialisation
- Packaging and QA automation that opens documents under a service account
level: highThose false positives are real and they are the whole tuning problem. In most estates the finance team owns at least one workbook that runs cmd.exe /c on open, and it has run every weekday morning for six years. The rule is right and the alert is wrong.
Note the OriginalFileName matches. pwsh.dll is the original filename embedded in pwsh.exe, which is why renaming the binary to svchost.exe does not evade this field. That is the point of behavioural rules: they hang off something the attacker has less freedom to change.
This rung breaks when the attacker stops spawning. Macro shellcode injecting into an already-running process, or a loader using T1218.010 regsvr32 launched by a different parent, both walk straight past it.
Rung three: the statistical analytic
Nothing about "rare" fits in a boolean. Rarity needs a baseline, so it needs a query, not a rule.
WITH baseline AS (
SELECT DISTINCT host, image_name
FROM process_creation
WHERE event_time BETWEEN now() - INTERVAL '30' DAY
AND now() - INTERVAL '1' DAY
),
today AS (
SELECT host, image_name,
min(command_line) AS sample_cmdline,
count(*) AS execs
FROM process_creation
WHERE event_time >= now() - INTERVAL '1' DAY
GROUP BY host, image_name
),
spread AS (
SELECT image_name, count(DISTINCT host) AS hosts
FROM today
GROUP BY image_name
)
SELECT t.host, t.image_name, t.sample_cmdline, t.execs, s.hosts
FROM today t
JOIN spread s ON s.image_name = t.image_name
LEFT JOIN baseline b ON b.host = t.host AND b.image_name = t.image_name
WHERE b.image_name IS NULL
AND s.hosts <= 3
ORDER BY s.hosts, t.execs;New on this host in thirty days, and seen on three hosts or fewer estate-wide today. That second condition is what stops patch Tuesday from returning ten thousand rows. We have not run this against a production-scale dataset, and the thresholds are starting points rather than recommendations.
This rung breaks in two ways. It breaks when the attacker uses a binary that is already everywhere, which is the entire argument for living-off-the-land. It also breaks when your baseline window covers a period the attacker was already active in, at which point the malicious process is the baseline.
Rung four: correlation across events
Some behaviour only exists as a shape across multiple events. Password spraying is the clean example: individually every 4625 is boring, and the success at the end is a normal 4624.
title: Password Spray From A Single Source
id: 1e0f8b3d-7c42-4c9a-b0a5-2d8f6e4c1a90
name: password_spray_from_single_source
correlation:
type: value_count
rules:
- failed_logon_4625
group-by:
- IpAddress
timespan: 1h
condition:
field: TargetUserName
gte: 30
---
title: Password Spray Followed By A Successful Logon From The Same Source
id: 9b4c7a21-5d38-4f60-8e11-3a7c2b9d6f05
correlation:
type: temporal_ordered
rules:
- password_spray_from_single_source
- successful_network_logon_4624
group-by:
- IpAddress
timespan: 1h
falsepositives:
- A VPN concentrator or NAT gateway presenting hundreds of users as one IpAddress
- A scheduler retrying a service account with a stale password, then an admin resetting it
level: highThe chaining is real Sigma. A correlation can reference another correlation, so the spray rule feeds the ordered temporal rule rather than being duplicated inside it. Two honest caveats: 4625 does not populate IpAddress for every logon type, so console and some local failures arrive with a dash, and any correlation with a timespan is a published evasion budget. Spray at twenty-nine accounts an hour and this never fires.
Which rung breaks first
Rung | Matches on | Breaks when the attacker | Typical volume |
|---|---|---|---|
Atomic IOC | SHA-256, IP, domain | Recompiles, repacks, rotates infrastructure | Near zero |
Behavioural | Parent-child, | Changes the execution chain or injects in-process | Low to moderate |
Statistical | Deviation from a per-host baseline | Uses a binary already common in the estate | Moderate to high |
Correlation | A shape across events in a time window | Slows below the threshold or spreads the source | Low, but expensive to compute |
Read the second column top to bottom. It is a difficulty gradient. Breaking rung one is free, breaking rung four costs the operator tempo and infrastructure. That is the actual return on moving up the ladder, and it is why a catalog full of hashes reports high coverage and delivers little.
Alerts, analytics and dashboard widgets
Every rule you deploy answers one question: what happens on a match.
An alert needs a response. It fires, a human or a playbook does a specific thing, and the thing is written down before deployment. If you cannot name the first action, it is not an alert.
An analytic needs an analyst. Rung three output is not wrong when it returns forty rows, it is working. The rows go to a hunt queue and someone reads them. Judged as an alert it looks like a 95 percent false positive rate. Judged as an analytic it looks like a Tuesday.
A rule with neither is a dashboard widget. It matches, it increments a counter, nobody acts on it, and it appears in the coverage report as green. This is the most common failure mode in a mature detection library, and it is invisible unless you audit rules by what happened after the last hundred matches rather than by whether they exist.

Summary
A detection rule is a data source, matching logic and a response contract, and the good ones add a written hypothesis about what the attacker is forced to do. The four rungs are not better and worse versions of each other, they fail in different places, and a library needs all of them. Atomic indicators break on recompile, behavioural rules break when the execution chain changes, statistical analytics break against common binaries, and correlations break against patience. Before you deploy anything, write down what happens on a match. If the answer is "it shows up on a chart", delete it.
Collect these before writing anything on Windows:
Sysmon EID 1 process creation: Image, ParentImage, CommandLine, OriginalFileName, Hashes
Sysmon EID 3 network connection: Image, DestinationIp, DestinationPort, Initiated
Sysmon EID 11 file create: Image, TargetFilename
Security 4688 process creation (enable command line auditing)
Security 4624 successful logon: TargetUserName, LogonType, IpAddress
Security 4625 failed logon: TargetUserName, LogonType, IpAddressBrowse the catalog for rules that already cover these log sources at /rules?domain=windows, or search the parent-child cases directly with /rules?q=parent+process.
Related articles

What Is a Sigma Rule Catalog and Why Your SOC Needs One
On 2023-12-15 SigmaHQ changed the rule carrying id 5cc90652-4cbd-4241-aa3b-4b462fa5a248. The change added dnsgetdc: to the flag list and stripped the leading slash from most of the other strings, so…
2026-07-31 · 9 min read

What Is Detection Engineering?
vssadmin.exe delete shadows /all /quiet maps to T1490, Inhibit System Recovery. Writing the Sigma rule for it takes about ten minutes. Everything after those ten minutes is detection engineering.…
2026-07-31 · 8 min read

What Are Sigma Rules? The Open Detection Standard Explained
A Sigma rule is a YAML file with three required blocks: title, logsource and detection. Everything else is metadata. That is the whole format. The point of it is that the same file compiles to a…
2026-07-31 · 7 min read