KSKS Security Research
Home / DevOps & automation

Detection-as-Code: the pipeline & governance

Detection-as-Code for Sentinel· part 1 of 2
SentinelGitHub ActionsAzure DevOpsTerraformARM / Bicep

How analytic rules move from an engineer's idea to a production Sentinel workspace through git, pull requests, and pipelines, with every change visible, reviewed, reversible, and attributable.

TL;DR
Detection-as-Code is managing Sentinel analytic rules through git and CI/CD. Best practice: one repo, two branches, two workspaces — author in the dev UI, export JSON, PR into develop (auto-deploys to dev Sentinel), then promote to main (deploys to prod). The pipeline calls a deploy engine (ARM/Bicep or Terraform); both go through the Azure Resource Manager API. The payoff is governance: audit trail, rollback, drift correction, and scale.

What this practice is called

Managing SOC content (analytic rules, hunting queries, playbooks) through git and pipelines is called Detection-as-Code. It applies DevOps practices to security content. Getting the neighboring terms straight:

TermWhat it actually means
DevOpsThe practice family: git, branches, PRs, automated pipelines. Detection-as-Code is DevOps applied to detections.
DevSecOpsThe reverse direction: embedding security checks (SAST, secret scanning) into a software development pipeline.
SecOpsSecurity Operations generally, the SOC function. Not the name of this pipeline pattern.
Detection-as-CodeThe correct CV/proposal phrase for this whole practice.

Repo & environment architecture

The instinct to keep a "dev repo" and a "prod repo" is a common anti-pattern: syncing two repos creates drift and splits history. Best practice is one repo, two branches, two Sentinel workspaces: the branch determines the environment, not the repo.

Author in dev UIcreate and test ruleExport rule JSONARM templateFeature branchcommit the JSONPull requestpeer review gateAutomated checksKQL syntax, metadataMerge to developtriggers deployGitHub Actions deployto dev workspaceDev Sentineldoes it fire? noisy?Sign-offsenior approvalPR to mainprotected branchGitHub Actions deployto prod workspaceProd Sentinellive detectionshuman stepGitHub / pipelineSentinel workspace
The end-to-end flow: same JSON file all the way; only the branch decides which workspace it lands in.

Do you even need a dev workspace?

  • Mid/large client or MSSP: yes. It catches broken KQL, over-firing rules, and deployment errors before the SOC queue. It doesn't need full data ingestion; a subset of connectors or replayed sample logs is enough.
  • Small client: a single prod workspace is acceptable if the pipeline compensates: mandatory PR review, automated validation, and deploying new rules disabled or in audit mode first.

The UI-first authoring workflow

Detection engineers should not hand-write ARM JSON. The recommended pattern is UI-first (export-driven) authoring: create and test the rule in the dev Sentinel UI, export it as JSON, and commit that JSON to a feature branch. The UI is the authoring tool; git is the system of record.

Why DevOps? PR review is maybe 20% of the value

The peer-review gate is the visible benefit. Here is what the client is really buying:

Git history answers who changed this detection, when, why, and who approved it, forever. When a post-incident review asks why a rule stopped firing in March, git log is the answer.

The missing layer: the pipeline is not the deployer

A GitHub Action or Azure DevOps pipeline is just an orchestrator: a runner that executes steps when something happens. By itself it doesn't know what an analytic rule is. The pipeline calls a deployment engine (ARM/Bicep or Terraform), and both funnel into one door: the Azure Resource Manager API, through which every Azure change flows.

Authentication — the piece people forget
The pipeline logs into Azure as a service principal: an app identity with rights to write to the Sentinel resource group. Azure DevOps wraps it in a Service Connection; GitHub stores it as a secret or uses OIDC federation so no password is stored. Without it, the runner has no rights to deploy anything.

CI vs CD — two halves of one pipeline

CI — integrationCD — deployment
WhenOn the pull request, before mergeAfter the merge
JobValidate, never deployActually push the change
For SentinelKQL valid? JSON well-formed? MITRE tags, severity, entities present?Deploy command lands the rule in the workspace
Terraformterraform plan posts the diff onto the PRterraform apply executes it

ARM/Bicep vs Terraform — pick one deployment engine

Azure's native format, literally what comes out when you click Export on a rule in the Sentinel UI. Bicep is a cleaner language that compiles to ARM JSON. Zero translation, and it is what Sentinel's Repositories feature uses under the hood. Path of least resistance for a pure-Sentinel engagement.

The git lifecycle, properly named

git — feature branch to PR
git clone <repo-url>              # copy the repo locally (once)
git checkout -b feature/new-rule  # create AND switch to a new branch
# ...edit or paste your exported rule JSON into a file...
git status                        # see what changed
git add .                         # stage changes (to the staging area)
git commit -m "Add brute-force rule"   # snapshot staged changes
git push -u origin feature/new-rule    # publish your branch to the remote
# ...then in the web UI: open a Pull Request...

The mental map: working directory (your edits) → git add → staging area → git commit → local history → git push → remote. The PR/approval/merge happens in the web UI, and that is what kicks off the pipeline.

GitHub vs Azure DevOps — same concepts, different names

ConceptGitHubAzure DevOps
Where code livesGitHub repoAzure Repos
The pipelineGitHub ActionsAzure Pipelines
Azure loginSecret / OIDCService Connection
Merge protectionBranch protectionBranch policies
Deploy approvalEnvironments + reviewersEnvironments + checks
Two approval points — don't conflate them
1. PR approval: a human reviews the code before merge (branch protection). 2. Deployment approval: even after merge, the pipeline pauses before touching prod and waits for a named approver (Environment gate). A mature setup gates twice: once on the merge, once on the prod deploy.

IaC vs API — the solution architect's call

The question is never which service likes the API more. The real axis is state vs action:

  • Desired-state configuration: things that should exist and stay a certain way (analytic rules, workbooks, connectors) → declarative IaC. You get idempotency, drift detection, and rollback for free.
  • Imperative actions: things you do with no lasting state (bulk-close incidents, upload a watchlist, trigger a hunt) → API scripts. There is nothing to keep in a desired state.
Declarative IaCARM, Bicep, Terraformuse: stable confige.g. rules, workbookswin: drift + rollbackBridge layerAzAPI, deployScriptsuse: no native resourcee.g. lagging featureswin: state + governanceImperative APIPowerShell, RESTuse: actions, not statee.g. bulk operationswin: full API reachSame git, PR, and pipeline governancethe repo stays the source of truth
Declarative for state, imperative for actions, a bridge when the provider lags — all under one governance model.
The tradeoff when you go imperative
A raw API script is not idempotent by default: run it twice and it may create duplicates or error. You must write check-then-act logic yourself, and you lose drift detection. Never let anyone run an API call from a laptop against prod; the script lives in the repo and the pipeline runs it.

Hands-on lab path — five stages

Each stage adds one real concept, so you are never lost:

Break it on purpose
Once running, edit a rule directly in the prod workspace UI, then push a commit and watch the pipeline overwrite your manual change. Seeing drift correction happen is the fastest way to internalize why the repo is the source of truth.
Runnable companion example

Clone the repo and open examples/detection-as-code-cicd — the analytic rule (ARM + Terraform), the validation scripts, and the GitHub Actions pipeline from this article, ready to run in your own lab. Try it now: python3 scripts/lint_rule.py rules/*.json