Find-EwsApps.ps1

<#PSScriptInfo
 
.VERSION 1.0.0
 
.GUID 93bd44b9-a886-4025-a1c1-49e59102898d
 
.AUTHOR Sunsetless
 
.COMPANYNAME Sunsetless
 
.COPYRIGHT (c) Sunsetless. MIT License.
 
.TAGS EWS ExchangeOnline Exchange EntraID MicrosoftGraph EwsAllowedAppIDs
 
.LICENSEURI https://github.com/sunset-less/sunsetless-ews-samples/blob/main/LICENSE
 
.PROJECTURI https://sunsetless.com/guides/find-apps-using-ews
 
.RELEASENOTES
First release.
 
#>


#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Identity.SignIns, ExchangeOnlineManagement

<#
.SYNOPSIS
Lists the applications that use EWS in Exchange Online or hold access to it.
 
.DESCRIPTION
Collects application IDs from four places: the EWS usage report (optional), the EwsAllowedAppIDs list, EWS
permissions in Microsoft Entra ID (full_access_as_app and EWS.AccessAsUser.All) and RBAC for Applications in
Exchange Online. For each application it adds the name, whether it is Microsoft's, a vendor's or your own, the
verified publisher and the owners. The result goes to the screen and to ews-apps.csv in the current folder.
 
The script changes nothing in the tenant. It signs in to Microsoft Graph with Directory.Read.All and to Exchange Online PowerShell.
 
.PARAMETER UsageReport
The CSV exported from the EWS usage report in the Microsoft 365 admin center. Leave it out where the report is
not available.
 
.EXAMPLE
Find-EwsApps.ps1
 
.EXAMPLE
Find-EwsApps.ps1 -UsageReport .\EWSUsage.csv
 
.LINK
https://sunsetless.com/guides/find-apps-using-ews
#>

param(
    # The CSV exported from the EWS usage report. Leave it out where the report is not available.
    [string]$UsageReport
)
$ErrorActionPreference = "Stop"

Connect-MgGraph -Scopes "Directory.Read.All" -NoWelcome
Connect-ExchangeOnline -ShowBanner:$false

$exchangeOnlineAppId = "00000002-0000-0ff1-ce00-000000000000"
$microsoftGraphAppId = "00000003-0000-0000-c000-000000000000"
$microsoftTenantIds = "f8cdef31-a31e-4b4a-93e4-5f571e91255a", "72f988bf-86f1-41af-91ab-2d7cd011db47"
$tenantId = (Get-MgContext).TenantId
$guid = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
$apps = @{}

function Add-App($AppId, $FoundIn) {
    $id = "$AppId".Trim().ToLower()
    if (-not $apps[$id]) {
        $apps[$id] = [pscustomobject]@{
            AppId = $id; Name = ""; Kind = ""; Publisher = ""; Owners = ""
            FoundIn = @(); Calls = 0; SoapActions = @()
        }
    }
    if ($apps[$id].FoundIn -notcontains $FoundIn) { $apps[$id].FoundIn += $FoundIn }
    $apps[$id]
}

# The usage report
if ($UsageReport) {
    $report = Import-Csv $UsageReport
    $columns = $report[0].PSObject.Properties.Name
    $idColumn = $columns -match "app.*id" | Select-Object -First 1
    $actionColumn = $columns -match "action" | Select-Object -First 1
    $callsColumn = $columns -match "call" | Select-Object -First 1
    foreach ($row in $report) {
        if ($row.$idColumn -notmatch $guid) { continue }
        $app = Add-App $row.$idColumn "usage report"
        $app.Calls += [long]($row.$callsColumn -replace "\D", "")
        $action = $row.$actionColumn -replace ".*/", ""
        if ($app.SoapActions -notcontains $action) { $app.SoapActions += $action }
    }
}

# The allow list
$config = Get-OrganizationConfig -RetrieveEwsOperationAccessPolicy
"EwsEnabled: $($config.EwsEnabled)"
foreach ($id in $config.EwsAllowedAppIDs -split ",") {
    if ($id -match $guid) { $null = Add-App $id "allow list" }
}

# EWS permissions in Microsoft Entra ID
$exchange = Get-MgServicePrincipal -Filter "appId eq '$exchangeOnlineAppId'"
$graph = Get-MgServicePrincipal -Filter "appId eq '$microsoftGraphAppId'"
$fullAccess = ($exchange.AppRoles | Where-Object Value -eq "full_access_as_app").Id

foreach ($assignment in Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $exchange.Id -All) {
    if ($assignment.AppRoleId -eq $fullAccess) {
        $null = Add-App (Get-MgServicePrincipal -ServicePrincipalId $assignment.PrincipalId).AppId "full_access_as_app"
    }
}
foreach ($resource in $exchange, $graph) {
    foreach ($grant in Get-MgOauth2PermissionGrant -Filter "resourceId eq '$($resource.Id)'" -All) {
        if (($grant.Scope -split " ") -contains "EWS.AccessAsUser.All") {
            $null = Add-App (Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId).AppId "EWS.AccessAsUser.All"
        }
    }
}

# RBAC for Applications in Exchange Online
foreach ($principal in Get-ServicePrincipal) {
    $roles = Get-ManagementRoleAssignment -RoleAssignee $principal.Identity | Where-Object Role -like "*EWS*"
    if ($roles) { $null = Add-App $principal.AppId "Exchange RBAC" }
}

# A name, a kind and an owner for each application
foreach ($app in $apps.Values) {
    $sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'"
    if (-not $sp) { $app.Kind = "Not in your directory"; continue }
    $app.Name = $sp.DisplayName
    if ($sp.ServicePrincipalType -eq "ManagedIdentity" -or $sp.AppOwnerOrganizationId -eq $tenantId) {
        $app.Kind = "Your own"
        $registration = Get-MgApplication -Filter "appId eq '$($app.AppId)'"
        if ($registration) {
            $app.Owners = (Get-MgApplicationOwnerAsUser -ApplicationId $registration.Id).UserPrincipalName -join "; "
        }
    } elseif ($microsoftTenantIds -contains $sp.AppOwnerOrganizationId) {
        $app.Kind = "Microsoft"
    } else {
        $app.Kind = "Vendor"
        $app.Publisher = $sp.VerifiedPublisher.DisplayName
    }
}

$result = $apps.Values | Sort-Object Kind, Name | Select-Object AppId, Name, Kind, Publisher, Owners,
    @{ Name = "FoundIn"; Expression = { $_.FoundIn -join "; " } }, Calls,
    @{ Name = "SoapActions"; Expression = { $_.SoapActions -join " " } }
$result | Export-Csv .\ews-apps.csv -NoTypeInformation -Encoding UTF8
$result | Format-Table AppId, Name, Kind, FoundIn -AutoSize