Skip to main content

Challenge 51: Security Monitoring Architecture – End-to-End Detection Scenario

Exam skills covered

  • Design complete security monitoring architectures for enterprise environments
  • Define log sources and configure data connectors
  • Create analytics rules (scheduled, NRT, fusion)
  • Build automation playbooks with Logic Apps
  • Design security workbooks and dashboards
  • Write KQL queries for common threat detections
  • Implement data retention and cost optimization strategies

Scenario

Contoso Ltd is a financial services company with 5,000 employees undergoing cloud transformation. They have a hybrid infrastructure with on-premises Active Directory, Azure workloads, Microsoft 365, and a growing SaaS footprint. The CISO has tasked you with designing and implementing a comprehensive security monitoring architecture using Microsoft Sentinel.

Your design must detect the following threat categories:

  • Brute-force attacks against user accounts
  • Impossible travel sign-ins indicating credential theft
  • Privilege escalation via unauthorized role assignments
  • Data exfiltration from SharePoint/OneDrive
  • Malware/ransomware indicators on endpoints
  • Cloud resource abuse (cryptomining, unauthorized deployments)

Prerequisites

  • Azure subscription with Owner or Contributor role
  • Microsoft 365 E5 or equivalent security licensing
  • Azure CLI with sentinel and monitor extensions
  • Microsoft Sentinel Contributor role
  • Logic Apps Contributor role (for playbooks)
  • Understanding of KQL and MITRE ATT&CK framework

Task 1: Design the monitoring architecture and deploy the workspace

Define Contoso's security monitoring architecture with appropriate workspace design.

# Architecture design variables
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
RG_NAME="rg-contoso-soc-architecture"
LOCATION="eastus"
WORKSPACE_NAME="law-contoso-soc"

# Create resource group
az group create --name $RG_NAME --location $LOCATION

# Create Log Analytics workspace with appropriate retention
az monitor log-analytics workspace create \
--workspace-name $WORKSPACE_NAME \
--resource-group $RG_NAME \
--location $LOCATION \
--retention-time 90 \
--sku PerGB2018

# Enable Sentinel
az sentinel onboarding-state create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "default"

# Get workspace ID for later use
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--workspace-name $WORKSPACE_NAME \
--resource-group $RG_NAME \
--query id -o tsv)

# Configure data retention tiers
# Hot tier: 90 days (default interactive queries)
# Archive tier: 2 years (compliance requirement for financial services)
az monitor log-analytics workspace table update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "SecurityEvent" \
--retention-time 90 \
--total-retention-time 730

az monitor log-analytics workspace table update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "SigninLogs" \
--retention-time 90 \
--total-retention-time 730

az monitor log-analytics workspace table update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "AuditLogs" \
--retention-time 90 \
--total-retention-time 730

Architecture overview:

ComponentPurposeData Volume (est.)
Entra ID Sign-in/Audit logsIdentity threat detection~2 GB/day
Microsoft 365 activityEmail/SharePoint/Teams threats~5 GB/day
Azure Activity logsCloud resource abuse~500 MB/day
Defender for EndpointEndpoint malware/EDR~3 GB/day
Defender for CloudCloud posture alerts~200 MB/day
Azure Key Vault diagnosticsCredential access monitoring~100 MB/day
Azure Firewall logsNetwork-level threats~1 GB/day
On-premises AD (via AMA)Hybrid identity monitoring~1 GB/day
Total estimated ingestion~13 GB/day

Task 2: Configure data connectors for all log sources

Enable the data connectors that feed Contoso's monitoring architecture.

# Microsoft Entra ID connector (Sign-in and Audit logs)
az sentinel data-connector create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--data-connector-id "AzureActiveDirectory" \
--azure-active-directory "{
\"tenantId\": \"$(az account show --query tenantId -o tsv)\",
\"dataTypes\": {
\"alerts\": {\"state\": \"Enabled\"},
\"msGraphSignIns\": {\"state\": \"Enabled\"}
}
}"

# Microsoft Defender XDR connector
az sentinel data-connector create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--data-connector-id "MicrosoftThreatProtection" \
--microsoft-protection "{
\"tenantId\": \"$(az account show --query tenantId -o tsv)\",
\"dataTypes\": {
\"incidents\": {\"state\": \"Enabled\"},
\"alerts\": {\"state\": \"Enabled\"}
}
}"

# Microsoft Defender for Cloud connector
az sentinel data-connector create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--data-connector-id "AzureSecurityCenter" \
--azure-security-center "{
\"subscriptionId\": \"${SUBSCRIPTION_ID}\",
\"dataTypes\": {\"alerts\": {\"state\": \"Enabled\"}}
}"

# Azure Activity connector (configured via diagnostic settings)
az monitor diagnostic-settings create \
--name "activity-to-sentinel" \
--resource "/subscriptions/${SUBSCRIPTION_ID}" \
--workspace $WORKSPACE_ID \
--logs '[{"category": "Administrative", "enabled": true},
{"category": "Security", "enabled": true},
{"category": "Alert", "enabled": true}]'

# Microsoft 365 connector (Exchange, SharePoint, Teams)
az sentinel data-connector create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--data-connector-id "Office365" \
--office365 "{
\"tenantId\": \"$(az account show --query tenantId -o tsv)\",
\"dataTypes\": {
\"exchange\": {\"state\": \"Enabled\"},
\"sharePoint\": {\"state\": \"Enabled\"},
\"teams\": {\"state\": \"Enabled\"}
}
}"

# Configure diagnostic settings for Key Vault
az monitor diagnostic-settings create \
--name "send-to-sentinel" \
--resource "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG_NAME/providers/Microsoft.KeyVault/vaults/kv-contoso-prod" \
--workspace $WORKSPACE_ID \
--logs '[{"category":"AuditEvent","enabled":true},{"category":"AllMetrics","enabled":false}]' \
2>/dev/null || echo "Key Vault diagnostic settings - configure after KV is created"

# Configure diagnostic settings for Azure Firewall
az monitor diagnostic-settings create \
--name "send-to-sentinel" \
--resource "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG_NAME/providers/Microsoft.Network/azureFirewalls/fw-contoso-hub" \
--workspace $WORKSPACE_ID \
--logs '[{"category":"AzureFirewallApplicationRule","enabled":true},{"category":"AzureFirewallNetworkRule","enabled":true},{"category":"AzureFirewallDnsProxy","enabled":true}]' \
2>/dev/null || echo "Firewall diagnostic settings - configure after FW is created"

Verify connector status:

# List all configured data connectors
az sentinel data-connector list \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--query "[].{Name:name, Kind:kind}" -o table

Task 3: Create analytics rules for threat detection

Implement detection rules for each threat category in Contoso's requirements.

Detection 1: Brute-force attack detection

# Create scheduled analytics rule for brute-force detection
az rest --method PUT \
--uri "https://management.azure.com${WORKSPACE_ID}/providers/Microsoft.SecurityInsights/alertRules/detect-brute-force?api-version=2024-03-01" \
--body '{
"kind": "Scheduled",
"properties": {
"displayName": "Brute Force Attack Detected",
"description": "Detects multiple failed sign-in attempts followed by a success from the same IP",
"severity": "High",
"enabled": true,
"query": "let failureThreshold = 10;\nlet timeWindow = 15m;\nlet successWindow = 1h;\nlet bruteForceAttempts = SigninLogs\n| where TimeGenerated > ago(successWindow)\n| where ResultType != 0\n| summarize\n FailureCount = count(),\n FailedAccounts = make_set(UserPrincipalName, 20),\n FirstFailure = min(TimeGenerated),\n LastFailure = max(TimeGenerated)\n by IPAddress\n| where FailureCount >= failureThreshold;\nlet successfulLogins = SigninLogs\n| where TimeGenerated > ago(successWindow)\n| where ResultType == 0\n| project SuccessTime=TimeGenerated, UserPrincipalName, IPAddress,\n AppDisplayName, DeviceDetail, Location=LocationDetails;\nbruteForceAttempts\n| join kind=inner (successfulLogins) on IPAddress\n| where SuccessTime > LastFailure\n| project IPAddress, FailureCount, FailedAccounts,\n CompromisedUser=UserPrincipalName, SuccessTime,\n AppDisplayName, FirstFailure, LastFailure",
"queryFrequency": "PT10M",
"queryPeriod": "PT1H",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"tactics": ["CredentialAccess"],
"techniques": ["T1110"],
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": true,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities"
}
}
}
}'

Detection 2: Impossible travel

# Create scheduled rule for impossible travel detection
az rest --method PUT \
--uri "https://management.azure.com${WORKSPACE_ID}/providers/Microsoft.SecurityInsights/alertRules/detect-impossible-travel?api-version=2024-03-01" \
--body '{
"kind": "Scheduled",
"properties": {
"displayName": "Impossible Travel Detected",
"description": "Detects sign-ins from geographically distant locations within an impossible timeframe",
"severity": "Medium",
"enabled": true,
"query": "let maxTimeDiffMinutes = 60;\nlet maxDistanceKm = 500;\nSigninLogs\n| where TimeGenerated > ago(1h)\n| where ResultType == 0\n| extend City = tostring(LocationDetails.city),\n State = tostring(LocationDetails.state),\n Country = tostring(LocationDetails.countryOrRegion),\n Latitude = toreal(LocationDetails.geoCoordinates.latitude),\n Longitude = toreal(LocationDetails.geoCoordinates.longitude)\n| where isnotempty(Latitude) and isnotempty(Longitude)\n| sort by UserPrincipalName, TimeGenerated asc\n| serialize\n| extend PrevTime = prev(TimeGenerated, 1),\n PrevLatitude = prev(Latitude, 1),\n PrevLongitude = prev(Longitude, 1),\n PrevCity = prev(City, 1),\n PrevCountry = prev(Country, 1),\n PrevUser = prev(UserPrincipalName, 1)\n| where UserPrincipalName == PrevUser\n| extend TimeDiffMinutes = datetime_diff(\"minute\", TimeGenerated, PrevTime)\n| where TimeDiffMinutes <= maxTimeDiffMinutes and TimeDiffMinutes > 0\n| extend DistanceKm = 6371 * acos(\n sin(radians(Latitude)) * sin(radians(PrevLatitude)) +\n cos(radians(Latitude)) * cos(radians(PrevLatitude)) *\n cos(radians(PrevLongitude - Longitude)))\n| where DistanceKm > maxDistanceKm\n| project TimeGenerated, UserPrincipalName,\n CurrentLocation=strcat(City, \", \", Country),\n PreviousLocation=strcat(PrevCity, \", \", PrevCountry),\n TimeDiffMinutes, DistanceKm, IPAddress",
"queryFrequency": "PT15M",
"queryPeriod": "PT2H",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"tactics": ["InitialAccess"],
"techniques": ["T1078"],
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": true,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities"
}
}
}
}'

Detection 3: Privilege escalation via unauthorized role assignment

# Create NRT rule for unauthorized privilege escalation
az rest --method PUT \
--uri "https://management.azure.com${WORKSPACE_ID}/providers/Microsoft.SecurityInsights/alertRules/detect-privilege-escalation?api-version=2024-03-01" \
--body '{
"kind": "NRT",
"properties": {
"displayName": "Unauthorized Privileged Role Assignment (NRT)",
"description": "Near real-time detection of privileged role assignments outside PIM or approved processes",
"severity": "High",
"enabled": true,
"query": "let privilegedRoles = dynamic([\"Global Administrator\", \"Privileged Role Administrator\", \"Security Administrator\", \"Exchange Administrator\", \"SharePoint Administrator\", \"User Administrator\", \"Application Administrator\", \"Cloud Application Administrator\"]);\nAuditLogs\n| where OperationName == \"Add member to role\"\n| extend TargetRole = tostring(TargetResources[0].displayName)\n| where TargetRole in (privilegedRoles)\n| where OperationName != \"Add eligible member to role in PIM\"\n| extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName),\n TargetUser = tostring(TargetResources[0].userPrincipalName),\n InitiatedByIP = tostring(InitiatedBy.user.ipAddress)\n| where InitiatedBy !in (\"pim-service@contoso.com\", \"identity-governance@contoso.com\")\n| project TimeGenerated, InitiatedBy, InitiatedByIP,\n TargetUser, TargetRole, OperationName, AdditionalDetails",
"tactics": ["PrivilegeEscalation", "Persistence"],
"techniques": ["T1078.004", "T1098"],
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": true,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities"
}
}
}
}'

Detection 4: Data exfiltration from SharePoint/OneDrive

# Create scheduled rule for data exfiltration detection
az rest --method PUT \
--uri "https://management.azure.com${WORKSPACE_ID}/providers/Microsoft.SecurityInsights/alertRules/detect-data-exfiltration?api-version=2024-03-01" \
--body '{
"kind": "Scheduled",
"properties": {
"displayName": "Anomalous Data Download from SharePoint/OneDrive",
"description": "Detects unusually large file downloads that may indicate data exfiltration",
"severity": "Medium",
"enabled": true,
"query": "let lookback = 14d;\nlet threshold_multiplier = 3;\nlet baseline = OfficeActivity\n| where TimeGenerated > ago(lookback) and TimeGenerated < ago(1d)\n| where Operation in (\"FileDownloaded\", \"FileSyncDownloadedFull\")\n| where OfficeWorkload in (\"SharePoint\", \"OneDrive\")\n| summarize\n AvgDailyDownloads = count() / 14.0,\n AvgDailyOps = count() / 14.0\n by UserId;\nlet today_activity = OfficeActivity\n| where TimeGenerated > ago(1d)\n| where Operation in (\"FileDownloaded\", \"FileSyncDownloadedFull\")\n| where OfficeWorkload in (\"SharePoint\", \"OneDrive\")\n| summarize\n TodayDownloads = count(),\n UniqueFiles = dcount(OfficeObjectId),\n Sites = make_set(Site_Url, 10)\n by UserId;\ntoday_activity\n| join kind=inner (baseline) on UserId\n| where TodayDownloads > AvgDailyDownloads * threshold_multiplier\n| where TodayDownloads > 50\n| project UserId, TodayDownloads, AvgDailyDownloads,\n AnomalyRatio = round(TodayDownloads / AvgDailyDownloads, 1),\n UniqueFiles, Sites",
"queryFrequency": "PT1H",
"queryPeriod": "P14D",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"tactics": ["Exfiltration"],
"techniques": ["T1567"],
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": true,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities"
}
}
}
}'

Detection 5: Cloud resource abuse (cryptomining indicators)

# Create scheduled rule for cryptomining detection
az rest --method PUT \
--uri "https://management.azure.com${WORKSPACE_ID}/providers/Microsoft.SecurityInsights/alertRules/detect-cryptomining?api-version=2024-03-01" \
--body '{
"kind": "Scheduled",
"properties": {
"displayName": "Potential Cryptomining - Unusual VM Deployments",
"description": "Detects bulk VM creation or GPU VM deployments that may indicate cryptomining",
"severity": "High",
"enabled": true,
"query": "let cryptoVmSizes = dynamic([\"Standard_NC\", \"Standard_ND\", \"Standard_NV\",\n \"Standard_HB\", \"Standard_HC\", \"Standard_F72s\"]);\nlet bulkDeployment = AzureActivity\n| where TimeGenerated > ago(1h)\n| where OperationNameValue == \"Microsoft.Compute/virtualMachines/write\"\n| where ActivityStatusValue == \"Success\"\n| summarize\n VMCount = count(),\n ResourceGroups = make_set(ResourceGroup, 5),\n VMNames = make_set(Resource, 10)\n by Caller, bin(TimeGenerated, 15m)\n| where VMCount > 5;\nlet gpuDeployment = AzureActivity\n| where TimeGenerated > ago(1h)\n| where OperationNameValue == \"Microsoft.Compute/virtualMachines/write\"\n| where ActivityStatusValue == \"Success\"\n| where Properties_d has_any (cryptoVmSizes)\n| project TimeGenerated, Caller, ResourceGroup, Resource,\n VMSize = tostring(parse_json(tostring(Properties_d)).responseBody);\nunion\n (bulkDeployment | extend DetectionType = \"BulkVMDeployment\"),\n (gpuDeployment | extend DetectionType = \"GPUVMDeployment\")",
"queryFrequency": "PT15M",
"queryPeriod": "PT1H",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"tactics": ["Impact"],
"techniques": ["T1496"],
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": true,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities"
}
}
}
}'

Task 4: Build automation playbooks with Logic Apps

Create a Logic App playbook that enriches and responds to brute-force incidents.

# Create Logic App for brute-force response
az logic workflow create \
--resource-group $RG_NAME \
--name "playbook-brute-force-response" \
--location $LOCATION \
--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": {
"body": {
"callback_url": "@{listCallbackUrl()}"
},
"host": {
"connection": {
"name": "@parameters('$connections')['azuresentinel']['connectionId']"
}
},
"path": "/incident-creation"
}
}
},
"actions": {
"Get_incident_entities": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azuresentinel']['connectionId']"
}
},
"method": "post",
"path": "/entities/@{triggerBody()?['object']?['properties']?['relatedAnalyticRuleIds']}"
},
"runAfter": {}
},
"Block_IP_in_named_location": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations",
"body": {
"@odata.type": "#microsoft.graph.ipNamedLocation",
"displayName": "Auto-blocked: Brute force source",
"isTrusted": false,
"ipRanges": [
{
"@odata.type": "#microsoft.graph.iPv4CidrRange",
"cidrAddress": "@{body('Get_incident_entities')?['IPs']?[0]}/32"
}
]
}
},
"runAfter": {"Get_incident_entities": ["Succeeded"]}
},
"Send_Teams_notification": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['teams']['connectionId']"
}
},
"method": "post",
"path": "/v1.0/teams/SOC-Alerts/channels/General/messages",
"body": {
"content": "🚨 Brute Force Alert: @{triggerBody()?['object']?['properties']?['title']} - Severity: @{triggerBody()?['object']?['properties']?['severity']}"
}
},
"runAfter": {"Block_IP_in_named_location": ["Succeeded"]}
},
"Add_comment_to_incident": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azuresentinel']['connectionId']"
}
},
"method": "post",
"path": "/comment",
"body": {
"incidentArmId": "@triggerBody()?['object']?['id']",
"message": "Automated response: Source IP blocked in Conditional Access. Teams notification sent to SOC channel."
}
},
"runAfter": {"Send_Teams_notification": ["Succeeded"]}
}
}
}'

Link the playbook to an automation rule:

PLAYBOOK_ID=$(az logic workflow show \
--resource-group $RG_NAME \
--name "playbook-brute-force-response" \
--query id -o tsv)

az sentinel automation-rule create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--automation-rule-name "auto-brute-force-response" \
--display-name "Auto-Respond to Brute Force" \
--order 1 \
--triggering-logic "{
\"isEnabled\": true,
\"triggersOn\": \"Incidents\",
\"triggersWhen\": \"Created\",
\"conditions\": [{
\"conditionType\": \"Property\",
\"conditionProperties\": {
\"propertyName\": \"IncidentRelatedAnalyticRuleIds\",
\"operator\": \"Contains\",
\"propertyValues\": [\"detect-brute-force\"]
}
}]
}" \
--actions "[{
\"actionType\": \"RunPlaybook\",
\"order\": 1,
\"actionConfiguration\": {
\"logicAppResourceId\": \"${PLAYBOOK_ID}\",
\"tenantId\": \"$(az account show --query tenantId -o tsv)\"
}
}]"

Task 5: Design workbooks and dashboards

Create a security operations workbook for the SOC team.

Portal Steps (workbooks are best created in the portal):

  1. Navigate to Microsoft SentinelWorkbooks+ Add workbook
  2. Click Edit to enter edit mode
  3. Add the following components:

SOC Operations Dashboard - KQL Queries

Component 1: Incident Summary (last 24h)

SecurityIncident
| where TimeGenerated > ago(24h)
| summarize
Total = count(),
High = countif(Severity == "High"),
Medium = countif(Severity == "Medium"),
Low = countif(Severity == "Low"),
Open = countif(Status == "New" or Status == "Active"),
Closed = countif(Status == "Closed")
| project Total, High, Medium, Low, Open, Closed

Component 2: Alert trend (7 days)

SecurityAlert
| where TimeGenerated > ago(7d)
| summarize AlertCount = count() by bin(TimeGenerated, 1h), AlertSeverity
| render timechart

Component 3: Top targeted users

SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != 0
| summarize FailedAttempts = count(),
UniqueIPs = dcount(IPAddress),
LastAttempt = max(TimeGenerated)
by UserPrincipalName
| top 10 by FailedAttempts desc
| project UserPrincipalName, FailedAttempts, UniqueIPs, LastAttempt

Component 4: Geographic threat map

SigninLogs
| where TimeGenerated > ago(24h)
| where RiskLevelDuringSignIn in ('high', 'medium')
| extend Latitude = toreal(LocationDetails.geoCoordinates.latitude),
Longitude = toreal(LocationDetails.geoCoordinates.longitude),
Country = tostring(LocationDetails.countryOrRegion)
| where isnotempty(Latitude)
| summarize RiskySignIns = count() by Country, Latitude, Longitude
| top 20 by RiskySignIns desc

Component 5: MITRE ATT&CK coverage

SecurityAlert
| where TimeGenerated > ago(30d)
| extend Tactics = parse_json(ExtendedProperties).Tactics
| mv-expand Tactic = Tactics
| summarize AlertCount = count() by tostring(Tactic)
| sort by AlertCount desc
| render barchart

Component 6: Mean Time to Respond (MTTR)

SecurityIncident
| where TimeGenerated > ago(30d)
| where Status == "Closed"
| extend CreatedTime = CreatedTime,
ClosedTime = ClosedTime
| extend MTTR_hours = datetime_diff('hour', ClosedTime, CreatedTime)
| summarize
AvgMTTR = avg(MTTR_hours),
MedianMTTR = percentile(MTTR_hours, 50),
P95_MTTR = percentile(MTTR_hours, 95)
by bin(TimeGenerated, 1d)
| render timechart
  1. Save the workbook as "Contoso SOC Operations Dashboard"
  2. Pin to the Sentinel overview page

Task 6: Implement cost optimization and data management

Configure data collection rules and cost optimization for sustainable operations.

# Create a Data Collection Rule (DCR) to filter noisy logs
az monitor data-collection rule create \
--resource-group $RG_NAME \
--name "dcr-contoso-filtering" \
--location $LOCATION \
--data-flows '[{
"streams": ["Microsoft-SecurityEvent"],
"destinations": ["law-contoso-soc"],
"transformKql": "source | where EventID in (4624, 4625, 4648, 4672, 4720, 4722, 4723, 4724, 4725, 4726, 4732, 4733, 4740, 4756, 4757, 4767, 4769, 4771, 4776, 5136, 5145)"
}]' \
--destinations '{
"logAnalytics": [{
"workspaceResourceId": "'$WORKSPACE_ID'",
"name": "law-contoso-soc"
}]
}'

# Configure Basic Logs tier for high-volume, low-priority tables
az monitor log-analytics workspace table update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "ContainerLog" \
--plan "Basic"

az monitor log-analytics workspace table update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name "AppTraces" \
--plan "Basic"

# Set up daily cap as cost safeguard (10x normal to catch anomalies only)
az monitor log-analytics workspace update \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--daily-quota-gb 130

Cost optimization summary:

StrategyImplementationSavings
DCR filteringOnly ingest security-relevant EventIDs~40% on SecurityEvent
Basic Logs tierLow-priority tables at reduced cost~60% on container/app logs
Archive tierCompliance data after 90 days~80% vs. interactive retention
Daily capSafety net at 10x normal ingestionPrevents bill shock
Commitment tier100 GB/day commitment~20% discount

Break & Fix

Scenario 1: Analytics rule generates too many false positives

The impossible travel rule triggers 50+ alerts daily, mostly from VPN users who appear to connect from the VPN gateway and their home location simultaneously.

Show solution

Root cause: VPN connections create sign-in events from both the user's actual location and the VPN gateway location.

Fix: Add VPN gateway IPs to an exclusion list in the KQL query:

// Add this filter before the distance calculation
let vpnGatewayIPs = dynamic(["203.0.113.0/24", "198.51.100.0/24"]);
let trustedLocations = dynamic(["Contoso HQ", "Contoso DC"]);
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == 0
// Exclude VPN and trusted locations
| where not(ipv4_is_in_any_range(IPAddress, vpnGatewayIPs))
| where not(tostring(LocationDetails.city) in (trustedLocations))
// ... rest of the impossible travel query

Also consider increasing the maxTimeDiffMinutes from 60 to 90 to account for session overlap.

Scenario 2: Playbook fails with "Forbidden" error

The brute-force response playbook triggers but fails at the "Block IP" step with a 403 Forbidden error.

Show solution

Root cause: The Logic App's managed identity doesn't have Graph API permissions to create named locations.

Fix:

  1. Enable system-assigned managed identity on the Logic App:
    az logic workflow identity assign \
    --resource-group $RG_NAME \
    --name "playbook-brute-force-response" \
    --system-assigned
  2. Grant the managed identity the Policy.ReadWrite.ConditionalAccess permission:
    LOGIC_APP_OBJECT_ID=$(az logic workflow show \
    --resource-group $RG_NAME \
    --name "playbook-brute-force-response" \
    --query identity.principalId -o tsv)

    # Grant Graph API app role to managed identity (requires admin consent)
    # Get the Microsoft Graph service principal
    GRAPH_SP_ID=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv)
    # Get the app role ID for Policy.ReadWrite.ConditionalAccess
    ROLE_ID=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?value=='Policy.ReadWrite.ConditionalAccess'].id | [0]" -o tsv)

    az rest --method POST \
    --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$GRAPH_SP_ID/appRoleAssignedTo" \
    --body "{
    \"principalId\": \"$LOGIC_APP_OBJECT_ID\",
    \"resourceId\": \"$GRAPH_SP_ID\",
    \"appRoleId\": \"$ROLE_ID\"
    }"
  3. Alternatively, use an API connection with a service account that has Conditional Access Administrator role

Scenario 3: Data ingestion costs spike unexpectedly

Monthly costs jump from $3,000 to $12,000 due to a sudden increase in log ingestion volume.

Show solution

Root cause: A misconfigured diagnostic setting is sending verbose debug logs from all resources.

Fix:

  1. Identify the source of increased ingestion:
    Usage
    | where TimeGenerated > ago(7d)
    | summarize IngestionGB = sum(Quantity) / 1000.0 by DataType, bin(TimeGenerated, 1d)
    | where IngestionGB > 1
    | sort by IngestionGB desc
  2. Find the specific resource flooding logs:
    AzureDiagnostics
    | where TimeGenerated > ago(1d)
    | summarize Count = count(), SizeGB = sum(_BilledSize) / (1024*1024*1024) by ResourceProvider, ResourceType
    | sort by SizeGB desc
  3. Remove or fix the misconfigured diagnostic setting
  4. Set a daily cap as immediate protection:
    az monitor log-analytics workspace update \
    --resource-group $RG_NAME \
    --workspace-name $WORKSPACE_NAME \
    --daily-quota-gb 20
  5. Implement DCR filtering for the noisy resource

Knowledge check

1. Which type of Sentinel analytics rule is most appropriate for detecting privilege escalation that requires immediate alerting within 1-2 minutes?

2. What KQL function is used to calculate geographic distance between two sign-in locations for impossible travel detection?

3. To reduce ingestion costs for high-volume but rarely queried tables, which Log Analytics plan should you use?

4. A Logic App playbook needs to block an IP address in Conditional Access. What permission must the managed identity have?

5. In a Data Collection Rule (DCR), what is the purpose of the transformKql property?


Cleanup

# Delete automation rules
az sentinel automation-rule delete \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--automation-rule-name "auto-brute-force-response" --yes

# Delete analytics rules
for RULE_ID in "detect-brute-force" "detect-impossible-travel" "detect-privilege-escalation" "detect-data-exfiltration" "detect-cryptomining"; do
az sentinel alert-rule delete \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--name $RULE_ID --yes
done

# Delete Logic App playbook
az logic workflow delete \
--resource-group $RG_NAME \
--name "playbook-brute-force-response" --yes

# Delete the resource group (removes everything)
az group delete --name $RG_NAME --yes --no-wait