Skip to content

Commit 3678cbd

Browse files
authored
SecOps - 41207 - Active analytics rules are configured in Microsoft Sentinel to detect threats (#1342)
2 parents fc5ed40 + dd7750c commit 3678cbd

2 files changed

Lines changed: 291 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Analytics rules are the detection engine of Microsoft Sentinel. Scheduled rules run KQL queries on a schedule (typically every 5 minutes to 1 hour); Near-Real-Time (NRT) rules evaluate the last minute of data on every event arrival; Microsoft Security rules forward Microsoft Defender alerts as Sentinel incidents; Fusion rules use Microsoft machine learning to correlate low-fidelity signals across data sources into high-fidelity, multi-stage incidents; Threat Intelligence rules match indicators from the threat-intel store against ingested telemetry; UEBA rules surface anomalous user/entity behavior; ML Behavior Analytics rules surface anomalous SSH and RDP login patterns. Without active analytics rules, the workspace ingests data but generates zero incidents — every threat actor TTP that could be detected passes without alerting. Microsoft publishes hundreds of analytics rule templates; production deployments enable a curated subset matched to the connector inventory and tune them over time. The check confirms at least one analytics rule is enabled and the workspace has more than zero rules across the documented kinds.
2+
3+
**Remediation action**
4+
5+
- [Detect threats out-of-the-box with Microsoft Sentinel analytics rules](https://learn.microsoft.com/azure/sentinel/detect-threats-built-in)
6+
- [Create custom analytics rules to detect threats](https://learn.microsoft.com/azure/sentinel/detect-threats-custom)
7+
- [Microsoft Sentinel content hub](https://learn.microsoft.com/azure/sentinel/sentinel-solutions-deploy)
8+
9+
<!--- Results --->
10+
%TestResult%
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<#
2+
.SYNOPSIS
3+
Checks that active analytics rules are configured in Microsoft Sentinel to detect threats.
4+
5+
.DESCRIPTION
6+
Verifies that at least one Sentinel-onboarded Log Analytics workspace has at least one
7+
analytics (alert) rule of a substantive kind (Scheduled, NRT, or
8+
MicrosoftSecurityIncidentCreation) enabled. A workspace with only the default Fusion rule
9+
enabled is considered non-compliant because Fusion alone is not sufficient detection
10+
coverage for a production environment.
11+
12+
.NOTES
13+
Test ID: 41207
14+
Category: Security information and event management
15+
Pillar: Security Operations
16+
Required API: Azure Resource Manager (management.azure.com)
17+
#>
18+
19+
function Test-Assessment-41207 {
20+
[ZtTest(
21+
Category = 'Security information and event management',
22+
ImplementationCost = 'Medium',
23+
MinimumLicense = ('Consumption-based: Microsoft Sentinel'),
24+
Pillar = 'SecOps',
25+
RiskLevel = 'High',
26+
Service = ('Azure'),
27+
SfiPillar = 'Monitor and detect cyberthreats',
28+
TenantType = ('Workforce'),
29+
TestId = 41207,
30+
Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats',
31+
UserImpact = 'Low'
32+
)]
33+
[CmdletBinding()]
34+
param()
35+
36+
#region Data Collection
37+
38+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
39+
$activity = 'Checking active analytics rules in Sentinel workspaces'
40+
41+
# Q1 + Q2 + onboarding check via shared helper.
42+
# Returns 'Forbidden' on ARG 401/403 (Investigate).
43+
# Returns $null on unexpected ARG failure (Investigate).
44+
# Returns 'NoSubscriptions' when no enabled subscriptions are accessible (Skip).
45+
# Returns 'NoWorkspaces' when no Log Analytics workspaces exist in scope (Skip).
46+
$allWorkspaces = Get-SentinelWorkspaceData -Activity $activity
47+
48+
if ($null -eq $allWorkspaces) {
49+
$params = @{
50+
TestId = '41207'
51+
Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats'
52+
Status = $false
53+
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.'
54+
CustomStatus = 'Investigate'
55+
}
56+
Add-ZtTestResultDetail @params
57+
return
58+
}
59+
60+
if ($allWorkspaces -eq 'Forbidden') {
61+
$params = @{
62+
TestId = '41207'
63+
Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats'
64+
Status = $false
65+
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.'
66+
CustomStatus = 'Investigate'
67+
}
68+
Add-ZtTestResultDetail @params
69+
return
70+
}
71+
72+
if ($allWorkspaces -eq 'NoSubscriptions') {
73+
Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel analytics-rules check.' -Tag Test -Level VeryVerbose
74+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
75+
return
76+
}
77+
78+
if ($allWorkspaces -eq 'NoWorkspaces') {
79+
Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel analytics-rules check.' -Tag Test -Level VeryVerbose
80+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
81+
return
82+
}
83+
84+
$checkableWorkspaces = @($allWorkspaces | Where-Object { -not $_.PermissionError })
85+
$forbiddenWorkspaces = @($allWorkspaces | Where-Object { $_.PermissionError })
86+
$onboardedWorkspaces = @($checkableWorkspaces | Where-Object { $_.SentinelOnboarded })
87+
88+
if ($onboardedWorkspaces.Count -eq 0) {
89+
if ($forbiddenWorkspaces.Count -gt 0) {
90+
# Auth errors mean we cannot confirm whether those workspaces have Sentinel onboarded;
91+
# we cannot rule out a passing workspace exists among the inaccessible ones.
92+
$params = @{
93+
TestId = '41207'
94+
Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats'
95+
Status = $false
96+
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.'
97+
CustomStatus = 'Investigate'
98+
}
99+
Add-ZtTestResultDetail @params
100+
}
101+
else {
102+
# Spec: no Sentinel-onboarded workspaces with full visibility — Skipped.
103+
Write-PSFMessage 'No Sentinel-onboarded workspaces found — skipping Sentinel analytics-rules check.' -Tag Test -Level VeryVerbose
104+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
105+
}
106+
return
107+
}
108+
109+
# Q1 (spec) / Q3 (implementation — Q1+Q2 handled by Get-SentinelWorkspaceData):
110+
# Fetch all analytics (alert) rules for each Sentinel-onboarded workspace.
111+
$rawRulesByWorkspace = @{}
112+
113+
foreach ($workspace in $onboardedWorkspaces) {
114+
Write-ZtProgress -Activity $activity -Status "Fetching analytics rules for $($workspace.WorkspaceName) in $($workspace.SubscriptionName)"
115+
$alertRulesPath = "$($workspace.WorkspaceId)/providers/Microsoft.SecurityInsights/alertRules?api-version=2024-09-01"
116+
117+
try {
118+
$rawRulesByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $alertRulesPath -ErrorAction Stop)
119+
}
120+
catch {
121+
$rawRulesByWorkspace[$workspace.WorkspaceId] = $null
122+
Write-PSFMessage "Error querying analytics rules for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning
123+
}
124+
}
125+
126+
#endregion Data Collection
127+
128+
#region Assessment Logic
129+
130+
$workspaceResults = foreach ($workspace in $onboardedWorkspaces) {
131+
$rawRules = $rawRulesByWorkspace[$workspace.WorkspaceId]
132+
133+
$totalRules = 0
134+
$enabledRules = 0
135+
$enabledKinds = @()
136+
$mitreTactics = @()
137+
$onlyFusionEnabled = $false
138+
139+
if ($null -ne $rawRules) {
140+
$totalRules = $rawRules.Count
141+
$enabledRuleObjects = @($rawRules | Where-Object { $_.properties.enabled -eq $true })
142+
$enabledRules = $enabledRuleObjects.Count
143+
144+
# Collect the kind of every enabled rule; only Scheduled and NRT rules expose
145+
# properties.tactics so limit the tactics harvest to those two kinds.
146+
$enabledKinds = @($enabledRuleObjects | Select-Object -ExpandProperty kind -Unique | Sort-Object)
147+
$mitreTactics = @(
148+
$enabledRuleObjects |
149+
Where-Object { $_.kind -iin @('Scheduled', 'NRT') } |
150+
ForEach-Object { $_.properties.tactics } |
151+
Where-Object { $_ } |
152+
Sort-Object -Unique
153+
)
154+
}
155+
156+
# Spec: pass only when at least one actionable rule kind is enabled.
157+
# Fusion is enabled by default in new workspaces and must not be credited alone.
158+
$actionableKinds = @('Scheduled', 'NRT', 'MicrosoftSecurityIncidentCreation')
159+
$hasActionableRule = ($enabledKinds | Where-Object { $_ -iin $actionableKinds }).Count -gt 0
160+
161+
$rowStatus = if ($null -eq $rawRules) {
162+
'Investigate'
163+
}
164+
elseif ($enabledRules -gt 0 -and $hasActionableRule) {
165+
'Pass'
166+
}
167+
elseif ($enabledRules -eq 0) {
168+
'Fail'
169+
}
170+
else {
171+
# Enabled rules exist but none are of an actionable kind.
172+
$onlyFusionEnabled = ($enabledKinds.Count -eq 1 -and ($enabledKinds -icontains 'Fusion'))
173+
'Fail'
174+
}
175+
176+
[PSCustomObject]@{
177+
SubscriptionName = $workspace.SubscriptionName
178+
SubscriptionId = $workspace.SubscriptionId
179+
WorkspaceName = $workspace.WorkspaceName
180+
ResourceGroup = $workspace.ResourceGroup
181+
WorkspaceId = $workspace.WorkspaceId
182+
TotalRules = $totalRules
183+
EnabledRules = $enabledRules
184+
EnabledKinds = ($enabledKinds -join ', ')
185+
MitreTactics = ($mitreTactics -join ', ')
186+
OnlyFusionEnabled = $onlyFusionEnabled
187+
RowStatus = $rowStatus
188+
}
189+
}
190+
$workspaceResults = @($workspaceResults)
191+
192+
$passedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' })
193+
$investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' })
194+
$failedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Fail' })
195+
196+
$passed = $passedItems.Count -gt 0
197+
$customStatus = $null
198+
199+
if (-not $passed -and $investigateItems.Count -gt 0) {
200+
$customStatus = 'Investigate'
201+
$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%"
202+
}
203+
elseif ($passed) {
204+
$testResultMarkdown = "✅ Active analytics rules are configured in the Sentinel workspace.`n`n%TestResult%"
205+
}
206+
else {
207+
$onlyFusionItems = @($failedItems | Where-Object { $_.OnlyFusionEnabled })
208+
if ($onlyFusionItems.Count -gt 0 -and $failedItems.Count -eq $onlyFusionItems.Count) {
209+
# Every failing workspace has only the default Fusion rule — surface the specific condition.
210+
$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%"
211+
}
212+
else {
213+
$testResultMarkdown = "❌ No active analytics rules are configured in the Sentinel workspace.`n`n%TestResult%"
214+
}
215+
}
216+
217+
#endregion Assessment Logic
218+
219+
#region Report Generation
220+
221+
$portalSentinelLink = 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel'
222+
$tableTitle = 'Analytics rules per workspace'
223+
224+
$formatTemplate = @'
225+
226+
227+
## [{0}]({1})
228+
229+
| Subscription | Workspace | Total rules | Enabled rules | Enabled rule types | MITRE tactics | Status |
230+
| :----------- | :-------- | ----------: | ------------: | :----------------- | :------------ | :----- |
231+
{2}
232+
'@
233+
234+
$tableRows = ''
235+
$maxDisplay = 10
236+
$statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2 }
237+
$displayResults = @($workspaceResults | Sort-Object { $statusPriority[$_.RowStatus] }, SubscriptionName, WorkspaceName)
238+
$hasMoreItems = $false
239+
if ($workspaceResults.Count -gt $maxDisplay) {
240+
$displayResults = @($displayResults | Select-Object -First $maxDisplay)
241+
$hasMoreItems = $true
242+
}
243+
244+
foreach ($result in $displayResults) {
245+
$subLink = "https://portal.azure.com/#resource/subscriptions/$($result.SubscriptionId)"
246+
$sentinelId = "/subscriptions/$($result.SubscriptionId)/resourcegroups/$($result.ResourceGroup)/providers/microsoft.securityinsightsarg/sentinel/$($result.WorkspaceName)"
247+
$analyticsLink = "https://portal.azure.com/#view/Microsoft_Azure_Security_Insights/MainMenuBlade/~/AnalyticRules/id/$($sentinelId -replace '/', '%2F')"
248+
$subMd = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)"
249+
$workspaceMd = "[$(Get-SafeMarkdown $result.WorkspaceName)]($analyticsLink)"
250+
$kindsMd = if ($result.EnabledKinds) { Get-SafeMarkdown -Text $result.EnabledKinds } else { '' }
251+
$tacticsMd = if ($result.MitreTactics) { Get-SafeMarkdown -Text $result.MitreTactics } else { '' }
252+
$statusDisplay = switch ($result.RowStatus) {
253+
'Pass' { '✅ Pass' }
254+
'Fail' { '❌ Fail' }
255+
'Investigate' { '⚠️ Investigate' }
256+
}
257+
$tableRows += "| $subMd | $workspaceMd | $($result.TotalRules) | $($result.EnabledRules) | $kindsMd | $tacticsMd | $statusDisplay |`n"
258+
}
259+
260+
if ($hasMoreItems) {
261+
$remainingCount = $workspaceResults.Count - $maxDisplay
262+
$tableRows += "`n... and $remainingCount more. [View all in Microsoft Sentinel]($portalSentinelLink)`n"
263+
}
264+
265+
$mdInfo = $formatTemplate -f $tableTitle, $portalSentinelLink, $tableRows
266+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
267+
268+
#endregion Report Generation
269+
270+
$params = @{
271+
TestId = '41207'
272+
Title = 'Active analytics rules are configured in Microsoft Sentinel to detect threats'
273+
Status = $passed
274+
Result = $testResultMarkdown
275+
}
276+
if ($customStatus) {
277+
$params.CustomStatus = $customStatus
278+
}
279+
280+
Add-ZtTestResultDetail @params
281+
}

0 commit comments

Comments
 (0)