All scripts
Governance 777

Mailbox Inbox Rule Audit Report

Scans every Exchange Online mailbox for inbox rules that forward, redirect, or silently delete mail, the pattern most often left behind by a compromised account, and exports the results to CSV.

Get-MailboxInboxRuleReport.ps1
<#
.SYNOPSIS
    Reports inbox rules across Exchange Online mailboxes, flagging rules that forward, redirect, or silently delete mail.
.DESCRIPTION
    Connects to Exchange Online and loops through mailboxes, pulling every inbox rule set on each one. It flags rules that forward or redirect mail to an external domain, or that delete messages without moving them to a folder first, since these are the patterns most often left behind by a compromised account. Results are exported to a CSV you can review or hand to your security team.
.PARAMETER OutputPath
    Path for the CSV report. Defaults to .\InboxRuleReport.csv in the current folder.
.PARAMETER ExternalDomains
    Optional list of your own accepted domains, used to tell internal forwarding apart from external forwarding. If you skip this, every forwarding or redirect rule gets flagged so you can review it yourself.
.EXAMPLE
    .\Get-MailboxInboxRuleReport.ps1 -OutputPath C:\Reports\InboxRules.csv -ExternalDomains "contoso.com"
    Runs the report against every mailbox and writes the results to the given path, treating contoso.com as internal.
.AUTHOR
    Shehryar Hassan
#>

[CmdletBinding()]
param(
    [string]$OutputPath = ".\InboxRuleReport.csv",
    [string[]]$ExternalDomains = @()
)

if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
    Write-Error "The ExchangeOnlineManagement module is not installed. Run: Install-Module ExchangeOnlineManagement"
    return
}

Import-Module ExchangeOnlineManagement

try {
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
}
catch {
    Write-Error "Could not connect to Exchange Online: $($_.Exception.Message)"
    return
}

$results = New-Object System.Collections.Generic.List[Object]

$mailboxes = Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox, SharedMailbox

foreach ($mailbox in $mailboxes) {
    $rules = Get-InboxRule -Mailbox $mailbox.PrimarySmtpAddress -ErrorAction SilentlyContinue
    if (-not $rules) { continue }

    foreach ($rule in $rules) {
        $forwardTargets = @()
        if ($rule.ForwardTo) { $forwardTargets += $rule.ForwardTo }
        if ($rule.ForwardAsAttachmentTo) { $forwardTargets += $rule.ForwardAsAttachmentTo }
        if ($rule.RedirectTo) { $forwardTargets += $rule.RedirectTo }

        $isExternalForward = $false
        foreach ($target in $forwardTargets) {
            $targetText = $target.ToString()
            $isKnownInternal = $false
            foreach ($domain in $ExternalDomains) {
                if ($targetText -like "*$domain*") { $isKnownInternal = $true }
            }
            if ($forwardTargets.Count -gt 0 -and -not $isKnownInternal) { $isExternalForward = $true }
        }

        $isSilentDelete = ($rule.DeleteMessage -eq $true)

        if ($forwardTargets.Count -gt 0 -or $isSilentDelete) {
            $results.Add([PSCustomObject]@{
                Mailbox          = $mailbox.PrimarySmtpAddress
                RuleName         = $rule.Name
                Enabled          = $rule.Enabled
                ForwardsMail     = ($forwardTargets.Count -gt 0)
                ForwardTargets   = ($forwardTargets -join "; ")
                PossiblyExternal = $isExternalForward
                DeletesSilently  = $isSilentDelete
                StopsProcessing  = $rule.StopProcessingRules
            })
        }
    }
}

if ($results.Count -eq 0) {
    Write-Host "No forwarding, redirect, or silent delete rules found."
}
else {
    $results | Sort-Object PossiblyExternal -Descending | Export-Csv -Path $OutputPath -NoTypeInformation
    $flagged = ($results | Where-Object { $_.PossiblyExternal -eq $true }).Count
    Write-Host "Found $($results.Count) rule(s) worth reviewing, $flagged possibly forwarding outside your domains."
    Write-Host "Report saved to $OutputPath"
}

Disconnect-ExchangeOnline -Confirm:$false

Read it before you run it, and test in a safe tenant first.