Skip to content

Commit c28b05a

Browse files
authored
SecOps - 41211 - Auditing and health monitoring is enabled for Microsoft Sentinel (#1431)
2 parents 0bb98a8 + 239d9fd commit c28b05a

2 files changed

Lines changed: 321 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Auditing and health monitoring populates the SentinelAudit and SentinelHealth tables in the Sentinel workspace and is the documented mechanism for detecting two classes of failure: detection-pipeline degradation (a data connector that has stopped ingesting, an analytics rule whose query is failing, an automation rule that is unable to trigger its playbook, a playbook that is throwing errors) and SOC tampering (a threat actor or insider modifying or disabling analytics rules, automation rules, or data connectors to blind the SIEM). The risk of operating Sentinel without health and audit enabled is direct: a connector silently drops, the relevant analytics rules produce no results, the SOC believes the absence of alerts means absence of threats, and the threat actor's persistence and lateral-movement activity completes undetected during the outage. The audit risk is also direct: a Microsoft Sentinel Contributor whose account is taken over can disable rules or modify their KQL to exclude the IPs they are operating from (defense evasion, Impair Defenses), and only the audit table records the change with actor identity, timestamp, before/after content. Auditing and health monitoring is enabled by configuring an Azure Monitor diagnostic setting on the Microsoft Sentinel solution that streams the Sentinel log categories to the same workspace (or another workspace), and the same diagnostic setting can be authored through the Sentinel Settings > Auditing and health monitoring UI. The check confirms a diagnostic setting exists for the Sentinel solution that includes the documented Sentinel categories.
2+
3+
**Remediation action**
4+
5+
- [Turn on auditing and health monitoring for Microsoft Sentinel](https://learn.microsoft.com/azure/sentinel/enable-monitoring)
6+
- [Auditing and health monitoring in Microsoft Sentinel](https://learn.microsoft.com/azure/sentinel/health-audit)
7+
- [SentinelHealth table reference](https://learn.microsoft.com/azure/sentinel/health-table-reference)
8+
- [SentinelAudit table reference](https://learn.microsoft.com/azure/sentinel/audit-table-reference)
9+
10+
<!--- Results --->
11+
%TestResult%
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
<#
2+
.SYNOPSIS
3+
Checks that auditing and health monitoring is enabled for Microsoft Sentinel.
4+
5+
.DESCRIPTION
6+
Verifies that at least one Sentinel-onboarded Log Analytics workspace has an Azure Monitor
7+
diagnostic setting whose logs[] contains at least one entry with enabled == true and whose
8+
destination is a Log Analytics workspace (properties.workspaceId is non-empty).
9+
This is the documented mechanism for populating the SentinelAudit and SentinelHealth tables
10+
and for detecting detection-pipeline degradation and SOC tampering.
11+
12+
.NOTES
13+
Test ID: 41211
14+
Workshop Task: SECOPS_105
15+
Pillar: SecOps
16+
Category: Security information and event management
17+
Required API: Azure Resource Manager (management.azure.com), azure.insights diagnosticSettings
18+
#>
19+
function Test-Assessment-41211 {
20+
[ZtTest(
21+
Category = 'Security information and event management',
22+
ImplementationCost = 'Low',
23+
MinimumLicense = ('Consumption-based: Microsoft Sentinel'),
24+
Pillar = 'SecOps',
25+
RiskLevel = 'Medium',
26+
Service = ('Azure'),
27+
SfiPillar = 'Accelerate response and remediation',
28+
TenantType = ('Workforce'),
29+
TestId = 41211,
30+
Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel',
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 Sentinel auditing and health monitoring diagnostic settings'
40+
41+
# Q1 + Q2 + onboarding check delegated to the shared helper.
42+
# 'Forbidden' → 401/403 on ARG subscription or workspace query (spec: Investigate).
43+
# $null → unexpected ARG failure (spec: Investigate).
44+
# 'NoSubscriptions' → no enabled subscriptions accessible (spec: Skip).
45+
# 'NoWorkspaces' → no Log Analytics workspaces found (spec: Skip).
46+
# array → per-workspace results; PermissionError=$true means 401/403 on onboarding check.
47+
$allWorkspaces = Get-SentinelWorkspaceData -Activity $activity
48+
49+
if ($null -eq $allWorkspaces) {
50+
$params = @{
51+
TestId = '41211'
52+
Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
53+
Status = $false
54+
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.'
55+
CustomStatus = 'Investigate'
56+
}
57+
Add-ZtTestResultDetail @params
58+
return
59+
}
60+
61+
if ($allWorkspaces -eq 'Forbidden') {
62+
$params = @{
63+
TestId = '41211'
64+
Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
65+
Status = $false
66+
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.'
67+
CustomStatus = 'Investigate'
68+
}
69+
Add-ZtTestResultDetail @params
70+
return
71+
}
72+
73+
if ($allWorkspaces -eq 'NoSubscriptions') {
74+
Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
75+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
76+
return
77+
}
78+
79+
if ($allWorkspaces -eq 'NoWorkspaces') {
80+
Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
81+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
82+
return
83+
}
84+
85+
$checkableWorkspaces = @($allWorkspaces | Where-Object { -not $_.PermissionError })
86+
$forbiddenWorkspaces = @($allWorkspaces | Where-Object { $_.PermissionError })
87+
$onboardedWorkspaces = @($checkableWorkspaces | Where-Object { $_.SentinelOnboarded })
88+
89+
if ($onboardedWorkspaces.Count -eq 0) {
90+
if ($forbiddenWorkspaces.Count -gt 0) {
91+
# Cannot confirm whether inaccessible workspaces have Sentinel onboarded.
92+
$params = @{
93+
TestId = '41211'
94+
Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
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 → Skipped.
103+
Write-PSFMessage 'No Sentinel-onboarded workspaces found — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
104+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
105+
}
106+
return
107+
}
108+
109+
# Q1: For each Sentinel-onboarded workspace, retrieve the list of diagnostic settings.
110+
# A setting qualifies when logs[].enabled == true and properties.workspaceId is non-empty
111+
# (confirming routing to a Log Analytics workspace rather than Event Hub or Storage Account).
112+
$diagSettingsByWorkspace = @{}
113+
114+
foreach ($workspace in $onboardedWorkspaces) {
115+
Write-ZtProgress -Activity $activity -Status "Fetching diagnostic settings for workspace '$($workspace.WorkspaceName)' in '$($workspace.SubscriptionName)'"
116+
$diagPath = "$($workspace.WorkspaceId)/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview"
117+
118+
try {
119+
$diagSettingsByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $diagPath -ErrorAction Stop)
120+
}
121+
catch {
122+
$diagSettingsByWorkspace[$workspace.WorkspaceId] = $null
123+
Write-PSFMessage "Diagnostic settings API call failed — cannot determine setting state for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning
124+
}
125+
}
126+
127+
#endregion Data Collection
128+
129+
#region Assessment Logic
130+
131+
$workspaceResults = foreach ($workspace in $onboardedWorkspaces) {
132+
$diagSettings = $diagSettingsByWorkspace[$workspace.WorkspaceId]
133+
134+
$totalSettingCount = $null
135+
$settingDetails = @()
136+
$rowStatus = 'Fail'
137+
138+
if ($null -eq $diagSettings) {
139+
# Diagnostic settings API call failed — cannot determine setting state for this workspace.
140+
$rowStatus = 'Investigate'
141+
}
142+
elseif ($diagSettings.Count -eq 0) {
143+
# Q1 returned an empty collection — no diagnostic settings configured (spec: Fail).
144+
$totalSettingCount = 0
145+
$rowStatus = 'Fail'
146+
}
147+
else {
148+
$totalSettingCount = $diagSettings.Count
149+
150+
# Build per-setting detail for display, preserving the category-to-destination
151+
# association for each diagnostic setting. Include categoryGroup (e.g. allLogs, audit)
152+
# as fallback for logs[].category — both are valid shapes for a log entry; a given
153+
# entry uses one or the other, never both simultaneously.
154+
$settingDetails = @(foreach ($setting in $diagSettings) {
155+
$enabledLogs = @($setting.properties.logs | Where-Object { $_.enabled -eq $true })
156+
$settingCats = @($enabledLogs | ForEach-Object {
157+
if ($_.category) { $_.category } else { $_.categoryGroup }
158+
} | Where-Object { $_ })
159+
[PSCustomObject]@{
160+
Name = $setting.name
161+
EnabledCategories = $settingCats
162+
DestinationId = $setting.properties.workspaceId
163+
}
164+
})
165+
166+
# A qualifying setting has at least one logs[] entry with enabled == true
167+
# AND routes to a Log Analytics workspace (properties.workspaceId non-empty).
168+
$qualifyingSettings = @($diagSettings | Where-Object {
169+
($_.properties.logs | Where-Object { $_.enabled -eq $true }).Count -gt 0 -and
170+
-not [string]::IsNullOrEmpty($_.properties.workspaceId)
171+
})
172+
173+
# Settings with enabled logs but no LAW destination — cannot confirm the feature is on.
174+
$enabledLogsNoLaw = @($diagSettings | Where-Object {
175+
($_.properties.logs | Where-Object { $_.enabled -eq $true }).Count -gt 0 -and
176+
[string]::IsNullOrEmpty($_.properties.workspaceId)
177+
})
178+
179+
if ($qualifyingSettings.Count -gt 0) {
180+
$rowStatus = 'Pass'
181+
}
182+
elseif ($enabledLogsNoLaw.Count -gt 0) {
183+
# Logs are enabled but destination is not a Log Analytics workspace (spec: Investigate).
184+
$rowStatus = 'Investigate'
185+
}
186+
else {
187+
# Settings exist but none have any logs[].enabled == true (spec: Investigate).
188+
$rowStatus = 'Investigate'
189+
}
190+
}
191+
192+
[PSCustomObject]@{
193+
SubscriptionName = $workspace.SubscriptionName
194+
SubscriptionId = $workspace.SubscriptionId
195+
WorkspaceName = $workspace.WorkspaceName
196+
ResourceGroup = $workspace.ResourceGroup
197+
WorkspaceId = $workspace.WorkspaceId
198+
TotalSettingCount = $totalSettingCount
199+
DiagnosticSettings = $settingDetails # array of per-setting objects; preserves category-to-destination association
200+
RowStatus = $rowStatus
201+
}
202+
}
203+
$workspaceResults = @($workspaceResults)
204+
205+
$passedItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' })
206+
$investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' })
207+
208+
# Pass when at least one Sentinel workspace has a qualifying diagnostic setting.
209+
$passed = $passedItems.Count -gt 0
210+
$customStatus = $null
211+
212+
if (-not $passed -and ($investigateItems.Count -gt 0 -or $forbiddenWorkspaces.Count -gt 0)) {
213+
$customStatus = 'Investigate'
214+
$testResultMarkdown = "⚠️ Auditing and health monitoring could not be confirmed for one or more Sentinel workspaces. This may be due to a diagnostic settings API failure, all log categories being disabled, or diagnostic settings routing to a non-Log Analytics destination. Re-run after verifying Monitoring Reader access on each affected workspace.`n`n%TestResult%"
215+
}
216+
elseif ($passed) {
217+
$testResultMarkdown = "✅ Auditing and health monitoring is enabled for the Sentinel workspace.`n`n%TestResult%"
218+
}
219+
else {
220+
$testResultMarkdown = "❌ Auditing and health monitoring is not enabled for the Sentinel workspace.`n`n%TestResult%"
221+
}
222+
223+
#endregion Assessment Logic
224+
225+
#region Report Generation
226+
227+
$azContext = Get-AzContext -ErrorAction SilentlyContinue
228+
$portalHost = if ($azContext -and $azContext.Environment.Name -eq 'AzureUSGovernment') { 'https://portal.azure.us' } else { 'https://portal.azure.com' }
229+
$portalSentinelLink = "$portalHost/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel"
230+
$tableTitle = 'Auditing and health monitoring status per workspace'
231+
232+
$formatTemplate = @'
233+
234+
235+
## [{0}]({1})
236+
237+
| Subscription | Workspace | Diagnostic settings | Setting name | Enabled categories | Destination workspace | Status |
238+
| :----------- | :-------- | ------------------: | :----------- | :----------------- | :-------------------- | :----- |
239+
{2}
240+
'@
241+
242+
$tableRows = ''
243+
$maxDisplay = 10
244+
$statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2 }
245+
$displayResults = @($workspaceResults | Sort-Object { $statusPriority[$_.RowStatus] }, SubscriptionName, WorkspaceName)
246+
$hasMoreItems = $false
247+
if ($workspaceResults.Count -gt $maxDisplay) {
248+
$displayResults = @($displayResults | Select-Object -First $maxDisplay)
249+
$hasMoreItems = $true
250+
}
251+
252+
foreach ($result in $displayResults) {
253+
$subLink = "$portalHost/#resource/subscriptions/$($result.SubscriptionId)"
254+
$diagLink = "$portalHost/#resource$($result.WorkspaceId)/diagnosticSettings"
255+
$subMd = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)"
256+
$workspaceMd = "[$(Get-SafeMarkdown $result.WorkspaceName)]($diagLink)"
257+
$countMd = if ($null -eq $result.TotalSettingCount) { '' } else { $result.TotalSettingCount }
258+
259+
if ($result.DiagnosticSettings.Count -gt 0) {
260+
# One row per diagnostic setting — preserves the category-to-destination association.
261+
foreach ($setting in $result.DiagnosticSettings) {
262+
$settingNameMd = Get-SafeMarkdown -Text $setting.Name
263+
$categoriesMd = if ($setting.EnabledCategories.Count -gt 0) {
264+
$setting.EnabledCategories -join ', '
265+
} else { '' }
266+
$destMd = if (-not [string]::IsNullOrEmpty($setting.DestinationId)) {
267+
$wsName = ($setting.DestinationId -split '/')[-1]
268+
"[$(Get-SafeMarkdown $wsName)]($portalHost/#resource$($setting.DestinationId)/overview)"
269+
} elseif ($setting.EnabledCategories.Count -gt 0) {
270+
# Logs enabled but destination is not a Log Analytics workspace.
271+
'⚠️ Non-LAW destination'
272+
} else { '' }
273+
$settingStatus = if ($setting.EnabledCategories.Count -gt 0 -and -not [string]::IsNullOrEmpty($setting.DestinationId)) {
274+
'✅ Active'
275+
} elseif ($setting.EnabledCategories.Count -gt 0) {
276+
'⚠️ Non-LAW'
277+
} else {
278+
'❌ Logs disabled'
279+
}
280+
$tableRows += "| $subMd | $workspaceMd | $countMd | $settingNameMd | $categoriesMd | $destMd | $settingStatus |`n"
281+
}
282+
} else {
283+
# No diagnostic settings (Fail) or API error (Investigate) — single placeholder row.
284+
$placeholderStatus = if ($result.RowStatus -eq 'Investigate') { '⚠️ Investigate' } else { '❌ No settings' }
285+
$tableRows += "| $subMd | $workspaceMd | $countMd | — | — | — | $placeholderStatus |`n"
286+
}
287+
}
288+
289+
if ($hasMoreItems) {
290+
$remainingCount = $workspaceResults.Count - $maxDisplay
291+
$tableRows += "`n... and $remainingCount more. [View all in Microsoft Sentinel]($portalSentinelLink)`n"
292+
}
293+
294+
$mdInfo = $formatTemplate -f $tableTitle, $portalSentinelLink, $tableRows
295+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
296+
297+
#endregion Report Generation
298+
299+
$params = @{
300+
TestId = '41211'
301+
Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
302+
Status = $passed
303+
Result = $testResultMarkdown
304+
}
305+
if ($null -ne $customStatus) {
306+
$params.CustomStatus = $customStatus
307+
}
308+
309+
Add-ZtTestResultDetail @params
310+
}

0 commit comments

Comments
 (0)