tests/Test-Assessment.41207.ps1
|
<#
.SYNOPSIS Checks that active analytics rules are configured in Microsoft Sentinel to detect threats. .DESCRIPTION Verifies that at least one Sentinel-onboarded Log Analytics workspace has at least one analytics (alert) rule of a substantive kind (Scheduled, NRT, or MicrosoftSecurityIncidentCreation) enabled. A workspace with only the default Fusion rule enabled is considered non-compliant because Fusion alone is not sufficient detection coverage for a production environment. .NOTES Test ID: 41207 Category: Security information and event management Pillar: Security Operations Required API: Azure Resource Manager (management.azure.com) #> function Test-Assessment-41207 { [ZtTest( Category = 'Security information and event management', ImplementationCost = 'Medium', MinimumLicense = ('Consumption-based: Microsoft Sentinel'), Pillar = 'SecOps', RiskLevel = 'High', Service = ('Azure'), SfiPillar = 'Monitor and detect cyberthreats', TenantType = ('Workforce'), TestId = 41207, Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking active analytics 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 = '41207' Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats' 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 = '41207' Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats' 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 analytics-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 analytics-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 = '41207' Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats' 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 analytics-rules check.' -Tag Test -Level VeryVerbose Add-ZtTestResultDetail -SkippedBecause NotApplicable } return } # Q1 (spec) / Q3 (implementation — Q1+Q2 handled by Get-SentinelWorkspaceData): # Fetch all analytics (alert) rules for each Sentinel-onboarded workspace. $rawRulesByWorkspace = @{} foreach ($workspace in $onboardedWorkspaces) { Write-ZtProgress -Activity $activity -Status "Fetching analytics rules for $($workspace.WorkspaceName) in $($workspace.SubscriptionName)" $alertRulesPath = "$($workspace.WorkspaceId)/providers/Microsoft.SecurityInsights/alertRules?api-version=2024-09-01" try { $rawRulesByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $alertRulesPath -ErrorAction Stop) } catch { $rawRulesByWorkspace[$workspace.WorkspaceId] = $null Write-PSFMessage "Error querying analytics rules for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning } } #endregion Data Collection #region Assessment Logic $workspaceResults = foreach ($workspace in $onboardedWorkspaces) { $rawRules = $rawRulesByWorkspace[$workspace.WorkspaceId] $totalRules = 0 $enabledRules = 0 $enabledKinds = @() $mitreTactics = @() $onlyFusionEnabled = $false if ($null -ne $rawRules) { $totalRules = $rawRules.Count $enabledRuleObjects = @($rawRules | Where-Object { $_.properties.enabled -eq $true }) $enabledRules = $enabledRuleObjects.Count # Collect the kind of every enabled rule; only Scheduled and NRT rules expose # properties.tactics so limit the tactics harvest to those two kinds. $enabledKinds = @($enabledRuleObjects | Select-Object -ExpandProperty kind -Unique | Sort-Object) $mitreTactics = @( $enabledRuleObjects | Where-Object { $_.kind -iin @('Scheduled', 'NRT') } | ForEach-Object { $_.properties.tactics } | Where-Object { $_ } | Sort-Object -Unique ) } # Spec: pass only when at least one actionable rule kind is enabled. # Fusion is enabled by default in new workspaces and must not be credited alone. $actionableKinds = @('Scheduled', 'NRT', 'MicrosoftSecurityIncidentCreation') $hasActionableRule = ($enabledKinds | Where-Object { $_ -iin $actionableKinds }).Count -gt 0 $rowStatus = if ($null -eq $rawRules) { 'Investigate' } elseif ($enabledRules -gt 0 -and $hasActionableRule) { 'Pass' } elseif ($enabledRules -eq 0) { 'Fail' } else { # Enabled rules exist but none are of an actionable kind. $onlyFusionEnabled = ($enabledKinds.Count -eq 1 -and ($enabledKinds -icontains 'Fusion')) 'Fail' } [PSCustomObject]@{ SubscriptionName = $workspace.SubscriptionName SubscriptionId = $workspace.SubscriptionId WorkspaceName = $workspace.WorkspaceName ResourceGroup = $workspace.ResourceGroup WorkspaceId = $workspace.WorkspaceId TotalRules = $totalRules EnabledRules = $enabledRules EnabledKinds = ($enabledKinds -join ', ') MitreTactics = ($mitreTactics -join ', ') OnlyFusionEnabled = $onlyFusionEnabled RowStatus = $rowStatus } } $workspaceResults = @($workspaceResults) $passedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' }) $investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' }) $failedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Fail' }) $passed = $passedItems.Count -gt 0 $customStatus = $null if (-not $passed -and $investigateItems.Count -gt 0) { $customStatus = 'Investigate' $testResultMarkdown = "⚠️ The alert-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 = "✅ Active analytics rules are configured in the Sentinel workspace.`n`n%TestResult%" } else { $onlyFusionItems = @($failedItems | Where-Object { $_.OnlyFusionEnabled }) if ($onlyFusionItems.Count -gt 0 -and $failedItems.Count -eq $onlyFusionItems.Count) { # Every failing workspace has only the default Fusion rule — surface the specific condition. $testResultMarkdown = "❌ No active analytics rules are configured in the Sentinel workspace. Only the default Fusion rule is enabled, which is not sufficient for threat detection coverage.`n`n%TestResult%" } else { $testResultMarkdown = "❌ No active analytics 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 = 'Analytics rules per workspace' $formatTemplate = @' ## [{0}]({1}) | Subscription | Workspace | Total rules | Enabled rules | Enabled rule types | MITRE tactics | 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)" $analyticsLink = "https://portal.azure.com/#view/Microsoft_Azure_Security_Insights/MainMenuBlade/~/AnalyticRules/id/$($sentinelId -replace '/', '%2F')" $subMd = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)" $workspaceMd = "[$(Get-SafeMarkdown $result.WorkspaceName)]($analyticsLink)" $kindsMd = if ($result.EnabledKinds) { Get-SafeMarkdown -Text $result.EnabledKinds } else { '—' } $tacticsMd = if ($result.MitreTactics) { Get-SafeMarkdown -Text $result.MitreTactics } else { '—' } $statusDisplay = switch ($result.RowStatus) { 'Pass' { '✅ Pass' } 'Fail' { '❌ Fail' } 'Investigate' { '⚠️ Investigate' } } $tableRows += "| $subMd | $workspaceMd | $($result.TotalRules) | $($result.EnabledRules) | $kindsMd | $tacticsMd | $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 = '41207' Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |