As Exchange Online administrators, visibility is our most valuable asset. One of the most common security “blind spots” in growing organizations involves mailbox delegation. Specifically: Who has the right to send emails as someone else?
While the Microsoft 365 Admin Center allows you to check permissions one mailbox at a time, auditing an entire tenant for Send As permissions and global ApplicationImpersonation roles can be a nightmare. Today, I’m sharing a PowerShell solution that automates this process and generates a beautiful, searchable HTML report.
“Send As” allows a user (or hacker) to send an email that appears to come directly from the mailbox owner, with no “on behalf of” tag. “ApplicationImpersonation” is even more powerful, allowing applications to act as the user completely.
The Solution: A Custom PowerShell Auditor
I wrote a PowerShell script that bridges the gap between raw data and actionable insights. Instead of staring at a CSV file, this script generates a modern, interactive HTML dashboard.
Key Features
- Full Tenant Scan: Iterates through all User and Shared Mailboxes.
- Send-As Detection: Identifies explicit AD permissions (Active Directory Rights).
- Impersonation Checks: Scans RBAC (Role Based Access Control) for the high-privilege
ApplicationImpersonationrole. - Visual Reporting: Produces a user-friendly HTML file with directional arrows indicating flow (Who → Can Access → Whom).
- Searchable: Includes an embedded JavaScript search engine to filter results instantly.
How It Works
The script operates in three distinct phases:
1. Data Collection (Send As)
First, it connects to Exchange Online. It then loops through every mailbox using Get-Mailbox. For each mailbox, it runs Get-RecipientPermission to find any security identifier (SID) that has “SendAs” rights, filtering out the owner themselves (Self).
2. RBAC Analysis (Impersonation)
Unlike standard folder permissions, ApplicationImpersonation is an administrative role. The script uses Get-ManagementRoleAssignment to find service accounts or users who have been granted this role, and importantly, checks the Scope (whether it applies to the whole organization or just specific users).
3. Report Generation
Finally, it constructs an HTML file. It embeds CSS for styling and a small JavaScript snippet to handle the search bar logic, ensuring the report is a single, portable file you can share with auditors or management.
The Script
Here is the complete PowerShell code. Save this as Audit-ExchangePermissions.ps1 and run it in a PowerShell terminal with Exchange Online Administrator rights.
<#
.SYNOPSIS
Generates an HTML Report for "Send As" and "ApplicationImpersonation" permissions.
.DESCRIPTION
Scans all User and Shared mailboxes for 'SendAs' rights.
Checks RBAC assignments for 'ApplicationImpersonation'.
Outputs a searchable HTML file.
#>
# --- Configuration ---
$ReportPath = "$env:USERPROFILE\Desktop\Exchange_SendAs_Impersonation_Report.html"
$LogoUrl = "https://img.icons8.com/color/48/000000/microsoft-exchange.png"
# --- Connection Check ---
try {
Get-Mailbox -ResultSize 1 -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Write-Host "Connection to Exchange Online verified." -ForegroundColor Green
}
catch {
Write-Host "Connecting to Exchange Online..." -ForegroundColor Yellow
try {
Connect-ExchangeOnline -ErrorAction Stop
}
catch {
Write-Error "Could not connect. Please check internet and permissions."
exit
}
}
# --- Data Collection ---
$ReportData = @()
# 1. Mailbox SendAs Checks
Write-Host "Scanning Mailboxes for 'Send As' rights..." -ForegroundColor Cyan
$Mailboxes = Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox,SharedMailbox
$Counter = 0
$Total = $Mailboxes.Count
foreach ($Mailbox in $Mailboxes) {
$Counter++
Write-Progress -Activity "Auditing Permissions" -Status "Processing: $($Mailbox.DisplayName)" -PercentComplete (($Counter / $Total) * 100)
# Filter 'SendAs' rights, excluding the user themselves (Self)
$Permissions = Get-RecipientPermission -Identity $Mailbox.Id -ResultSize Unlimited -Trustee $null | Where-Object {
($_.AccessRights -like "*SendAs*") -and
($_.Trustee -ne "NT AUTHORITY\SELF") -and
($_.Trustee -ne $Mailbox.UserPrincipalName)
}
foreach ($Perm in $Permissions) {
$ReportData += [PSCustomObject]@{
OwnerName = $Mailbox.DisplayName
OwnerUPN = $Mailbox.UserPrincipalName
Type = if ($Mailbox.RecipientTypeDetails -eq "UserMailbox") { "User Mailbox" } else { "Shared Mailbox" }
Direction = "Sent By"
Actor = $Perm.Trustee
AccessRights = "Send As"
}
}
}
# 2. ApplicationImpersonation Checks (RBAC)
Write-Host "Checking for 'ApplicationImpersonation' roles..." -ForegroundColor Cyan
$ImpersonationRoles = Get-ManagementRoleAssignment -Role "ApplicationImpersonation" -ResultSize Unlimited -ErrorAction SilentlyContinue
foreach ($RoleAssignment in $ImpersonationRoles) {
$ScopeName = "Entire Organization"
if ($RoleAssignment.CustomRecipientWriteScope) {
$ScopeName = "Scope: " + $RoleAssignment.CustomRecipientWriteScope
} elseif ($RoleAssignment.RecipientReadScope -ne "Organization") {
$ScopeName = "Scope: " + $RoleAssignment.RecipientReadScope
}
$ReportData += [PSCustomObject]@{
OwnerName = $ScopeName
OwnerUPN = "RBAC Scope"
Type = "App Impersonation"
Direction = "Impersonated By"
Actor = $RoleAssignment.RoleAssigneeName
AccessRights = "ApplicationImpersonation"
}
}
Write-Progress -Activity "Auditing Permissions" -Completed
# --- HTML Generation ---
Write-Host "Generating HTML Report..." -ForegroundColor Cyan
$CSS = @"
<style>
body { font-family: 'Segoe UI', sans-serif; background-color: #f4f4f9; color: #333; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
h1 { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 10px; display: flex; align-items: center; }
.info-box { background-color: #e1f0fa; border-left: 5px solid #0078d4; padding: 15px; margin-bottom: 20px; }
#searchInput { width: 100%; padding: 12px; margin: 8px 0; border: 1px solid #ccc; border-radius: 4px; background-color: #f8f8f8; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 14px; }
th { background-color: #0078d4; color: white; text-align: left; padding: 12px 15px; }
td { padding: 12px 15px; border-bottom: 1px solid #ddd; }
tr:nth-child(even) { background-color: #f9f9f9; }
.badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; color: white; }
.badge-shared { background-color: #ffc107; color: #333; }
.badge-user { background-color: #17a2b8; }
.badge-imper { background-color: #6f42c1; }
.arrow { color: #d9534f; font-weight: bold; }
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
</style>
"@
$ScriptJS = @"
<script>
function searchTable() {
var input = document.getElementById("searchInput");
var filter = input.value.toUpperCase();
var table = document.getElementById("permissionTable");
var tr = table.getElementsByTagName("tr");
for (var i = 1; i < tr.length; i++) {
var td = tr[i].getElementsByTagName("td");
var found = false;
for (var j = 0; j < td.length; j++) {
if (td[j] && (td[j].textContent || td[j].innerText).toUpperCase().indexOf(filter) > -1) {
found = true; break;
}
}
tr[i].style.display = found ? "" : "none";
}
}
</script>
"@
$HTMLHeader = @"
<!DOCTYPE html>
<html>
<head><title>Exchange Permissions Report</title>$CSS $ScriptJS</head>
<body>
<div class="container">
<h1><img src="$LogoUrl" width="40" style="margin-right:15px"> Exchange Permissions Audit</h1>
<div class="info-box">
<strong>Audit Summary:</strong><br>
Found $($ReportData.Count) permission entries.
</div>
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Search users, emails, or permissions...">
<table id="permissionTable">
<thead>
<tr>
<th>Type</th><th>Target (Owner/Scope)</th><th>Access</th><th>Actor (Delegate)</th>
</tr>
</thead>
<tbody>
"@
$HTMLRows = ""
foreach ($Row in $ReportData) {
$BadgeClass = switch ($Row.Type) {
"Shared Mailbox" { "badge-shared" }
"App Impersonation" { "badge-imper" }
Default { "badge-user" }
}
$HTMLRows += @"
<tr>
<td><span class="badge $BadgeClass">$($Row.Type)</span></td>
<td><strong>$($Row.OwnerName)</strong><br><span style="font-size:11px; color:#666;">$($Row.OwnerUPN)</span></td>
<td style="text-align:center;"><span class="arrow">← $($Row.AccessRights) ←</span></td>
<td><strong>$($Row.Actor)</strong></td>
</tr>
"@
}
$HTMLFooter = @"
</tbody>
</table>
<div class="footer">Generated on $(Get-Date -Format "yyyy-MM-dd")</div>
</div>
</body>
</html>
"@
$FinalHTML = $HTMLHeader + $HTMLRows + $HTMLFooter
$FinalHTML | Out-File -FilePath $ReportPath -Encoding UTF8
Invoke-Item $ReportPath
Conclusion
Regular auditing of Exchange permissions is critical for maintaining a “Zero Trust” environment. By automating the discovery of Send As and Impersonation rights, you ensure that no unauthorized account holds keys to your organization’s communication channels.
Feel free to customize the script to add more checks (like “Full Access” permissions) or change the branding to match your company colors!
















































































































































































































































