Back to script library
Entra / Microsoft 365 · Applications

Report permissions apps

A script using Azure Automation and a managed identity to scan for apps assigned high-priority permissions and report them.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Identity -Scopes Application.Read.All, AuditLog.Read.All

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[string] $TenantId = "$Tenant.Id",
[int] $LookbackDays = 360
)
Connect-MgGraph -Identity -Scopes Application.Read.All, AuditLog.Read.All
# Get tenant information
$Tenant = (Get-MgOrganization)
$TenantName = $Tenant.DisplayName
# Define the set of high-priority app roles (permissions) we're interested in
[array]$HighPriorityPermissions = "User.Read.All", "User.ReadWrite.All", "Mail.ReadWrite", `
"Files.ReadWrite.All", "Calendars.ReadWrite", "Mail.Send", "User.Export.All", "Directory.Read.All", `
"Exchange.ManageAsApp", "Directory.ReadWrite.All", "Sites.ReadWrite.All", "Application.ReadWrite.All", `
"Group.ReadWrite.All", "ServicePrincipalEndPoint.ReadWrite.All", "GroupMember.ReadWrite.All", `
"RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All"
# Define check period for new service principals
[datetime]$CheckforRecent = (Get-Date).AddDays(-$LookbackDays)
# Populate a set of hash tables with permissions used for different Office 365 management functions
$GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
# Populate hash table with Graph permissions
$GraphRoles = @{}
ForEach ($Role in $GraphApp.AppRoles) { $GraphRoles.Add([string]$Role.Id, [string]$Role.Value) }
# Populate hash table with Exchange Online permissions
$ExoPermissions = @{}
$ExoApp = Get-MgServicePrincipal -Filter "AppId eq '00000002-0000-0ff1-ce00-000000000000'"
ForEach ($Role in $ExoApp.AppRoles) { $ExoPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$O365Permissions = @{}
$O365API = Get-MgServicePrincipal -Filter "DisplayName eq 'Office 365 Management APIs'"
ForEach ($Role in $O365API.AppRoles) { $O365Permissions.Add([string]$Role.Id, [string]$Role.Value) }
$AzureADPermissions = @{}
$AzureAD = Get-MgServicePrincipal -Filter "DisplayName eq 'Windows Azure Active Directory'"
ForEach ($Role in $AzureAD.AppRoles) { $AzureADPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$TeamsPermissions = @{}
$TeamsApp = Get-MgServicePrincipal -Filter "DisplayName eq 'Skype and Teams Tenant Admin API'"
ForEach ($Role in $TeamsApp.AppRoles) { $TeamsPermissions.Add([string]$Role.Id, [string]$Role.Value) }
$RightsManagementPermissions = @{}
$RightsManagementApp = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Rights Management Services'"
ForEach ($Role in $RightsManagementApp.AppRoles) { $RightsManagementPermissions.Add([string]$Role.Id, [string]$Role.Value) }
# Create output tables
$Report = [System.Collections.Generic.List[Object]]::new()
$ProblemApps = [System.Collections.Generic.List[Object]]::new()
# Find all service principals belonging to registered apps
[array]$SPs = Get-MgServicePrincipal -All -Filter "Tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')"
# And those for managed identities
[array]$ManagedIdentities = Get-MgServicePrincipal -All -Filter "ServicePrincipalType eq 'ManagedIdentity'"
[array]$Apps = $SPs + $ManagedIdentities
# Loop through the apps and managed identities we've found to resolve the permissions assigned to each app
Write-Output ("{0} service principals for Entra ID registered apps found" -f $Apps.Count)
$i = 0
ForEach ($App in $Apps) {
$i++; $AppRecentFlag = $False
# Write-Output ("Processing app {0} {1}/{2}" -f $App.DisplayName, $i, $Apps.Count)
If ($App.AdditionalProperties.createdDateTime) {
[datetime]$AppCreationDate = $App.AdditionalProperties.createdDateTime
} Else {
# For some reason, no app created date is available, so set it to a date in 1970
[datetime]$AppCreationDate = '1970-01-01'
}
If ($AppCreationDate -gt $CheckforRecent) {$AppRecentFlag = $True}
[array]$AppRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $App.Id
If ($AppRoles) {
ForEach ($AppRole in $AppRoles) {
$Permission = $Null
Switch ($AppRole.ResourceDisplayName) {
"Microsoft Graph" {
[string]$Permission = $GraphRoles[$AppRole.AppRoleId] }
"Office 365 Exchange Online" {
[string]$Permission = $ExoPermissions[$AppRole.AppRoleId] }
"Office 365 Management APIs" {
[string]$Permission = $O365Permissions[$AppRole.AppRoleId] }
"Windows Azure Active Directory" {
[string]$Permission = $AzureADPermissions[$AppRole.AppRoleId] }
"Skype and Teams Tenant Admin API" {
[string]$Permission = $TeamsPermissions[$AppRole.AppRoleId] }
"Microsoft Rights Management Services" {
[string]$Permission = $RightsManagementPermissions[$AppRole.AppRoleId] }
}
If ($App.ServicePrincipalType -ne "ManagedIdentity") {
If ($App.AppOwnerOrganizationId -eq $TenantId) { #Resolve tenant name
$Name = $TenantName
} Else {
$LookUpId = $App.AppOwnerOrganizationId.toString()
$Uri = "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$LookUpId')"
$ExternalTenantData = Invoke-MgGraphRequest -Uri $Uri -Method Get
$Name = $ExternalTenantData.DisplayName
}
$VerifiedPublisher = $Null
If ($App.AdditionalProperties["verifiedpublisher"]) {
$VerifiedPublisher = $App.AdditionalProperties["verifiedpublisher"]
}
}
$ReportLine = [PSCustomObject] @{
DisplayName = $App.DisplayName
ServicePrincipalId = $App.Id
Publisher = $App.PublisherName
VerifiedPublisher = $VerifiedPublisher
Homepage = $App.Homepage
OwnerOrgId = $App.AppOwnerOrganizationId
OwnerOrgName = $Name
AppRoleId = $AppRole.AppRoleId
AppRoleCreation = $AppRole.CreationTimeStamp
Id = $AppRole.Id
Resource = $AppRole.ResourceDisplayName
ResourceId = $AppRole.ResourceId
Permission = $Permission
SPType = $App.ServicePrincipalType
'App Creation Date' = Get-Date($AppCreationDate) -format g
RecentApp = $AppRecentFlag
}
$Report.Add($ReportLine)
}
} # End ForEach AppRole
} #End ForEach App
Write-Output ("{0} apps scanned and {1} permissions found" -f $Apps.Count, $Report.Count)
Write-Output ""
Write-Output "Permissions found"
Write-Output "-----------------"
Write-Output ""
$Report | Group-Object Permission -NoElement | Sort-Object Name | Format-Table Name, Count
# Uncomment the next two lines if you want to generate the output file (interactive use only)
# $Report | Export-CSV -NoTypeInformation $OutputFile
# Write-Output ("Full permissions report is available in {0}" -f $OutputFile)
ForEach ($Record in $Report) {
If ($Record.Permission -in $HighPriorityPermissions) {
$ProblemApps.Add($Record) }
}
# Uncomment the next two lines if you want to generate the report about problem apps
# $ProblemApps | Export-CSV -NoTypeInformation $OutputFile2
# Write-Output ("List of apps for review is available in {0}" -f $OutputFile2)
[array]$AppSet = $ProblemApps | Sort-Object ServicePrincipalId -Unique
$AppOutput = [System.Collections.Generic.List[Object]]::new()
ForEach ($App in $AppSet) {
$Records = $ProblemApps | Where-Object {$_.ServicePrincipalId -eq $App.ServicePrincipalId}
$AppPermissions = $Records.Permission -join ", "
$ReportLine = [PSCustomObject] @{
DisplayName = $App.DisplayName
ServicePrincipalId = $App.ServicePrincipalId
Publisher = $App.Publisher
Permissions = $AppPermissions
SPType = $App.SPType
CreatedDate = $App.CreatedDate
RecentApp = $App.RecentApp}
$AppOutput.Add($ReportLine)
}
# Add sign-in information for apps
[array]$MIAuditRecords = Get-MgAuditLogSignIn -Filter "(signInEventTypes/any(t:t eq 'managedIdentity'))" -Top 2000 -Sort "createdDateTime DESC"
[array]$AuditRecords = Get-MgAuditLogSignIn -Filter "(signInEventTypes/any(t:t eq 'servicePrincipal'))" -Top 2000 -Sort "createdDateTime DESC"
$AuditRecords = $AuditRecords + $MIAuditRecords
ForEach ($AppRecord in $AppOutput) {
$SignInFound = $AuditRecords | Where-Object {$_.ServicePrincipalId -eq $AppRecord.ServicePrincipalId} | Select-Object -First 1
If ($SignInFound) { Write-Output ("App {0} last signed in at {1}" -f $AppRecord.DisplayName, $SignInFound.CreatedDateTIme)
$AppRecord | Add-Member -NotePropertyName LastSignIn -NotePropertyValue $SignInFound.CreatedDateTime
}
}
#
If ($AppOutput) { # Generate a report and post it to Teams
$AppOutput = $AppOutput | Sort-Object {$_.LastSignIn -as [datetime] } -Descending
$Today = Get-Date -format dd-MMM-yyyy
$Body = '
<style>
.UserTable {
border:1px solid #C0C0C0;
border-collapse:collapse;
padding:5px;
}
.UserTable th {
border:1px solid #C0C0C0;
padding:5px;
background:#F0F0F0;
}
.UserTable td {
border:1px solid #C0C0C0;
padding:5px;
}
</style>
<p><font size="2" face="Segoe UI">
<h3>Generated: ' + $Today + '</h3></font></p>
<table class="UserTable">
<caption><h2><font face="Segoe UI">Azure Automation: Apps with High-Priority Permissions for Review</h2></font></caption>
<thead>
<tr>
<th>App Name</th>
<th>Service Principal</th>
<th>Publisher</th>
<th>Permissions</th>
<th>App Type</th>
<th>Created</th>
<th>Last Signin</th>
</tr>
</thead>
<tbody>'
ForEach ($A in $AppOutput) {
$Body += "<tr><td><font face='Segoe UI'>$($A.DisplayName)</font></td><td><font face='Segoe UI'>$($A.ServicePrincipalId)</td></font><td><font face='Segoe UI'>$($A.Publisher)</td></font><td><font face='Segoe UI'>$($A.Permissions)</td></font><td><font face='Segoe UI'>$($A.SPType)</td><td><font face='Segoe UI'>$($A.CreatedDate)</td><td><font face='Segoe UI'>$($A.LastSignIn)</td></tr></font>"
}
$Body += "</tbody></table><p>"
$Body += '</body></html>'
Write-Output "Posting to Channel"
# Get username and password from Key Vault
$UserName = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -Name "ExoAccountName" -AsPlainText
$UserPassword = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "ExoAccountPassword" -AsPlainText
# Create credentials object from the username and password
[securestring]$SecurePassword = ConvertTo-SecureString $UserPassword -AsPlainText -Force
[pscredential]$UserCredentials = New-Object System.Management.Automation.PSCredential ($UserName, $SecurePassword)
# Get Site URL to use with PnP connection
$SiteURL = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "SPOSiteURL" -AsPlainText
# Target team and channel in that team to which we post a message containing the report
$TargetTeamId = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "TargetTeamId" -AsPlainText
$TargetTeamChannel = Get-AzKeyVaultSecret -VaultName "Office365ITPros" -name "TargetChannelID" -AsPlainText
# Connect to PnP using the account credentials we just retrieved
$PnpConnection = Connect-PnPOnline $SiteURL -Credentials $UserCredentials -ReturnConnection
# Post message to Teams
Submit-PnPTeamsChannelMessage -Team $TargetTeamId -Channel $TargetTeamChannel -Message $Body -ContentType Html -Important -Connection $PnpConnection
}

Parameters

ParameterDefaultNotes
-TenantId$Tenant.IdMicrosoft Entra tenant ID for app-only Graph authentication.
-LookbackDays360Number of days back to search the unified audit log.
Attribution