Checks/EXO/MET-EXO012-MailboxForwarding.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo',
    Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')]
param()

$METCheckInfo = @{
    Name           = 'Mailbox Forwarding'
    Severity       = 'High'
    Description    = 'Checks ForwardingSmtpAddress, ForwardingAddress, and DeliverToMailboxAndForward on every mailbox, flagging silent forwarding with no local copy.'
    RequiresModule = @('ExchangeOnlineManagement')
}

$referenceUrl = 'https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/get-exomailbox'

try {
    $mailboxes = Get-EXOMailbox -ResultSize Unlimited -Properties ForwardingSmtpAddress,ForwardingAddress,DeliverToMailboxAndForward,PrimarySmtpAddress -Filter "ForwardingSmtpAddress -ne `$null -or ForwardingAddress -ne `$null" -ErrorAction Stop
}
catch {
    New-METCheckResult -CheckId 'MET-EXO012' -Category EXO -Name 'Mailbox Forwarding' `
        -Result Fail -Severity High -AffectedObject 'Mailboxes' `
        -Finding 'Unable to retrieve mailboxes with forwarding configured' `
        -Recommendation 'Ensure the account has Exchange View-Only Recipients permission.' `
        -ReferenceUrl $referenceUrl `
        -ErrorMessage $_.ToString()
    return
}

$mailboxes = @($mailboxes | Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress })

if ($mailboxes.Count -eq 0) {
    New-METCheckResult -CheckId 'MET-EXO012' -Category EXO -Name 'Mailbox Forwarding' `
        -Result Pass -Severity High -AffectedObject 'Mailboxes' `
        -Finding 'No mailboxes have a forwarding address configured, so no mail is being automatically forwarded out of the tenant by mailbox-level forwarding' `
        -ReferenceUrl $referenceUrl
    return
}

$totalCount = $mailboxes.Count
$silent = [System.Collections.Generic.List[string]]::new()
$unconfirmed = [System.Collections.Generic.List[string]]::new()

foreach ($mbx in $mailboxes) {
    $address = [string]$mbx.PrimarySmtpAddress
    if ($null -eq $mbx.PSObject.Properties['DeliverToMailboxAndForward'] -or $null -eq $mbx.DeliverToMailboxAndForward) {
        $unconfirmed.Add($address)
    }
    elseif ($mbx.DeliverToMailboxAndForward -eq $false) {
        $silent.Add($address)
    }
}

$sampleLines = [System.Collections.Generic.List[string]]::new()
$sampleCap = 10
$index = 0

foreach ($mbx in $mailboxes) {
    if ($index -ge $sampleCap) {
        break
    }

    $target = if ($mbx.ForwardingSmtpAddress) { $mbx.ForwardingSmtpAddress } else { $mbx.ForwardingAddress }
    $address = [string]$mbx.PrimarySmtpAddress
    $suffix = if ($unconfirmed.Contains($address)) {
        ' [DeliverToMailboxAndForward not returned - local copy retention unconfirmed]'
    }
    elseif ($silent.Contains($address)) {
        ' [silent - no local copy retained]'
    }
    else {
        ''
    }

    $sampleLines.Add("$address -> $target$suffix")
    $index++
}

if ($totalCount -gt $sampleCap) {
    $sampleLines.Add("...and $($totalCount - $sampleCap) more")
}

$sampleText = $sampleLines -join '; '
$unconfirmedSample = @($unconfirmed | Select-Object -First $sampleCap) -join ', '
$unconfirmedSuffix = if ($unconfirmed.Count -gt $sampleCap) { " (and $($unconfirmed.Count - $sampleCap) more)" } else { '' }
$removalGuidance = 'Confirm each entry is a known, intentional business need (e.g. shared mailbox routing, employee departure handoff). Remove unexpected entries immediately and treat them as a potential compromise indicator. Run: Set-Mailbox -Identity <mailbox> -ForwardingSmtpAddress $null to remove.'

if ($silent.Count -gt 0) {
    $unconfirmedText = if ($unconfirmed.Count -gt 0) { " DeliverToMailboxAndForward was not returned for a further $($unconfirmed.Count) mailbox(es) ($unconfirmedSample$unconfirmedSuffix), so whether those retain a local copy was not established." } else { '' }

    New-METCheckResult -CheckId 'MET-EXO012' -Category EXO -Name 'Mailbox Forwarding' `
        -Result Warning -Severity High `
        -AffectedObject "Mailboxes ($totalCount with forwarding)" `
        -Finding "$($silent.Count) of $totalCount mailbox(es) with forwarding configured forward silently - DeliverToMailboxAndForward is `$false, so no local copy is retained and the mailbox owner never sees the forwarded mail.$unconfirmedText $sampleText" `
        -Recommendation "Review each forwarding mailbox, starting with the silent ones. Attacker-configured forwarding after a credential compromise is a common way to exfiltrate mail (invoices, wire approvals, credentials) even after the password is reset - especially silent forwarding, since the mailbox owner never sees a copy and has no visual cue anything is wrong. $removalGuidance" `
        -ReferenceUrl $referenceUrl
    return
}

if ($unconfirmed.Count -gt 0) {
    New-METCheckResult -CheckId 'MET-EXO012' -Category EXO -Name 'Mailbox Forwarding' `
        -Result Warning -Severity High `
        -AffectedObject "Mailboxes ($totalCount with forwarding)" `
        -Finding "$totalCount mailbox(es) have forwarding configured, and DeliverToMailboxAndForward was not returned for $($unconfirmed.Count) of them ($unconfirmedSample$unconfirmedSuffix), so whether a local copy is retained was not established for those. An unconfirmed state is reported as a gap rather than a clean result, because nothing here distinguishes a mailbox that retains a local copy from one forwarding silently. $sampleText" `
        -Recommendation "Confirm the state directly: Get-EXOMailbox -Identity <mailbox> -Properties DeliverToMailboxAndForward | Format-List PrimarySmtpAddress, ForwardingSmtpAddress, DeliverToMailboxAndForward. Forwarding with no local copy retained is the higher-risk pattern, since the mailbox owner never sees a copy and has no visual cue anything is wrong. $removalGuidance" `
        -ReferenceUrl $referenceUrl
    return
}

New-METCheckResult -CheckId 'MET-EXO012' -Category EXO -Name 'Mailbox Forwarding' `
    -Result Info -Severity High `
    -AffectedObject "Mailboxes ($totalCount with forwarding)" `
    -Finding "$totalCount mailbox(es) have forwarding configured and every one retains a local copy (DeliverToMailboxAndForward is `$true), so none of them forwards silently. They are listed here for review rather than as a gap. $sampleText" `
    -Recommendation "Forwarding that retains a local copy is often a legitimate configuration (shared mailbox routing, employee departure handoff), so this is a review item rather than a finding. $removalGuidance" `
    -ReferenceUrl $referenceUrl