Src/Private/Get-AbrExoExternalSharing.ps1

function Get-AbrExoExternalSharing {
    <#
    .SYNOPSIS
    Documents Exchange Online external sharing and collaboration controls:
    sharing policies, federation, external email tagging, auto-forwarding controls,
    and external recipient restrictions.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

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

    process {
        Section -Style Heading2 'External Controls' {
            Paragraph "The following section documents how tenant $TenantId controls email sharing, calendar/free-busy federation, external recipient access, and auto-forwarding policies."
            BlankLine

            #region External Email Tagging
            try {
                Write-Host " - Retrieving external email tagging configuration..."
                # Get-ExternalInOutlook is available in EXO v3 REST mode
                $ExternalTagging = Get-ExternalInOutlook -ErrorAction Stop

                Section -Style Heading3 'External Email Tagging' {
                    Paragraph "External email tagging displays a visible label on messages received from outside the organisation, helping users identify potential phishing or social engineering attempts."
                    BlankLine

                    $EtObj = [System.Collections.ArrayList]::new()
                    $etInObj = [ordered] @{
                        'External Tagging Enabled'  = $ExternalTagging.Enabled
                        'Allowed Senders (bypass)'  = if ($ExternalTagging.AllowList -and $ExternalTagging.AllowList.Count -gt 0) {
                                                          $ExternalTagging.AllowList -join ', '
                                                      } else { 'None (all external senders tagged)' }
                    }
                    $EtObj.Add([pscustomobject](ConvertTo-HashToYN $etInObj)) | Out-Null

                    $null = (& {
                        if ($HealthCheck.ExchangeOnline.AntiPhishing) {
                            $null = ($EtObj | Where-Object { $_.'External Tagging Enabled' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $EtTableParams = @{ Name = "External Email Tagging - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                    if ($Report.ShowTableCaptions) { $EtTableParams['Caption'] = "- $($EtTableParams.Name)" }
                    $EtObj | Table @EtTableParams
                }
            } catch {
                Write-ExoError 'ExternalSharing' "Unable to retrieve external email tagging: $($_.Exception.Message)"
                Section -Style Heading3 'External Email Tagging' {
                    Paragraph "External email tagging status could not be retrieved: $($_.Exception.Message)"
                }
            }
            #endregion

            #region Auto-Forwarding Controls (cross-reference from Remote Domains + Outbound Spam)
            try {
                Write-Host " - Reviewing auto-forwarding controls..."
                $RemoteDomains = Get-RemoteDomain -ErrorAction Stop
                $DefaultRemoteDomain = $RemoteDomains | Where-Object { $_.DomainName -eq '*' } | Select-Object -First 1

                $OutboundSpam = Get-HostedOutboundSpamFilterPolicy -ErrorAction SilentlyContinue |
                    Where-Object { $_.IsDefault } | Select-Object -First 1

                Section -Style Heading3 'Auto-Forwarding Controls' {
                    Paragraph "Auto-forwarding of email to external recipients is a significant data exfiltration risk. Exchange Online provides controls at the remote domain level and via outbound spam policy."
                    BlankLine

                    $AfObj = [System.Collections.ArrayList]::new()
                    $afInObj = [ordered] @{
                        'Default Remote Domain Auto-Forward'        = if ($DefaultRemoteDomain) { $DefaultRemoteDomain.AutoForwardEnabled } else { 'Unknown' }
                        'Outbound Spam Auto-Forward Mode'           = if ($OutboundSpam) { $OutboundSpam.AutoForwardingMode } else { 'Unknown' }
                        'Risk Assessment'                           = $(
                            $dfEnabled = ($DefaultRemoteDomain.AutoForwardEnabled -eq $true)
                            $spamMode  = if ($OutboundSpam) { $OutboundSpam.AutoForwardingMode } else { 'Automatic' }
                            if ($dfEnabled -and $spamMode -eq 'On') { 'HIGH RISK: Both controls allow unrestricted forwarding.' }
                            elseif ($dfEnabled -or $spamMode -ne 'Off') { 'MEDIUM RISK: Auto-forwarding partially restricted.' }
                            else { 'LOW RISK: Auto-forwarding blocked at domain and spam policy level.' }
                        )
                    }
                    $AfObj.Add([pscustomobject](ConvertTo-HashToYN $afInObj)) | Out-Null

                    $null = (& {
                        if ($HealthCheck.ExchangeOnline.TransportRules) {
                            $null = ($AfObj | Where-Object { $_.'Default Remote Domain Auto-Forward' -eq 'Yes' } | Set-Style -Style Critical | Out-Null)
                            $null = ($AfObj | Where-Object { $_.'Outbound Spam Auto-Forward Mode' -eq 'On' } | Set-Style -Style Critical | Out-Null)
                            $null = ($AfObj | Where-Object { $_.'Outbound Spam Auto-Forward Mode' -eq 'Automatic' } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $AfTableParams = @{ Name = "Auto-Forwarding Controls - $TenantId"; List = $true; ColumnWidths = 45, 55 }
                    if ($Report.ShowTableCaptions) { $AfTableParams['Caption'] = "- $($AfTableParams.Name)" }
                    $AfObj | Table @AfTableParams
                }
            } catch {
                Write-ExoError 'ExternalSharing' "Unable to retrieve auto-forwarding control data: $($_.Exception.Message)"
            }
            #endregion

            #region Sharing Policies (Calendar / Free-Busy)
            try {
                Write-Host " - Retrieving sharing policies..."
                $SharingPolicies = Get-SharingPolicy -ErrorAction Stop | Sort-Object Default -Descending

                if ($SharingPolicies -and @($SharingPolicies).Count -gt 0) {
                    Section -Style Heading3 'Calendar & Free-Busy Sharing Policies' {
                        Paragraph "Sharing policies control how users can share calendar and free-busy information with external recipients. $(@($SharingPolicies).Count) policy/policies are configured."
                        BlankLine

                        $SpObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $SharingPolicies) {
                            # EXO REST returns Domains as strings ("domain.com:CalendarSharingFreeBusySimple")
                            # or as objects with .Domains and .SharingLevel properties depending on version
                            $Domains = 'None'
                            if ($Policy.Domains -and @($Policy.Domains).Count -gt 0) {
                                $DomainParts = @($Policy.Domains) | ForEach-Object {
                                    $d = "$_"
                                    if ($d -match ':') { $d }  # already "domain:level" string format
                                    elseif ($_.Domain -and $_.SharingLevel) { "$($_.Domain):$($_.SharingLevel)" }
                                    else { $d }
                                }
                                $Domains = ($DomainParts | Where-Object { $_ -ne '' }) -join '; '
                                if (-not $Domains) { $Domains = 'None' }
                            }

                            $spInObj = [ordered] @{
                                'Policy Name'       = $Policy.Name
                                'Enabled'           = $Policy.Enabled
                                'Default Policy'    = $Policy.Default
                                'Domain Sharing'    = $Domains
                            }
                            $SpObj.Add([pscustomobject](ConvertTo-HashToYN $spInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                $null = ($SpObj | Where-Object { $_.Enabled -eq 'Yes' -and $_.'Domain Sharing' -like '*CalendarSharingFreeBusyDetail*' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $SpTableParams = @{ Name = "Sharing Policies - $TenantId"; List = $false; ColumnWidths = 22, 10, 12, 56 }
                        if ($Report.ShowTableCaptions) { $SpTableParams['Caption'] = "- $($SpTableParams.Name)" }
                        if ($SpObj.Count -gt 0) { $SpObj | Table @SpTableParams }
                        $script:ExcelSheets['Sharing Policies'] = $SpObj
                    }
                }
            } catch {
                Write-ExoError 'ExternalSharing' "Unable to retrieve sharing policies: $($_.Exception.Message)"
            }
            #endregion

            #region Federation Information
            try {
                Write-Host " - Retrieving federation configuration..."
                $FedInfo = Get-FederationInformation -DomainName $script:TenantDomain -ErrorAction SilentlyContinue
                $FedTrust = Get-FederationTrust -ErrorAction SilentlyContinue

                Section -Style Heading3 'Federation Configuration' {
                    Paragraph "Federation allows Exchange Online to share free-busy and calendar data with other Microsoft 365 tenants or on-premises Exchange organisations without a full trust relationship."
                    BlankLine

                    $FedObj = [System.Collections.ArrayList]::new()
                    $fedInObj = [ordered] @{
                        'Federation Trust Configured'   = ($FedTrust -ne $null -and @($FedTrust).Count -gt 0)
                        'Federation Trust Name'         = if ($FedTrust) { ($FedTrust | Select-Object -First 1).Name } else { 'None' }
                        'Application URI'               = if ($FedTrust) { ($FedTrust | Select-Object -First 1).ApplicationUri } else { 'None' }
                        'Federated Domains'             = if ($FedInfo -and $FedInfo.DomainNames) { $FedInfo.DomainNames -join ', ' } else { 'None detected' }
                    }
                    $FedObj.Add([pscustomobject](ConvertTo-HashToYN $fedInObj)) | Out-Null

                    $FedTableParams = @{ Name = "Federation Configuration - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                    if ($Report.ShowTableCaptions) { $FedTableParams['Caption'] = "- $($FedTableParams.Name)" }
                    $FedObj | Table @FedTableParams
                }
            } catch {
                Write-ExoError 'ExternalSharing' "Unable to retrieve federation configuration: $($_.Exception.Message)"
            }
            #endregion

            #region Distribution Group External Restrictions
            try {
                Write-Host " - Checking distribution group external sender restrictions..."
                $DGsOpenToExternal = Get-DistributionGroup -ResultSize Unlimited -Filter {
                    RequireSenderAuthenticationEnabled -eq $false
                } -ErrorAction SilentlyContinue | Sort-Object Name

                Section -Style Heading3 'Distribution Groups Open to External Senders' {
                    if ($DGsOpenToExternal -and @($DGsOpenToExternal).Count -gt 0) {
                        Paragraph "WARNING: The following $(@($DGsOpenToExternal).Count) distribution group(s) do not require sender authentication, meaning external senders can send to them directly. Review whether this is intentional."
                        BlankLine

                        $DgExtObj = [System.Collections.ArrayList]::new()
                        foreach ($Grp in $DGsOpenToExternal) {
                            $dgExtInObj = [ordered] @{
                                'Display Name'          = $Grp.DisplayName
                                'Primary SMTP'          = $Grp.PrimarySmtpAddress
                                'Group Type'            = $Grp.RecipientTypeDetails
                                'Moderated'             = $Grp.ModerationEnabled
                                'Hidden from GAL'       = $Grp.HiddenFromAddressListsEnabled
                            }
                            $DgExtObj.Add([pscustomobject](ConvertTo-HashToYN $dgExtInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.AntiSpam) {
                                $null = ($DgExtObj | Where-Object { $_.Moderated -eq 'No' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $DgExtTableParams = @{ Name = "DGs Open to External Senders - $TenantId"; List = $false; ColumnWidths = 25, 28, 18, 15, 14 }
                        if ($Report.ShowTableCaptions) { $DgExtTableParams['Caption'] = "- $($DgExtTableParams.Name)" }
                        if ($DgExtObj.Count -gt 0) { $DgExtObj | Table @DgExtTableParams }
                        $script:ExcelSheets['DGs Open to External'] = $DgExtObj
                    } else {
                        Paragraph "All distribution groups require sender authentication. External senders cannot directly email any distribution group."
                    }
                }
            } catch {
                Write-ExoError 'ExternalSharing' "Unable to retrieve distribution group external settings: $($_.Exception.Message)"
            }
            #endregion
        }
    }

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