public/Get-MsecExchangeTransportRule.ps1
|
function Get-MsecExchangeTransportRule { <# .SYNOPSIS Mail flow rules, with the two things that make one dangerous: whether it bypasses filtering, and whether it sends mail somewhere else. .DESCRIPTION A transport rule runs on every message in the tenant, before the user sees it. That makes it a favourite for persistence: one rule can exempt an attacker's sender from filtering, or copy every message to an outside address, and it lives in a part of the portal nobody browses. BYPASSING FILTERING IS A SPAM CONFIDENCE LEVEL OF -1, which does not read as dangerous unless you know what it means. SCL -1 tells Exchange to trust the message completely and skip spam, phishing and bulk filtering. BypassesFiltering is a derived column saying so in words. Measured on one tenant: 7 of 12 rules set it, 3 of them enabled - mostly for phishing-simulation training, which is legitimate and is also exactly what an attacker's rule would be named. THE REDIRECT PROPERTIES ARE FOUR, NOT ONE. RedirectMessageTo diverts the message, BlindCopyTo and CopyTo duplicate it, and AddToRecipients adds a recipient. They behave differently for the sender and the original recipient; a command that checked only one would miss the other three. RedirectsMail is true when any of them is set, and the individual columns say which. ExternalRecipients NAMES THE ONES OUTSIDE THE TENANT. A rule copying mail to an internal archive mailbox is ordinary; the same rule pointing at a personal address is not. Resolved against accepted domains, and $null rather than empty when those could not be read - "not checked" must not look like "all internal". State IS NOT THE WHOLE ANSWER. A rule can be Enabled and still inert because its Mode is Audit rather than Enforce; both are on the row, and IsActive is true only when the rule is enabled AND enforcing. .PARAMETER Name Limit to rules whose name matches. Wildcards allowed. .PARAMETER RiskyOnly Only rules that bypass filtering or redirect mail. .EXAMPLE Connect-Msec -KeyVaultName kv-msec Get-MsecExchangeTransportRule -RiskyOnly | Format-Table Name, IsActive, BypassesFiltering, RedirectsMail, ExternalRecipients .EXAMPLE # Live rules that exempt a sender from all filtering. Get-MsecExchangeTransportRule | Where-Object { $_.IsActive -and $_.BypassesFiltering } | Format-Table Name, Comments, LastModifiedBy, WhenChangedUtc .OUTPUTS One PSCustomObject per rule, PSTypeName 'MsecExchangeTransportRule'. .NOTES Needs Connect-Msec. The Exchange session is opened on first use. Read-only. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [string] $Name, [Parameter()] [switch] $RiskyOnly ) Initialize-MsecExoSession -Endpoint Exchange $rules = @(Get-TransportRule -ErrorAction Stop) if ($Name) { $rules = @($rules | Where-Object { $_.Name -like $Name }) } # Decides what "external" means below. $null when unreadable, so ExternalRecipients can be # $null rather than an empty list that would read as "checked, all internal". $domains = $null try { $domains = @((Get-AcceptedDomain -ErrorAction Stop).DomainName | ForEach-Object { "$_".ToLower() }) } catch { Write-Warning "Could not read accepted domains, so ExternalRecipients is null rather than empty: $($_.Exception.Message)" } foreach ($rule in $rules) { # All four. They differ for sender and recipient, and checking one misses the rest. $redirect = @($rule.RedirectMessageTo | ForEach-Object { [string] $_ } | Where-Object { $_ }) $bcc = @($rule.BlindCopyTo | ForEach-Object { [string] $_ } | Where-Object { $_ }) $copy = @($rule.CopyTo | ForEach-Object { [string] $_ } | Where-Object { $_ }) $added = @($rule.AddToRecipients | ForEach-Object { [string] $_ } | Where-Object { $_ }) $allTargets = @($redirect + $bcc + $copy + $added) $external = $null if ($null -ne $domains) { $external = @($allTargets | Where-Object { $suffix = ($_ -split '@')[-1] $suffix -and ($_ -like '*@*') -and $suffix.ToLower() -notin $domains }) } # SCL -1 means "trust completely, skip filtering". The number alone does not say that. $bypasses = ([string] $rule.SetSCL -eq '-1') $redirects = [bool] $allTargets.Count if ($RiskyOnly -and -not ($bypasses -or $redirects)) { continue } [PSCustomObject]@{ PSTypeName = 'MsecExchangeTransportRule' Name = [string] $rule.Name State = [string] $rule.State Mode = [string] $rule.Mode # Enabled AND enforcing. A rule in Audit mode is live and does nothing. IsActive = ([string] $rule.State -eq 'Enabled' -and [string] $rule.Mode -eq 'Enforce') Priority = $rule.Priority BypassesFiltering = $bypasses SetScl = $(if ($null -ne $rule.SetSCL) { [string] $rule.SetSCL } else { $null }) RedirectsMail = $redirects RedirectMessageTo = $redirect BlindCopyTo = $bcc CopyTo = $copy AddToRecipients = $added # $null when accepted domains could not be read - see the help. ExternalRecipients = $external DeletesMessage = [bool] $rule.DeleteMessage SetsHeader = $(if ($rule.SetHeaderName) { "$($rule.SetHeaderName)=$($rule.SetHeaderValue)" } else { $null }) FromScope = [string] $rule.FromScope SentToScope = [string] $rule.SentToScope StopRuleProcessing = [bool] $rule.StopRuleProcessing Comments = [string] $rule.Comments CreatedBy = [string] $rule.CreatedBy LastModifiedBy = [string] $rule.LastModifiedBy WhenChangedUtc = $rule.WhenChanged } } } |