Back to Archive

Detections-as-Code for SIEM

TLP:AMBER June 2026
Detection Engineering SIEM Engineering

Detection rules that live only in a SIEM console have no history worth the name. Who moved this threshold, and why? What did the rule look like before the tuning that made it quiet? Which rules got deleted last quarter, and did anyone mean to? The console can't answer any of it — while every other piece of production logic in the same company is versioned, reviewed and recoverable.

Detections-as-code fixes that, and getting the content into version control is step one rather than the goal. Once rules are files, everything downstream becomes possible: CI that validates them, pipeline jobs, coverage metrics, review on a diff instead of on a screenshot.

Two kinds of content, one sync

The nightly run exports both halves of what a detection team actually owns:

Dashboard code is the half people forget, and it drifts just as badly. The navigation menu decides what counts: only views linked in it are current, so the nav is the table of contents — and it gets committed too, since the grouping it defines is what drives the folder layout. Anything not in the menu is a leftover draft. Private dashboards stay private, framework views are skipped, and a nav entry pointing at a view that belongs to a different app gets skipped rather than read by name.

Details that make the history trustworthy

Hashes, not timestamps. A SIEM bumps modification times for reasons unrelated to content — a re-save, a permissions change, an acceleration rebuild. Diff on a hash of the serialised object and the history contains real changes only. That's the difference between a git log worth reading and noise.

Retired isn't deleted. A rule or dashboard that's disabled, removed, or unlinked from the nav moves to an archive path with its last-synced source intact. "This existed and was retired in June" is worth keeping, and a silent disappearance is indistinguishable from a sync bug.

A failed read is not an empty result. If an app's nav can't be read during a run, that app is excluded from reconciliation entirely — otherwise one transient error reads as "every dashboard was removed" and archives the lot.

Each run batches the night's changes into one merge request that auto-merges once the pipeline passes — so the repo stays current without a human in the loop, and the pipeline is the gate.

The rest was the unglamorous half: a checkpoint doubling as a lock so clustered search heads don't sync twice at once, scoping every read to the app that owns it, and dry-run as the default so a manual trigger shows you the diff before it commits anything.