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' }
}