Public/Get-TntInboxForwardingRuleReport.ps1
|
function Get-TntInboxForwardingRuleReport { <# .SYNOPSIS Reports on inbox forwarding rules targeting external addresses. .DESCRIPTION Retrieves all user mailboxes and checks inbox rules for forwarding to external recipients. External addresses are determined by comparing against the tenant's accepted domains. .PARAMETER TenantId The Azure AD Tenant ID (GUID) to connect to. .PARAMETER ClientId The Application (Client) ID of the app registration created for security reporting. .PARAMETER ClientSecret The client secret for the app registration. Use this for automated scenarios. .PARAMETER CertificateThumbprint The thumbprint of the certificate to use for authentication instead of client secret. .EXAMPLE Get-TntInboxForwardingRuleReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret Checks all user mailboxes for external forwarding rules. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject Returns a structured object containing: - Summary: Total rules checked, external forwards found - ForwardingRules: Detailed per-rule information .NOTES Author: Tom de Leeuw Website: https://systom.dev Module: TenantReports Required Permissions: - Exchange Online app access .LINK https://systom.dev #> [CmdletBinding(DefaultParameterSetName = 'ClientSecret')] [OutputType([System.Management.Automation.PSCustomObject])] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')] [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')] [ValidateNotNullOrEmpty()] [Alias('Tenant')] [string]$TenantId, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')] [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')] [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')] [Alias('ApplicationId')] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Secret', 'ApplicationSecret')] [SecureString]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Thumbprint')] [string]$CertificateThumbprint, # Use interactive authentication (no app registration required). [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')] [switch]$Interactive ) begin { Write-Information 'Starting inbox forwarding rule analysis... (this may take a while)' -InformationAction Continue } process { try { $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters $ConnectionInfo = Connect-TntGraphSession @ConnectionParams # Connect to Exchange Online try { if ($PSCmdlet.ParameterSetName -eq 'ClientSecret') { $TokenParams = @{ TenantId = $TenantId ClientId = $ClientId ClientSecret = $ClientSecret Scope = 'Exchange' } $ExchangeToken = Get-GraphToken @TokenParams Connect-ExchangeOnline -Organization $TenantId -AccessToken $ExchangeToken.AccessToken -ShowBanner:$false -ErrorAction Stop } else { Connect-ExchangeOnline -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -Organization $TenantId -ShowBanner:$false -ErrorAction Stop } Write-Verbose 'Successfully connected to Exchange Online.' } catch { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Exchange Online connection required: $($_.Exception.Message)"), 'ExchangeConnectionError', [System.Management.Automation.ErrorCategory]::ConnectionError, $null )) } $ForwardingRules = [System.Collections.Generic.List[PSCustomObject]]::new() # Get accepted domains to determine internal vs external Write-Verbose 'Retrieving accepted domains...' $AcceptedDomains = (Get-AcceptedDomain).DomainName # Get all user mailboxes Write-Verbose 'Retrieving user mailboxes...' $UserMailboxes = Get-EXOMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited -Properties UserPrincipalName, DisplayName Write-Verbose "Checking inbox rules for $($UserMailboxes.Count) mailboxes..." $TotalRulesChecked = 0 foreach ($Mbx in $UserMailboxes) { try { $Rules = Get-InboxRule -Mailbox $Mbx.UserPrincipalName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue if (-not $Rules) { continue } foreach ($Rule in $Rules) { $TotalRulesChecked++ # Collect all forward targets $ForwardTargets = @() if ($Rule.ForwardTo) { $ForwardTargets += $Rule.ForwardTo } if ($Rule.ForwardAsAttachmentTo) { $ForwardTargets += $Rule.ForwardAsAttachmentTo } if ($Rule.RedirectTo) { $ForwardTargets += $Rule.RedirectTo } if (-not $ForwardTargets) { continue } # Check each target for external domains foreach ($Target in $ForwardTargets) { # Extract email address - targets can be in format "DisplayName [SMTP:user@domain.com]" $EmailMatch = [regex]::Match($Target, '[Ss][Mm][Tt][Pp]:([^\]]+)') $Email = if ($EmailMatch.Success) { $EmailMatch.Groups[1].Value } else { $Target } $Domain = ($Email -split '@')[-1] $IsExternal = $Domain -and ($Domain -notin $AcceptedDomains) if ($IsExternal) { $ForwardType = if ($Rule.ForwardTo -contains $Target) { 'ForwardTo' } elseif ($Rule.ForwardAsAttachmentTo -contains $Target) { 'ForwardAsAttachment' } else { 'RedirectTo' } $ForwardingRules.Add([PSCustomObject]@{ MailboxUPN = $Mbx.UserPrincipalName MailboxDisplay = $Mbx.DisplayName RuleName = $Rule.Name RuleEnabled = $Rule.Enabled ForwardType = $ForwardType ForwardTarget = $Email TargetDomain = $Domain RulePriority = $Rule.Priority RuleDescription = $Rule.Description }) } } } } catch { Write-Warning "Failed to retrieve inbox rules for $($Mbx.UserPrincipalName): $($_.Exception.Message)" } } # Build summary $EnabledForwards = @($ForwardingRules | Where-Object RuleEnabled -EQ $true) $UniqueMailboxes = ($ForwardingRules.MailboxUPN | Select-Object -Unique).Count $UniqueExternalDomains = ($ForwardingRules.TargetDomain | Select-Object -Unique) $Summary = [PSCustomObject]@{ TenantId = $TenantId ReportGeneratedDate = Get-Date TotalMailboxesChecked = $UserMailboxes.Count TotalRulesChecked = $TotalRulesChecked ExternalForwardsFound = $ForwardingRules.Count EnabledExternalForwards = $EnabledForwards.Count MailboxesWithForwards = $UniqueMailboxes ExternalDomains = $UniqueExternalDomains } Write-Information "Inbox forwarding analysis completed - $($ForwardingRules.Count) external forwarding rules found across $UniqueMailboxes mailboxes." -InformationAction Continue [PSCustomObject][Ordered]@{ Summary = $Summary ForwardingRules = $ForwardingRules.ToArray() } } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Get-TntInboxForwardingRuleReport failed: $($_.Exception.Message)", $_.Exception), 'GetTntInboxForwardingRuleReportError', [System.Management.Automation.ErrorCategory]::OperationStopped, $TenantId ) $PSCmdlet.ThrowTerminatingError($errorRecord) } finally { try { if ($ConnectionInfo.ShouldDisconnect) { Disconnect-TntGraphSession -ConnectionState $ConnectionInfo } } catch { Write-Verbose "Could not disconnect from services: $($_.Exception.Message)" } } } } |