UPDATED · 24 JUN 2026 · EDIT ON GITHUB
GUIDES · DETECT

Detection-as-code, end to end.

The quickstart shipped one detection in a single command. This is the version you run in production: a detection is a versioned, reviewed artifact in Git that is linted, tested against fixtures, back-tested against history, and promoted through environments — never hand-edited in a console.

12 min read Intermediate CLI · YAML · CI By M. Rao

Anatomy of a detection plan

A detection plan is declarative YAML. Three blocks do the work: when describes the events to match on the live stream, then describes what happens when they match, and the metadata above them makes the rule reviewable and attributable.

FieldPurpose
idStable slug. Renaming it creates a new detection; keep it for the life of the rule.
severityLOW · MEDIUM · HIGH · CRITICAL. Drives case priority and default response gates.
tacticMITRE ATT&CK tactic / technique IDs. Powers coverage maps and reporting.
when.streamWhich fabric stream to evaluate against (identity.signin, endpoint.process, …).
when.windowCorrelation window for stateful matches. Omit for stateless, per-event rules.
then.containOptional response action, with its own approval gate. See the response engine.
YAMLdetections/svc-account-from-corp-ip.yml
version: 1 id: svc-account-from-corp-ip title: Service account authenticated from a corporate IP severity: HIGH tactic: [TA0006] # Credential Access owner: detections@acme.io when: stream: identity.signin match: actor.type: service_account net.src_geo.cidr_label: corp-egress window: 5m then: create_case: true contain: action: disable-actor requires_approval: true

Test before you ship

Two commands stand between a draft and production. td detect test runs the plan against checked-in fixtures — synthetic events with expected verdicts — so the rule's logic is pinned by assertions. td detect backtest replays it across real history on the fabric, which is how you find the false positives a fixture never anticipated.

SHELLtest + backtest
$ td detect test ./detections/svc-account-from-corp-ip.yml -> schema valid -> 4 fixtures . 4 passed ok all assertions green (38ms) $ td detect backtest ./detections/svc-account-from-corp-ip.yml --since 30d -> replaying 1.84B events from the fabric... -> 3 matches . 0 in allowlisted windows ok backtest complete - review at /detections/_preview/backtest
Back-testing is free and non-destructive. Because compute is decoupled from storage, a replay never re-ingests events and never fires a real response — it scores the rule against the past so you can read the match rate before a single alert reaches an analyst.

Promote through environments

Detections live in a Git repository and move with your normal review flow. A pull request runs lint, fixture tests, and a bounded back-test in CI; merging to main deploys to production with a green-gate so a regression never ships.

YAML.github/workflows/detections.yml
name: detections on: [pull_request, push] jobs: validate: steps: - run: td detect lint ./detections - run: td detect test ./detections - run: td detect backtest ./detections --since 14d --max-fp 0 deploy: if: github.ref == 'refs/heads/main' steps: - run: td detect deploy ./detections --env prod --require-green
StageRunsGate
Pull requestlint · test · 14-day back-testZero new false positives
StagingShadow-evaluate on live stream, no actions48h soak, match rate within band
ProductionFull evaluation + gated responseAuto-rollback on error-rate spike

Tune to cut false positives

The fastest way to lose a SOC's trust is a noisy rule. Tune in the plan itself — keep the exceptions in version control next to the logic they qualify, each with a reason and a ticket — rather than muting alerts downstream.

YAMLrefining the match
when: stream: identity.signin match: actor.type: service_account net.src_geo.cidr_label: corp-egress window: 5m except: - actor.id: svc-backup # nightly job, ticket SEC-1182 - net.src_geo.cidr_label: vpn-admins confidence: min: 0.7 baseline: per_entity # learn normal per service account

Prefer baseline: per_entity over a global threshold wherever an entity has a stable rhythm. A service account that signs in from one CIDR every night is held to its own history, so the same rule that is quiet for it still fires loudly the first time it appears somewhere new.

Operate live detections

Once a rule is live, it is observable like any other production service. List detections by evaluation lag to find the expensive ones, and roll back to any prior version in seconds — every deploy is an immutable, signed revision on the fabric.

SHELLobserve + rollback
$ td detect ls --env prod --sort eval_lag ID TITLE STATE MATCH/24H EVAL p50/p99 det_2vK4nT svc-account-from-corp-ip LIVE 3 218ms / 612ms det_91Lc0a kerberoasting-burst LIVE 11 240ms / 705ms det_77Qd1x oauth-grant-to-new-app PAUSED - - $ td detect rollback det_2vK4nT --to v4 ok det_2vK4nT reverted to v4 (was v5) . live in 1.2s
Containment inherits the detection's gate. A rule that sets requires_approval: true queues its response for analyst sign-off. Flip it to autonomous only once the rule has soaked and you have gated it to a risk tier — see SOAR playbook authoring.

Where to go next

← ALL GUIDES Guides NEXT → SOAR Playbook Authoring