Hands-on lab: Sentinel as code, end to end
Parts 1 and 2 explained the ideas. Here you do it, on your own subscription, as the implementer: build the workspace from code, ship detections through a reviewed pipeline, switch Sentinel settings on and off from Git, then catch someone changing a rule by hand.
Everything you copy is in examples/sentinel-as-code-lab in this site's public repository: the bootstrap script, the Terraform, the rule files, both pipelines and the exercise solutions. The Terraform passes terraform validate against azurerm 4.x, at the start and with every solution applied.
What you will build
| Lab | You will | Sentinel resources | Time |
|---|---|---|---|
| 1 | Create the repository, the state storage and the pipeline's identity | None yet | 30 min |
| 2 | Deploy the workspace and enable Sentinel through the pipeline | log_analytics_workspace, sentinel_log_analytics_workspace_onboarding | 30 min + wait |
| 3 | Ship a scheduled and an NRT rule, then write your own | sentinel_alert_rule_scheduled, sentinel_alert_rule_nrt | 45 min |
| 4 | Add a watchlist from CSV and an automation rule, then make a rule use the watchlist | sentinel_watchlist, sentinel_watchlist_item, sentinel_automation_rule | 30 min |
| 5 | Adopt Fusion, toggle it from Git, enable a connector | sentinel_alert_rule_fusion, sentinel_data_connector_microsoft_threat_intelligence | 20 min |
| 6 | Change a rule in the portal and watch the system catch it | All of the above | 30 min + wait |
| 7 | Roll back with git revert; break the pipeline on purpose | — | 20 min |
| 8 | Run the same pipeline from Azure DevOps | — | 45 min |
| 9 | Delete everything | — | 5 min |
The detections watch the Azure Activity log: who created, changed or deleted what in the subscription. It is free to ingest, and it means the lab detects your own actions. The main rule fires when a person, rather than the pipeline, changes an analytic rule. In Lab 6, that person is you.
Lab 0 · Before you start
- An Azure subscription where you are Owner, in a tenant where you can sign in to the Defender portal. A personal pay-as-you-go subscription is ideal. Do not use a work tenant.
- A GitHub account. Azure DevOps is optional, for Lab 8.
- Azure Cloud Shell (Bash) for the one-time commands. Nothing needs installing on your laptop. Installing Terraform locally helps if you want to run
terraform validateyourself. - Cost: the workspace has a 0.5 GB daily ingestion cap, and the Activity log is free to ingest. With no other data sources, expect a few cents. The threat-intelligence connector in Lab 5 adds a small amount of data, still under the cap. Lab 9 deletes everything.
Lab 1 · Repository and bootstrap
Goal: a private repository with the lab files, and the three things the pipeline needs before it can run at all: state storage, an identity, and permissions.
1.1 Create your repository
- On GitHub, create a new private repository, for example
sentinel-as-code-lab. Do not add a README. - Copy the lab files into it:
git clone --depth 1 https://github.com/iamkaushiksaha/learning-hub.git /tmp/hub
git clone https://github.com/<you>/sentinel-as-code-lab.git
cp -R /tmp/hub/examples/sentinel-as-code-lab/. sentinel-as-code-lab/
cd sentinel-as-code-lab
# Start with the platform only. The other files come back one lab at a time.
mkdir later
mv terraform/{rules,nrt,watchlists,automation,settings}.tf later/
git add . && git commit -m "Lab 1: scaffold" && git push origin main1.2 Run the bootstrap in Cloud Shell
- Open
shell.azure.com, choose Bash, and check the right subscription is selected withaz account show. - Clone your repository there (or upload
bootstrap/bootstrap.sh) and run it:
git clone https://github.com/<you>/sentinel-as-code-lab.git && cd sentinel-as-code-lab
./bootstrap/bootstrap.sh github <you>/sentinel-as-code-labRead the script before you run it. In order, it:
- registers the Azure resource providers the lab uses. The pipeline will not be allowed to.
- creates an empty
rg-sentinel-labresource group for Terraform to deploy into, andrg-sentinel-lab-tfstatefor the state. - creates the state storage account with key access disabled, public blob access off and versioning on.
- creates the Entra app the pipeline signs in as, with three federated credentials: one for
main, one for pull requests, and one for thesentinel-labenvironment. - grants it Contributor on
rg-sentinel-labonly, and Storage Blob Data Contributor on the state account.
It ends by printing five values and the teardown commands. Keep that output.
1.3 Configure the repository
- Settings → Secrets and variables → Actions → Variables tab. Add the five printed values:
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID,TFSTATE_RESOURCE_GROUPandTFSTATE_STORAGE_ACCOUNT. Use variables, not secrets: none of them are credentials. - Settings → Environments → New environment →
sentinel-lab. Tick Required reviewers and add yourself. - Settings → Branches → add a protection rule for
main: require a pull request before merging, and require thevalidatestatus check.
sentinel-as-code-lab-pipeline → Certificates & secrets → Federated credentials shows three entries, and the Client secrets tab is empty. That empty tab is the point of OIDC.Lab 2 · Platform as code
Goal: the workspace and Sentinel itself, created by the pipeline through a reviewed pull request. This is Phase 0 and Phase 1 from part 2.
- Read
terraform/platform.tf. Three things matter: the workspace with itsdaily_quota_gbcost guard;azurerm_sentinel_log_analytics_workspace_onboarding, the one resource that means "enable Sentinel"; andlocal.sentinel_workspace_id, which every later resource uses so it waits for Sentinel to be on. - Make any small change on a branch, for example a comment in
lab.tfvars, and open a pull request. The Lab 1 push only ranvalidate, because the variables did not exist yet. This time the whole review flow runs. - Watch the checks.
validateruns with no Azure access.plansigns in through OIDC and posts a comment ending inPlan: 2 to add, 0 to change, 0 to destroy. - Merge. The
applyjob pauses at Waiting for review. Approve it.
rg-sentinel-lab contains law-sentinel-lab, and Microsoft Sentinel lists it. Then open the Defender portal (security.microsoft.com). If the workspace was not connected automatically, connect it from the Microsoft Sentinel workspace settings. This is the first human step: it needs a Security Administrator or Global Administrator, and it is a tenant action, not a resource.2.1 Connect the Activity log (the second human step)
The detections need the AzureActivity table, which exists only once Activity log data flows into the workspace. That setting lives at subscription scope. The pipeline is deliberately limited to one resource group, so you do this once by hand. Put the reason in your runbook, not just the step.
WS_ID=$(az monitor log-analytics workspace show -g rg-sentinel-lab -n law-sentinel-lab --query id -o tsv)
az monitor diagnostic-settings subscription create \
--name activity-to-sentinel-lab \
--location centralindia \
--workspace "$WS_ID" \
--logs '[{"category":"Administrative","enabled":true},{"category":"Security","enabled":true},{"category":"Policy","enabled":true}]'AzureActivity | take 10 returns rows, including the pipeline's own deployment from a moment ago. Do not start Lab 3 until it does.Lab 3 · Analytic rules through CI/CD
Goal: detections deployed only through a pull request, and one written by you.
3.1 Ship the first two rules
- On a new branch:
git mv later/rules.tf later/nrt.tf terraform/. - Read
content/rules/sentinel-rule-changed-by-person.yaml, thenterraform/rules.tf, which turns every YAML file into a rule withfor_each. Compare it withnrt.tf, where the rule is written straight in HCL. - Push and open a pull request. The plan should say
2 to add: one scheduled rule, one NRT rule. Read the plan as a reviewer would. Is the query what you expect? Are the entity mappings there? - Merge and approve.
[LAB] Sentinel analytic rule changed by a person and [LAB] Azure role assignment created, both enabled, and the apply log lists both resource IDs under scheduled_rules and the NRT rule.3.2 Exercise: write a detection yourself
Write a High-severity rule that fires when a resource group is deleted. Work from AzureActivity where OperationNameValue is MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE. Map it to tactic Impact and technique T1485, and generate a fresh GUID with uuidgen. Add only a YAML file. You should not need to touch Terraform. The answer is in solutions/lab3-resource-group-deleted.yaml.
3.3 Make them fire
# Fires the resource-group rule (within the hour)
az group create -n rg-lab-scratch -l centralindia
az group delete -n rg-lab-scratch --yes
# Fires the NRT rule (within minutes)
ME=$(az ad signed-in-user show --query id -o tsv)
RG_ID=$(az group show -n rg-sentinel-lab --query id -o tsv)
az role assignment create --assignee-object-id "$ME" --assignee-principal-type User --role Reader --scope "$RG_ID"
az role assignment delete --assignee "$ME" --role Reader --scope "$RG_ID"Lab 4 · Watchlist and automation rule
Goal: content beyond rules, and a lesson in deployment order.
- On a branch:
git mv later/watchlists.tf later/automation.tf terraform/. Read howwatchlists.tfusescsvdecode(), so each CSV row becomes one watchlist item. - Pull request → the plan adds the watchlist, one item and the automation rule → merge → approve.
- Second pull request: replace
content/rules/sentinel-rule-changed-by-person.yamlwithsolutions/lab4-rule-with-watchlist.yaml. The rule now ignores people on the approved-admins watchlist. - Read this plan carefully. It should say
~ update in-placewith only the query changed, not destroy-and-create. An in-place update keeps the rule's identity and incident history.
_GetWatchlist("lab-approved-admins"). Shipping the rule change in the same pull request as the watchlist risks Terraform updating the rule before the watchlist exists. Sequencing dependent changes is a real release-management skill. The other fix is an explicit depends_on.Do not add yourself to the CSV yet. If you do, Lab 6's detection will correctly ignore you.
Lab 5 · Turn Sentinel settings on and off from Git
Goal: manage capabilities, not just content, using two more of the 32 resource types.
- On a branch:
git mv later/settings.tf terraform/. Read the comments first. - Pull request. The plan adds a Fusion rule and the threat-intelligence connector. Merge and approve.
What just happened with Fusion. Every new workspace already has Fusion enabled, as a rule named BuiltInFusion. The provider writes to that same rule, so Terraform did not create a second one. It adopted the existing setting. From now on, Fusion's on/off state lives in Git.
5.1 Exercise: switch Fusion off and on
- Pull request setting
enabled = falseinsettings.tf. The plan shows~ enabled: true → false. Merge and approve, then confirm in Analytics that Fusion shows as disabled. - Pull request setting it back to
true. The history of who turned detection off, when, and who approved it is now ingit log.
BuiltInFusion, terraform destroy, or deleting settings.tf, deletes the rule and turns Fusion off. Owning a setting means owning its removal too.WS_ID=$(az monitor log-analytics workspace show -g rg-sentinel-lab -n law-sentinel-lab --query id -o tsv)
az rest --method get --url "https://management.azure.com$WS_ID/providers/Microsoft.SecurityInsights/dataConnectors?api-version=2023-02-01" --query "value[].{name:name, kind:kind}" -o tableIf the apply failed on the connector instead, read the error, remove that resource, and write down why. Connectors are where Terraform coverage is weakest, and finding that out yourself is part of the lesson.
Not everything is one of the 32. UEBA, anomaly settings and Content Hub solutions have no azurerm resource. Enable UEBA in the portal and add it to your human-steps runbook. That runbook is a deliverable in a real engagement, not an apology.
Lab 6 · Catch drift
Goal: see the whole system respond when someone bypasses it. This is the lab worth describing in an interview.
- In the portal, open
[LAB] Azure role assignment createdand change its severity from Low to High. Save. You have just done what an analyst does at 2 a.m. during an incident. - Wait up to an hour.
[LAB] Sentinel analytic rule changed by a personfires, with you as the account entity. The incident carries thesentinel-as-code-lablabel, because your automation rule from Lab 4 labelled it on creation. - In GitHub → Actions → sentinel → Run workflow on
main. The plan shows~ severity: "High" → "Low". Terraform has noticed that reality no longer matches the code. - Decide. To keep the change, open a pull request putting
Highinnrt.tf, so the code catches up with reality. To revert it, approve the apply job of the run you just started. If you keep the change, reject that pending apply instead.
You do not have to remember to check. The workflow also runs at 01:30 UTC every night, and fails with a drift error whenever the plan is not empty.
Lab 7 · Roll back, and break it on purpose
7.1 Roll back a bad change
git checkout main && git pull
git log --oneline # find the Lab 4 watchlist-query commit
git checkout -b revert-watchlist-query
git revert <commit-sha>
git push -u origin revert-watchlist-query # PR: the plan shows the old query coming backMerge and approve: the rule is back to its earlier version in minutes, with a record of why. Then revert the revert.
7.2 Break it on purpose
Open a pull request for each of these and watch where it fails. Nothing should reach Azure:
- Run
terraform fmt, then deliberately mis-indent a.tffile:validatefails on format. - Remove the
event_groupingblock fromnrt.tf:terraform validatefails. NRT rules require it, and the author of this lab found that out exactly this way. - Set a rule's tactic to
Initial Access, with a space:planfails, because the provider checks tactics against Sentinel's list. Nothing was approved, nothing reached Azure. - Give a rule a period shorter than its frequency, for example
frequency: PT1Handperiod: PT30M.validateandplanboth pass. The provider only refuses it duringapply, after you have approved the deployment. A mistake caught after approval is the case for your own validator in front of Terraform, catching it on the pull request instead. Part 1 of the Detection-as-Code series covers one.
Lab 8 · The same pipeline in Azure DevOps
Goal: prove the Terraform does not care which platform runs it. Before you start, disable the GitHub workflow (Actions → sentinel → ⋯ → Disable workflow) so two pipelines never fight over one state file.
- Sign in at
dev.azure.com, create an organisation and link your Azure subscription for billing. That is what unlocks the free Microsoft-hosted pipeline minutes. Create a private project. - Repos → Import repository → your GitHub repository URL. Alternatively, keep the code in GitHub and connect it when you create the pipeline.
- Project settings → Service connections → New → Azure Resource Manager → Workload identity federation (manual). Name it
sc-sentinel-lab, enter the subscription, then the client ID and tenant ID from Lab 1. It displays an issuer and a subject. Leave it open. - In Cloud Shell, add a fourth federated credential to the existing app with that issuer and subject, then return and verify and save the service connection:
az ad app federated-credential create --id <AZURE_CLIENT_ID> --parameters '{
"name": "ado-sc-sentinel-lab",
"issuer": "<issuer shown by Azure DevOps>",
"subject": "<subject shown by Azure DevOps>",
"audiences": ["api://AzureADTokenExchange"]
}'- Pipelines → Library → variable group
sentinel-labwith the same five values as Lab 1, plusSERVICE_CONNECTION_ID: the GUID in the service connection page's URL. - Pipelines → Environments →
sentinel-lab→ Approvals and checks → Approvals → add yourself. - Repos → Branches →
main→ Branch policies: require a minimum number of reviewers, and add Build validation pointing at the pipeline. In Azure Repos, that policy is what runs the pipeline on pull requests. - Pipelines → New pipeline → your repository → Existing Azure Pipelines YAML file →
/azure-pipelines.yml→ Run. Permit access to the service connection, variable group and environment when asked.
No changes, because Azure DevOps read the same state file GitHub wrote. Same code, same state, different engine.Lab 9 · Tear down
az monitor diagnostic-settings subscription delete --name activity-to-sentinel-lab --yes
az group delete --name rg-sentinel-lab --yes --no-wait
az group delete --name rg-sentinel-lab-tfstate --yes --no-wait
az ad app delete --id <AZURE_CLIENT_ID>
az group list --query "[?tags.purpose=='sentinel-as-code-lab'].name" -o tsv # empty when doneDeleting the resource groups deletes the workspace, every rule and the state together. In a real environment you would run terraform destroy through the pipeline instead, so the deletion is reviewed like any other change.
Troubleshooting
sentinel-lab environment, so its subject is repo:<you>/<repo>:environment:sentinel-lab. Check the repository name, the environment name and the casing exactly. In Azure DevOps, re-copy the issuer and subject from the service connection.What you can now say, honestly
After completing Labs 1 to 7, these are true statements about your own work, and you can defend each one in detail:
- Built a Microsoft Sentinel deployment as code with Terraform: workspace, Sentinel onboarding, scheduled and NRT analytic rules, watchlists, automation rules, Fusion and a threat-intelligence connector.
- Designed a CI/CD pipeline in GitHub Actions (and Azure DevOps) with OIDC workload identity federation, least-privilege role scoping, plan review on pull requests, an approval gate before deployment, and nightly drift detection.
- Wrote a detection that alerts when analytic rules are changed outside the pipeline, closing the loop between change control and the SOC.
Questions you should now be able to answer without notes: