Sentinel-As-Code: Wave 3

Wave 2 closed with one specific promise: end-to-end Pester tests wired in as a PR gate, with branch protection on main so nothing merged without a green run. Wave 3 lands that gate, plus a handful of other things that took shape alongside it.

The short list of what's in this release:

  • The PR-validation gate is live on both Azure DevOps and GitHub Actions. About six thousand Pester assertions plus four sidecar jobs cover content schemas, deployer scripts, ARM templates, Bicep, KQL syntax, and dependency-manifest drift.
  • The daily drift detector now absorbs ContentHub and Orphan rules into the repo, not just the Custom bucket. Portal edits to Microsoft-managed rules and to ungoverned orphan rules flow back as YAML.
  • A shared PowerShell module, Sentinel.Common, replaces the copy-pasted helpers that used to live in every deploy script. While I was in there I also bumped Deploy-CustomContent.ps1 to the GA Sentinel API.
  • GitHub Actions and Azure DevOps are now at full pipeline parity, with two composite actions doing the work that used to be duplicated in every workflow.
  • dependencies.json writes itself. Build-DependencyManifest.ps1 walks the repo, parses KQL, and emits the manifest. The gate runs it in Verify mode and a daily workflow runs it in Update mode and opens a PR if anything drifted.
  • Plus a GitHub Copilot customisation set with thirteen agents, nine path-scoped instruction files, and six reusable prompts.

There are no big content adds in this release. Wave 2's 319 core items remain the catalogue and David Alonso's 111-rule community library is unchanged. Wave 3 is the operating-system release, not a content release.

The detail follows. Most of the deeper material has its own doc under Docs/, so I'm linking rather than re-explaining.


The PR gate Wave 2 promised

Scripts/Invoke-PRValidation.ps1 is the single entrypoint both pipelines call. It installs the modules it needs, runs every Pester suite under Tests/, emits a NUnit-2.5 XML report, and exits non-zero on any failure. You can run the same thing locally:

./Scripts/Invoke-PRValidation.ps1

The GitHub workflow at .github/workflows/pr-validation.yml runs five parallel jobs, each a separate status check:

JobWhat it covers
validateEvery Pester suite under Tests/. About 6,000 assertions today, and growing as content lands.
bicep-buildaz bicep build against every file under Bicep/.
arm-validateTest-AzResourceGroupDeployment -WhatIf against every playbook ARM template. Uses OIDC.
kql-validateKQL syntax check via the Microsoft.Azure.Kusto.Language parser across every rule and hunting query.
dependency-manifestBuild-DependencyManifest.ps1 -Mode Verify. Fails if dependencies.json has drifted from what discovery produces.

The Pester suite splits into the three phases documented in Docs/Development/Pester-Tests.mdPhase A validates content schemas, with one It per YAML or JSON file across every content type. Phase B unit-tests the deployer scripts using an AST-extraction helper module (Tests/_helpers/Import-ScriptFunctions.psm1) so source scripts never run their MainPhase C covers cross-cutting things: Copilot customisations, watchlist contents, dependency-manifest integrity.

Branch protection on main requires the gate to pass before any PR can merge. The same workflow runs as a push trigger on feature branches so you see results before you open the PR.

For the runtime side, a separate sentinel-deploy-nightly.yml workflow runs the full deploy against the test workspace every night. The offline gate catches what can be checked statically; the nightly catches anything that depends on actually talking to a workspace.


The drift detector now absorbs the other two buckets

Wave 2's daily drift detector resolved every deployed analytics rule into one of four buckets (Custom, ContentHub, Orphan, Managed) and only absorbed the Custom bucket back into the repo. ContentHub and Orphan drift were reported but left for manual triage.

Wave 3's detector absorbs all three editable buckets:

  • A drifted Custom rule rewrites the matching YAML in place and bumps the patch version, as before.
  • A drifted ContentHub rule writes a new YAML to AnalyticalRules/AbsorbedFromPortal/ContentHub/{Solution}/{Slug}.yaml. The new YAML reuses the deployed rule's resource GUID as its id:, so the next deploy run takes governance away from the Content Hub template and treats the rule as repo-managed from then on.
  • An Orphan rule writes a new YAML to AnalyticalRules/AbsorbedFromPortal/Orphans/{Slug}.yaml. Same idea: the GUID becomes the YAML id: and the rule joins the standard Custom path on every subsequent run.

Managed rules (Fusion, MicrosoftSecurityIncidentCreation, MLBehaviorAnalytics, ThreatIntelligence) stay excluded. Their content is not user-editable.

Workbook authoring has been the other surface where portal edits don't flow back to the repo. Wave 3 ships Scripts/Export-SentinelWorkbooks.ps1, which exports every user-authored workbook in the workspace into Workbooks/<DisplayName>/{workbook.json, metadata.json}, filters out Content Hub workbooks, strips workspace ARM IDs to a placeholder, restores PascalCase folder naming, and preserves author-curated metadata on re-export. I used it to seed the Workbooks/ folder with eight workbooks from the test workspace, including the Sentinel Cost workbook, the Workspace Usage Report, and the Data Collection Rule Toolkit.

Honest scope note: this is a one-shot export tool. There is no daily workbook-drift detector yet. Building one is on the list for a future wave.

Drift-detector mechanics, comparison surface, and the bucketed resolution logic are in Docs/Operations/Sentinel-Drift-Detection.md.


Sentinel.Common: one shared module

Every deploy script in Wave 2 had its own copy of the same handful of helpers. Write-PipelineMessage for ADO/GitHub-aware logging, Test-RuleIsCustomised for customisation comparison, Get-NormalizedQuery for KQL whitespace handling, the dependency-graph loader, the smart-deployment timestamp comparator. Five scripts, five copies, five places to update when anything changed.

Wave 3 introduces Modules/Sentinel.Common/. The .psm1 carries the helpers, the .psd1 manifest declares an explicit FunctionsToExport list so PSScriptAnalyzer can verify every public function has a real implementation. Four scripts import it:

A small KQL dependency-discovery block lives in the module too, so the drift detector and Build-DependencyManifest.ps1 share the same parser.

One downstream side benefit: the Pester suites now Import-Module Sentinel.Common -Force instead of stubbing Write-PipelineMessage inline, so the tests exercise the real code path. The pattern is documented in Docs/Development/Pester-Tests.md.


Composite actions and ADO ↔ GitHub Actions parity

Wave 2 favoured Azure DevOps. The only GitHub workflow was the main deploy. Drift detection, the DCR-inventory runbook, and the dependency-manifest auto-PR were ADO-only.

Wave 3 mirrors them all:

CapabilityAzure DevOpsGitHub Actions
Five-stage deployPipelines/Sentinel-Deploy.yml.github/workflows/sentinel-deploy.yml
Daily drift detection + PRPipelines/Sentinel-Drift-Detect.yml.github/workflows/sentinel-drift-detect.yml
DCR-watchlist syncPipelines/Sentinel-DCR-Inventory.yml.github/workflows/sentinel-dcr-inventory.yml
PR-validation gatePipelines/Sentinel-PR-Validation.yml.github/workflows/pr-validation.yml
Daily dependency-manifest auto-PRPipelines/Sentinel-Dependency-Update.yml.github/workflows/sentinel-dependency-update.yml
Nightly E2E deploy validationcovered by the Test stage of the main pipeline.github/workflows/sentinel-deploy-nightly.yml

The two sides share composite actions for the bits that used to be cut-and-paste:

  • .github/actions/azure-login-oidc/ wraps Azure/login@v2 with the standard parameter set and enable-AzPSSession: true.
  • .github/actions/setup-pwsh-modules/ installs and verifies pinned Pester and powershell-yaml versions with cache support.

Quick credit on that first one: the enable-AzPSSession: true flag in azure-login-oidc is the work of @egglessness in PR #3, the first external PR against this project. Without it, every PowerShell-based deploy step would fail at runtime with "No Azure context found". Genuinely glad to have it in.

Bumping a pinned module version is now a single edit, not a half-dozen.


Build-DependencyManifest: the manifest writes itself

Wave 2's dependencies.json was hand-curated. Add a rule that calls a new parser, and you also had to remember to add the parser to the manifest. Forget, and the deploy ordering quietly lost its prerequisite check on that rule.

Wave 3 turns the manifest into a build artefact. Scripts/Build-DependencyManifest.ps1 walks AnalyticalRules/ and HuntingQueries/, parses the embedded KQL, and emits an authoritative manifest. Three operating modes:

  • Generate: write a fresh dependencies.json from scratch. Authors run this locally after editing rules.
  • Verify: walk content, build the manifest in-memory, compare against the on-disk file. Exit 0 on match, 1 on drift. This is the mode the PR-gate runs.
  • Update: like Verify, but on detected drift writes the regenerated manifest and exits 0. The calling pipeline owns the commit and PR.

A daily workflow runs Update and opens a PR titled chore(deps): refresh dependency manifest <date> if anything drifted. So even if a path-filter edge case lets a query change slip past the gate, the manifest still self-corrects within twenty-four hours.

Discovery mechanics, the bare-identifier extractor, and the watchlist cross-validation logic are in Docs/Operations/Dependency-Manifest.md.


GitHub Copilot agents

If you use GitHub Copilot or a Copilot-compatible editor, Wave 3 ships a full customisation set under .github/. Repo-wide instructions in .github/copilot-instructions.md, nine path-scoped instruction files in .github/instructions/ (loaded automatically based on the file you're editing), six reusable prompt templates in .github/prompts/, and thirteen agents in .github/agents/.

The agent set is split into two tiers. Five persona-broad agents (pick by the kind of help you want):

Eight engineering specialists (pick by area of expertise):

All thirteen prefix their display name with Sentinel-As-Code: so they group together in the agent picker. Everything lives at .github/agents/ rather than .vscode/ or any IDE-specific path, so the same set works across github.com, VS Code, Visual Studio, JetBrains, and Copilot CLI.

The full platform-support matrix and per-agent docs live at Docs/Development/GitHub-Copilot.md. A Pester suite at Tests/Test-CopilotCustomisations.Tests.ps1 covers the customisation set for schema drift.


What's next

Wave 3 was always going to be the foundation wave. The PR gate enforces what gets in, the drift detector keeps the workspace and the repo aligned for analytics rules, and Sentinel.Common cleans up the deploy code itself. The next two waves build on top of that foundation.

Wave 4: pipelined documentation

Wave 3 proves the inputs to a deployment are correct. Wave 4 is about proving the outputs match what was expected. I want to build a Sentinel Documenter: a read-only inventory and gap-analysis tool that walks every Sentinel artefact plus the supporting Log Analytics, DCR, and subscription state, dumps it to JSON, and renders a structured Markdown report.

The shape I'm aiming for, all subject to change once I'm in the code:

  • A structured report aligned to the formal Sentinel Configuration table-of-contents consultancies use for delivery work, so the auto-generated content slots straight into the mechanical chapters of a customer report and leaves the architectural narrative for the human author.
  • A gap engine with rules scored against documented Microsoft Learn guidance. Daily cap unconfigured, workspace retention below the Sentinel free benefit, UEBA disabled, analytics-plan tables on long retention that should be archive, connectors reporting "connected" with no data in the last 24 hours, and so on.
  • A defensible cost estimator that combines per-table billable ingest from the workspace Usage table with the public Azure Retail Prices API and applies the Sentinel free benefit where eligible.
  • A MITRE ATT&CK coverage matrix that goes past tactics into base techniques and sub-techniques.
  • A strict private-repo guard on the PR channel, so tenant configuration never leaks to a public mirror.

None of this is in Wave 3. It's the design I'm working towards, not a deliverable.

Future waves: full bidirectional sync

Wave 3's drift absorption is scoped to analytics rules. The same pattern (drift detector, bucketed comparison, repo-side absorption) generalises. Later waves extend it to watchlists, hunting queries, playbook ARM templates, workbooks (so the one-shot exporter gets a daily drift detector layered on top), parsers, and automation rules. End state: every artefact the deploy pipeline ships is mirrored bidirectionally, and portal edits become PRs across the entire content tree, regardless of which content type they touch.


If you deployed Wave 2, the upgrade path is to pull, refresh your pipeline variables, and run. The PR-validation gate will fire on your first PR; expect a couple of small fixes if your repo has been drifting from the documented schemas. Smart deployment will skip everything that hasn't changed since your last successful run.

If you're starting fresh, everything is in the repository.

If your organisation is deploying Sentinel-As-Code into production and it has saved your team meaningful engineering time, the work that goes into each wave is funded by recurring Organisation tiers, one-off tips, and annual invoiced sponsorships at sentinel.blog/support. All blog content stays free for everyone regardless, and supporting the project does not create a support contract.

For anything not covered above, or if you'd like to talk through a deployment before committing to a tier, find me on LinkedIn. Happy to compare notes either way.

Sentinel-As-Code on GitHub, infrastructure, content, pipelines, tests, and now Copilot agents. Open source.


SPONSORED
CTA Image

If you've enjoyed this content and would like to support more like it, please consider subscribing. Your support helps me continue creating practical security automation content for the community.

Learn more