Manual Test Runbook — J3: Microsoft Sentinel Deployment¶
Owner: Sagar | Time: ~8 min (Parts A + B offline) · +20 min (optional Part C integration apply) | Sandbox: snowops-sandbox-01
Promotes J3 (
modules/azure/sentinel/) from 🟦 Code Complete → 🟩 Shipped. Parts A + B are offline ($0). Part C onboards Sentinel onto a sandbox Log Analytics workspace and deploys the analytics rules (Sentinel itself is billed on data ingested — an empty sandbox workspace for a few minutes is ~$0) then destroys.
Prerequisites¶
- Sandbox subscription access active (PIM activated if required)
-
az logindone; sandbox subscription selected - Identity has Contributor + Microsoft Sentinel Contributor on the sandbox sub
-
SNOWOPS_SANDBOX_SUBSCRIPTION_ID+SNOWOPS_SANDBOX_TENANT_IDexported - Local tooling:
terraform >= 1.6,go >= 1.22,az CLI >= 2.50 - Working directory: repo root
Steps¶
Part A — terraform fmt + validate (offline, ~3 min)¶
- Module + example:
terraform -chdir=modules/azure/sentinel fmt -recursive -check
terraform -chdir=modules/azure/sentinel init -backend=false -input=false
terraform -chdir=modules/azure/sentinel validate
terraform -chdir=modules/azure/sentinel/examples/basic init -backend=false -input=false
terraform -chdir=modules/azure/sentinel/examples/basic validate
Expected: Success! for both.
- Offline Terratest case:
Expected: PASS — exercises onboarding, two connectors, the curated rule pack (override raising severity + override disabling a rule + entity mappings + tactics + incident grouping), a custom rule, and the Microsoft incident rule, offline.
Part B — full Terratest suite (offline, ~5 min)¶
bash cd tests/terratest && go test -count=1 -timeout 15m ./...
Expected: the full suite green (the new TestSentinelValidate included).
Part C — integration apply (sandbox, ~20 min, ~$0)¶
No build-tagged integration test ships for J3 — Sentinel onboarding + a rule actually raising an incident needs log data flowing into a real workspace, so the "incident raises within N min of a planted signal" criterion belongs here, not in a teardown-on-completion test.
- Stand up a throwaway workspace (reuse the J1 fixture), then point the J3
fixture's
workspace_idat that real workspace ARM ID and apply:
# 1. workspace
cd tests/terratest/fixtures/log-analytics
terraform init -input=false
terraform apply -auto-approve \
-var "subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID" \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID"
WS_ID=$(terraform output -raw id)
# 2. sentinel — edit fixtures/sentinel/main.tf workspace_id = "$WS_ID" first
cd ../sentinel
terraform init -input=false
terraform apply -auto-approve \
-var "subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID" \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID"
- Confirm Sentinel is onboarded and the rules exist:
az sentinel alert-rule list \
--resource-group snowops-obs-rg \
--workspace-name "$(basename "$WS_ID")" \
--query "[].{name:displayName, kind:kind, enabled:enabled}" -o table
Expected: the three enabled scheduled rules (identity-password-spray at
High per the fixture override, identity-impossible-travel,
privilege-rbac-grant) + the custom data-exfil-large-blob-egress + the
Microsoft incident rule. privilege-policy-tamper is absent (disabled via
the fixture override).
- (Optional, signal test) Confirm an incident raises. Easiest: temporarily add
a custom rule whose KQL matches data that exists (e.g.
Heartbeat | take 1), wait one evaluation window, and confirm a Sentinel incident appears:
az sentinel incident list \
--resource-group snowops-obs-rg \
--workspace-name "$(basename "$WS_ID")" \
--query "[].{title:title, severity:severity, status:status}" -o table
Expected: an incident is created with the rule's severity and the mapped entities attached.
- Destroy (Sentinel first, then the workspace):
cd tests/terratest/fixtures/sentinel
terraform destroy -auto-approve \
-var "subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID" \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID"
cd ../log-analytics
terraform destroy -auto-approve \
-var "subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID" \
-var "tenant_id=$SNOWOPS_SANDBOX_TENANT_ID"
Expected: clean destroy — the analytics rules + connectors + onboarding are removed (Sentinel offboards from the workspace), then the workspace itself.
Pass criteria¶
- Part A — module + example validate;
TestSentinelValidatepasses - Part B — full offline suite passes
- (Part C) Sentinel onboards; rule set + the High override + the disabled rule match; destroys clean
- (Part C, optional) a planted signal raises a Sentinel incident with entities
- All test resources removed
Failure mode¶
A rule that can never fire because its source table (SigninLogs,
AzureActivity, …) isn't flowing into the workspace — the corresponding
connector or J2 diagnostic setting isn't wired. The rule validates and deploys
but stays silent. Mitigation: enable the matching connector / diagnostic source
first; the per-domain toggle drops a category whose source isn't available.
Cost impact¶
The onboarding resource, connectors, and analytics rules are free. Sentinel is billed on data ingested into the workspace (per-GB analytics ingestion + any commitment tier) on top of the underlying Log Analytics cost. An empty sandbox workspace for the duration of Part C is ~$0. In production, cost scales with which connectors are enabled and how chatty their tables are.
Removal path¶
terraform destroy (Part C step 7) offboards Sentinel and removes every rule +
connector. The consumed workspace is owned by J1, not J3. Verified clean in
Part C.
Sign-Off¶
| Field | Value |
|---|---|
| Part A (validate) | ☐ PASS |
| Part B (offline suite) | ☐ PASS |
| Part C (integration apply) | ☐ PASS / ☐ skipped |
| Part C signal test | ☐ PASS / ☐ skipped |
| Tester | |
| Date | |
| Result | ☐ PASS |