Skip to content

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 login done; sandbox subscription selected
  • Identity has Contributor + Microsoft Sentinel Contributor on the sandbox sub
  • SNOWOPS_SANDBOX_SUBSCRIPTION_ID + SNOWOPS_SANDBOX_TENANT_ID exported
  • 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)

  1. 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.

  1. Offline Terratest case:
cd tests/terratest
go test -v -timeout 5m ./modules/azure/... -run TestSentinelValidate

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)

  1. 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.

  1. Stand up a throwaway workspace (reuse the J1 fixture), then point the J3 fixture's workspace_id at 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"
  1. 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).

  1. (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.

  1. 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; TestSentinelValidate passes
  • 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