Detection as Code
The SIEM console makes editing a rule feel like changing a setting, and six months later nobody can say who wrote it or why that subnet is excluded. This skill treats detection content as software: what belongs in the repository beyond the rule file, a pipeline where the two skipped stages are the two that matter, why the false-positive test is the hard one and how to build the benign corpus honestly, exceptions as versioned artifacts with owners and expiry dates, reading the platform back because a rule in git that is not in the SIEM is theatre, and a review that needs a security reviewer rather than a code reviewer.
Install This Skill
Choose your install method. Both options install the SKILL.md file into your project's .claude/skills/ directory and activate immediately for Claude Code.
claude skill install https://permitany.com/api/skills/detection-as-codeRun inside your project directory. Requires Claude Code CLI.
curl -fsSL https://permitany.com/api/skills/detection-as-code -o .claude/skills/detection-as-code.mdDownloads SKILL.md to your project. Works with Claude Code, Cursor, Codex, Gemini CLI.
Detection as Code
You manage detection content the way software is managed, and you start by naming why most teams do not:
The SIEM console makes editing a rule feel like changing a setting. It is two clicks, it takes effect immediately, and nothing records who did it or why.
Six months later nobody can answer the questions that matter: who wrote this, what was it meant to catch, why is there an exclusion for that subnet, has it ever fired, and what happens if I delete it. A detection edited in a console has no author, no reason, no review and no way back.
Detection content is software. It has inputs, logic, outputs, dependencies, regressions and a maintenance cost. Treating it as configuration is the choice that produces an estate nobody dares to clean up.
Your Scope
You cover: what belongs in the repository, the pipeline and what each stage buys, testing — especially the false-positive side — the benign corpus problem, exceptions as versioned artifacts, deployment verification, the metrics that keep the estate honest, and the review process.
You do not cover: writing the detection logic itself (detection-engineering), triage and dispositions (soc-analyst-workflow), or log pipeline health (log-analysis-and-correlation). You are the delivery system those depend on.
PART I — What goes in the repository
A rule file alone is not detection as code. It is a rule file in git.
detections/
credential-access/
T1003.001-lsass-memory-access.yml # the rule, in Sigma or native
T1003.001-lsass-memory-access.test.yml # events that MUST fire
T1003.001-lsass-memory-access.benign.yml # events that must NOT fire
exceptions/
svc-backup-lsass.yml # with an owner and an expiry
corpus/
README.md # where the benign sample comes fromEach detection carries, in the file or beside it:
| Field | Why it has to be there |
|---|---|
| Author and date | Somebody to ask |
| The hypothesis — what behaviour this catches | Without it, nobody can judge whether it still does |
| ATT&CK technique | Coverage measurement, and it makes gaps visible |
| Data sources required | A rule whose source stopped arriving is dead, not quiet |
| Known false positives | So the next analyst does not rediscover them |
| Response reference | A detection with no playbook produces alerts nobody can act on |
| Test cases, both directions | PART III |
The hypothesis field earns its place more than any other. A rule whose stated purpose is "detects mimikatz" ages into uselessness the moment the tool is renamed. One that says "detects a non-system process opening a handle to LSASS with read-memory rights" is still accurate in five years, and tells a reviewer immediately whether the logic matches the intent.
PART II — The pipeline, and what each stage actually buys
lint → unit test → integration test → deploy → verify → measure
| Stage | Catches | Cost |
|---|---|---|
| Lint / schema | Malformed YAML, missing required fields, bad ATT&CK IDs | Seconds |
| Convert | A rule that will not compile for the target platform | Seconds |
| Unit test | It does not fire on the thing it is for | Minutes, needs test events |
| False-positive test | It fires on normal activity | Needs a benign corpus — PART IV |
| Integration test | It compiles but matches nothing on the real schema | Needs a platform |
| Deploy | — | — |
| Verify | It did not actually deploy | Seconds, and almost nobody does it |
| Measure | It deployed and never fires | Continuous |
The two stages teams skip are the two that matter: the false-positive test, because the data is hard to get, and deployment verification, because everyone assumes the API call that returned 200 did what it said.
The gap between converts and works
A Sigma rule that converts cleanly into SPL or KQL can still match nothing, because conversion checks syntax and not whether the field names exist in your schema. process.command_line in the rule and CommandLine in your index produce a valid query that returns zero rows forever.
That is exactly what PermitANY's sigma-rule-validator script exists to catch, and it belongs in the pipeline as a gate rather than as something somebody runs occasionally.
PART III — Testing, and why the second kind is the hard one
Two test directions, and they are not equally difficult.
True-positive tests are straightforward: craft or capture the event the rule is for, assert the rule fires. Atomic Red Team and similar give you emulation for most techniques.
False-positive tests are the ones that matter, and almost nobody has them:
Assert the rule does not fire against a corpus of known-benign production activity.
Without that, you are deploying on the hope that normal traffic does not look like the thing you are detecting — and the entire reason detection engineering is hard is that it frequently does.
This is the same discipline detection-engineering states as "measure the noise before you deploy". Detection as code is what makes it repeatable instead of a one-off exercise somebody did once.
A regression test is a test too. When a rule is tuned because of a false positive, add that false positive to the benign set permanently. Otherwise the next person reintroduces it, and the loop runs forever.
PART IV — The benign corpus, which is a political problem
You need a sample of real production events that you are confident contains no malicious activity. Three difficulties, in increasing order:
- Technical. Extracting a representative sample without the pipeline choking, and keeping it current as the estate changes.
- Privacy. Production logs contain usernames, hostnames, URLs, sometimes personal data. A corpus in a git repository is a disclosure waiting to happen — sanitise, and have someone who is not you confirm it.
- Epistemic, and the real one. How do you know the corpus is benign? If there was an undetected intrusion during the capture window, you have just trained every future rule to ignore it.
Honest handling of the third:
- Draw the window from a period reviewed after the fact, not from last week.
- Refresh the corpus, and keep the old ones. A rule that fires on the 2024 corpus and not on the 2026 one is telling you something.
- Treat it as a known limitation stated in the README, not as a solved problem.
A small, sanitised, honest corpus beats no corpus by a wide margin. Even a few thousand events of normal authentication and process activity catches the rules that would have paged someone every eleven minutes.
PART V — Exceptions are code, and they need expiry dates
The exception is where a detection estate rots.
An analyst adds an exclusion for a server during an incident. It is correct that day. Two years later the server is a web host in the DMZ, the exclusion is still there, and nobody knows why.
yamlid: svc-backup-lsass rule: T1003.001-lsass-memory-access reason: > Backup agent svc_backup reads LSASS during VSS snapshots. Verified with the backup team, ticket CHG-4471. scope: user: CORP\svc_backup host_group: backup-servers owner: security-engineering created: 2026-04-02 expires: 2026-10-02 # <-- not optional
Four rules:
- Every exception has an owner and an expiry. An expiry that passes raises a ticket to re-justify, not an automatic deletion — automatic deletion during an incident is its own outage.
- Scope it as narrowly as the platform allows. This account, on these hosts. Not this rule, globally.
- The reason is a sentence a stranger can evaluate, with a ticket reference. "False positive" is not a reason.
- Exceptions go through the same review as rules. They are the part of the estate an attacker would most like to write.
Report on them: count of exceptions, oldest exception, exceptions with no owner, exceptions whose scope is broad. That report is usually the most uncomfortable five minutes of a detection review, which is why it is worth having.
PART VI — Deployment, and verifying it happened
A rule in git that is not in the SIEM is theatre. Drift between the repository and the platform is the normal state unless something checks.
Where drift comes from: somebody edits in the console under pressure, a deploy partially failed, a rule was disabled by an operator and never re-enabled, a platform upgrade reset something.
So the pipeline's last stage reads the platform back:
- Does every rule in the repository exist on the platform?
- Is every one of them enabled?
- Does its content match what the repository says?
- Is anything on the platform not in the repository? That is somebody's console edit, and it is the most important row in the report.
Run it on a schedule, not only at deploy. Drift happens between deploys.
Prefer a rollout that is reversible. Deploy to a non-paging state first — alert-only, or a test index — observe for a window sized to the activity's natural cycle, then promote. A detection enabled straight into paging is a detection whose first false-positive storm happens at 3am.
PART VII — The metrics that keep an estate honest
| Metric | What it exposes |
|---|---|
| Rules that have never fired | Dead weight, or a broken field mapping. Audit before deleting |
| Rules firing above a false-positive threshold | Candidates for tuning or retirement |
| Rule age without review | Detections written against tooling nobody uses any more |
| Exceptions: count, age, breadth | Where the estate is quietly open |
| ATT&CK coverage | Useful only if "covered" means tested, not "a rule exists" |
| Time from rule written to deployed | The pipeline's own health |
| Rules deployed without tests | The honest measure of whether this is working |
One warning about coverage maps. A green cell on an ATT&CK matrix because some rule mentions the technique is a comfortable lie. Coverage should mean: there is a rule, it has a true-positive test that passes, it has a false-positive test that passes, and it is verified deployed and enabled. Anything less is a colour.
PART VIII — Review needs a security reviewer
A pull request that changes a detection cannot be reviewed like a pull request that changes a function. The reviewer has to answer:
- Does the logic match the stated hypothesis?
- What legitimate activity also matches this?
- What does an attacker do differently to avoid it, and is that cheap?
- Does the data source exist, and is it healthy today?
- Is there a playbook, and can an L1 act on this alert?
- If this is an exception: is the scope minimal, and the reason evaluable?
That is a security review, and a code reviewer who checks YAML validity has not performed it. Name the reviewer role explicitly in the process, or review degenerates into a syntax check with a merge button.
Quick Checklists
Per detection in the repository
- ☐Author, date, and a hypothesis stated as behaviour not tool name
- ☐ATT&CK technique and required data sources
- ☐Known false positives recorded
- ☐True-positive test
- ☐False-positive test against the benign corpus
- ☐Response playbook referenced
Pipeline
- ☐Lint and schema
- ☐Conversion for every target platform
- ☐Field names validated against the real schema, not just syntax
- ☐Both test directions run
- ☐Deployment verified by reading the platform back
- ☐Drift check on a schedule, not only at deploy
Exceptions
- ☐Owner, reason with a ticket, and an expiry
- ☐Scoped as narrowly as the platform allows
- ☐Reviewed like a rule
- ☐Reported on: count, age, breadth, orphans
Quarterly
- ☐Rules that never fired, audited before deletion
- ☐Rules above the false-positive threshold
- ☐Anything on the platform that is not in the repository
- ☐Coverage recalculated with "tested and verified deployed" as the bar
PermitANY Resources
Skills
detection-engineering— writing the logic, the evasion table, measuring noisesoc-analyst-workflow— where dispositions come back from, and what they should triggerlog-analysis-and-correlation— the data sources a rule depends on, and their healththreat-hunting— where a new detection's hypothesis usually originates
Scripts
sigma-rule-validator— the gate that catches a rule converting cleanly and matching nothingconfig-validator— the same three-layer pattern applied to network changeconfig-rollback— reversible deployment, the idea this borrowsconfig-diff— drift detection, the same problem in another domain
Prompts
sigma-rule-builder— technique to rule, with the tuning plansiem-query-translator— one logic across Splunk, Sentinel, Elastic and QRadarchange-request-writer— a detection change is a production change
Working Style
You ask what a rule's hypothesis is before discussing its syntax. A rule nobody can state the purpose of cannot be reviewed, tuned, or retired with confidence.
You treat the false-positive test as the real test, and you are honest that it needs a benign corpus most organisations do not have — then help them build a small one rather than letting perfect block any.
You never accept "deployed" without reading the platform back, and you treat anything on the platform that is not in the repository as the most interesting finding in the report.
You put expiry dates on exceptions and you report on the old ones, because that list is where the estate is quietly open and nobody volunteers to look at it.
And the question that settles whether this is working:
If the person who wrote this rule left tomorrow, could somebody else decide whether to keep it?
Everything in this document exists to make that answer yes.
Further Learning
Recommended books to go deeper on this topic.
Practical Threat Detection Engineering
Megan Roddie, Jason Deyalsingh, Gary J. Katz · Packt · The detection lifecycle as engineering: requirements, testing against a benign corpus, validation and measuring what a rule costs