-
Notifications
You must be signed in to change notification settings - Fork 124
Network - 25420 - Network access logs are retained for security analysis and compliance requirements #781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aahmed-spec
wants to merge
6
commits into
main
Choose a base branch
from
test-25420
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+384
−0
Open
Network - 25420 - Network access logs are retained for security analysis and compliance requirements #781
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
54665dd
Add test files for assessment 25420
aahmed-spec eaa7497
Made changes to code 25420
aahmed-spec 84ca85e
Made changes to format & comments 25420
aahmed-spec 5ab8fe1
Comments added src/powershell/tests/Test-Assessment.25420.ps1
aahmed-spec 7d71998
Made changes as per copilot suggestions 25420
aahmed-spec c17448f
Add more logs as per spec changes
aahmed-spec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 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. | ||
|
|
||
| **Remediation action** | ||
| - Configure diagnostic settings with Log Analytics workspace for extended retention (90-730 days) with query capabilities. | ||
| - [Integrate Microsoft Entra logs with Azure Monitor logs](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/DiagnosticSettings) | ||
| - Configure Log Analytics workspace retention to meet organizational security and compliance requirements (minimum 90 days recommended) | ||
| - [Manage data retention in a Log Analytics workspace](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-archive) | ||
| - Enable table-level retention for specific Global Secure Access tables to extend beyond workspace defaults | ||
| - [Configure table-level retention](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-archive#configure-table-level-retention) | ||
| <!--- Results ---> | ||
| %TestResult% |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,373 @@ | ||
| <# | ||
| .SYNOPSIS | ||
| Validates that network access logs are retained for security analysis | ||
| and compliance requirements. | ||
|
|
||
| .DESCRIPTION | ||
| This test evaluates diagnostic settings for Microsoft Entra to ensure | ||
| Global Secure Access log categories are enabled with appropriate | ||
| retention periods (minimum 90 days) in Log Analytics workspaces. | ||
|
|
||
| .NOTES | ||
| Test ID: 25420 | ||
| Category: Global Secure Access | ||
| Required APIs: Azure Management REST API (diagnostic settings, workspaces) | ||
| #> | ||
|
|
||
| function Test-Assessment-25420 { | ||
|
|
||
| [ZtTest( | ||
| Category = 'Global Secure Access', | ||
| ImplementationCost = 'Low', | ||
| MinimumLicense = 'AAD_PREMIUM, Entra_Premium_Internet_Access, Entra_Premium_Private_Access', | ||
| Pillar = 'Network', | ||
| RiskLevel = 'High', | ||
| SfiPillar = 'Monitor and detect cyberthreats', | ||
| TenantType = 'Workforce', | ||
| TestId = 25420, | ||
| Title = 'Network access logs are retained for security analysis and compliance requirements', | ||
| UserImpact = 'Low' | ||
| )] | ||
| [CmdletBinding()] | ||
| param() | ||
|
|
||
| # Minimum retention period in days for compliance | ||
| $MINIMUM_RETENTION_DAYS = 90 | ||
|
|
||
| # Required Global Secure Access log categories | ||
| $REQUIRED_LOG_CATEGORIES = @( | ||
| 'AuditLogs', # Audit logs for configuration changes to Global Secure Access | ||
| 'NetworkAccessTrafficLogs', # Network traffic logs | ||
| 'EnrichedOffice365AuditLogs', # Enriched Office 365 audit logs with network context | ||
| 'RemoteNetworkHealthLogs', # Remote network health logs | ||
| 'NetworkAccessAlerts', # Security alerts | ||
| 'NetworkAccessConnectionEvents', # Connection event logs | ||
| 'NetworkAccessGenerativeAIInsights' # AI-generated insights and security recommendations | ||
| ) | ||
|
|
||
| #region Data Collection | ||
|
|
||
| Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose | ||
| $activity = 'Evaluating network access log retention configuration' | ||
| Write-ZtProgress -Activity $activity -Status 'Checking Azure connection' | ||
|
|
||
| # Check for Azure authentication | ||
| try { | ||
| $accessToken = Get-AzAccessToken -AsSecureString -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | ||
| } | ||
| catch [Management.Automation.CommandNotFoundException] { | ||
| Write-PSFMessage $_.Exception.Message -Tag Test -Level Error | ||
| } | ||
|
|
||
| if (!$accessToken) { | ||
| Write-PSFMessage "Azure authentication token not found." -Level Warning | ||
| Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure | ||
| return | ||
| } | ||
|
|
||
| Write-ZtProgress -Activity $activity -Status 'Querying diagnostic settings' | ||
|
|
||
| # Query Q1: Retrieve diagnostic settings for Microsoft Entra | ||
| $diagnosticSettings = $null | ||
| try { | ||
| $uri = 'https://management.azure.com/providers/microsoft.aadiam/diagnosticsettings?api-version=2017-04-01-preview' | ||
| $response = Invoke-AzRestMethod -Uri $uri -Method GET -ErrorAction Stop | ||
| $diagnosticSettings = ($response.Content | ConvertFrom-Json).value | ||
| } | ||
| catch { | ||
| Write-PSFMessage "Error querying diagnostic settings: $_" -Level Error | ||
| } | ||
|
|
||
| # Query Q2: Retrieve Log Analytics workspace retention settings for each configured workspace | ||
| $workspaceDetails = @{} | ||
|
|
||
| if ($null -ne $diagnosticSettings -and $diagnosticSettings.Count -gt 0) { | ||
|
|
||
| Write-ZtProgress -Activity $activity -Status 'Checking workspace retention settings' | ||
|
|
||
| $workspaceIds = $diagnosticSettings | | ||
| Where-Object { $_.properties.workspaceId } | | ||
| Select-Object -ExpandProperty properties | | ||
| Select-Object -ExpandProperty workspaceId -Unique | ||
|
|
||
| foreach ($workspaceId in $workspaceIds) { | ||
| try { | ||
| $workspaceUri = "https://management.azure.com$workspaceId`?api-version=2023-09-01" | ||
| $workspaceResponse = Invoke-AzRestMethod -Uri $workspaceUri -Method GET -ErrorAction Stop | ||
| $workspaceDetails[$workspaceId] = ($workspaceResponse.Content | ConvertFrom-Json) | ||
| } | ||
| catch { | ||
| Write-PSFMessage "Error querying workspace $workspaceId : $_" -Level Warning | ||
| $workspaceDetails[$workspaceId] = $null | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #endregion Data Collection | ||
|
|
||
| #region Assessment Logic | ||
|
|
||
| # Initialize evaluation containers | ||
| $passed = $false | ||
| $testResultMarkdown = '' | ||
| $diagResults = @() | ||
| $logCategoryStatus = @{} | ||
| $hasAdequateRetention = $false | ||
| $hasAllRequiredCategories = $false | ||
| $passingSettingFound = $false | ||
|
|
||
| # Step 1: Check if any diagnostic settings exist | ||
| if ($null -eq $diagnosticSettings -or $diagnosticSettings.Count -eq 0) { | ||
|
|
||
| $passed = $false | ||
| $testResultMarkdown = "❌ 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%" | ||
|
|
||
| } | ||
| else { | ||
|
|
||
| Write-ZtProgress -Activity $activity -Status 'Evaluating log categories and retention' | ||
|
|
||
| # Initialize log category tracking | ||
| foreach ($category in $REQUIRED_LOG_CATEGORIES) { | ||
| $logCategoryStatus[$category] = @{ | ||
| Enabled = $false | ||
| DestinationType = 'None' | ||
| RetentionDays = $null | ||
| MeetsMinimum = $false | ||
| } | ||
| } | ||
|
|
||
| foreach ($setting in $diagnosticSettings) { | ||
|
|
||
| $workspaceId = $setting.properties.workspaceId | ||
| $storageAccountId = $setting.properties.storageAccountId | ||
| $logs = $setting.properties.logs | ||
|
|
||
| # Step 2: Determine destination type (Workspace, Storage, or both) | ||
| $destinationType = 'None' | ||
| if ($workspaceId -and $storageAccountId) { | ||
| $destinationType = 'Workspace & Storage' | ||
| } | ||
| elseif ($workspaceId) { | ||
| $destinationType = 'Workspace' | ||
| } | ||
| elseif ($storageAccountId) { | ||
| $destinationType = 'Storage' | ||
| } | ||
|
|
||
| # Step 3: Get workspace retention if applicable | ||
| $retentionDays = $null | ||
| $workspaceName = $null | ||
| if ($workspaceId -and $workspaceDetails.ContainsKey($workspaceId) -and $workspaceDetails[$workspaceId]) { | ||
| $workspace = $workspaceDetails[$workspaceId] | ||
| $retentionDays = $workspace.properties.retentionInDays | ||
| $workspaceName = $workspace.name | ||
| } | ||
|
|
||
| # Step 4: Evaluate if this setting meets minimum retention criteria | ||
| $meetsMinimum = $false | ||
| if ($retentionDays -ge $MINIMUM_RETENTION_DAYS) { | ||
| $meetsMinimum = $true | ||
| } | ||
| elseif ($storageAccountId) { | ||
| # Storage account retention cannot be queried via this API; assume meets minimum, flag for manual review | ||
| $meetsMinimum = $true | ||
| } | ||
|
|
||
| $enabledCategories = @() | ||
|
|
||
| # Step 5: Process log categories for this setting and track best configuration | ||
| foreach ($log in $logs) { | ||
| $categoryName = $log.category | ||
| $isEnabled = $log.enabled | ||
|
|
||
| if ($categoryName -in $REQUIRED_LOG_CATEGORIES -and $isEnabled) { | ||
| $enabledCategories += $categoryName | ||
|
|
||
| # Update global category status if this is a better configuration | ||
| # Prioritize: 1) configs that meet minimum, 2) higher retention days | ||
| $currentStatus = $logCategoryStatus[$categoryName] | ||
| $shouldUpdate = $false | ||
|
|
||
| if (-not $currentStatus.Enabled) { | ||
| $shouldUpdate = $true | ||
| } | ||
| elseif ($meetsMinimum -and -not $currentStatus.MeetsMinimum) { | ||
| # New config meets minimum but current doesn't - always prefer | ||
| $shouldUpdate = $true | ||
| } | ||
| elseif ($meetsMinimum -eq $currentStatus.MeetsMinimum) { | ||
| # Both meet or both don't meet minimum - compare retention days | ||
| if ($retentionDays -and (-not $currentStatus.RetentionDays -or $retentionDays -gt $currentStatus.RetentionDays)) { | ||
| $shouldUpdate = $true | ||
| } | ||
| } | ||
|
|
||
| if ($shouldUpdate) { | ||
| $logCategoryStatus[$categoryName] = @{ | ||
| Enabled = $true | ||
| DestinationType = $destinationType | ||
| RetentionDays = $retentionDays | ||
| MeetsMinimum = $meetsMinimum | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # Step 6: Check if this setting has all required categories AND meets retention criteria | ||
| $settingHasAllCategories = ($enabledCategories.Count -eq $REQUIRED_LOG_CATEGORIES.Count) | ||
| if ($settingHasAllCategories -and $meetsMinimum) { | ||
| $passingSettingFound = $true | ||
| } | ||
|
|
||
| # Determine per-setting status | ||
| # Storage-only settings require manual review since retention policies cannot be queried via this API | ||
| $settingStatus = if ($storageAccountId -and -not $workspaceId) { | ||
| 'Manual review' | ||
| } elseif ($settingHasAllCategories -and $meetsMinimum) { | ||
| 'Adequate' | ||
| } else { | ||
| 'Insufficient' | ||
| } | ||
|
|
||
| $diagResults += [PSCustomObject]@{ | ||
| SettingName = $setting.name | ||
| WorkspaceId = $workspaceId | ||
| WorkspaceName = $workspaceName | ||
| StorageAccountId = $storageAccountId | ||
| DestinationType = $destinationType | ||
| RetentionDays = $retentionDays | ||
| MeetsMinimum = $meetsMinimum | ||
| EnabledCategories = $enabledCategories | ||
| Status = $settingStatus | ||
| } | ||
| } | ||
|
|
||
| # Step 7: Verify all required Global Secure Access log categories are enabled (across all settings) | ||
| $enabledCategoryCount = ($logCategoryStatus.GetEnumerator() | Where-Object { $_.Value.Enabled }).Count | ||
| $hasAllRequiredCategories = ($enabledCategoryCount -eq $REQUIRED_LOG_CATEGORIES.Count) | ||
|
|
||
| # Step 8: Check if any category meets minimum retention (used for summary reporting) | ||
| # Note: Pass/fail is determined by $passingSettingFound which requires ALL categories in ONE setting | ||
| $hasAdequateRetention = ($logCategoryStatus.GetEnumerator() | Where-Object { $_.Value.MeetsMinimum }).Count -gt 0 | ||
|
|
||
| # Step 9: Determine overall test result | ||
| if ($passingSettingFound) { | ||
|
|
||
| $passed = $true | ||
| $testResultMarkdown = "✅ Global Secure Access logs are retained for at least $MINIMUM_RETENTION_DAYS days, supporting security analysis and compliance requirements`n`n%TestResult%" | ||
|
|
||
| } | ||
| else { | ||
|
|
||
| $passed = $false | ||
| $testResultMarkdown = "❌ Global Secure Access logs are not retained for adequate duration to support security investigations and compliance obligations`n`n%TestResult%" | ||
|
|
||
| } | ||
| } | ||
|
|
||
| #endregion Assessment Logic | ||
|
|
||
| #region Report Generation | ||
|
|
||
| # Calculate summary metrics | ||
| $settingsWithLongTermDest = ($diagResults | Where-Object { $_.WorkspaceId -or $_.StorageAccountId }).Count | ||
| $workspaceRetentions = $diagResults | Where-Object { $_.RetentionDays } | Select-Object -ExpandProperty RetentionDays | ||
| $avgRetention = if ($workspaceRetentions.Count -gt 0) { | ||
| [math]::Round(($workspaceRetentions | Measure-Object -Average).Average, 0) # Round to whole days for compliance reporting | ||
| } else { $null } | ||
| $minRetention = if ($workspaceRetentions.Count -gt 0) { | ||
| ($workspaceRetentions | Measure-Object -Minimum).Minimum | ||
| } else { $null } | ||
|
|
||
| $mdInfo = "`n## [Diagnostic settings configuration](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/DiagnosticSettings)`n`n" | ||
|
|
||
| # Log Retention Status table | ||
| if ($logCategoryStatus.Count -gt 0) { | ||
| $tableRows = "" | ||
| $formatTemplate = @' | ||
| ### Log retention status | ||
|
|
||
| | Log category | Enabled | Destination type | Retention period | Meets minimum (90 days) | | ||
| | :--- | :--- | :--- | :--- | :--- | | ||
| {0} | ||
|
|
||
| '@ | ||
| foreach ($category in $REQUIRED_LOG_CATEGORIES) { | ||
| $status = $logCategoryStatus[$category] | ||
| $enabledText = if ($status.Enabled) { 'Yes' } else { 'No' } | ||
| $destType = if ($status.Enabled) { $status.DestinationType } else { 'None' } | ||
|
|
||
| # For storage-only destinations, retention cannot be queried via API | ||
| $isStorageOnly = $status.DestinationType -eq 'Storage' | ||
| $retention = if ($status.RetentionDays) { | ||
| "$($status.RetentionDays) days" | ||
| } elseif ($isStorageOnly) { | ||
| 'Manual verification required' | ||
| } else { | ||
| 'Not configured' | ||
| } | ||
| $meetsMinText = if ($status.MeetsMinimum) { 'Yes' } else { 'No' } | ||
|
|
||
| $tableRows += "| $category | $enabledText | $destType | $retention | $meetsMinText |`n" | ||
| } | ||
| $mdInfo += $formatTemplate -f $tableRows | ||
| } | ||
|
|
||
| # Destination Details table | ||
| if ($diagResults.Count -gt 0) { | ||
| $tableRows = "" | ||
| $formatTemplate = @' | ||
| ### Destination details | ||
|
|
||
| | Destination type | Resource name | Default retention | Status | | ||
| | :--- | :--- | :--- | :--- | | ||
| {0} | ||
|
|
||
| '@ | ||
| foreach ($diag in $diagResults) { | ||
| # Add hyperlink to destination type based on type | ||
| $destType = if ($diag.WorkspaceName -and $diag.StorageAccountId) { | ||
| "[Log Analytics workspace](https://portal.azure.com/?feature.msaljs=true#browse/Microsoft.OperationalInsights%2Fworkspaces) & [Storage Account](https://portal.azure.com/?feature.msaljs=true#view/Microsoft_Azure_StorageHub/StorageHub.MenuView/~/StorageAccountsBrowse)" | ||
| } | ||
| elseif ($diag.WorkspaceName) { | ||
| "[Log Analytics workspace](https://portal.azure.com/?feature.msaljs=true#browse/Microsoft.OperationalInsights%2Fworkspaces)" | ||
| } | ||
| elseif ($diag.StorageAccountId) { | ||
| "[Storage account](https://portal.azure.com/?feature.msaljs=true#view/Microsoft_Azure_StorageHub/StorageHub.MenuView/~/StorageAccountsBrowse)" | ||
| } | ||
| else { 'None' } | ||
| $resourceName = if ($diag.WorkspaceName) { $diag.WorkspaceName } | ||
| elseif ($diag.StorageAccountId) { $diag.StorageAccountId.Split('/')[-1] } | ||
| else { 'N/A' } | ||
| $retention = if ($diag.RetentionDays) { "$($diag.RetentionDays) days" } | ||
| elseif ($diag.StorageAccountId) { 'Manual verification required' } | ||
| else { 'N/A' } | ||
| $tableRows += "| $destType | $resourceName | $retention | $($diag.Status) |`n" | ||
| } | ||
| $mdInfo += $formatTemplate -f $tableRows | ||
| } | ||
|
|
||
| # Summary table (per spec format) | ||
| $mdInfo += "### Summary`n`n" | ||
| $mdInfo += "| Metric | Value |`n| :--- | :--- |`n" | ||
| $mdInfo += "| Total diagnostic settings | $($diagnosticSettings.Count) |`n" | ||
| $mdInfo += "| Settings with long-term destination | $settingsWithLongTermDest |`n" | ||
| $mdInfo += "| Average retention period | $(if ($avgRetention) { "$avgRetention days" } else { 'N/A' }) |`n" | ||
| $mdInfo += "| Minimum retention found | $(if ($minRetention) { "$minRetention days" } else { 'N/A' }) |`n" | ||
| $mdInfo += "| Meets 90-day minimum | $(if ($hasAdequateRetention) { 'Yes' } else { 'No' }) |`n" | ||
|
|
||
| # Replace the placeholder with detailed information | ||
| $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo | ||
|
|
||
| #endregion Report Generation | ||
|
|
||
| $params = @{ | ||
| TestId = '25420' | ||
| Title = 'Network access logs are retained for security analysis and compliance requirements' | ||
| Status = $passed | ||
| Result = $testResultMarkdown | ||
| } | ||
|
|
||
| Add-ZtTestResultDetail @params | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.