Src/Private/Get-AbrExoAlerting.ps1

function Get-AbrExoAlerting {
    <#
    .SYNOPSIS
    Documents Exchange Online and Defender for Office 365 alert policies,
    notification recipients, and operational monitoring configuration.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Exchange Online Alerting configuration for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Alerting'
    }

    process {
        Section -Style Heading2 'Alert Policies' {
            Paragraph "The following section documents alert policies configured in Microsoft 365 Defender and Purview compliance for tenant $TenantId."
            BlankLine

            #region Protection Alert Policies
            try {
                Write-Host " - Retrieving protection alert policies..."
                $AlertPolicies = Get-ProtectionAlert -ErrorAction Stop | Sort-Object Severity, Name

                if ($AlertPolicies -and @($AlertPolicies).Count -gt 0) {
                    # Split into enabled vs disabled
                    $EnabledAlerts  = @($AlertPolicies | Where-Object { $_.Disabled -ne $true })
                    $DisabledAlerts = @($AlertPolicies | Where-Object { $_.Disabled -eq $true })
                    # IsSystemPolicy is unreliable in REST API (always null/false).
                    # Identify system/built-in policies by IsDefault flag or known Microsoft policy name patterns.
                    $SystemAlerts   = @($AlertPolicies | Where-Object {
                        $_.IsDefault -eq $true -or $_.IsSystemPolicy -eq $true -or
                        $_.Name -match '^(Malware|Phish|Suspicious|Messages have been|Reply-all|User restricted|Potential Nation|A potentially|A user clicked|Form blocked|Form flagged|DLP-|Failed exact|Noisy Alert)'
                    })
                    $CustomAlerts   = @($AlertPolicies | Where-Object {
                        $_.IsDefault -ne $true -and $_.IsSystemPolicy -ne $true -and
                        $_.Name -notmatch '^(Malware|Phish|Suspicious|Messages have been|Reply-all|User restricted|Potential Nation|A potentially|A user clicked|Form blocked|Form flagged|DLP-|Failed exact|Noisy Alert)'
                    })

                    Section -Style Heading3 'Alert Policy Summary' {
                        Paragraph "$(@($AlertPolicies).Count) alert policy/policies configured. $($EnabledAlerts.Count) enabled, $($DisabledAlerts.Count) disabled. $($SystemAlerts.Count) are built-in Microsoft policies and $($CustomAlerts.Count) are custom."
                        BlankLine

                        $SumObj = [System.Collections.ArrayList]::new()
                        $sumInObj = [ordered] @{
                            'Total Alert Policies'      = @($AlertPolicies).Count
                            'Enabled Policies'          = $EnabledAlerts.Count
                            'Disabled Policies'         = $DisabledAlerts.Count
                            'System (Built-in) Policies'= $SystemAlerts.Count
                            'Custom Policies'           = $CustomAlerts.Count
                        }
                        $SumObj.Add([pscustomobject]$sumInObj) | Out-Null
                        $SumTableParams = @{ Name = "Alert Policy Summary - $TenantId"; List = $true; ColumnWidths = 50, 50 }
                        if ($Report.ShowTableCaptions) { $SumTableParams['Caption'] = "- $($SumTableParams.Name)" }
                        $SumObj | Table @SumTableParams
                    }

                    # High + Medium severity policies
                    $HighMedPolicies = $AlertPolicies | Where-Object {
                        $_.Severity -in @('High', 'Medium') -and $_.Disabled -ne $true
                    } | Sort-Object Severity, Name

                    if (@($HighMedPolicies).Count -gt 0) {
                        Section -Style Heading3 'High & Medium Severity Alerts (Enabled)' {
                            Paragraph "The following $(@($HighMedPolicies).Count) high and medium severity alert policies are currently enabled."
                            BlankLine

                            $HmObj = [System.Collections.ArrayList]::new()
                            foreach ($Policy in $HighMedPolicies) {
                                $NotifyCount = if ($Policy.NotifyUser) { @($Policy.NotifyUser).Count } else { 0 }
                                $hmInObj = [ordered] @{
                                    'Alert Name'            = $Policy.Name
                                    'Severity'              = $Policy.Severity
                                    'Category'              = $Policy.Category
                                    'Notify Recipients'     = $NotifyCount
                                    'System Policy'         = $Policy.IsSystemPolicy
                                    'Threshold (count)'     = if ($Policy.Threshold) { $Policy.Threshold } else { 'N/A' }
                                }
                                $HmObj.Add([pscustomobject](ConvertTo-HashToYN $hmInObj)) | Out-Null
                            }

                            $null = (& {
                                if ($HealthCheck.ExchangeOnline.AuditLogging) {
                                    $null = ($HmObj | Where-Object { [int]"$($_.'Notify Recipients')" -eq 0 } | Set-Style -Style Warning | Out-Null)
                                }
                            })

                            $HmTableParams = @{ Name = "High-Med Severity Alerts - $TenantId"; List = $false; ColumnWidths = 28, 10, 16, 12, 12, 12, 10 }
                            if ($Report.ShowTableCaptions) { $HmTableParams['Caption'] = "- $($HmTableParams.Name)" }
                            if ($HmObj.Count -gt 0) { $HmObj | Table @HmTableParams }
                            $script:ExcelSheets['High-Med Alerts'] = $HmObj
                        }
                    }

                    # Disabled high-severity alerts (risk)
                    $DisabledHighAlerts = $AlertPolicies | Where-Object {
                        $_.Severity -eq 'High' -and $_.Disabled -eq $true
                    }

                    if (@($DisabledHighAlerts).Count -gt 0) {
                        Section -Style Heading3 'Disabled High-Severity Alerts' {
                            Paragraph "WARNING: The following $(@($DisabledHighAlerts).Count) high-severity alert policy/policies are currently disabled. Re-enable these to ensure critical security events are detected."
                            BlankLine

                            $DhObj = [System.Collections.ArrayList]::new()
                            foreach ($Policy in $DisabledHighAlerts) {
                                $dhInObj = [ordered] @{
                                    'Alert Name'    = $Policy.Name
                                    'Category'      = $Policy.Category
                                    'System Policy' = $Policy.IsSystemPolicy
                                }
                                $DhObj.Add([pscustomobject](ConvertTo-HashToYN $dhInObj)) | Out-Null
                            }

                            $null = (& {
                                if ($HealthCheck.ExchangeOnline.AuditLogging) {
                                    $null = ($DhObj | Set-Style -Style Critical | Out-Null)
                                }
                            })

                            $DhTableParams = @{ Name = "Disabled High-Severity Alerts - $TenantId"; List = $false; ColumnWidths = 55, 25, 20 }
                            if ($Report.ShowTableCaptions) { $DhTableParams['Caption'] = "- $($DhTableParams.Name)" }
                            if ($DhObj.Count -gt 0) { $DhObj | Table @DhTableParams }
                            $script:ExcelSheets['Disabled High Alerts'] = $DhObj
                        }
                    }

                    # All alerts at InfoLevel 2
                    if ($InfoLevel.Alerting -ge 2) {
                        Section -Style Heading3 'All Alert Policies' {
                            Paragraph "Complete inventory of all $(@($AlertPolicies).Count) alert policy/policies."
                            BlankLine

                            $AllAlObj = [System.Collections.ArrayList]::new()
                            foreach ($Policy in $AlertPolicies) {
                                $NotifyCount = if ($Policy.NotifyUser) { @($Policy.NotifyUser).Count } else { 0 }
                                $allAlInObj = [ordered] @{
                                    'Alert Name'        = $Policy.Name
                                    'Severity'          = $Policy.Severity
                                    'Category'          = $Policy.Category
                                    'Enabled'           = -not $Policy.Disabled
                                    'Notify Recipients' = $NotifyCount
                                    'System Policy'     = $Policy.IsSystemPolicy
                                }
                                $AllAlObj.Add([pscustomobject](ConvertTo-HashToYN $allAlInObj)) | Out-Null
                            }

                            $AllAlTableParams = @{ Name = "All Alert Policies - $TenantId"; List = $false; ColumnWidths = 34, 10, 16, 10, 12, 10, 8 }
                            if ($Report.ShowTableCaptions) { $AllAlTableParams['Caption'] = "- $($AllAlTableParams.Name)" }
                            if ($AllAlObj.Count -gt 0) { $AllAlObj | Table @AllAlTableParams }
                            $script:ExcelSheets['All Alert Policies'] = $AllAlObj
                        }
                    }
                } else {
                    Paragraph "No protection alert policies found in tenant $TenantId. Verify that the account has Security Reader or higher permissions."
                }
            } catch {
                Write-ExoError 'Alerting' "Unable to retrieve alert policies: $($_.Exception.Message)"
                Paragraph "Unable to retrieve alert policies. Verify the Security and Compliance PowerShell session was established successfully during connection."
            }
            #endregion

            #region Mail Flow Alert Policies (best effort via Get-MailDetailTransportRuleReport)
            Section -Style Heading3 'Monitoring Recommendations' {
                Paragraph "The following monitoring capabilities should be configured for Exchange Online in tenant ${TenantId}:"
                BlankLine

                $MonObj = [System.Collections.ArrayList]::new()
                $monRows = @(
                    [pscustomobject]@{ 'Monitoring Area'='Malware detected in messages'; 'Recommended Alert'='Malware campaign detected after delivery'; 'Priority'='High'; 'Source'='Microsoft Defender for Office 365' }
                    [pscustomobject]@{ 'Monitoring Area'='Phishing clicks by users'; 'Recommended Alert'='User clicked a potentially malicious URL'; 'Priority'='High'; 'Source'='Microsoft Defender for Office 365' }
                    [pscustomobject]@{ 'Monitoring Area'='Account compromise / suspicious forwarding'; 'Recommended Alert'='Email forwarding rules created'; 'Priority'='High'; 'Source'='Microsoft 365 Defender' }
                    [pscustomobject]@{ 'Monitoring Area'='Admin activity'; 'Recommended Alert'='Exchange transport rule created'; 'Priority'='Medium'; 'Source'='Microsoft 365 Compliance' }
                    [pscustomobject]@{ 'Monitoring Area'='Mail flow degradation'; 'Recommended Alert'='Messages have been delayed'; 'Priority'='Medium'; 'Source'='Exchange Online (built-in)' }
                    [pscustomobject]@{ 'Monitoring Area'='Outbound spam'; 'Recommended Alert'='User restricted from sending email'; 'Priority'='High'; 'Source'='Exchange Online Protection' }
                )
                foreach ($row in $monRows) { $MonObj.Add($row) | Out-Null }

                $MonTableParams = @{ Name = "Monitoring Recommendations - $TenantId"; List = $false; ColumnWidths = 25, 32, 10, 33 }
                if ($Report.ShowTableCaptions) { $MonTableParams['Caption'] = "- $($MonTableParams.Name)" }
                if ($MonObj.Count -gt 0) { $MonObj | Table @MonTableParams }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Alerting'
    }
}