tests/Test-Assessment.41213.ps1
|
<#
.SYNOPSIS Checks whether at least one automation rule is configured in Microsoft Sentinel to manage incident response. .DESCRIPTION This test enumerates all Sentinel-onboarded Log Analytics workspaces across in-scope Azure subscriptions and verifies that at least one has an enabled, non-expired automation rule configured. 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, and execute actions such as assigning owners, changing status or severity, adding tags, suppressing duplicates, and running playbooks. Without automation rules, every incident is processed manually from the raw queue, inflating mean-time-to-acknowledge (MTTA) and mean-time-to-respond (MTTR). Evaluation steps: 1. Enumerate Sentinel-onboarded Log Analytics workspaces via the shared Get-SentinelWorkspaceData helper. 2. For each Sentinel-onboarded workspace, query the automation rules collection via the ARM API. 3. Pass if at least one workspace has an enabled, non-expired automation rule. 4. Fail if no enabled, non-expired automation rules exist across all checked workspaces. 5. Investigate if the automation-rules API returns an auth or server error. 6. Skip if no Sentinel-onboarded workspaces are found. .NOTES Test ID: 41213 Workshop Task: SECOPS_108 Pillar: SecOps Category: Security information and event management Required permissions: - Reader on each subscription (for subscription and workspace enumeration) - Microsoft Sentinel Reader on each workspace (for automation rules query) #> function Test-Assessment-41213 { [ZtTest( Category = 'Security information and event management', ImplementationCost = 'Low', Service = ('Azure'), MinimumLicense = ('Consumption-based: Microsoft Sentinel'), Pillar = 'SecOps', RiskLevel = 'Medium', SfiPillar = 'Accelerate response and remediation', TenantType = ('Workforce'), TestId = 41213, Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking automation rules in Sentinel workspaces' # Q1 + Q2 + onboarding check via shared helper. # Returns 'Forbidden' on ARG 401/403 (Investigate). # Returns $null on unexpected ARG failure (Investigate). # Returns 'NoSubscriptions' when no enabled subscriptions are accessible (Skip). # Returns 'NoWorkspaces' when no Log Analytics workspaces exist in scope (Skip). $allWorkspaces = Get-SentinelWorkspaceData -Activity $activity if ($null -eq $allWorkspaces) { $params = @{ TestId = '41213' Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response' Status = $false 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.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } if ($allWorkspaces -eq 'Forbidden') { $params = @{ TestId = '41213' Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response' Status = $false 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.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } if ($allWorkspaces -eq 'NoSubscriptions') { Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotApplicable return } if ($allWorkspaces -eq 'NoWorkspaces') { Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotApplicable return } $checkableWorkspaces = @($allWorkspaces | Where-Object { -not $_.PermissionError }) $forbiddenWorkspaces = @($allWorkspaces | Where-Object { $_.PermissionError }) $onboardedWorkspaces = @($checkableWorkspaces | Where-Object { $_.SentinelOnboarded }) if ($onboardedWorkspaces.Count -eq 0) { if ($forbiddenWorkspaces.Count -gt 0) { # Auth errors mean we cannot confirm whether those workspaces have Sentinel onboarded; # we cannot rule out a passing workspace exists among the inaccessible ones. $params = @{ TestId = '41213' Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response' Status = $false 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.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params } else { # Spec: no Sentinel-onboarded workspaces with full visibility — Skipped. Write-PSFMessage 'No Sentinel-onboarded workspaces found — skipping Sentinel automation-rules check.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotApplicable } return } # Q1 (spec): List automation rules for each Sentinel-onboarded workspace. $rawRulesByWorkspace = @{} foreach ($workspace in $onboardedWorkspaces) { Write-ZtProgress -Activity $activity -Status "Fetching automation rules for $($workspace.WorkspaceName) in $($workspace.SubscriptionName)" $automationRulesPath = "$($workspace.WorkspaceId)/providers/Microsoft.SecurityInsights/automationRules?api-version=2024-09-01" try { $rawRulesByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $automationRulesPath -ErrorAction Stop) } catch { $rawRulesByWorkspace[$workspace.WorkspaceId] = $null Write-PSFMessage "Error querying automation rules for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning } } #endregion Data Collection #region Assessment Logic $now = [DateTime]::UtcNow $workspaceResults = foreach ($workspace in $onboardedWorkspaces) { $rawRules = $rawRulesByWorkspace[$workspace.WorkspaceId] $totalRules = 0 $enabledRules = 0 $actionTypeCounts = @{} $triggersOnCounts = @{} if ($null -ne $rawRules) { $totalRules = $rawRules.Count foreach ($rule in $rawRules) { $trigLogic = $rule.properties.triggeringLogic # Tally action types and triggersOn across all rules for the distribution columns. foreach ($action in $rule.properties.actions) { if ($action.actionType) { $actionTypeCounts[$action.actionType] = [int]$actionTypeCounts[$action.actionType] + 1 } } if ($trigLogic.triggersOn) { $triggersOnCounts[$trigLogic.triggersOn] = [int]$triggersOnCounts[$trigLogic.triggersOn] + 1 } # Count enabled, non-expired rules per spec pass condition. # A rule with expirationTimeUtc in the past is effectively disabled even when isEnabled=true. if ($trigLogic.isEnabled -eq $true) { $expiry = $trigLogic.expirationTimeUtc $notExpired = ($null -eq $expiry) -or ([DateTime]$expiry -gt $now) if ($notExpired) { $enabledRules++ } } } } $actionTypesStr = if ($actionTypeCounts.Count -gt 0) { ($actionTypeCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ', ' } else { '' } $triggersOnStr = if ($triggersOnCounts.Count -gt 0) { ($triggersOnCounts.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ', ' } else { '' } $rowStatus = if ($null -eq $rawRules) { 'Investigate' } elseif ($enabledRules -gt 0) { 'Pass' } else { 'Fail' } [PSCustomObject]@{ SubscriptionName = $workspace.SubscriptionName SubscriptionId = $workspace.SubscriptionId WorkspaceName = $workspace.WorkspaceName ResourceGroup = $workspace.ResourceGroup WorkspaceId = $workspace.WorkspaceId TotalRules = $totalRules EnabledRules = $enabledRules ActionTypes = $actionTypesStr TriggersOn = $triggersOnStr RowStatus = $rowStatus } } $workspaceResults = @($workspaceResults) $passedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' }) $investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' }) $passed = $passedItems.Count -gt 0 $customStatus = $null if (-not $passed -and $investigateItems.Count -gt 0) { $customStatus = 'Investigate' $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%" } elseif ($passed) { $testResultMarkdown = "✅ Automation rules are configured in the Sentinel workspace to manage incident response.`n`n%TestResult%" } else { $testResultMarkdown = "❌ No enabled, non-expired automation rules are configured in the Sentinel workspace.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation $portalSentinelLink = 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel' $tableTitle = 'Automation rules per workspace' $formatTemplate = @' ### [{0}]({1}) | Subscription | Workspace | Total rules | Enabled rules | Action types | Triggers on | Status | | :----------- | :-------- | ----------: | ------------: | :----------- | :---------- | :----- | {2} '@ $tableRows = '' $maxDisplay = 10 $statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2 } $displayResults = @($workspaceResults | Sort-Object { $statusPriority[$_.RowStatus] }, SubscriptionName, WorkspaceName) $hasMoreItems = $false if ($workspaceResults.Count -gt $maxDisplay) { $displayResults = @($displayResults | Select-Object -First $maxDisplay) $hasMoreItems = $true } foreach ($result in $displayResults) { $subLink = "https://portal.azure.com/#resource/subscriptions/$($result.SubscriptionId)" $sentinelId = "/subscriptions/$($result.SubscriptionId)/resourcegroups/$($result.ResourceGroup)/providers/microsoft.securityinsightsarg/sentinel/$($result.WorkspaceName)" $automationLink = "https://portal.azure.com/#view/Microsoft_Azure_Security_Insights/MainMenuBlade/~/Automation/id/$($sentinelId -replace '/', '%2F')" $subMd = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)" $workspaceMd = "[$(Get-SafeMarkdown $result.WorkspaceName)]($automationLink)" $actionTypesMd = if ($result.ActionTypes) { Get-SafeMarkdown -Text $result.ActionTypes } else { '—' } $triggersOnMd = if ($result.TriggersOn) { Get-SafeMarkdown -Text $result.TriggersOn } else { '—' } $statusDisplay = switch ($result.RowStatus) { 'Pass' { '✅ Pass' } 'Fail' { '❌ Fail' } 'Investigate' { '⚠️ Investigate' } } $tableRows += "| $subMd | $workspaceMd | $($result.TotalRules) | $($result.EnabledRules) | $actionTypesMd | $triggersOnMd | $statusDisplay |`n" } if ($hasMoreItems) { $remainingCount = $workspaceResults.Count - $maxDisplay $tableRows += "`n... and $remainingCount more. [View all in Microsoft Sentinel]($portalSentinelLink)`n" } $mdInfo = $formatTemplate -f $tableTitle, $portalSentinelLink, $tableRows $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '41213' Title = 'At least one automation rule is configured in Microsoft Sentinel to manage incident response' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |