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