Home› Blog› Week 12 — Detections as Code, and the Rule That Ha…
Azure Weekly Lab Azure Terraform

Week 12 — Detections as Code, and the Rule That Had to Stay Silent

Two Sentinel analytics rules in Terraform: one deliberately triggered, one that must not fire. A detection nobody has ever seen fire is indistinguishable from one that cannot.

Verified against current vendor documentation on 7 November 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
Microsoft Sentinel Log Analytics Azure Activity Analytics Rules Terraform
Azure Platform Engineering Lab · Week 12 of 52

📋 In This Post

Lab Overview

The problem. A SIEM rule is code, and it ships like code — reviewed, merged, deployed. Then it is never tested. A scheduled analytics rule with a typo in a column name deploys perfectly. So does one whose query window is shorter than the ingestion delay it has to span. Both report healthy forever, and a detector that never fires looks exactly like an environment nobody is attacking.

The objective. Declare two Sentinel analytics rules in Terraform, then prove the pipeline end to end by generating a real attack-shaped event and watching it become an incident — while a second rule, pointed at something that did not happen, stays silent.

The outcome. Seven resources, eight validation checks, 13 alerts grouped into 3 incidents, zero false positives from the negative control, and a measured cost of effectively nothing.

Cloud Services and Concepts

ServicePurpose hereWhat it is
Microsoft Sentinel Evaluates the two detection rules and raises incidents A SIEM that is enabled on a Log Analytics workspace rather than deployed as its own thing. The workspace holds the data; Sentinel adds detection, incidents and automation on top.
Log Analytics workspace Stores the Activity log the rules query The table store and query engine behind Azure Monitor. Sentinel has no storage of its own — this is it.
Azure Activity log The only signal in this lab, and it is free The subscription's control-plane record: who created, changed or deleted what. It lands in the AzureActivity table.
Diagnostic setting Forwards the Activity log into the workspace The routing rule that says "send these log categories from this scope to that destination". Attached to the subscription, not to a resource.
Scheduled analytics rule The detection itself — a KQL query on a timer A saved query, a frequency, a lookback window and a threshold. When the query returns more rows than the threshold, it raises an alert and opens an incident.

Alert versus incident. Worth holding before the numbers later make no sense. An alert is one firing of one rule. An incident is the case an analyst works. Sentinel's grouping decides how many alerts become one incident, and that ratio is the thing this lab ends up measuring.

Architecture and Workflow

Architecture diagram. At the subscription, a diagnostic setting forwards three Azure Activity log categories — Administrative, Policy and Security — into a Log Analytics workspace that has Microsoft Sentinel enabled on it. Sentinel has no icon in Microsoft's published set, so it is named on the workspace node rather than drawn with a borrowed logo, and the Sentinel-specific objects are plain cards for the same reason. Two scheduled analytics rules read the AzureActivity table every five minutes over a one-hour window: a positive control on role assignment creation, which is deliberately triggered, and a negative control on policy assignment deletion, which must stay silent. The positive control raised thirteen alerts, which incident grouping collapsed into three incidents, and an automation rule applied three labels to each without a Logic App.
The whole week on one page. The diagnostic setting sits at subscription scope, outside the resource group everything else lives in — which is why teardown has to remove it separately.
  1. A diagnostic setting on the subscription forwards the Administrative, Policy and Security Activity categories into the workspace.
  2. Enabling Sentinel on that workspace starts the 31-day free trial and turns the table store into a SIEM.
  3. Two analytics rules query AzureActivity every five minutes, each looking back one hour.
  4. A role assignment is created deliberately. The positive control matches it and raises an alert.
  5. Incident grouping collapses repeated alerts for the same event into one incident instead of one per run.
  6. An automation rule labels each new incident the moment it is created.
  7. The negative control — policy assignment deletion — matches nothing, and must report zero.

Three design decisions worth stating.

The lookback is twelve times the frequency, on purpose. Activity records reach a workspace minutes after the operation, and that delay is not a documented constant. A rule whose window equals its frequency inspects five minutes that have probably not been filled yet, finds nothing, and reports healthy forever. A one-hour window over a five-minute cadence costs duplicate evaluation to buy immunity from a delay nobody controls.

No playbook. The obvious way to automate incident handling is a playbook, which is a Logic App — and Microsoft documents that automation charges are not waived by the Sentinel trial. An automation rule does what this lab needs, costs nothing, and keeps a zero-cost week honestly zero.

The negative control is not padding. It is half the experiment.

Prerequisites

  • An Azure subscription where you hold Owner or User Access Administrator — the test creates a role assignment.
  • Four resource providers registered: Microsoft.OperationalInsights, Microsoft.Insights, Microsoft.SecurityInsights and Microsoft.OperationsManagement. The fourth is the one that catches people out.
  • Terraform ≥ 1.10, hashicorp/azurerm ~> 5.2.
  • Region: southcentralus. Nothing here is region-specific.
  • No cross-week dependency. Week 08 built a Log Analytics workspace for cost design; this week builds its own, because the Sentinel free trial is granted per workspace.

Implementation

Step 1 — Price it before building it

Why. Week 11 of this lab shipped a cost note that never mentioned Managed Grafana's flat $6/user/month meter. It was in the price API the whole time. The fix is not "read more carefully" — it is a gate.

Action. python scripts/price_check.py week-12-detections-as-code. It enumerates every meter the week's services can bill on, separates the flat recurring ones — those that charge while a thing merely exists — and refuses to pass until each is acknowledged in a committed meters.json with a written reason.

Terminal output of price_check.py for week 12, listing 43 flat recurring meters across Sentinel, Log Analytics and Azure Monitor grouped into 13 families, each marked ack, with the dearest being a Sentinel commitment tier at 117,990 per day
43 flat meters, 13 decisions, every one acknowledged. The dearest is a Sentinel commitment tier at $117,990 per day. It cannot be reached by accident — which is exactly what I believed about the $6.

Step 2 — The workspace, and Sentinel on top of it

Why. These are two objects, not one, and the distinction has a teardown consequence later.

resource "azurerm_sentinel_log_analytics_workspace_onboarding" "detections" {
  workspace_id = azurerm_log_analytics_workspace.detections.id
}

Validation. Ask the SecurityInsights API for the onboarding state rather than inferring it — a workspace without Sentinel looks identical from every other angle. Full source: main.tf.

Step 3 — The signal

Why. Sentinel's dedicated Azure Activity connector is gone. The legacy forwarding solution was retired on 30 September 2026; the current path is an ordinary subscription-scope diagnostic setting. The provider still ships azurerm_sentinel_data_connector_* resources, and they are the wrong answer now.

Action. One azurerm_monitor_diagnostic_setting targeting the subscription id.

Step 4 — The matched pair

AzureActivity
| where OperationNameValue =~ "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
| where ActivityStatusValue =~ "Success"

The second rule is identical in shape and points at POLICYASSIGNMENTS/DELETE — removing a guardrail, which is quieter than violating one. Both map the caller and source IP as entities, so an incident arrives with something to pivot on.

The Microsoft Sentinel Analytics blade listing both scheduled rules deployed by Terraform: Role assignment created at Medium severity and Policy assignment deleted at High severity, both enabled
Both rules, declared in Terraform. Medium for the role assignment, High for guardrail removal.

Step 5 — Attack it

Terminal output showing a Reader role assignment created through the ARM REST API at the week's resource group scope, with the created timestamp, because the az role assignment subcommand fails with MissingSubscription on this CLI version
The input to the test, not evidence of detection. A Reader assignment at the week's own resource group, revoked by cleanup before the destroy.

Validation and Evidence

TestExpectedActualStatus
Workspace existspresentlaw-wk12-dev-scus-001PASS
Sentinel enabled on itonboarding state presentpresentPASS
Both detections enabled2 enabled2 enabledPASS
Activity log reached the workspace> 0 rows3 role-assignment rowsPASS
Positive control fired≥ 1 incident3 incidents, 13 alertsPASS
Negative control silentexactly 00PASS
Automation rule labelled it3 labels3 labelsPASS
validate.sh output showing eight checks passing, including POSITIVE control fired with three incidents and NEGATIVE control stayed silent with zero incidents as required
What each result proves. Rows 1–3 prove Terraform ran. Row 4 proves the data arrived. Only the last three say the detection works, and the script fails if the negative control is noisy even when the positive one passes.

Note on what is missing. There is no console screenshot of the incident itself. The Azure portal's Sentinel Incidents page rendered a full grid once and then, twenty minutes later, served "This page has been moved to the Defender portal" on three consecutive attempts; its Overview tile separately reported Incidents (0) while the API returned two. The Defender portal needs a sign-in this subscription's identity cannot complete. Rather than publish a frame that contradicts the data, the slot is retired and the gap recorded.

Security and Cost

Security. No secrets exist in this week — there is nothing to authenticate. The deliberately-created role assignment is Reader, at the week's own resource group, revoked explicitly before teardown rather than left to disappear with the group. The detection is deliberately untuned: a real tenant writes role assignments constantly through automation, and shipping this as-is would page someone hourly. Production adds a baseline — known automation principals excluded by object id, scope narrowed to privileged roles. That is a watchlist, and it is next week's problem.

Cost: effectively zero, and here is each leg of it.

  • AzureActivity is a free data type in both Log Analytics ingestion and Sentinel analysis.
  • Enabling Sentinel grants 10 GB/day free for 31 days.
  • Retention stays at the 90 free days; a Terraform validation block refuses a higher value rather than trusting a comment.
  • No Logic App and no data lake tier — both are explicitly outside the trial's waiver.

The measured invoice figure is not in yet; this post will carry it rather than a rate-card estimate. What is established is the complete list of meters that could have applied, written down before deployment.

Troubleshooting and Lessons

Problem. Sentinel onboarding failed with MissingSubscriptionRegistration after the workspace was already built.
Root cause. Microsoft.OperationsManagement — a fourth provider, beyond the three you would predict.
Resolution. Register it; 50 seconds.
Lesson. Provider registration is a deployment precondition, and the list is not guessable. Check before applying, not after.

Problem. The analytics rules failed with "Workspace is not onboarded to Microsoft Sentinel" — which reads like the onboarding resource failed. It had not.
Root cause. Every Sentinel resource references the workspace id, so the only dependency in the graph was on the workspace. Nothing said Sentinel had to be enabled first, and Terraform created the rules in parallel with the onboarding.
Resolution. An explicit depends_on.
Lesson. A semantic dependency with no value flowing across it is not a dependency until you declare it.

Problem. I reported "one incident from three rows, grouping working." Eighteen minutes later there were two. Seven hours later, three.
Root cause. I was measuring the wrong thing. The incident count slides as the query window moves.
Resolution. Measure the ratio: 13 alerts into 3 incidents, one of which absorbed eleven by itself.
Lesson. A number that changes when you look again was never the measurement. Ungrouped, this would have been thirteen incidents for one thing that happened.

Problem. The portal would have produced a screenshot asserting the opposite of the truth — twice.
Root cause. The Overview tile is an aggregate with its own refresh, not a view of the data; and the Incidents page is mid-migration to the Defender portal.
Lesson. Validate against the API and the table, not against a console tile. Sentinel loses Azure portal support after 31 March 2027; on this tenant the incident views went first.

Cleanup and Key Takeaways

cleanup.sh output showing the role assignment revoked, seven resources destroyed, and three post-destroy checks confirming against Azure that the resource group, the Log Analytics workspace and the subscription diagnostic setting are all gone
Verified against Azure, not against an exit code. The soft-delete note is correct behaviour: a deleted workspace is recoverable by name for 14 days. It stops billing; the name stays reserved.

The teardown trap, in Microsoft's words: "Removing Microsoft Sentinel doesn't remove the Log Analytics workspace Microsoft Sentinel was deployed on, or any separate charges that workspace might be incurring." Offboarding Sentinel is not cleanup. The workspace is the billable object.

The subscription-scope diagnostic setting is also outside the resource group, so deleting the group leaves it behind, pointed at a workspace that no longer exists. Both are checked explicitly.

Takeaways

  1. A detection is only proved by a pair. One that fires and one that must not. A rule that fires on a true positive might fire on anything; a rule silent on a true negative might simply be broken.
  2. The query window must exceed the ingestion delay. Set it equal to the frequency and the rule silently never fires — and looks exactly like peace.
  3. Measure the ratio, not the count. 13 alerts into 3 incidents says what grouping did. "3 incidents" says almost nothing.
  4. Price the week before you build it. Not by reading carefully — by a gate that will not pass until every flat recurring meter is acknowledged in writing.
  5. Trust the API over the console. Here the portal disagreed with the data twice in one afternoon.

Repository: week-12-detections-as-code — Terraform, validation, cleanup, and the committed meters.json recording every charge this week accepted.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent