Anatomy of a Good Detection Rule
Most detection rules in production are not good. They fire on noise, they miss real attacks, and nobody can tell you why they exist. You…
Anatomy of a Good Detection Rule
Most detection rules in production are not good. They fire on noise, they miss real attacks, and nobody can tell you why they exist. You find them as orphaned queries with no author, no test, no description , just a severity label someone optimistically set to “High” and forgot.

This isn’t a dig at the engineers who wrote them. It’s a structural problem. Detection rules are treated as artifacts, not as code. They get written once, deployed, and left to rot until a SOC analyst gets angry enough to file a ticket.
A good detection rule is something different. It’s a documented, tested, scoped piece of logic that encodes a specific threat hypothesis, fires when that hypothesis is satisfied, and tells the analyst exactly what to do next. This article breaks down the components that make that possible.
If you want the short version first, skip to the checklist at the end and work backwards.
Start With a Hypothesis, Not a Query
The most common failure mode in detection engineering starts before a single line of code is written. Someone sees a technique on a threat intel report, copies a Sigma rule from GitHub, transpiles it to KQL, and calls it done. What they’ve built is a signature, not a detection.
A detection rule should begin with a hypothesis: a falsifiable claim about adversary behavior.
“An attacker who has achieved code execution on a Windows host will attempt to dump credentials from LSASS memory by either injecting into lsass.exe, calling MiniDumpWriteDump directly, or using a tool like Mimikatz that reads from process memory. This activity will surface as either a handle request to lsass.exe with PROCESS_VM_READ access, or an unusual process making that handle request.”
That’s a hypothesis. It tells you what technique you’re detecting (credential dumping via LSASS), what observable you’re relying on (handle access telemetry from Sysmon Event ID 10), and what assumptions you’re making (Sysmon is deployed, configured with targeted process access rules).
A hypothesis forces you to be specific about three things before you write any logic:
- What behavior are you modeling? Not “suspicious PowerShell” — what specific TTP, at what stage of an attack, with what adversarial objective?
- Where does that behavior leave a trace? Which log source, which event type, which field?
- What does the benign version of this behavior look like? Because it almost always has one.
If you can’t answer all three before opening your SIEM, you’re not ready to write the rule.
The Four Layers of a Detection Rule
A production-grade detection rule has four distinct layers: the hypothesis (why), the data dependency (what), the logic (how), and the metadata (who and when). Most rules only have the third one.
1. Hypothesis Layer
This doesn’t live in the rule syntax — it lives in documentation attached to the rule. It should answer:
- Goal: What adversary behavior are you detecting?
- MITRE ATT&CK mapping: Tactic and technique (e.g., Credential Access / T1003.001 — OS Credential Dumping: LSASS Memory)
- Threat model context: Is this relevant to your environment? Why did you build this now?
- Expected adversary tools or procedures: Mimikatz, ProcDump, custom loaders?
The Palantir ADS Framework formalizes this as the “Strategy Abstract” — a plain-language description of what the rule is looking for, what data it relies on, and what false positive handling is in place. It’s a simple pattern, and it’s effective.
If your detection platform doesn’t support attached documentation, put it in a companion doc in the same repository. The rule ID is the link.
2. Data Dependency Layer
A rule can only fire on data that exists. This sounds obvious. It isn’t.
Every rule should explicitly declare its data dependencies:
# Sigma rule example
logsource:
category: process_access
product: windows
detection:
selection:
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains:
- '0x1010'
- '0x1410'
- '0x147a' # observed in specific tooling contexts; verify against your environment
- '0x143a'
In this case, the dependency is Sysmon Event ID 10 (ProcessAccess), with a specific configuration that enables process access logging. If Sysmon isn’t deployed or is deployed without the right configuration the rule produces nothing and you’ll never know.
Documenting data dependencies forces you to answer:
- Is this log source available in all the environments this rule will run?
- What Sysmon configuration is required?
- If the source goes dark, how will we know?
This is especially critical in multi-tenant or hybrid cloud environments where logging coverage is inconsistent. A rule with undocumented dependencies has an invisible failure mode: the alert just never fires.
3. Logic Layer
This is where most of the engineering effort goes, and where most of the mistakes live.
Specificity over breadth
A common mistake is writing detection logic that’s deliberately broad “to avoid missing things.” The result is a rule that fires on everything, gets suppressed by analysts, and eventually becomes invisible. A rule nobody trusts is worse than no rule at all.
Compare these two approaches for detecting suspicious encoded PowerShell:
Broad (low precision):
// Microsoft Sentinel KQL
DeviceProcessEvents
| where FileName =~ "powershell.exe"
| where ProcessCommandLine contains "-enc"
This fires on every encoded PowerShell invocation — including legitimate admin scripts, scheduled tasks, and monitoring agents. On any enterprise network, this is hundreds of events per day.
Specific (higher precision):
DeviceProcessEvents
| where FileName =~ "powershell.exe"
| where ProcessCommandLine matches regex @"-[eE][nN][cC][oO]?[dD]?[eE]?[dD]?\s+[A-Za-z0-9+/]{100,}"
| where InitiatingProcessFileName !in~ ("msiexec.exe", "sccm.exe", "ccmexec.exe")
| where InitiatingProcessParentFileName !in~ ("services.exe")
| where AccountName !in (trusted_service_accounts) // lookup table
// The trusted_service_accounts lookup table should be maintained in version control alongside the rule and reviewed quarterly or whenever service account provisioning changes.
This is still not perfect — nothing is — but it narrows the population significantly by filtering known legitimate callers, requiring a meaningful payload length, and using a regex that accounts for common obfuscation of the -EncodedCommand flag itself.
Evasion resistance
Every rule sits somewhere on David Bianco’s Pyramid of Pain. Rules built on atomic indicators, file hashes, IP addresses, specific command strings are trivially evaded. An attacker changes one byte and you miss them.
MITRE Engenuity’s Summiting the Pyramid methodology frames this more precisely: a robust detection identifies a “spanning set” of observables that trigger regardless of how a technique is implemented. The higher up the pyramid the observable sits, process behavior, API call patterns, authentication sequences , the harder it is for an adversary to change it without abandoning the technique entirely.
In practice, building a spanning set means identifying two or three observables at different abstraction levels (e.g., API call pattern + process relationship + access mask combination) such that evading one still triggers another. A single high-level observable isn’t a spanning set.

For LSASS credential dumping, detecting on process name (mimikatz.exe) is trivially bypassed. Detecting on the combination of GrantedAccess flags + the calling process not being a known security tool is much harder to evade without changing how credential dumping fundamentally works at the OS level.
Correlation over atomics
Single-event rules have inherent noise. An admin opening a handle to LSASS once, from a known workstation, with a short-lived tool, is probably not an attack. But that same pattern, preceded by a failed authentication event, followed by a lateral movement indicator, is a different story.
Correlation rules those that link events across time windows or entities produce more confident signals. The trade-off is implementation complexity and the requirement for more reliable telemetry. But an alert generated from correlated evidence is worth three atomic alerts.
// Sentinel: Correlating LSASS access with preceding failed auth
let suspicious_lsass = DeviceEvents
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| where not(InitiatingProcessFileName has_any ("MsMpEng.exe", "svchost.exe", "csrss.exe"))
| project LsassTime = Timestamp, DeviceId, InitiatingProcessFileName;
let failed_auth = DeviceLogonEvents
| where ActionType == "LogonFailed"
| where LogonType in ("Network", "NetworkCleartext")
| project AuthTime = Timestamp, DeviceId, AccountName;
failed_auth
| join kind=inner suspicious_lsass on DeviceId
| where LsassTime between (AuthTime .. (AuthTime + 10m))
| summarize count() by DeviceId, AccountName, InitiatingProcessFileName
| where count_ >= 2 // require at least 2 LSASS access events to reduce noise; adjust per environment| where count_ >= 2
False positive handling inside the logic
Listing false positives in rule documentation doesn’t stop them from firing. If you know an FP, filter it. Sigma provides filter blocks for exactly this purpose:
detection:
selection:
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains:
- '0x1010'
- '0x1410'
filter_legit:
SourceImage|endswith:
- '\MsMpEng.exe'
- '\csrss.exe'
- '\wininit.exe'
- '\svchost.exe'
condition: selection and not filter_legit
The list of known-good callers should be maintained and version-controlled alongside the rule. It’s a living document as your environment changes, so does the filter set.
4. Metadata Layer
A rule with no metadata is forensically useless. When it fires on a Tuesday at 3 AM and the on-call analyst has never seen it before, the metadata is the difference between a 10-minute investigation and a 90-minute one.
Minimum required metadata:
title: LSASS Process Access by Non-System Process
id: a7ba7a59-7f14-4e8c-9c32-4f2e2e3b63a1
status: production
description: >
Detects a non-system process requesting memory read access to lsass.exe.
Commonly observed during credential dumping with Mimikatz, ProcDump,
or custom loaders. Access mask 0x1010 = PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION.
author: detection-engineering-team
date: 2024-11-12
modified: 2025-03-01
references:
- https://attack.mitre.org/techniques/T1003/001/
- https://github.com/RedCanaryco/atomic-red-team/blob/master/atomics/T1003.001/T1003.001.md
tags:
- attack.credential_access
- attack.t1003.001
logsource:
category: process_access
product: windows
level: high
data_sources:
- Sysmon EventID 10
- Required config: ProcessAccess targeting lsass.exe
status in particular is underused. It communicates to everyone downstream analysts, other engineers, automation where this rule is in its lifecycle. experimental means it's noisy and needs tuning. test means it's been validated in staging. production means it's passed review and FP testing. deprecated means don't rely on it. These aren't just labels; they gate operational response.
Blind Spots Are Part of the Spec
Every rule has gaps. A good rule documents them explicitly rather than pretending they don’t exist.
From the LSASS example, known blind spots include:
- Kernel-mode attacks: Techniques that bypass user-mode API calls entirely (e.g., direct
NtReadVirtualMemorysyscalls) may not generate Sysmon Event ID 10. - PPL/ELAM bypasses: If LSASS is running as a Protected Process Light and is bypassed via a kernel driver, the access may not be logged at all.
- Non-Sysmon endpoints: Any host without Sysmon deployed creates a complete blind spot.
- Trusted binaries as loaders: If malicious code is injected into a legitimate process already on the allowlist (e.g.,
svchost.exe), it bypasses the filter.
Documenting blind spots does two things. First, it sets honest expectations for the SOC, this rule doesn’t mean you’re covered for all LSASS attacks. Second, it creates a roadmap for follow-on detections that cover the gaps.
The Rule Needs a Test
A rule without a test is a rule you don’t know works.
The test should be a reproducible procedure that generates a true positive. This doesn’t mean “we once saw it fire on something.” It means you have a documented, repeatable way to make the alert fire a script, an Atomic Red Team test number, or a manual procedure:
validation:
description: >
Use Atomic Red Team test T1003.001-#1 (Mimikatz LSASS Memory Access).
Run on a Windows host with Sysmon configured.
Expected: Alert fires within 2 minutes of execution.
atomic_red_team_id: T1003.001
test_number: 1
expected_alert_fields:
InitiatingProcessFileName: mimikatz.exe
GrantedAccess: "0x1010"
If you’re running detection-as-code, this test runs in your CI/CD pipeline before any rule reaches production. A failing test blocks deployment. This is not optional, it’s what separates a detection program from a detection library nobody trusts.
At minimum, even without automated pipelines: every new rule should require a documented true positive before it goes live.
Triage Instructions Are Part of the Rule
A rule that fires without telling the analyst what to do next has offloaded its complexity onto the worst possible moment, an active investigation, under time pressure, with incomplete context.
The triage steps should live in the alert itself or in a directly linked runbook. They should be specific, not generic:
Triage steps:
1. Identify the calling process (InitiatingProcessFileName, InitiatingProcessCommandLine).
2. Is the calling process on the known-good list? If yes, review the filter set for updates.
3. Check if the initiating process was spawned by a suspicious parent (e.g., Office apps, browser).
4. Cross-reference AccountName against the privileged accounts list.
5. Look for preceding failed logon events (DeviceLogonEvents) within 10 minutes on the same host.
6. If source process is unknown and parent is unusual: isolate the host and escalate.
The key test: could a new analyst who has never seen this rule before complete a triage in under 15 minutes using only these steps and the linked documentation? If not, the runbook isn’t done.
Triage steps also expose automation opportunities. If step 2 is happening hundreds of times a week and the answer is always “yes, filter it,” that’s a signal to update the rule logic, not to keep paying an analyst to do it.
Lifecycle and Ownership

Rules rot. The environment changes, new tooling gets deployed, threat actors evolve their techniques, and yesterday’s high-fidelity rule becomes today’s noise generator.
A rule should have an owner. A team or named engineer who is responsible for reviewing it when the environment changes or when it starts producing anomalous volumes. It should have a review cadence: at minimum, quarterly. Rules that haven’t been reviewed in over a year should be considered candidates for deprecation unless there’s documented justification.
Version control is not optional. Every rule change should be a commit with a message that explains what changed and why. Pull requests enforce peer review. This is the same quality bar you’d apply to production application code, and for good reason . A broken detection rule has the same blast radius as a broken service.
A rule with no owner is a liability.
The Checklist
Before a rule ships to production, it should satisfy all of the following:
- [ ] Hypothesis documented : Specific TTP, adversary objective, expected observables
- [ ] Data dependency declared : Log source, event type, required configuration
- [ ] MITRE ATT&CK mapped : Tactic + technique (sub-technique where applicable)
- [ ] Logic is specific: Not intentionally broad, FPs filtered in the condition where possible
- [ ] Known FPs listed: With documented rationale for any that are accepted rather than filtered
- [ ] Blind spots documented: Explicit enumeration of what the rule won’t catch
- [ ] True positive test exists: Reproducible, ideally automated
- [ ] Severity is justified: Not “High” by default
- [ ] Triage steps written: Specific enough for a new analyst
- [ ] Status field set :
experimental,test, orproduction - [ ] Owner assigned: Team or person responsible for maintenance
- [ ] In version control: With peer review before merge
Most orgs will look at that list and realize their current rule library fails six or seven items for the average rule. That’s the gap. It’s not a tooling problem , it’s a process problem, and it’s fixable.
The Actual Goal
The goal of a detection rule is not to fire. It’s not to hit ATT&CK coverage percentages or satisfy a compliance checkbox. The goal is to give an analyst at 2 AM, under pressure, without the engineer who wrote the rule in the room, the information they need to determine whether a real attack is happening and what to do about it.
Every element of the anatomy above serves that goal. The hypothesis tells the analyst what behavior to look for. The data dependency tells them what telemetry the rule relies on. The logic produces a high-confidence signal. The metadata tells them who to call. The triage steps tell them what to do first. The test proves the rule works.
A rule that does all of that is not just good engineering. It’s operational leverage.
Further Reading
These are the primary sources this article draws from, worth reading in full:
- **Palantir ADS Framework** — The canonical template for structuring detection strategies. Read the README and at least one example ADS before building your own process.
- **Alerting and Detection Strategy Framework (Palantir Blog)** — The original post explaining why they built it and what problems it solves.
- **Pyramid of Pain — David Bianco** — The foundational framing for thinking about indicator types and evasion cost.
- **Summiting the Pyramid — MITRE Engenuity CTID** — Extends Bianco’s framework with a methodology for quantifying detection robustness and building spanning sets.
- **Detection-as-Code: Testing — Kyle Bailey** — Practical breakdown of how to treat detection logic like software, including unit tests and CI/CD integration.
- **The Anatomy of a High Quality SIEM Rule — Jack Naglieri** — Complementary framing around confidence, impact, and triage steps.
- **Baselines 101 — Alex Teixeira (Detect FYI)** — Deep dive on why most rules fail without environmental baselines, and how to build them properly.
- **SigmaHQ Rule Specification** — Reference for Sigma rule structure, status lifecycle, and field conventions.
- **Atomic Red Team** — Red Canary’s library of ATT&CK-mapped attack simulations. The fastest way to get repeatable true positive validation for common techniques.
- **MITRE ATT&CK** — The framework everything else maps to. Use it for technique context, data source identification, and coverage gap analysis.
Built for practitioners. Share if it’s useful.
메타데이터
- post_id
- 52942aa0141d
- slug
- anatomy-of-a-good-detection-rule-52942aa0141d
- url
- https://medium.com/@itsmayank227/anatomy-of-a-good-detection-rule-52942aa0141d
- canonical_url
- https://medium.com/@itsmayank227/anatomy-of-a-good-detection-rule-52942aa0141d
- author_url
- https://medium.com/@itsmayank227
- status
- ok
- fetched_at
- 2026-06-12 18:14:10