Src/Private/Get-AbrEntraIDDiagnostics.ps1
|
function Get-AbrEntraIDDiagnostics { [CmdletBinding()] param ([Parameter(Position=0,Mandatory)][string]$TenantId) begin { Write-PScriboMessage "Collecting Diagnostic Settings for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Diagnostics' } process { Section -Style Heading1 'Diagnostic Settings & Audit Logging' { Paragraph "The following section documents whether Entra ID sign-in logs and audit logs are being exported to a SIEM or Log Analytics workspace for tenant $TenantId. Log export is critical for security monitoring, incident response, and compliance." BlankLine try { Write-Host " - Retrieving diagnostic settings..." # Diagnostic settings are available via Azure Monitor REST API # The Graph API doesn't expose this directly -- use beta endpoint $DiagResp = Invoke-MgGraphRequest -Method GET ` -Uri "https://management.azure.com/providers/microsoft.aadiam/diagnosticSettings?api-version=2017-04-01-preview" ` -ErrorAction SilentlyContinue if ($DiagResp -and $DiagResp.value -and @($DiagResp.value).Count -gt 0) { $DiagSettings = $DiagResp.value $DiagObj = [System.Collections.ArrayList]::new() foreach ($DS in $DiagSettings) { $Destinations = @() if ($DS.properties.workspaceId) { $Destinations += 'Log Analytics' } if ($DS.properties.storageAccountId) { $Destinations += 'Storage Account' } if ($DS.properties.eventHubName) { $Destinations += 'Event Hub' } $EnabledLogs = @($DS.properties.logs | Where-Object { $_.enabled }).Count $dsInObj = [ordered] @{ 'Setting Name' = $DS.name 'Destinations' = if ($Destinations) { $Destinations -join ', ' } else { 'None configured' } 'Enabled Logs' = $EnabledLogs 'Sign-In Logs' = if ($DS.properties.logs | Where-Object { $_.category -eq 'SignInLogs' -and $_.enabled }) { '[OK] Enabled' } else { '[FAIL] Disabled' } 'Audit Logs' = if ($DS.properties.logs | Where-Object { $_.category -eq 'AuditLogs' -and $_.enabled }) { '[OK] Enabled' } else { '[FAIL] Disabled' } 'NonInteractive Sign-Ins' = if ($DS.properties.logs | Where-Object { $_.category -eq 'NonInteractiveUserSignInLogs' -and $_.enabled }) { '[OK] Enabled' } else { '[WARN] Disabled' } 'Service Principal Sign-Ins' = if ($DS.properties.logs | Where-Object { $_.category -eq 'ServicePrincipalSignInLogs' -and $_.enabled }) { '[OK] Enabled' } else { '[WARN] Disabled' } } $DiagObj.Add([pscustomobject]$dsInObj) | Out-Null } $null = (& { if ($HealthCheck.EntraID.ConditionalAccess) { $null = ($DiagObj | Where-Object { $_.'Sign-In Logs' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null) $null = ($DiagObj | Where-Object { $_.'Audit Logs' -like '*FAIL*' } | Set-Style -Style Critical | Out-Null) $null = ($DiagObj | Where-Object { $_.'NonInteractive Sign-Ins' -like '*WARN*' } | Set-Style -Style Warning | Out-Null) } }) $DiagTableParams = @{ Name = "Diagnostic Settings - $TenantId"; List = $false; ColumnWidths = 16, 14, 8, 12, 10, 18, 18 } if ($Report.ShowTableCaptions) { $DiagTableParams['Caption'] = "- $($DiagTableParams.Name)" } $DiagObj | Table @DiagTableParams $null = ($script:ExcelSheets['Diagnostic Settings'] = $DiagObj) } else { $NoDiagObj = [System.Collections.ArrayList]::new() $noDiagInObj = [ordered] @{ 'Diagnostic Settings Configured' = '[FAIL] No' 'Sign-In Logs Export' = '[FAIL] Not configured' 'Audit Logs Export' = '[FAIL] Not configured' 'Recommendation' = 'Configure Diagnostic Settings to export SignInLogs, AuditLogs, NonInteractiveUserSignInLogs, and ServicePrincipalSignInLogs to a Log Analytics workspace or SIEM (Microsoft Sentinel, Splunk, etc.)' } $NoDiagObj.Add([pscustomobject]$noDiagInObj) | Out-Null $null = (& { if ($HealthCheck.EntraID.ConditionalAccess) { $null = ($NoDiagObj | Set-Style -Style Critical | Out-Null) }}) $NoDiagTableParams = @{ Name = "Diagnostic Settings - $TenantId"; List = $true; ColumnWidths = 35, 65 } if ($Report.ShowTableCaptions) { $NoDiagTableParams['Caption'] = "- $($NoDiagTableParams.Name)" } $NoDiagObj | Table @NoDiagTableParams $null = ($script:ExcelSheets['Diagnostic Settings'] = $NoDiagObj) } #region ACSC E8 + CIS Diagnostics Assessment BlankLine $HasDiag = ($DiagResp -and $DiagResp.value -and @($DiagResp.value).Count -gt 0) $SignInExported = $HasDiag -and ($DiagResp.value.properties.logs | Where-Object { $_.category -eq 'SignInLogs' -and $_.enabled }) $AuditExported = $HasDiag -and ($DiagResp.value.properties.logs | Where-Object { $_.category -eq 'AuditLogs' -and $_.enabled }) if ($script:IncludeACSCe8) { Paragraph "ACSC Essential Eight Maturity Level Assessment -- Audit Logging:" BlankLine #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json) $_ComplianceVars = @{ 'SignInExported' = $SignInExported 'AuditExported' = $AuditExported 'SignInLogDetail' = $SignInLogDetail 'AuditLogDetail' = $AuditLogDetail 'SignInConfigDetail' = $SignInConfigDetail 'AuditConfigDetail' = $AuditConfigDetail } $E8DiagChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'Diagnostics') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8DiagChecks -Name 'Diagnostic Settings' -TenantId $TenantId # Consolidated into ACSC E8 Assessment sheet if ($E8DiagChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8DiagChecks | Select-Object @{N='Section';E={'Diagnostics'}}, ML, Control, Status, Detail ))) } #endregion } if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Diagnostic Settings:" BlankLine #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json) $CISDiagChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'Diagnostics') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISDiagChecks -Name 'Diagnostic Settings' -TenantId $TenantId # Consolidated into CIS Assessment sheet if ($CISDiagChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISDiagChecks | Select-Object @{N='Section';E={'Diagnostics'}}, CISControl, Level, Status, Detail ))) } #endregion } #endregion } catch { # Azure Monitor API requires Monitoring Reader RBAC on the subscription. # Show a clear advisory table rather than a raw error message. $errMsg = $_.Exception.Message $IsUnauthorized = $errMsg -like '*Unauthorized*' -or $errMsg -like '*Forbidden*' -or $errMsg -like '*403*' -or $errMsg -like '*401*' $NotAvailObj = [System.Collections.ArrayList]::new() $null = $NotAvailObj.Add([pscustomobject][ordered]@{ 'Item' = 'Diagnostic Settings' 'Status' = if ($IsUnauthorized) { '[NOT AVAILABLE]' } else { '[ERROR]' } 'Detail' = if ($IsUnauthorized) { 'Azure Monitor API access is required to read Diagnostic Settings. ' + 'Assign the Monitoring Reader role on the Azure subscription to the account running this report, ' + 'then re-run to see log export configuration. ' + 'Without log export to Log Analytics or a SIEM, security events cannot be detected or investigated.' } else { $errMsg } }) $null = (& { if ($HealthCheck.EntraID.ConditionalAccess) { $null = ($NotAvailObj | Set-Style -Style Warning | Out-Null) }}) $NotAvailTableParams = @{ Name = "Diagnostic Settings - $TenantId"; List = $false; ColumnWidths = 22, 12, 66 } if ($Report.ShowTableCaptions) { $NotAvailTableParams['Caption'] = "- $($NotAvailTableParams.Name)" } $NotAvailObj | Table @NotAvailTableParams Write-AbrDebugLog "Diagnostic Settings unavailable: $errMsg" 'WARN' 'DIAGNOSTICS' } } # end Section Diagnostics } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Diagnostics' } } |