Investigation Actions in Exchange Online
A persistent challenge for security teams is to find where critical artifacts reside during an investigation. Of course, most of the data can be found in the unified audit log, but the process of extracting event details for examination may prove to be challenging.
When an incident happens – be it a compromised account, malicious inbox rules, or mass email deletions, time becomes your most limited resource. And without a consistent way to run investigations, the time lost in searching for what matters can be costly.
To make this faster and more repeatable, I’ve put together a script that performs six specific investigation actions in Exchange Online. Each action targets a common post-incident scenario using either audit log searches or Graph API queries, depending on what’s most effective for the individual scenario.
The investigations cover common post-incident scenarios like detecting malicious forwarding rules, spotting unusual send volumes, tracing permission changes, investigating suspicious IP-based access, checking for unauthorized mailbox exports, and quickly blocking compromised users with a forced password reset. The full PowerShell script is available from GitHub, but before you run any code, it’s important to understand that the script performs some pre-processing steps:
- Prompts for a reporting period (7, 15, or 30 days)
- Auto-connects all required roles to run the script
- Pulls trusted domains in your org for validation
- Offers the choice to run any single check or all six at once
Each of these checks maps to real-world behaviors that often follow a breach. Together, they offer a repeatable, efficient process for investigating Exchange Online incidents. In this article, I walk through what each check does, how it works, and why it matters in your security response playbook.
1. Detect Malicious Inbox Rules
One of the most insidious tactics used by intruders is creating malicious inbox rules in compromised user mailboxes. These rules often silently forward emails to external addresses or auto-delete specific messages – a subtle yet powerful method to exfiltrate sensitive information.
Identifying suspicious inbox rules demands a meticulous approach. While a mailbox owner can view their own rules in Outlook, attackers often create rules that blend in with legitimate ones, making them easy to overlook. PowerShell provides a better and more robust solution to easily inspect all inbox rules across user mailboxes. Using the Get-InboxRule cmdlet, we can inspect all the rules in user mailboxes for any suspicious traits such as forwarding to unfamiliar domains or unusual actions.
You should periodically verify if any rules are forwarding emails outside your organization. If you happen to discover any such rules, see if those external addresses are trusted domains or if they’re completely unfamiliar. This script uses mailbox cmdlets to check if there are any rules set up to forward emails to external addresses that are not in the list of accepted domains. Before running the script, make sure you connect to Exchange Online PowerShell and have the required role to run the script.
try { Get-InternalDomains $mailboxes = Get-ExoMailbox -Filter "RecipientTypeDetails -eq 'UserMailbox' -or RecipientTypeDetails -eq ‘SharedMailbox'" -ResultSize Unlimited -ErrorAction Stop foreach ($mailbox in $mailboxes) { try { $rules = Get-InboxRule -Mailbox $mailbox.UserPrincipalName -ErrorAction Stop foreach ($rule in $rules) { if ($rule.ForwardTo -or $rule.ForwardAsAttachmentTo -or $rule.RedirectTo) { $recipients = @() if ($rule.ForwardTo) { $recipients += $rule.ForwardTo } if ($rule.ForwardAsAttachmentTo) { $recipients += $rule.ForwardAsAttachmentTo } if ($rule.RedirectTo) { $recipients += $rule.RedirectTo } $externalRecipients = $recipients | Where-Object { if ($_ -match "SMTP:") { $email = $_ -replace "^SMTP:" -replace "^smtp:" $domain = $email.Split("@")[1] return -not ($Global:InternalDomains -contains $domain) } else { return $false } }
2. Identify Anomalous Outbound Email Volumes
Another sign of a potential breach is a sudden surge in a user’s outbound email volume. It’s a telltale signal of an attacker using a compromised account for spamming purposes or data exfiltration.
While legitimate scenarios exist, such anomalies warrant immediate investigation. To this end, I suggest using the Microsoft Graph API instead of the Get-MessageTrace cmdlet, because the cmdlet can only return data for the last 10 days. Although an Exchange Online historical search extends the lookback period to 90 days, the usage reports API provides insights into sending patterns beyond simple numbers.
But one thing to be noted is that the usage reports API has a two‑day lag, so the script cannot capture real‑time sending spikes from the past 48 hours.
The script detects potential email-sending behavior in users who exceed a threshold (sending over 100 emails), showing the user’s UPN and the number of emails sent. It uses the Microsoft Graph usage report API to detect sudden spikes in usual email activity.
try { $Uri = "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D$Global:days')" $outputPath = "$env:TEMP\email_activity_$(Get-Date -Format 'yyyyMMddHHmmss').csv" Invoke-MgGraphRequest -Method GET -Uri $Uri -OutputFilePath $outputPath -ErrorAction Stop if (Test-Path $outputPath) { $parsed = Import-Csv $outputPath $highVolumeSenders = $parsed | Where-Object { [int]::TryParse($_.SendCount, [ref]$null) -and [int]$_.SendCount -gt 100 } if ($highVolumeSenders) { foreach ($sender in $highVolumeSenders) { Write-Host "[WARNING] $($sender.UserPrincipalName) sent $($sender.SendCount) emails in last ($Global:days) days." -ForegroundColor Red } } else { Write-Host "[INFO] No users with unusual email volume detected." -ForegroundColor Green } # Optional: Export results to a CSV file on Desktop #$HighVolumeSenders | Export-Csv "$env:USERPROFILE\Desktop\HighVolumeSenders_${selectedPeriod}days.csv" -NoTypeInformation
The script looks for mailboxes with > 100 sent emails. You can modify the threshold if your company generally sends high mail volumes. Remember that it’s not just about the sheer quantity. We must also search for:
- Large bursts of emails to external recipients.
- Emails sent to a wide variety of recipients that don’t align with the user’s usual communication patterns.
- Unusual email attachments or suspicious links embedded in outgoing emails.
I suggest that you couple this kind of monitoring with advanced threat protection features such as Safe Links and Safe Attachments to help pre-emptively block malicious payloads before they spread. These features are part of Microsoft Defender for Office 365 (Plan 1 or Plan 2), so availability depends on your organization’s licensing.
3. Spot Privilege Escalations in Mailbox Permissions
Only Exchange or global admins should make high-level mailbox permission changes. Unusual changes that you happen to catch are a good indication that a hacked account is attempting to access unauthorized sensitive information. This is the reason that any security investigation should begin by checking for privilege escalations first.
For investigating mailbox rule activity and broader changes, I recommend searching for audit events using the Search-UnifiedAuditLog, the Graph AuditLog Query API, or the Entra ID audit log. All give visibility into who made what changes and when, which is critical for any in-depth security investigation. The key operations to look for include Add-MailboxPermission, Remove-MailboxPermission, Set-MailboxPermission, Add-MailboxFolderPermission, Set-MailboxFolderPermission, and Remove-MailboxFolderPermission.
Here’s an example to detect any changes to mailbox permissions using events from the Entra ID audit log:
$start = (Get-Date).AddDays(-[int]$days) $end = Get-Date $ops = @( "Add-MailboxPermission", "Remove-MailboxPermission", "Set-MailboxPermission", "Add-MailboxFolderPermission", "Remove-MailboxFolderPermission", "Set-MailboxFolderPermission" ) Write-Host "`nSearching mailbox permission changes for the last $days days..." -ForegroundColor Cyan $sessionId = [guid]::NewGuid().ToString() $cmd = "Initialize" $results = @() do { $batch = Search-UnifiedAuditLog -StartDate $start -EndDate $end -Operations $ops ` -ResultSize 5000 -SessionId $sessionId -SessionCommand $cmd if ($batch) { $results += $batch $cmd = "ReturnNextPreviewPage" } } while ($batch.Count -eq 5000) if ($results.Count -gt 0) { Write-Host "`nFound $($results.Count) permission change events. Displaying and exporting..." -ForegroundColor Yellow $parsed = $results | ForEach-Object { $data = $_.AuditData | ConvertFrom-Json [PSCustomObject]@{ Date = $_.CreationDate.ToString("yyyy-MM-dd HH:mm") Actor = if ($_.UserIds) { ($_.UserIds -join ", ") } else { "System" } Action = ($_.Operations -join ", ") Target = $data.ObjectId Cmdlet = $data.Operation Parameters = ($data.Parameters | ConvertTo-Json -Compress) } }
The investigation examines mailbox permission changes for any signs of privilege escalation or unauthorized access. My preferred approach is to set up alerts for any mailbox permission changes. This helps me remediate potential privilege escalation attempts as soon as I spot one.
4. Detect Suspicious Mailbox Access
After compromising a mailbox, attackers often access emails, forward messages externally, or search for valuable information, such as internal contacts, financial data, or shared links, to support further compromise. This type of reconnaissance can help them move laterally or escalate their privileges. Monitoring the MailItemsAccessed event can offer visibility into such early-stage activities, but due to the sensitive nature of this data (which may include email metadata), it should only be reviewed when there’s a clear signal of compromise or suspicious behavior.
You can use the script to find details of unusual mailbox activity. The script fetches audit logs for the MailItemsAccessed events for a selected period. You can easily determine who has accessed & read your messages in the mailbox, from where, and how often.
Before running the script, replace the IP ranges defined in the code with those that your environment flags as suspicious or doesn’t want access from. Also, you can change the severity level if the default level I have mentioned doesn’t suit your requirements.
Finally, the script creates a CSV report for security investigations and forensic analysis.
function Get-MailboxAccess { try { Write-Host "`n[SCANNING] Scanning for Suspicious Mailbox Access..." -ForegroundColor Yellow # Fetch MailItemsAccessed events in current window $logEntries = Search-UnifiedAuditLog -StartDate $Global:StartDate -EndDate $Global:EndDate -Operations MailItemsAccessed -ResultSize 5000 # Process data and extract relevant details $report = $logEntries | ForEach-Object { try { $record = $_.AuditData | ConvertFrom-Json -ErrorAction Stop $actorType = if ($record.LogonType -eq 0) { 'Owner' } elseif ($record.LogonType -eq 2) { 'Delegate' } elseif ($record.LogonType -eq 3) { 'Admin' } elseif ($record.LogonType -eq 4) { 'Service' } else { 'Unknown' } $app = if ($record.ClientAppId) { $record.ClientAppId } elseif ($record.ApplicationId) { $record.ApplicationId } else { 'Unknown' } [PSCustomObject]@{ MailboxOwner = $record.MailboxOwnerUPN AccessedBy = $record.UserId AccessTime = $record.CreationTime ClientApp = $app AccessLocation = $record.ClientIPAddress ActorType = $actorType AccessType = $record.AccessType AccessCount = 1 RiskLevel = if ($record.ClientIPAddress -match '185\\.220|194\\.88') { 'High' } elseif ($record.ClientIPAddress -match '102\\.54') { 'Medium' } else { 'Low' } } } catch { Write-Host "[WARNING] Failed to parse record: $_" -ForegroundColor DarkYellow } } | Where-Object { $_ } | Group-Object -Property MailboxOwner, AccessedBy, ClientApp, AccessLocation | ForEach-Object { $entry = $_.Group[0] $entry.AccessCount = $_.Count $entry } # Export results to CSV $report | Export-Csv -Path "SuspiciousMailAccessReport.csv" -NoTypeInformation Write-Host "Report generated: SuspiciousMailAccessReport.csv" -ForegroundColor Green } catch { Write-Host "[ERROR] Suspicious Mailbox Access: $_" -ForegroundColor Red } }
5. Suspicious Mailbox Exports to Spot Data Exfiltration Attempts
A user must have the ‘Mailbox Import Export’ role assigned to perform mailbox exports to.pst files. If you see an unusual or unexpected mailbox export, privilege escalation has likely already taken place! Monitoring export operations, especially New-MailboxExportRequest, is non-negotiable. This script finds if any mailbox export requests have been initiated via the New-MailboxExportRequest or New-ComplianceSearchAction cmdlets.’
function Get-MailboxExportEvents { Write-Host "`n[SCANNING] Checking for suspicious mailbox exports..." -ForegroundColor Yellow # Optional UPN filter prompt (leave blank for all) $filterUPN = Read-Host "Enter UPN to filter export events (leave blank for all)" try { $searchParams = @{ StartDate = $Global:StartDate EndDate = $Global:EndDate Operations = @("New-MailboxExportRequest", "New-ComplianceSearchAction") ResultSize = 5000 SessionId = [guid]::NewGuid().ToString() SessionCommand = "ReturnLargeSet" ErrorAction = "Stop" } $allExports = @() $batch = Search-UnifiedAuditLog @searchParams
Other critical factors that warrant close attention include:
- Unusual users initiating mailbox exports, especially those who don’t typically handle such tasks.
- Multiple exports from the same account in a short time frame.
Mailbox exports are one of the most critical things to monitor. Luckily, there are plenty of third-party tools out there, for example, Quest Change Auditor for Exchange, that can automatically notify whenever a mailbox export request is made in your Microsoft 365 environment.
6. The Go-to Immediate Response – Block!
Are you the kind of admin who reacts instantly to threats? Then this one’s for you! After all, what good is a security investigation tool if it can’t block a compromised user? That would be a shame to the tool itself! So, how could I leave my ‘investigator tool’ without it?
As the saying goes, “A single compromised account can trigger a full-scale breach.” That’s the reality, and speed is everything. You can take the first step by immediately revoking sign-in sessions in the Entra admin center, resetting the user’s password, and plenty of other options!
But for a swift response, why not use a single cmdlet? Update-MgUser.
However, blocking the account is just a temporary fix. You need a full post-incident investigation to uncover how the breach happened—phishing, weak passwords, or token theft?
After blocking a compromised user, the script resets the user’s password, revokes their sessions, and lists the last 5 sign-in activities. A few recommended actions include:
- Monitoring for logins from unusual locations or at odd hours.
- Looking for deviations in email sending patterns, file access, or other user behaviors.
- Correlating incidents, like combining multiple indicators (e.g., email spikes, permission changes) to detect broader threats.
# Option 1: Block user account Update-MgUser -UserId $userUPN -AccountEnabled:$false # Option 2: Reset user password and force change at next login $newPassword = -join ((33..126) | Get-Random -Count 16 | ForEach-Object { [char]$_ }) Update-MgUser -UserId $userUPN -PasswordProfile @{ Password = $newPassword ForceChangePasswordNextSignIn = $true } # Option 3: Revoke all active sessions Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$userUPN/revokeSignInSessions" # Additional checks Write-Host "`nRunning security checks and retrieving recent sign-in activity..." -ForegroundColor Cyan Get-MgAuditLogSignIn -Filter "userPrincipalName eq '$userUPN'" -Top 5 | Select-Object CreatedDateTime, AppDisplayName, IpAddress, @{N = "Status"; E = { $_.Status.ErrorCode } } | Format-Table -AutoSize
Connect the Dots!
Connecting the dots is more important than simply running queries when investigating security incidents in Exchange Online. Attackers don’t operate in isolation; they chain multiple tactics together. A compromised account with a malicious forwarding rule might also have unauthorized permissions assigned. A user sending a high volume of emails might also have an unusual login pattern.
Instead of logging into different admin centers or manually pulling logs, the script gives you a practical way to start answering the right questions: Is this account compromised? Was there a permissions change? Is data being exfiltrated? And if yes, what’s the fastest way to contain it?
The checks are designed to mirror real-world attack behavior—malicious forwarding rules, sudden email volume, stealthy permission escalations, and export attempts that can quietly siphon off data. For some, I use the Graph API for richer mailbox-level visibility, while other scenarios use unified audit logs for event-based traces. Together, they offer a solid baseline for investigating user-reported incidents or proactively reviewing high-risk accounts. So, yes—connect the dots. Because in security, the faster you see the pattern, the faster you stop the damage.