Skip to content

Commit 92e4815

Browse files
authored
SecOps - 41213 - At least one automation rule is configured in Microsoft Sentinel to manage incident response (#1378)
2 parents f9415a5 + dc08362 commit 92e4815

2 files changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Automation rules are Sentinel's central control plane for incident handling: they fire on incident creation, incident update, or alert creation, evaluate conditions against incident properties (severity, tactics, entities, analytics rule of origin), and execute one or more actions — assign owner, change status, change severity, add tags, suppress duplicates, and run a playbook. Without automation rules, every incident is processed manually by a tier-1 analyst from raw queue: the analyst must triage, assign, set severity, write context, and decide whether to escalate, drastically inflating the mean-time-to-acknowledge (MTTA) and mean-time-to-respond (MTTR). The detection-blind-spot risk is operational: a high-severity incident from a high-fidelity rule (for example, a Defender for Identity DCSync alert correlating with the Microsoft Threat Intelligence map for a known ransomware affiliate; tactic, technique - DCSync) sits unhandled in queue while the threat actor proceeds with privilege escalation and lateral movement. Automation rules also enable noise reduction (auto-close known-benign alerts) so analyst attention concentrates on real incidents. The check confirms at least one automation rule exists. Mature deployments maintain rules that route by analytics-rule, severity, or entity tag.
2+
3+
**Remediation action**
4+
5+
- [Create and use Microsoft Sentinel automation rules to manage response](https://learn.microsoft.com/azure/sentinel/create-manage-use-automation-rules)
6+
- [Automate threat response in Microsoft Sentinel with automation rules](https://learn.microsoft.com/azure/sentinel/automate-incident-handling-with-automation-rules)
7+
8+
<!--- Results --->
9+
%TestResult%
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
<#
2+
.SYNOPSIS
3+
Checks whether at least one automation rule is configured in Microsoft Sentinel to manage incident response.
4+
5+
.DESCRIPTION
6+
This test enumerates all Sentinel-onboarded Log Analytics workspaces across in-scope Azure
7+
subscriptions and verifies that at least one has an enabled, non-expired automation rule
8+
configured. Automation rules are Sentinel's central control plane for incident handling: they
9+
fire on incident creation, incident update, or alert creation, evaluate conditions against
10+
incident properties, and execute actions such as assigning owners, changing status or severity,
11+
adding tags, suppressing duplicates, and running playbooks. Without automation rules, every
12+
incident is processed manually from the raw queue, inflating mean-time-to-acknowledge (MTTA)
13+
and mean-time-to-respond (MTTR).
14+
15+
Evaluation steps:
16+
1. Enumerate Sentinel-onboarded Log Analytics workspaces via the shared Get-SentinelWorkspaceData helper.
17+
2. For each Sentinel-onboarded workspace, query the automation rules collection via the ARM API.
18+
3. Pass if at least one workspace has an enabled, non-expired automation rule.
19+
4. Fail if no enabled, non-expired automation rules exist across all checked workspaces.
20+
5. Investigate if the automation-rules API returns an auth or server error.
21+
6. Skip if no Sentinel-onboarded workspaces are found.
22+
23+
.NOTES
24+
Test ID: 41213
25+
Workshop Task: SECOPS_108
26+
Pillar: SecOps
27+
Category: Security information and event management
28+
Required permissions:
29+
- Reader on each subscription (for subscription and workspace enumeration)
30+
- Microsoft Sentinel Reader on each workspace (for automation rules query)
31+
#>
32+
33+
function Test-Assessment-41213 {
34+
35+
[ZtTest(
36+
Category = 'Security information and event management',
37+
ImplementationCost = 'Low',
38+
Service = ('Azure'),
39+
MinimumLicense = ('Consumption-based: Microsoft Sentinel'),
40+
Pillar = 'SecOps',
41+
RiskLevel = 'Medium',
42+
SfiPillar = 'Accelerate response and remediation',
43+
TenantType = ('Workforce'),
44+
TestId = 41213,
45+
Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response',
46+
UserImpact = 'Low'
47+
)]
48+
[CmdletBinding()]
49+
param()
50+
51+
#region Data Collection
52+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
53+
$activity = 'Checking automation rules in Sentinel workspaces'
54+
55+
# Q1 + Q2 + onboarding check via shared helper.
56+
# Returns 'Forbidden' on ARG 401/403 (Investigate).
57+
# Returns $null on unexpected ARG failure (Investigate).
58+
# Returns 'NoSubscriptions' when no enabled subscriptions are accessible (Skip).
59+
# Returns 'NoWorkspaces' when no Log Analytics workspaces exist in scope (Skip).
60+
$allWorkspaces = Get-SentinelWorkspaceData -Activity $activity
61+
62+
if ($null -eq $allWorkspaces) {
63+
$params = @{
64+
TestId = '41213'
65+
Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response'
66+
Status = $false
67+
Result = '⚠️ Azure Resource Graph returned an unexpected error while querying subscriptions or Log Analytics workspaces. This is likely a transient issue, please re-run the assessment.'
68+
CustomStatus = 'Investigate'
69+
}
70+
Add-ZtTestResultDetail @params
71+
return
72+
}
73+
74+
if ($allWorkspaces -eq 'Forbidden') {
75+
$params = @{
76+
TestId = '41213'
77+
Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response'
78+
Status = $false
79+
Result = '⚠️ Azure Resource Graph returned insufficient permissions when querying subscriptions or workspaces. Ensure you have at least Reader access to the Azure subscriptions being tested.'
80+
CustomStatus = 'Investigate'
81+
}
82+
Add-ZtTestResultDetail @params
83+
return
84+
}
85+
86+
if ($allWorkspaces -eq 'NoSubscriptions') {
87+
Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose
88+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
89+
return
90+
}
91+
92+
if ($allWorkspaces -eq 'NoWorkspaces') {
93+
Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose
94+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
95+
return
96+
}
97+
98+
$checkableWorkspaces = @($allWorkspaces | Where-Object { -not $_.PermissionError })
99+
$forbiddenWorkspaces = @($allWorkspaces | Where-Object { $_.PermissionError })
100+
$onboardedWorkspaces = @($checkableWorkspaces | Where-Object { $_.SentinelOnboarded })
101+
102+
if ($onboardedWorkspaces.Count -eq 0) {
103+
if ($forbiddenWorkspaces.Count -gt 0) {
104+
# Auth errors mean we cannot confirm whether those workspaces have Sentinel onboarded;
105+
# we cannot rule out a passing workspace exists among the inaccessible ones.
106+
$params = @{
107+
TestId = '41213'
108+
Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response'
109+
Status = $false
110+
Result = '⚠️ One or more Log Analytics workspaces returned insufficient permissions when checking Sentinel onboarding state. No Sentinel-onboarded workspace was confirmed among accessible workspaces — the overall state cannot be determined. Ensure Microsoft Sentinel Reader is granted on all workspaces and re-run the assessment.'
111+
CustomStatus = 'Investigate'
112+
}
113+
Add-ZtTestResultDetail @params
114+
}
115+
else {
116+
# Spec: no Sentinel-onboarded workspaces with full visibility — Skipped.
117+
Write-PSFMessage 'No Sentinel-onboarded workspaces found — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose
118+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
119+
}
120+
return
121+
}
122+
123+
# Q1 (spec): List automation rules for each Sentinel-onboarded workspace.
124+
$rawRulesByWorkspace = @{}
125+
126+
foreach ($workspace in $onboardedWorkspaces) {
127+
Write-ZtProgress -Activity $activity -Status "Fetching automation rules for $($workspace.WorkspaceName) in $($workspace.SubscriptionName)"
128+
$automationRulesPath = "$($workspace.WorkspaceId)/providers/Microsoft.SecurityInsights/automationRules?api-version=2024-09-01"
129+
130+
try {
131+
$rawRulesByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $automationRulesPath -ErrorAction Stop)
132+
}
133+
catch {
134+
$rawRulesByWorkspace[$workspace.WorkspaceId] = $null
135+
Write-PSFMessage "Error querying automation rules for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning
136+
}
137+
}
138+
139+
#endregion Data Collection
140+
141+
#region Assessment Logic
142+
143+
$now = [DateTime]::UtcNow
144+
145+
$workspaceResults = foreach ($workspace in $onboardedWorkspaces) {
146+
$rawRules = $rawRulesByWorkspace[$workspace.WorkspaceId]
147+
148+
$totalRules = 0
149+
$enabledRules = 0
150+
$actionTypeCounts = @{}
151+
$triggersOnCounts = @{}
152+
153+
if ($null -ne $rawRules) {
154+
$totalRules = $rawRules.Count
155+
156+
foreach ($rule in $rawRules) {
157+
$trigLogic = $rule.properties.triggeringLogic
158+
159+
# Tally action types and triggersOn across all rules for the distribution columns.
160+
foreach ($action in $rule.properties.actions) {
161+
if ($action.actionType) {
162+
$actionTypeCounts[$action.actionType] = [int]$actionTypeCounts[$action.actionType] + 1
163+
}
164+
}
165+
if ($trigLogic.triggersOn) {
166+
$triggersOnCounts[$trigLogic.triggersOn] = [int]$triggersOnCounts[$trigLogic.triggersOn] + 1
167+
}
168+
169+
# Count enabled, non-expired rules per spec pass condition.
170+
# A rule with expirationTimeUtc in the past is effectively disabled even when isEnabled=true.
171+
if ($trigLogic.isEnabled -eq $true) {
172+
$expiry = $trigLogic.expirationTimeUtc
173+
$notExpired = ($null -eq $expiry) -or ([DateTime]$expiry -gt $now)
174+
if ($notExpired) {
175+
$enabledRules++
176+
}
177+
}
178+
}
179+
}
180+
181+
$actionTypesStr = if ($actionTypeCounts.Count -gt 0) {
182+
($actionTypeCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ', '
183+
} else { '' }
184+
185+
$triggersOnStr = if ($triggersOnCounts.Count -gt 0) {
186+
($triggersOnCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ', '
187+
} else { '' }
188+
189+
$rowStatus = if ($null -eq $rawRules) {
190+
'Investigate'
191+
}
192+
elseif ($enabledRules -gt 0) {
193+
'Pass'
194+
}
195+
else {
196+
'Fail'
197+
}
198+
199+
[PSCustomObject]@{
200+
SubscriptionName = $workspace.SubscriptionName
201+
SubscriptionId = $workspace.SubscriptionId
202+
WorkspaceName = $workspace.WorkspaceName
203+
ResourceGroup = $workspace.ResourceGroup
204+
WorkspaceId = $workspace.WorkspaceId
205+
TotalRules = $totalRules
206+
EnabledRules = $enabledRules
207+
ActionTypes = $actionTypesStr
208+
TriggersOn = $triggersOnStr
209+
RowStatus = $rowStatus
210+
}
211+
}
212+
$workspaceResults = @($workspaceResults)
213+
214+
$passedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' })
215+
$investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' })
216+
217+
$passed = $passedItems.Count -gt 0
218+
$customStatus = $null
219+
220+
if (-not $passed -and $investigateItems.Count -gt 0) {
221+
$customStatus = 'Investigate'
222+
$testResultMarkdown = "⚠️ The automation-rules API returned an unexpected response for one or more workspaces. Re-run after verifying Microsoft Sentinel Reader access on each affected workspace.`n`n%TestResult%"
223+
}
224+
elseif ($passed) {
225+
$testResultMarkdown = "✅ Automation rules are configured in the Sentinel workspace to manage incident response.`n`n%TestResult%"
226+
}
227+
else {
228+
$testResultMarkdown = "❌ No enabled, non-expired automation rules are configured in the Sentinel workspace.`n`n%TestResult%"
229+
}
230+
231+
#endregion Assessment Logic
232+
233+
#region Report Generation
234+
235+
$portalSentinelLink = 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel'
236+
$tableTitle = 'Automation rules per workspace'
237+
238+
$formatTemplate = @'
239+
240+
241+
### [{0}]({1})
242+
243+
| Subscription | Workspace | Total rules | Enabled rules | Action types | Triggers on | Status |
244+
| :----------- | :-------- | ----------: | ------------: | :----------- | :---------- | :----- |
245+
{2}
246+
'@
247+
248+
$tableRows = ''
249+
$maxDisplay = 10
250+
$statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2 }
251+
$displayResults = @($workspaceResults | Sort-Object { $statusPriority[$_.RowStatus] }, SubscriptionName, WorkspaceName)
252+
$hasMoreItems = $false
253+
if ($workspaceResults.Count -gt $maxDisplay) {
254+
$displayResults = @($displayResults | Select-Object -First $maxDisplay)
255+
$hasMoreItems = $true
256+
}
257+
258+
foreach ($result in $displayResults) {
259+
$subLink = "https://portal.azure.com/#resource/subscriptions/$($result.SubscriptionId)"
260+
$sentinelId = "/subscriptions/$($result.SubscriptionId)/resourcegroups/$($result.ResourceGroup)/providers/microsoft.securityinsightsarg/sentinel/$($result.WorkspaceName)"
261+
$automationLink = "https://portal.azure.com/#view/Microsoft_Azure_Security_Insights/MainMenuBlade/~/Automation/id/$($sentinelId -replace '/', '%2F')"
262+
$subMd = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)"
263+
$workspaceMd = "[$(Get-SafeMarkdown $result.WorkspaceName)]($automationLink)"
264+
$actionTypesMd = if ($result.ActionTypes) { Get-SafeMarkdown -Text $result.ActionTypes } else { '' }
265+
$triggersOnMd = if ($result.TriggersOn) { Get-SafeMarkdown -Text $result.TriggersOn } else { '' }
266+
$statusDisplay = switch ($result.RowStatus) {
267+
'Pass' { '✅ Pass' }
268+
'Fail' { '❌ Fail' }
269+
'Investigate' { '⚠️ Investigate' }
270+
}
271+
$tableRows += "| $subMd | $workspaceMd | $($result.TotalRules) | $($result.EnabledRules) | $actionTypesMd | $triggersOnMd | $statusDisplay |`n"
272+
}
273+
274+
if ($hasMoreItems) {
275+
$remainingCount = $workspaceResults.Count - $maxDisplay
276+
$tableRows += "`n... and $remainingCount more. [View all in Microsoft Sentinel]($portalSentinelLink)`n"
277+
}
278+
279+
$mdInfo = $formatTemplate -f $tableTitle, $portalSentinelLink, $tableRows
280+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
281+
282+
#endregion Report Generation
283+
284+
$params = @{
285+
TestId = '41213'
286+
Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response'
287+
Status = $passed
288+
Result = $testResultMarkdown
289+
}
290+
if ($customStatus) {
291+
$params.CustomStatus = $customStatus
292+
}
293+
294+
Add-ZtTestResultDetail @params
295+
}

0 commit comments

Comments
 (0)