Src/Private/Get-AbrExoTransportRules.ps1

function Get-AbrExoTransportRules {
    <#
    .SYNOPSIS
    Documents Exchange Online transport rules (mail flow rules) with ACSC E8 and CIS compliance assessments.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

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

    process {
        Section -Style Heading2 'Transport Rules' {
            Paragraph "The following section documents the transport rules configured in tenant $TenantId."
            BlankLine

            $RulesBypassingSpamCount     = 0
            $ExternalForwardingRuleCount = 0

            try {
                Write-Host " - Retrieving transport rules..."
                $TransportRules = Get-TransportRule -ErrorAction Stop | Sort-Object Priority

                $TotalRules = @($TransportRules).Count

                if ($TransportRules) {
                    # Identify risk categories
                    $SpamBypassRules        = @($TransportRules | Where-Object { $_.SetSCL -eq -1 })
                    $ExternalForwardRules   = @($TransportRules | Where-Object {
                        $_.RedirectMessageTo -or ($_.AddToRecipients -and $_.BlindCopyTo)
                    } | Where-Object { $_.RouteMessageOutboundRequiresTls -ne $true -or $_.RouteMessageOutboundConnector })

                    # Narrow to rules that actually forward externally
                    $ExternalForwardRules   = @($TransportRules | Where-Object {
                        ($_.RedirectMessageTo -join ',') -notmatch $script:TenantDomain -and
                        ($_.RedirectMessageTo.Count -gt 0)
                    })

                    $RulesBypassingSpamCount     = $SpamBypassRules.Count
                    $ExternalForwardingRuleCount = $ExternalForwardRules.Count

                    Paragraph "Tenant $TenantId has $TotalRules transport rule(s) configured. $RulesBypassingSpamCount rule(s) bypass spam filtering (SCL -1). $ExternalForwardingRuleCount rule(s) redirect email to external destinations."
                    BlankLine

                    #region Transport Rule Summary Table
                    $RuleObj = [System.Collections.ArrayList]::new()
                    foreach ($Rule in $TransportRules) {
                        # Derive a human-readable action summary
                        $ActionSummary = [System.Collections.ArrayList]::new()
                        if ($Rule.SetSCL -eq -1)                         { $null = $ActionSummary.Add('Bypass Spam (SCL -1)') }
                        if ($Rule.DeleteMessage)                           { $null = $ActionSummary.Add('Delete Message') }
                        if ($Rule.RejectMessageEnhancedStatusCode)         { $null = $ActionSummary.Add('Reject') }
                        if ($Rule.RedirectMessageTo)                       { $null = $ActionSummary.Add("Redirect to $($Rule.RedirectMessageTo -join ', ')") }
                        if ($Rule.BlindCopyTo)                             { $null = $ActionSummary.Add("BCC $($Rule.BlindCopyTo -join ', ')") }
                        if ($Rule.AddToRecipients)                         { $null = $ActionSummary.Add("Add Recipient") }
                        if ($Rule.PrependSubject)                          { $null = $ActionSummary.Add('Prepend Subject') }
                        if ($Rule.ApplyHtmlDisclaimerText)                 { $null = $ActionSummary.Add('Apply Disclaimer') }
                        if ($Rule.SetHeaderName)                           { $null = $ActionSummary.Add("Set Header $($Rule.SetHeaderName)") }
                        if ($Rule.ApplyOME)                                { $null = $ActionSummary.Add('Apply OME Encryption') }
                        if ($Rule.RouteMessageOutboundConnector)           { $null = $ActionSummary.Add("Route via Connector: $($Rule.RouteMessageOutboundConnector)") }
                        if ($ActionSummary.Count -eq 0)                   { $null = $ActionSummary.Add('Other') }

                        $ruleInObj = [ordered] @{
                            'Priority'         = $Rule.Priority
                            'Rule Name'        = $Rule.Name
                            'State'            = $Rule.State
                            'Mode'             = $Rule.Mode
                            'Action Summary'   = $ActionSummary -join '; '
                            'Comments'         = if ($Rule.Comments) { $Rule.Comments } else { '--' }
                        }
                        $RuleObj.Add([pscustomobject]$ruleInObj) | Out-Null
                    }

                    $null = (& {
                        if ($HealthCheck.ExchangeOnline.TransportRules) {
                            $null = ($RuleObj | Where-Object { $_.'Action Summary' -like '*Bypass Spam*' } | Set-Style -Style Warning | Out-Null)
                            $null = ($RuleObj | Where-Object { $_.'Action Summary' -like '*Redirect*' -and $_.'Action Summary' -notlike "*$($script:TenantDomain)*" } | Set-Style -Style Critical | Out-Null)
                            $null = ($RuleObj | Where-Object { $_.State -eq 'Disabled' } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $RuleTableParams = @{ Name = "Transport Rules - $TenantId"; List = $false; ColumnWidths = 6, 22, 8, 8, 40, 16 }
                    if ($Report.ShowTableCaptions) { $RuleTableParams['Caption'] = "- $($RuleTableParams.Name)" }
                    $RuleObj | Table @RuleTableParams

                    $script:ExcelSheets['Transport Rules'] = $RuleObj
                    #endregion

                    #region Spam Bypass Rules Detail
                    if ($SpamBypassRules.Count -gt 0) {
                        Section -Style Heading3 'Rules Bypassing Spam Filtering (SCL -1)' {
                            Paragraph "WARNING: The following $($SpamBypassRules.Count) rule(s) set the spam confidence level to -1, bypassing all anti-spam filtering. These should be reviewed carefully."
                            BlankLine

                            $BypassObj = [System.Collections.ArrayList]::new()
                            foreach ($Rule in $SpamBypassRules) {
                                $condSummary = [System.Collections.ArrayList]::new()
                                if ($Rule.From)                   { $null = $condSummary.Add("From: $($Rule.From -join ', ')") }
                                if ($Rule.FromMemberOf)           { $null = $condSummary.Add("From Group: $($Rule.FromMemberOf -join ', ')") }
                                if ($Rule.SenderDomainIs)         { $null = $condSummary.Add("Sender Domain: $($Rule.SenderDomainIs -join ', ')") }
                                if ($Rule.ReceivedFromIPRanges)   { $null = $condSummary.Add("IP Range: $($Rule.ReceivedFromIPRanges -join ', ')") }
                                if ($condSummary.Count -eq 0)     { $null = $condSummary.Add('All Messages (NO CONDITION -- HIGH RISK)') }

                                $bypassInObj = [ordered] @{
                                    'Rule Name'     = $Rule.Name
                                    'Priority'      = $Rule.Priority
                                    'State'         = $Rule.State
                                    'Condition(s)'  = $condSummary -join '; '
                                }
                                $BypassObj.Add([pscustomobject]$bypassInObj) | Out-Null
                            }

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

                            $BypassTableParams = @{ Name = "Spam Bypass Rules - $TenantId"; List = $false; ColumnWidths = 30, 8, 8, 54 }
                            if ($Report.ShowTableCaptions) { $BypassTableParams['Caption'] = "- $($BypassTableParams.Name)" }
                            $BypassObj | Table @BypassTableParams

                            $script:ExcelSheets['Spam Bypass Rules'] = $BypassObj
                        }
                    }
                    #endregion

                    #region External Forwarding Rules Detail
                    if ($ExternalForwardRules.Count -gt 0) {
                        Section -Style Heading3 'Rules Redirecting to External Recipients' {
                            Paragraph "WARNING: The following $($ExternalForwardRules.Count) rule(s) redirect or forward email to external recipients. These represent potential data exfiltration paths."
                            BlankLine

                            $FwdRuleObj = [System.Collections.ArrayList]::new()
                            foreach ($Rule in $ExternalForwardRules) {
                                $fwdRuleInObj = [ordered] @{
                                    'Rule Name'       = $Rule.Name
                                    'Priority'        = $Rule.Priority
                                    'State'           = $Rule.State
                                    'Redirect To'     = $Rule.RedirectMessageTo -join ', '
                                }
                                $FwdRuleObj.Add([pscustomobject]$fwdRuleInObj) | Out-Null
                            }

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

                            $FwdRuleTableParams = @{ Name = "External Forwarding Rules - $TenantId"; List = $false; ColumnWidths = 35, 8, 8, 49 }
                            if ($Report.ShowTableCaptions) { $FwdRuleTableParams['Caption'] = "- $($FwdRuleTableParams.Name)" }
                            $FwdRuleObj | Table @FwdRuleTableParams

                            $script:ExcelSheets['External Forward Rules'] = $FwdRuleObj
                        }
                    }
                    #endregion
                } else {
                    Paragraph "No transport rules are currently configured in tenant $TenantId."
                }
            } catch {
                Write-ExoError 'TransportRules' "Unable to retrieve transport rules: $($_.Exception.Message)"
                Paragraph "Unable to retrieve transport rule data: $($_.Exception.Message)"
            }


            BlankLine
            Paragraph "ACSC Essential Eight Assessment"
            BlankLine
            $e8Vars = @{
                RulesBypassingSpamCount     = $RulesBypassingSpamCount
                ExternalForwardingRuleCount = $ExternalForwardingRuleCount
            }
            $e8Checks = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoE8Checks 'TransportRules') -Framework 'E8' -CallerVariables $e8Vars
            New-AbrExoE8AssessmentTable -Checks $e8Checks -Name 'Transport Rules' -TenantId $TenantId
            if ($e8Checks) {
                foreach ($row in $e8Checks) {
                    $null = $script:E8AllChecks.Add([pscustomobject]@{
                        Section = 'Transport Rules'
                        ML      = $row.ML
                        Control = $row.Control
                        Status  = $row.Status
                        Detail  = $row.Detail
                    })
                }
            }

            BlankLine
            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment"
            BlankLine
            $cisVars = @{
                RulesBypassingSpamCount     = $RulesBypassingSpamCount
                ExternalForwardingRuleCount = $ExternalForwardingRuleCount
            }
            $cisChecks = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoCISChecks 'TransportRules') -Framework 'CIS' -CallerVariables $cisVars
            New-AbrExoCISAssessmentTable -Checks $cisChecks -Name 'Transport Rules' -TenantId $TenantId
            if ($cisChecks) {
                foreach ($row in $cisChecks) {
                    $null = $script:CISAllChecks.Add([pscustomobject]@{
                        Section    = 'Transport Rules'
                        CISControl = $row.CISControl
                        Level      = $row.Level
                        Status     = $row.Status
                        Detail     = $row.Detail
                    })
                }
            }
        }

    }



    end {

        Show-AbrDebugExecutionTime -End -TitleMessage 'TransportRules'

    }

}