Export-AppPermissions
Exports Entra ID app permissions to an Excel report.
Description
Connects to Microsoft Graph and enumerates all service principals in the tenant, collecting both delegated (OAuth2) and application (app role) permissions. Results are exported to a formatted Excel workbook.
Parameters
-GraphCloud
The Microsoft Graph national cloud environment to connect to. Defaults to 'Global'. Accepted values: Global, USGov, USGovDoD, Germany, China.
| Property | Value |
|---|---|
| Type | String |
| Required | false |
| Default | Global |
-Type
Optional. Filters the permission type to export. Use 'app' for application permissions, 'delegate' for delegated permissions, or omit to export both.
| Property | Value |
|---|---|
| Type | String |
| Required | false |
-Claim
Optional. Filters results by permission claim value. Supports wildcards. Examples: 'Mail.Read', 'Mail.', 'Mail'
| Property | Value |
|---|---|
| Type | String |
| Required | false |
-Tenant
Optional. The tenant ID or domain to connect to. If omitted, uses the default tenant for the authenticated account.
| Property | Value |
|---|---|
| Type | String |
| Required | false |
-Out
Output path for the Excel file. If omitted, saves to the current user's Downloads folder with an auto-generated filename. Example: 'C:\Reports\permissions.xlsx'
| Property | Value |
|---|---|
| Type | String |
| Required | false |
Examples
EXAMPLE 1
EXAMPLE 2
.\Export-AppPermissions.ps1 -Type app -Claim 'Mail.*'
Exports only application permissions whose claim matches 'Mail.*'.
EXAMPLE 3
.\Export-AppPermissions.ps1 -Tenant contoso.onmicrosoft.com -Out C:\Reports\perms.xlsx
Exports all permissions for a specific tenant to a custom output path.
Requires -Modules Microsoft.Graph.Authentication, ImportExcel
Script
Download Export-AppPermissions.ps1
<#
.SYNOPSIS
Exports Entra ID app permissions to an Excel report.
.DESCRIPTION
Connects to Microsoft Graph and enumerates all service principals in the tenant,
collecting both delegated (OAuth2) and application (app role) permissions.
Results are exported to a formatted Excel workbook.
.PARAMETER GraphCloud
The Microsoft Graph national cloud environment to connect to.
Defaults to 'Global'. Accepted values: Global, USGov, USGovDoD, Germany, China.
.PARAMETER Type
Optional. Filters the permission type to export. Use 'app' for application permissions,
'delegate' for delegated permissions, or omit to export both.
.PARAMETER Claim
Optional. Filters results by permission claim value. Supports wildcards.
Examples: 'Mail.Read', 'Mail.*', 'Mail*'
.PARAMETER Tenant
Optional. The tenant ID or domain to connect to. If omitted, uses the default tenant
for the authenticated account.
.PARAMETER Out
Output path for the Excel file. If omitted, saves to the current user's
Downloads folder with an auto-generated filename.
Example: 'C:\Reports\permissions.xlsx'
.EXAMPLE
.\Export-AppPermissions.ps1
Exports all app and delegated permissions for the default tenant.
.EXAMPLE
.\Export-AppPermissions.ps1 -Type app -Claim 'Mail.*'
Exports only application permissions whose claim matches 'Mail.*'.
.EXAMPLE
.\Export-AppPermissions.ps1 -Tenant contoso.onmicrosoft.com -Out C:\Reports\perms.xlsx
Exports all permissions for a specific tenant to a custom output path.
#>
#Requires -Modules Microsoft.Graph.Authentication, ImportExcel
param(
[ValidateSet('Global', 'USGov', 'USGovDoD', 'Germany', 'China')]
[string]$GraphCloud = 'Global',
[ValidateSet('app', 'delegate')]
[string]$Type, # Empty = both
[string]$Claim, # Mail.Read, Mail.*, or Mail*
[string]$Tenant,
[string]$Out
)
function Log { param([string]$Msg,[ConsoleColor]$Color='White') Write-Host "[$(Get-Date -Format 'HH:mm')] $Msg" -ForegroundColor $Color }
$scopes = @(
'Organization.Read.All'
'Directory.Read.All'
'Application.Read.All'
'User.Read.All'
'DelegatedPermissionGrant.Read.All'
)
Log "Connecting to Graph..."
if ($Tenant) { Connect-MgGraph -Scopes $scopes -Environment $GraphCloud -NoWelcome -ErrorAction Stop -TenantId $Tenant }
else { Connect-MgGraph -Scopes $scopes -Environment $GraphCloud -NoWelcome -ErrorAction Stop }
$Org = Get-MgOrganization
Log "Connected to Graph: $($Org.DisplayName)" Green
if (!$Out) {
$friendlyName = "AppPermissions"
$Out = Join-Path $env:USERPROFILE "Downloads\$($Org.DisplayName)-$friendlyName-$(Get-Date -Format 'yyyy-MM-dd_HH-mm').xlsx"
}
elseif ($Out -notlike '*.xlsx') { $Out += '.xlsx' }
$doDelegate = (-not $Type) -or ($Type -eq 'delegate')
$doApp = (-not $Type) -or ($Type -eq 'app')
Log "Fetching service principals..."
$servicePrincipals = Get-MgServicePrincipal -All -ErrorAction Stop
if (-not $servicePrincipals) {
Log "No service principals found." Yellow
return
}
Log "Processing $($servicePrincipals.Count) service principal(s)..."
# Simple caches
$resourceSpCache = @{}
$userCache = @{}
$rows = New-Object System.Collections.Generic.List[object]
function Get-ResourceServicePrincipal {
param(
[Parameter(Mandatory)]
[string]$Id
)
if ($resourceSpCache.ContainsKey($Id)) {
return $resourceSpCache[$Id]
}
$sp = Get-MgServicePrincipal -ServicePrincipalId $Id -ErrorAction SilentlyContinue
if ($sp) {
$resourceSpCache[$Id] = $sp
}
return $sp
}
function Get-UserNameFromId {
param(
[Parameter(Mandatory)]
[string]$Id
)
if ($userCache.ContainsKey($Id)) {
return $userCache[$Id]
}
$u = Get-MgUser -UserId $Id -ErrorAction SilentlyContinue
$name = $null
if ($u) {
$name = $u.UserPrincipalName
if (-not $name) { $name = $u.DisplayName }
}
if (-not $name) { $name = $Id }
$userCache[$Id] = $name
return $name
}
foreach ($sp in $servicePrincipals) {
# Delegated permissions
if ($doDelegate) {
$grants = Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $sp.Id -All -ErrorAction SilentlyContinue
if ($grants) {
$permIndex = @{} # key: resourceId|scope
foreach ($grant in $grants) {
if (-not $grant.Scope) { continue }
$scopesInGrant = $grant.Scope -split ' ' | Where-Object { $_ -and $_.Trim() }
$resourceSp = Get-ResourceServicePrincipal -Id $grant.ResourceId
if (-not $resourceSp) { continue }
foreach ($scopeValue in $scopesInGrant) {
if ($Claim -and ($scopeValue -notlike $Claim)) {
continue
}
$permissionDef = $null
if ($resourceSp.Oauth2PermissionScopes) {
$permissionDef = $resourceSp.Oauth2PermissionScopes |
Where-Object { $_.Value -eq $scopeValue }
}
if ($permissionDef) {
$permissionDisplayName = $permissionDef.UserConsentDisplayName
if (-not $permissionDisplayName) { $permissionDisplayName = $permissionDef.AdminConsentDisplayName }
}
$resourceKey = "$($grant.ResourceId)|$scopeValue"
if (-not $permIndex.ContainsKey($resourceKey)) {
$row = [PSCustomObject]@{
DisplayName = $sp.DisplayName
Type = 'Delegated'
Claim = $scopeValue
Description = $permissionDisplayName
ResourceName = $resourceSp.DisplayName
AdminConsented = $false
UserConsented = $false
ConsentedUsers = ''
AppId = $sp.AppId
AppObjectId = $sp.Id
ResourceAppId = $resourceSp.AppId
}
$permIndex[$resourceKey] = $row
}
$current = $permIndex[$resourceKey]
if ($grant.ConsentType -eq 'AllPrincipals') {
$current.AdminConsented = $true
}
elseif ($grant.ConsentType -eq 'Principal') {
$current.UserConsented = $true
if ($grant.PrincipalId) {
$userName = Get-UserNameFromId -Id $grant.PrincipalId
if ($userName) {
if ([string]::IsNullOrWhiteSpace($current.ConsentedUsers)) {
$current.ConsentedUsers = $userName
}
else {
$existing = $current.ConsentedUsers -split ';' | ForEach-Object { $_.Trim() }
if ($existing -notcontains $userName) {
$current.ConsentedUsers += ";$userName"
}
}
}
}
}
}
}
foreach ($entry in $permIndex.GetEnumerator()) {
$row = $entry.Value
$rows.Add($row)
}
}
}
# Application permissions
if ($doApp) {
$appAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All -ErrorAction SilentlyContinue
if ($appAssignments) {
foreach ($assignment in $appAssignments) {
if ($null -ne $assignment.DeletedDateTime) { continue }
$resourceSp = Get-ResourceServicePrincipal -Id $assignment.ResourceId
if (-not $resourceSp) { continue }
$appRole = $null
if ($resourceSp.AppRoles) {
$appRole = $resourceSp.AppRoles |
Where-Object { $_.Id -eq $assignment.AppRoleId }
}
$claimValue = 'Unknown'
$permissionDisplay = '[UnknownAppRole]'
if ($appRole) {
$claimValue = $appRole.Value
$permissionDisplay = $appRole.DisplayName
}
if ($Claim -and $claimValue -and ($claimValue -notlike $Claim)) {
continue
}
$row = [PSCustomObject]@{
DisplayName = $sp.DisplayName
Type = 'Application'
Claim = $claimValue
Description = $permissionDisplay
ResourceName = $resourceSp.DisplayName
AdminConsented = $true
UserConsented = $false
ConsentedUsers = ''
AppId = $sp.AppId
AppObjectId = $sp.Id
ResourceAppId = $resourceSp.AppId
}
$rows.Add($row)
}
}
}
}
if (-not $rows -or $rows.Count -eq 0) {
if ($Claim) {
Log "No permissions found matching claim '$Claim'." Yellow
}
else {
Low "No permissions found." Yellow
}
return
}
$sheetName = switch ($Type) {
'app' { 'AppPermissions' }
'delegate' { 'DelegatedPermissions' }
default { 'AllPermissions' }
}
$tableName = $sheetName
$rows |
Export-Excel -Path $Out `
-AutoSize `
-FreezeTopRow `
-WorksheetName $sheetName `
-TableName $tableName `
-TableStyle Medium2
Log "Exported report: $Out" Green
$answer = Read-Host "Open the report now? [Y/n]"
if ($answer -eq '' -or $answer -match '^y') {
Start-Process $Out
}