Create Logic App playbooks to auto-enrich incidents with threat intelligence, post formatted notifications to Microsoft Teams, isolate compromised accounts in Entra ID, and build end-to-end automated remediation workflows triggered by Sentinel automation rules.
In this hands-on lab you will build a production-grade SOAR (Security Orchestration, Automation, and Response) workflow using Microsoft Sentinel playbooks powered by Azure Logic Apps. You will create a multi-action playbook that triggers automatically when a high-severity incident is created, enriches the incident with threat intelligence and geolocation data, posts a formatted adaptive card to a Microsoft Teams SOC channel, disables compromised user accounts in Microsoft Entra ID, revokes active sessions, and writes a complete evidence trail back to the incident timeline. By the end of this lab you will have a fully automated incident response pipeline that reduces mean time to respond from hours to seconds.
A healthcare company with 5,000 employees receives over 200 low and medium severity incidents daily across identity, endpoint, and cloud workloads. Their SOC team of 8 analysts cannot keep pace with manual triage: the current mean time to respond (MTTR) is 4 hours. Critical incidents involving compromised credentials sit in queues while analysts manually look up IP reputation, check geolocation, notify the on-call team via email, and navigate to the Entra ID portal to disable accounts. Regulatory requirements (HIPAA) demand that compromised accounts are contained within 15 minutes of detection. Leadership has mandated automated triage, enrichment, notification, and containment workflows to reduce MTTR from 4 hours to under 15 minutes while freeing analysts to focus on high-complexity investigations.
SOAR automation is the force multiplier that transforms a reactive SOC into a proactive security operation. According to industry research, organizations leveraging automated incident response reduce MTTR by up to 80% and handle 10x more incidents without adding headcount. Without automation, every minute a compromised account remains active is a minute an attacker can exfiltrate patient data, move laterally through the network, or establish persistence. Microsoft Sentinel’s native integration with Azure Logic Apps provides a no-code/low-code platform to orchestrate responses across Microsoft 365, Entra ID, Defender XDR, and hundreds of third-party services: making it the ideal SOAR platform for organizations already invested in the Microsoft security ecosystem.
Microsoft Sentinel provides three automation mechanisms that work together. Understanding when to use each is essential before building your first playbook.
Lightweight rules that run automatically when incidents are created or updated. They can change incident properties (severity, status, owner), suppress noisy alerts, or trigger playbooks. Automation rules execute in order of priority and are evaluated for every incident: think of them as the “traffic controller” for your SOAR pipeline.
Full workflow engines powered by Azure Logic Apps. Playbooks can call external APIs, perform complex branching logic, loop through entities, and orchestrate multi-step response actions. They are triggered by automation rules or manually by an analyst from the incident page. Playbooks are where the real SOAR magic happens.
Before creating playbooks, you must grant the Logic App managed identity permissions to interact with Microsoft Sentinel and Microsoft Graph. This step registers the required API connections.
Ensure the Microsoft.Logic resource provider is registered in your subscription:
# Register the Logic Apps resource provider in your subscription
# PURPOSE: Azure must register the provider before you can create Logic App resources
# This is a one-time operation per subscription
az provider register --namespace Microsoft.Logic
# Verify registration status
# Expected output: "Registered" - if "Registering", wait 1-2 min and re-check
# If "NotRegistered", you may lack subscription-level permissions
az provider show --namespace Microsoft.Logic --query "registrationState" -o tsvThe playbook’s managed identity needs the Microsoft Sentinel Responder role to read and update incidents:
# Variables - replace with your actual resource group and workspace names
RG="rg-sentinel-lab"
WORKSPACE="law-sentinel-lab"
SUB_ID=$(az account show --query id -o tsv) # Get current subscription ID
# After creating the Logic App (Step 3), assign the Sentinel Responder role:
# This grants the playbook’s managed identity permission to read/update incidents
# --query "identity.principalId" : Extract the managed identity’s object ID
LOGIC_APP_PRINCIPAL_ID=$(az logic workflow show \
--resource-group $RG \
--name "Sentinel-IncidentEnrich" \
--query "identity.principalId" -o tsv)
# Assign "Microsoft Sentinel Responder" role to the Logic App’s managed identity
# --assignee-object-id : The managed identity’s principal ID from above
# --assignee-principal-type : ServicePrincipal (not User) for managed identities
# --role : "Microsoft Sentinel Responder" allows reading & updating incidents
# --scope : Resource group scope - limits permissions to this RG only
# Expected output: JSON role assignment object with "provisioningState": "Created"
az role assignment create \
--assignee-object-id $LOGIC_APP_PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Microsoft Sentinel Responder" \
--scope "/subscriptions/$SUB_ID/resourceGroups/$RG"Now you will create the core Logic App that serves as your incident response playbook. This playbook uses the Microsoft Sentinel Incident trigger to receive the full incident context including mapped entities (IPs, accounts, hosts).
# Create a Consumption-tier Logic App with managed identity
az logic workflow create \
--resource-group $RG \
--name "Sentinel-IncidentEnrich" \
--location "eastus" \
--mi-system-assigned \
--definition '{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"Microsoft_Sentinel_incident": {
"type": "ApiConnectionWebhook",
"inputs": {
"host": {
"connection": {
"name": "@parameters($connections)[azuresentinel][connectionId]"
}
},
"body": {
"callback_url": "@listCallbackUrl()"
},
"path": "/incident-creation"
}
}
},
"actions": {},
"parameters": {
"$connections": {
"type": "Object"
}
}
}
}'Sentinel-IncidentEnrichAdd the Entities: Get IPs and Entities: Get Accounts actions from the Sentinel connector to parse entity arrays from the incident object:
Entities from the triggerWith IP entities extracted, you will now enrich each IP address with threat intelligence reputation data and geolocation information. This context helps analysts prioritize genuine threats over false positives.
Inside the For each IP loop, add an HTTP action to query a geolocation API:
https://ipapi.co/@{items('For_each_IP')?['Address']}/json/{
"type": "object",
"properties": {
"ip": { "type": "string" },
"city": { "type": "string" },
"region": { "type": "string" },
"country_name": { "type": "string" },
"org": { "type": "string" },
"asn": { "type": "string" },
"latitude": { "type": "number" },
"longitude":{ "type": "number" }
}
}You can also check the IP against threat intelligence indicators already ingested into your Sentinel workspace. Add a Run query and list results action (Azure Monitor Logs connector):
// Query Sentinel TI indicators to check if an IP is known-malicious
// PURPOSE: Match incident IP entities against your threat intelligence database
// WHY: Enriching with TI data helps analysts prioritize: known-bad IP = high confidence
// This runs inside a Logic App "Run query and list results" action
ThreatIntelligenceIndicator
| where TimeGenerated > ago(90d) // Search last 90 days of TI data
| where isnotempty(NetworkIP) // Only IP-type indicators
| where NetworkIP == "@{items('For_each_IP')?['Address']}" // Dynamic: current incident IP
| summarize
ThreatTypes = make_set(ThreatType), // e.g., ["malicious-activity", "C2"]
Confidence = max(ConfidenceScore), // Highest confidence score (0-100)
Sources = make_set(SourceSystem), // Which TI feed(s) flagged this IP
LastSeen = max(TimeGenerated) // Most recent sighting in TI feed
| project NetworkIP = "@{items('For_each_IP')?['Address']}",
ThreatTypes, Confidence, Sources, LastSeen
// Expected output: If match found → threat type, confidence, and source details
// No results = IP not in your TI database (doesn’t mean it’s safe - just unknown)Before the For each loop, initialize a string variable called EnrichmentSummary. Inside the loop, append geolocation and TI results to this variable. You will use it later to update the incident and post to Teams.
After enrichment, the playbook will post a formatted notification to your SOC Teams channel with all relevant incident details, enabling rapid situational awareness without requiring analysts to open the Sentinel portal.
Paste this adaptive card JSON into the Adaptive Card field, replacing dynamic content where indicated:
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "Container",
"style": "emphasis",
"items": [
{
"type": "TextBlock",
"text": "?? Sentinel Incident #@{triggerBody()?['object']?['properties']?['incidentNumber']}",
"weight": "Bolder",
"size": "Large"
}
]
},
{
"type": "FactSet",
"facts": [
{ "title": "Title:", "value": "@{triggerBody()?['object']?['properties']?['title']}" },
{ "title": "Severity:", "value": "@{triggerBody()?['object']?['properties']?['severity']}" },
{ "title": "Status:", "value": "@{triggerBody()?['object']?['properties']?['status']}" },
{ "title": "Created:", "value": "@{triggerBody()?['object']?['properties']?['createdTimeUtc']}" },
{ "title": "Alerts:", "value": "@{length(triggerBody()?['object']?['properties']?['alerts'])}" }
]
},
{
"type": "TextBlock",
"text": "**Enrichment Results:**",
"wrap": true
},
{
"type": "TextBlock",
"text": "@{variables('EnrichmentSummary')}",
"wrap": true
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "?? Open in Sentinel",
"url": "@{triggerBody()?['object']?['properties']?['incidentUrl']}"
}
]
}This step implements automated containment by disabling compromised user accounts in Microsoft Entra ID and revoking all active sessions. This is the highest-impact automation action: it stops an attacker’s access within seconds of incident creation.
Inside the For each Account loop, add HTTP actions that call the Microsoft Graph API using the Logic App’s managed identity:
HTTP Action Configuration:
Method: PATCH
URI: https://graph.microsoft.com/v1.0/users/@{items('For_each_Account')?['AadUserId']}
// AadUserId = Entra ID object ID of the compromised user from the incident entity
Headers: Content-Type: application/json
Body: { "accountEnabled": false }
// Sets the account to disabled - user cannot sign in anywhere
// Existing sessions remain active until revoked (see Action 2)
Authentication: Managed Identity
// Uses the Logic App’s system-assigned managed identity (no credentials stored)
Audience: https://graph.microsoft.com
// Token audience for Microsoft Graph API calls
// Expected result: HTTP 204 No Content = success
// Requires: User.ReadWrite.All application permission on the managed identityHTTP Action Configuration:
Method: POST
URI: https://graph.microsoft.com/v1.0/users/@{items('For_each_Account')?['AadUserId']}/revokeSignInSessions
// Invalidates ALL refresh tokens and session cookies for this user
// Forces re-authentication on every device and application
// Combined with account disable, this fully contains a compromised identity
Authentication: Managed Identity
Audience: https://graph.microsoft.com
// Expected result: HTTP 200 with { "value": true } = all sessions revoked
// Note: Active browser sessions may take up to 1 hour to expire due to token cachingThe Logic App’s managed identity must have User.ReadWrite.All application permission in Microsoft Graph. Use PowerShell to grant this:
# Connect to Microsoft Graph with admin consent
# Scopes needed: AppRoleAssignment.ReadWrite.All to grant permissions,
# Application.Read.All to look up the Graph service principal
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All"
# Get the Logic App managed identity service principal
# Replace with the actual principal ID from: az logic workflow show --query identity.principalId
$MIObjectId = "PASTE-LOGIC-APP-PRINCIPAL-ID"
# Get the Microsoft Graph service principal (it’s a well-known enterprise app)
$GraphSP = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" | Select-Object -First 1
# Find the User.ReadWrite.All app role (application permission, not delegated)
# This permission allows the managed identity to disable users and update profiles
$AppRole = $GraphSP.AppRoles | Where-Object { $_.Value -eq "User.ReadWrite.All" }
# Assign the User.ReadWrite.All permission to the Logic App’s managed identity
# -ServicePrincipalId : The managed identity receiving the permission
# -PrincipalId : Same as above (the identity making the API calls)
# -ResourceId : Microsoft Graph’s service principal ID
# -AppRoleId : The specific permission being granted
# Expected output: AppRoleAssignment object confirming the grant
New-MgServicePrincipalAppRoleAssignment `
-ServicePrincipalId $MIObjectId `
-PrincipalId $MIObjectId `
-ResourceId $GraphSP.Id `
-AppRoleId $AppRole.Id// KQL - Create a watchlist named "ProtectedAccounts" to safeguard critical accounts
// PURPOSE: Prevent automated containment from disabling break-glass or C-suite accounts
// WHY: Auto-disabling a break-glass admin could lock your entire org out of Entra ID
// Upload a CSV with column: UserPrincipalName
// Example entries:
// admin@contoso.com ← Global admin accounts
// breakglass01@contoso.com ← Emergency access accounts (NEVER auto-disable)
// ceo@contoso.com ← Executive accounts requiring manual review
// In the Logic App, before the disable action add a condition:
// Condition: items('For_each_Account')?['UPNSuffix'] is NOT in ProtectedAccounts watchlist
// This ensures protected accounts skip auto-containment and go to manual analyst reviewWith the playbook ready, you need an automation rule that triggers it automatically when matching incidents are created. Automation rules act as the bridge between Sentinel analytics and your SOAR playbooks.
Auto-Enrich-and-Contain-High-Severity# Get the Logic App resource ID
PLAYBOOK_ID=$(az logic workflow show \
--resource-group $RG \
--name "Sentinel-IncidentEnrich" \
--query "id" -o tsv)
# Create the automation rule via REST API
az rest --method PUT \
--url "https://management.azure.com/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WORKSPACE/providers/Microsoft.SecurityInsights/automationRules/auto-enrich-contain?api-version=2024-03-01" \
--body "{
\"properties\": {
\"displayName\": \"Auto-Enrich-and-Contain-High-Severity\",
\"order\": 1,
\"triggeringLogic\": {
\"isEnabled\": true,
\"triggersOn\": \"Incidents\",
\"triggersWhen\": \"Created\",
\"conditions\": [
{
\"conditionType\": \"Property\",
\"conditionProperties\": {
\"propertyName\": \"IncidentSeverity\",
\"operator\": \"Equals\",
\"propertyValues\": [\"High\"]
}
}
]
},
\"actions\": [
{
\"actionType\": \"ModifyProperties\",
\"order\": 1,
\"actionConfiguration\": {
\"status\": \"Active\"
}
},
{
\"actionType\": \"RunPlaybook\",
\"order\": 2,
\"actionConfiguration\": {
\"logicAppResourceId\": \"$PLAYBOOK_ID\",
\"tenantId\": \"$(az account show --query tenantId -o tsv)\"
}
}
]
}
}"Before relying on the playbook in production, you must validate it end-to-end with a controlled test incident. You can create a test incident programmatically or trigger one of your analytics rules deliberately.
# Create a test incident with IP and account entities
az rest --method PUT \
--url "https://management.azure.com/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WORKSPACE/providers/Microsoft.SecurityInsights/incidents/test-incident-001?api-version=2024-03-01" \
--body "{
\"properties\": {
\"title\": \"[TEST] Suspicious sign-in from malicious IP\",
\"severity\": \"High\",
\"status\": \"New\",
\"description\": \"Test incident for playbook validation. Simulates a compromised account signing in from a known malicious IP address.\"
}
}"If you have a brute-force detection rule from Lab 02, generate failed sign-ins against your test account to trigger it:
# PowerShell: Generate failed sign-ins to trigger brute-force detection
# PURPOSE: Create test events that will fire the analytics rule and trigger the playbook
# WARNING: Use a dedicated TEST ACCOUNT only - never test against production admin accounts
$TestUPN = "testuser@yourtenant.onmicrosoft.com" # Replace with your test account UPN
$WrongPassword = ConvertTo-SecureString "WrongPassword123!" -AsPlainText -Force
$Cred = New-Object PSCredential($TestUPN, $WrongPassword)
# Attempt 15 failed logins to exceed the brute-force threshold (10+)
# Each attempt generates a SigninLogs entry with ResultType = 50126 (bad password)
for ($i = 1; $i -le 15; $i++) {
try {
Connect-AzAccount -Credential $Cred -ErrorAction SilentlyContinue
} catch {
Write-Host "Attempt $i failed (expected)" # Failures are intentional
}
Start-Sleep -Seconds 2 # 2-second delay between attempts (realistic pacing)
}
# Expected result: 15 failed sign-in events in SigninLogs within ~30 seconds
# Wait 5-15 min for ingestion, then check the Incidents blade for a new incidentUpdate-MgUser -UserId "test-user-object-id" -AccountEnabled:$true. Leaving test accounts disabled can cause confusion and trigger additional alerts.Production playbooks will occasionally fail due to API throttling, expired connections, or unexpected data formats. Knowing how to monitor and debug Logic App runs is essential for maintaining reliable automation.
Logic App diagnostic logs can be sent to Log Analytics for centralized monitoring. Use this KQL query to identify failing playbooks:
// Monitor Logic App (playbook) failures using diagnostic logs
// PURPOSE: Identify playbooks that are silently failing and missing incidents
// WHY: A failed playbook means no enrichment, no notification, no containment
// PREREQUISITE: Enable Diagnostic settings on the Logic App → send to Log Analytics
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.LOGIC" // Logic Apps resource provider
| where Category == "WorkflowRuntime" // Runtime execution logs (not design-time)
| where status_s == "Failed" // Only failed runs
| summarize
FailedRuns = count(), // Total failures per playbook
LastFailure = max(TimeGenerated), // Most recent failure timestamp
ErrorCodes = make_set(code_s) // Unique error codes (e.g., 403, 429, 404)
by resource_workflowName_s // Group by Logic App name
| order by FailedRuns desc
// Expected output: Playbooks with failure counts and error codes
// ACTION: 403 = permission issue, 429 = throttling, 404 = invalid entity IDlength(items('For_each_Account')?['AadUserId']) > 0 before calling Graph// In Logic App Code View, add retry policy to HTTP actions:
// PURPOSE: Handle transient failures (API throttling, network blips) automatically
// WHY: Without retries, a single 429 response causes the entire playbook to fail
"retryPolicy": {
"type": "exponential", // Exponential backoff: wait longer between each retry
"count": 4, // Maximum 4 retry attempts before giving up
"interval": "PT10S", // Initial wait: 10 seconds before first retry
"minimumInterval": "PT5S", // Never wait less than 5 seconds
"maximumInterval": "PT1H" // Never wait more than 1 hour (cap for exponential growth)
}
// Retry sequence: 10s → 20s → 40s → 80s (exponential backoff)
// This handles Graph API 429 (throttling) responses gracefullyThe final step completes the automation loop by writing all actions taken back to the Sentinel incident as structured comments. This creates an auditable evidence trail for compliance, post-incident review, and SOC metrics reporting.
<h3>?? Automated Response Summary</h3>
<p><strong>Playbook:</strong> Sentinel-IncidentEnrich<br>
<strong>Execution Time:</strong> @{utcNow()}<br>
<strong>Run ID:</strong> @{workflow().run.name}</p>
<h4>?? IP Enrichment Results</h4>
@{variables('EnrichmentSummary')}
<h4>?? Containment Actions</h4>
<ul>
<li>Account disabled: @{variables('DisabledAccounts')}</li>
<li>Sessions revoked: @{variables('RevokedSessions')}</li>
</ul>
<h4>?? Notification</h4>
<ul>
<li>Teams notification posted to SOC-Alerts channel</li>
</ul>
<p><em>All actions completed successfully. Incident ready for analyst review.</em></p>Add one final action to update the incident with a tag indicating automated response was applied:
auto-enriched, auto-containedAfter running the playbook for a few days, use this KQL query to measure the impact on MTTR:
// Measure SOAR automation effectiveness: MTTR comparison
// PURPOSE: Quantify the impact of automated playbooks on incident response times
// WHY: This data justifies SOAR investment to leadership and identifies automation gaps
// Run after the playbook has been active for at least 7-30 days for meaningful data
SecurityIncident
| where TimeGenerated > ago(30d) // Last 30 days of incidents
| where Labels has "auto-contained" // Filter to auto-responded incidents
| extend MTTR = datetime_diff('minute', ClosedTime, CreatedTime) // Minutes to resolution
| summarize
TotalIncidents = count(), // Total incidents in the period
AvgMTTR_Minutes = avg(MTTR), // Average time to resolve (minutes)
MedianMTTR_Minutes = percentile(MTTR, 50), // Median - less skewed by outliers
P95_MTTR_Minutes = percentile(MTTR, 95), // 95th percentile - worst-case response
AutoContained = countif(Labels has "auto-contained"), // Auto-handled count
ManualOnly = countif(Labels !has "auto-contained") // Manual-only count
| project
TotalIncidents,
AvgMTTR_Minutes = round(AvgMTTR_Minutes, 1),
MedianMTTR_Minutes = round(MedianMTTR_Minutes, 1),
P95_MTTR_Minutes = round(P95_MTTR_Minutes, 1),
AutomationRate = round(100.0 * AutoContained / TotalIncidents, 1) // % automated
// Expected output: MTTR metrics and automation coverage percentage
// Target: MedianMTTR < 15 min, AutomationRate > 60%# Cleanup: Re-enable the test user account after testing
# IMPORTANT: Do this immediately after testing - don’t leave test accounts disabled
Update-MgUser -UserId "test-user-object-id" -AccountEnabled:$true
# Delete the Logic App playbook (optional - keep if continuing to Lab 04)
# --yes : Skip confirmation prompt
az logic workflow delete --resource-group $RG --name "Sentinel-IncidentEnrich" --yes
# Delete the automation rule via REST API
# This removes the trigger that fires the playbook on new incidents
az rest --method DELETE \
--url "https://management.azure.com/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WORKSPACE/providers/Microsoft.SecurityInsights/automationRules/auto-enrich-contain?api-version=2024-03-01"
# Remove Graph API permission from the managed identity (optional cleanup)
# Replace ASSIGNMENT-ID with the value from: Get-MgServicePrincipalAppRoleAssignment
Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MIObjectId -AppRoleAssignmentId "ASSIGNMENT-ID"| Resource | Description |
|---|---|
| Automate threat response with playbooks | Official guide to creating and managing playbooks in Microsoft Sentinel |
| Automation in Microsoft Sentinel | Overview of automation rules, playbooks, and SOAR capabilities |
| Azure Logic Apps overview | Comprehensive documentation for the Logic Apps workflow engine |
| Tutorial: Respond to threats with playbooks | Step-by-step tutorial for building your first Sentinel playbook |
| Automate incident handling with automation rules | Create and manage automation rules for incident triage |
| Authenticate playbooks to Sentinel | Managed identity and API connection authentication options |
| Microsoft Graph: Update user | API reference for disabling user accounts programmatically |
| Microsoft Graph: Revoke sign-in sessions | API reference for invalidating all user refresh tokens |