Skip to content

Commit 54665dd

Browse files
committed
Add test files for assessment 25420
1 parent 64f500d commit 54665dd

File tree

2 files changed

+358
-0
lines changed

2 files changed

+358
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Without extended retention for Global Secure Access audit and traffic logs, threat actors can operate beyond the default 30-day retention window knowing their activities will be automatically purged, eliminating security evidence before detection occurs. When security teams detect suspicious patterns or receive breach notifications from external sources, investigations often require historical analysis spanning weeks or months to identify initial compromise vectors, lateral movement patterns, and data exfiltration channels. The default 30-day retention period in Microsoft Entra proves insufficient for most enterprise investigations, particularly when threat actors employ low-and-slow techniques or maintain persistent access over extended periods. Organizations subject to regulatory frameworks including GDPR, HIPAA, PCI DSS, and SOX face compliance violations when unable to produce audit trails for mandated retention periods, typically ranging from 90 days to multiple years. Without accessible historical logs, security operations teams cannot establish baseline behavior patterns for users and devices, perform retrospective threat hunting when new indicators of compromise emerge, or correlate network access events with identity signals and endpoint telemetry across extended timeframes. Inadequate retention eliminates the organization's ability to conduct thorough root cause analysis during incident response, potentially allowing threat actors to maintain undetected persistence mechanisms while organizations focus only on visible symptoms rather than underlying compromise patterns.
2+
3+
**Remediation action**
4+
- Configure diagnostic settings with Log Analytics workspace for extended retention (90-730 days) with query capabilities.
5+
- [Integrate Microsoft Entra logs with Azure Monitor logs](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/DiagnosticSettings)
6+
- Configure Log Analytics workspace retention to meet organizational security and compliance requirements (minimum 90 days recommended)
7+
- [Manage data retention in a Log Analytics workspace](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-archive)
8+
- Enable table-level retention for specific Global Secure Access tables to extend beyond workspace defaults
9+
- [Configure table-level retention](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-archive#configure-table-level-retention)
10+
<!--- Results --->
11+
%TestResult%
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
<#
2+
.SYNOPSIS
3+
Validates that network access logs are retained for security analysis
4+
and compliance requirements.
5+
6+
.DESCRIPTION
7+
This test evaluates diagnostic settings for Microsoft Entra to ensure
8+
Global Secure Access log categories are enabled with appropriate
9+
retention periods (minimum 90 days) in Log Analytics workspaces.
10+
11+
.NOTES
12+
Test ID: 25420
13+
Category: Global Secure Access
14+
Required APIs: Azure Management REST API (diagnostic settings, workspaces)
15+
#>
16+
17+
function Test-Assessment-25420 {
18+
19+
[ZtTest(
20+
Category = 'Global Secure Access',
21+
ImplementationCost = 'Low',
22+
MinimumLicense = 'Entra_Premium_Internet_Access',
23+
Pillar = 'Network',
24+
RiskLevel = 'High',
25+
SfiPillar = 'Monitor and detect cyberthreats',
26+
TenantType = 'Workforce',
27+
TestId = 25420,
28+
Title = 'Network access logs are retained for security analysis and compliance requirements',
29+
UserImpact = 'Low'
30+
)]
31+
[CmdletBinding()]
32+
param()
33+
34+
# Minimum retention period in days for compliance
35+
$MINIMUM_RETENTION_DAYS = 90
36+
37+
# Required Global Secure Access log categories
38+
$REQUIRED_LOG_CATEGORIES = @(
39+
'NetworkAccessTrafficLogs',
40+
'RemoteNetworkHealthLogs',
41+
'NetworkAccessAlerts',
42+
'NetworkAccessConnectionEvents'
43+
)
44+
45+
#region Data Collection
46+
47+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
48+
$activity = 'Evaluating network access log retention configuration'
49+
Write-ZtProgress -Activity $activity -Status 'Checking Azure connection'
50+
51+
# Check for Azure authentication
52+
try {
53+
$accessToken = Get-AzAccessToken -AsSecureString -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
54+
}
55+
catch [Management.Automation.CommandNotFoundException] {
56+
Write-PSFMessage $_.Exception.Message -Tag Test -Level Error
57+
}
58+
59+
if (!$accessToken) {
60+
Write-PSFMessage "Azure authentication token not found." -Level Warning
61+
Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure
62+
return
63+
}
64+
65+
Write-ZtProgress -Activity $activity -Status 'Querying diagnostic settings'
66+
67+
# Query Q1: Retrieve diagnostic settings for Microsoft Entra
68+
$diagnosticSettings = $null
69+
try {
70+
$uri = 'https://management.azure.com/providers/microsoft.aadiam/diagnosticsettings?api-version=2017-04-01-preview'
71+
$response = Invoke-AzRestMethod -Uri $uri -Method GET -ErrorAction Stop
72+
$diagnosticSettings = ($response.Content | ConvertFrom-Json).value
73+
}
74+
catch {
75+
Write-PSFMessage "Error querying diagnostic settings: $_" -Level Error
76+
}
77+
78+
# Query Q2: Retrieve Log Analytics workspace retention settings for each configured workspace
79+
$workspaceDetails = @{}
80+
81+
if ($null -ne $diagnosticSettings -and $diagnosticSettings.Count -gt 0) {
82+
83+
Write-ZtProgress -Activity $activity -Status 'Checking workspace retention settings'
84+
85+
$workspaceIds = $diagnosticSettings |
86+
Where-Object { $_.properties.workspaceId } |
87+
Select-Object -ExpandProperty properties |
88+
Select-Object -ExpandProperty workspaceId -Unique
89+
90+
foreach ($workspaceId in $workspaceIds) {
91+
try {
92+
$workspaceUri = "https://management.azure.com$workspaceId`?api-version=2023-09-01"
93+
$workspaceResponse = Invoke-AzRestMethod -Uri $workspaceUri -Method GET -ErrorAction Stop
94+
$workspaceDetails[$workspaceId] = ($workspaceResponse.Content | ConvertFrom-Json)
95+
}
96+
catch {
97+
Write-PSFMessage "Error querying workspace $workspaceId : $_" -Level Warning
98+
$workspaceDetails[$workspaceId] = $null
99+
}
100+
}
101+
}
102+
103+
#endregion Data Collection
104+
105+
#region Assessment Logic
106+
107+
# Initialize evaluation containers
108+
$passed = $false
109+
$customStatus = $null
110+
$testResultMarkdown = ''
111+
$diagResults = @()
112+
$logCategoryStatus = @{}
113+
$hasAdequateRetention = $false
114+
$hasAllRequiredCategories = $false
115+
$settingsWithStorageOnly = @()
116+
117+
# Step 1: Check if any diagnostic settings exist
118+
if ($null -eq $diagnosticSettings -or $diagnosticSettings.Count -eq 0) {
119+
120+
$passed = $false
121+
$testResultMarkdown =
122+
"❌ No diagnostic settings are configured for Microsoft Entra. Global Secure Access logs are retained for only 30 days (default in-portal retention) which is insufficient for security investigations.`n`n%TestResult%"
123+
124+
}
125+
else {
126+
127+
Write-ZtProgress -Activity $activity -Status 'Evaluating log categories and retention'
128+
129+
# Initialize log category tracking
130+
foreach ($category in $REQUIRED_LOG_CATEGORIES) {
131+
$logCategoryStatus[$category] = @{
132+
Enabled = $false
133+
DestinationType = 'None'
134+
RetentionDays = $null
135+
MeetsMinimum = $false
136+
}
137+
}
138+
139+
foreach ($setting in $diagnosticSettings) {
140+
141+
$settingName = $setting.name
142+
$workspaceId = $setting.properties.workspaceId
143+
$storageAccountId = $setting.properties.storageAccountId
144+
$logs = $setting.properties.logs
145+
146+
# Step 2: Determine destination type
147+
$destinationType = 'None'
148+
if ($workspaceId -and $storageAccountId) {
149+
$destinationType = 'Workspace & Storage'
150+
}
151+
elseif ($workspaceId) {
152+
$destinationType = 'Workspace'
153+
}
154+
elseif ($storageAccountId) {
155+
$destinationType = 'Storage'
156+
}
157+
158+
# Step 3: Get workspace retention if applicable
159+
$retentionDays = $null
160+
$workspaceName = $null
161+
if ($workspaceId -and $workspaceDetails.ContainsKey($workspaceId) -and $workspaceDetails[$workspaceId]) {
162+
$workspace = $workspaceDetails[$workspaceId]
163+
$retentionDays = $workspace.properties.retentionInDays
164+
$workspaceName = $workspace.name
165+
}
166+
167+
# Step 4: Evaluate retention meets minimum
168+
$meetsMinimum = $false
169+
if ($retentionDays -ge $MINIMUM_RETENTION_DAYS) {
170+
$meetsMinimum = $true
171+
}
172+
elseif ($storageAccountId -and -not $workspaceId) {
173+
# Storage-only requires manual verification
174+
$settingsWithStorageOnly += $settingName
175+
}
176+
177+
$enabledCategories = @()
178+
179+
# Step 5: Process log categories
180+
foreach ($log in $logs) {
181+
$categoryName = $log.category
182+
$isEnabled = $log.enabled
183+
184+
if ($categoryName -in $REQUIRED_LOG_CATEGORIES -and $isEnabled) {
185+
$enabledCategories += $categoryName
186+
187+
# Update category status if this is a better configuration
188+
$currentStatus = $logCategoryStatus[$categoryName]
189+
if (-not $currentStatus.Enabled -or
190+
($retentionDays -and $retentionDays -gt $currentStatus.RetentionDays)) {
191+
$logCategoryStatus[$categoryName] = @{
192+
Enabled = $true
193+
DestinationType = $destinationType
194+
RetentionDays = $retentionDays
195+
MeetsMinimum = $meetsMinimum
196+
}
197+
}
198+
}
199+
}
200+
201+
# Determine per-setting status
202+
$settingStatus = if ($enabledCategories.Count -eq 0) {
203+
'No GSA categories'
204+
} elseif ($meetsMinimum) {
205+
'Adequate'
206+
} elseif ($storageAccountId -and -not $workspaceId) {
207+
'Manual Review'
208+
} else {
209+
'Insufficient'
210+
}
211+
212+
$diagResults += [PSCustomObject]@{
213+
SettingName = $settingName
214+
WorkspaceId = $workspaceId
215+
WorkspaceName = $workspaceName
216+
StorageAccountId = $storageAccountId
217+
DestinationType = $destinationType
218+
RetentionDays = $retentionDays
219+
MeetsMinimum = $meetsMinimum
220+
EnabledCategories = $enabledCategories
221+
Status = $settingStatus
222+
}
223+
}
224+
225+
# Step 6: Check if all required categories are enabled
226+
$enabledCategoryCount = ($logCategoryStatus.GetEnumerator() | Where-Object { $_.Value.Enabled }).Count
227+
$hasAllRequiredCategories = ($enabledCategoryCount -eq $REQUIRED_LOG_CATEGORIES.Count)
228+
229+
# Step 7: Check if any configuration meets minimum retention
230+
$hasAdequateRetention = ($logCategoryStatus.GetEnumerator() | Where-Object { $_.Value.MeetsMinimum }).Count -gt 0
231+
232+
# Step 8: Determine overall test result
233+
if ($hasAllRequiredCategories -and $hasAdequateRetention) {
234+
235+
$passed = $true
236+
$testResultMarkdown =
237+
"✅ Global Secure Access logs are retained for at least $MINIMUM_RETENTION_DAYS days, supporting security analysis and compliance requirements.`n`n%TestResult%"
238+
239+
}
240+
elseif ($settingsWithStorageOnly.Count -gt 0 -and $hasAllRequiredCategories) {
241+
242+
# Storage-only configuration requires manual review
243+
$customStatus = 'Investigate'
244+
$testResultMarkdown =
245+
"⚠️ Global Secure Access logs are exported to storage accounts. Manual verification required to confirm retention policies meet the $MINIMUM_RETENTION_DAYS-day minimum.`n`n%TestResult%"
246+
247+
}
248+
elseif (-not $hasAllRequiredCategories) {
249+
250+
$passed = $false
251+
$testResultMarkdown =
252+
"❌ Not all Global Secure Access log categories are enabled. Security investigations require complete log coverage across all four categories.`n`n%TestResult%"
253+
254+
}
255+
else {
256+
257+
$passed = $false
258+
$testResultMarkdown =
259+
"❌ Global Secure Access logs are not retained for adequate duration to support security investigations and compliance obligations.`n`n%TestResult%"
260+
261+
}
262+
}
263+
264+
#endregion Assessment Logic
265+
266+
#region Report Generation
267+
268+
$mdInfo = "`n## Summary`n`n"
269+
$mdInfo += "| Metric | Value |`n|---|---|`n"
270+
$mdInfo += "| Total diagnostic settings | $($diagnosticSettings.Count) |`n"
271+
$mdInfo += "| Settings with workspace destination | $(($diagResults | Where-Object { $_.WorkspaceId }).Count) |`n"
272+
$mdInfo += "| Settings with storage destination | $(($diagResults | Where-Object { $_.StorageAccountId }).Count) |`n"
273+
$mdInfo += "| All required categories enabled | $(if ($hasAllRequiredCategories) { 'Yes' } else { 'No' }) |`n"
274+
$mdInfo += "| Meets $MINIMUM_RETENTION_DAYS-day minimum | $(if ($hasAdequateRetention) { 'Yes' } else { 'No' }) |`n`n"
275+
276+
if ($logCategoryStatus.Count -gt 0) {
277+
$tableRows = ""
278+
$formatTemplate = @'
279+
## [Log retention status](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/DiagnosticSettings)
280+
281+
| Log category | Enabled | Destination type | Retention period | Meets minimum (90 days)|
282+
|---|---|---|---|---|
283+
{0}
284+
285+
'@
286+
foreach ($category in $REQUIRED_LOG_CATEGORIES) {
287+
$status = $logCategoryStatus[$category]
288+
$enabledText = if ($status.Enabled) { 'Yes' } else { 'No' }
289+
$destType = if ($status.Enabled) { $status.DestinationType } else { 'Not configured' }
290+
$retention = if ($status.RetentionDays) { "$($status.RetentionDays) days" } else { 'Not configured' }
291+
$meetsMinText = if ($status.MeetsMinimum) { 'Yes' } else { 'No' }
292+
$tableRows += "| $category | $enabledText | $destType | $retention | $meetsMinText |`n"
293+
}
294+
$mdInfo += $formatTemplate -f $tableRows
295+
}
296+
297+
if ($diagResults.Count -gt 0) {
298+
$tableRows = ""
299+
$formatTemplate = @'
300+
## Destination details
301+
302+
| Diagnostic setting | Destination type | Resource name | Retention | Status |
303+
|---|---|---|---|---|
304+
{0}
305+
306+
'@
307+
foreach ($diag in $diagResults) {
308+
$settingNameSafe = Get-SafeMarkdown $diag.SettingName
309+
# Add hyperlink to diagnostic setting based on destination type
310+
$settingName = if ($diag.WorkspaceName) {
311+
"[$settingNameSafe](https://portal.azure.com/?feature.msaljs=true#browse/Microsoft.OperationalInsights%2Fworkspaces)"
312+
}
313+
elseif ($diag.StorageAccountId) {
314+
"[$settingNameSafe](https://portal.azure.com/?feature.msaljs=true#view/Microsoft_Azure_StorageHub/StorageHub.MenuView/~/StorageAccountsBrowse)"
315+
}
316+
else { $settingNameSafe }
317+
$resourceName = if ($diag.WorkspaceName) { $diag.WorkspaceName }
318+
elseif ($diag.StorageAccountId) { $diag.StorageAccountId.Split('/')[-1] }
319+
else { 'N/A' }
320+
$retention = if ($diag.RetentionDays) { "$($diag.RetentionDays) days" }
321+
elseif ($diag.StorageAccountId) { 'Manual verification' }
322+
else { 'N/A' }
323+
$tableRows += "| $settingName | $($diag.DestinationType) | $resourceName | $retention | $($diag.Status) |`n"
324+
}
325+
$mdInfo += $formatTemplate -f $tableRows
326+
}
327+
328+
# Replace the placeholder with detailed information
329+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
330+
331+
#endregion Report Generation
332+
333+
$params = @{
334+
TestId = '25420'
335+
Title = 'Network access logs are retained for security analysis and compliance requirements'
336+
Status = $passed
337+
Result = $testResultMarkdown
338+
}
339+
340+
# Add CustomStatus if status is 'Investigate'
341+
if ($null -ne $customStatus) {
342+
$params.CustomStatus = $customStatus
343+
}
344+
345+
# Add test result details
346+
Add-ZtTestResultDetail @params
347+
}

0 commit comments

Comments
 (0)