Get-MgAllAppsByPermission.ps1

<#PSScriptInfo
 
.VERSION 0.8
 
.GUID 84a7dc33-b92a-4911-a0a3-f319bb65a059
 
.AUTHOR Daniel Bradley
 
.TAGS Microsoft Graph PowerShell Application Permissions
 
.RELEASENOTES
[Version 0.0.2] - Initial Release.
[Version 0.0.3] - Fixed dependant module install.
[Version 0.0.4] - Add ScopeType and orderd results.
[Version 0.0.5] - Removed placeholder permission.
[Version 0.0.6] - Added error handling.
[Version 0.0.7] - Added -All parameter
[Version 0.0.8] - Updated response
#>


<#
 
.DESCRIPTION
 Finds all delegated and application permissions for applications in Entra by permission scope.
.SYNOPSIS
 Loops through all applications and filters for the defined permissions.
.EXAMPLE
     
    # Find all apps with a specific permission assigned
    Get-MgAllAppsByPermission -Scope Mail.Send
 
    # Find all apps with any permission assigned
    Get-MgAllAppsByPermission -All
#>
 

[CmdletBinding()]
param (

    # Switch to display the current script version
    [switch]$Version,

    # String to define the permission scope search
    [string]$Scope,

    # Swith to display all permissions assigned to all applications
    [Switch]$All
)

# Script information
$CurrentVersion = '0.8'

if ($Version.IsPresent) {
    $CurrentVersion
    exit 0
}

#-------------Dependencies-------------#

$module = Import-Module microsoft.graph.authentication -PassThru -ErrorAction Ignore
if (-not $module) {
    Write-Host "Installing module microsoft.graph.authentication" -ForegroundColor Yellow
    Install-Module microsoft.graph.authentication -Scope CurrentUser -Force
}
Import-Module microsoft.graph.authentication

$module = Import-Module microsoft.graph.applications -PassThru -ErrorAction Ignore
if (-not $module) {
    Write-Host "Installing module microsoft.graph.applications" -ForegroundColor Yellow
    Install-Module microsoft.graph.applications -Scope CurrentUser -Force
}
Import-Module microsoft.graph.applications

$module = Import-Module Microsoft.Graph.Identity.SignIns -PassThru -ErrorAction Ignore
if (-not $module) {
    Write-Host "Installing module microsoft.graph.Identity.SignIns" -ForegroundColor Yellow
    Install-Module Microsoft.Graph.Identity.SignIns -Scope CurrentUser -Force
}
Import-Module Microsoft.Graph.Identity.SignIns

#-------------End Dependencies-------------#

#----------------Functions----------------#

function Get-AllAppsByPermission {

    <#
        .SYNOPSIS
        Retrieves all apps in Microsoft Entra by defined permission.
  
        .DESCRIPTION
        This function queries both delegated permissions and application (app roles) permissions on applications in Microsoft Entra. Then filters locally for only those with the defined permission.
  
        .EXAMPLE
        Get-AllAppsByPermission -Scope Mail.Send
  
        This example will find all applications with Mail.Send permission consented.
 
        .NOTES
        Credit to Luca Spolidoro @ Microsoft for sharing some core functionality of this script
    #>


    [CmdletBinding()]
    Param(
        $Permission,
        [switch]$all
    )

    #Check current context
    If((Get-MgContext).Scopes -notcontains "Application.Read.All"){
        Write-host "Connecting to Microsoft Graph..."
        Connect-MgGraph -Scopes Application.Read.All, DelegatedPermissionGrant.Read.All
    }

    #The the Graph Service Principal ID
    $graphSp = Get-MgServicePrincipalByAppId -AppId "00000003-0000-0000-c000-000000000000" -Property "id,appRoles"

    #Get all OAuth2 delegated Graph permissions for all apps
    $delegatedPermissions = Get-MgOauth2PermissionGrant -All -Top 999 -CountVariable CountVar -Property "clientId,scope" -Filter "ConsentType eq 'AllPrincipals' and resourceId eq '$($graphSp.Id)'" -Headers @{ 'ConsistencyLevel' = 'Eventual' } | Select-Object @{Name='ServicePrincipalId';Expression={$_.ClientId}}, @{Name='ScopeType';Expression={"Delegated"}}, @{Name='Scope'; Expression={$_.Scope -split ' ' -ne '' }}

    #Get all possible available app roles
    $graphAppRoles = $graphSp | Select-Object -ExpandProperty AppRoles | Select-Object Id, Value | Group-Object -Property Id -AsHashTable

    #Get all app role assignments for all apps
    $spRoleAssignments = Get-MgServicePrincipal -All -PageSize 999 -ExpandProperty "appRoleAssignments" -Property "id,appId,displayName" | Where-Object { ($_.AppRoleAssignments | Where-Object { $_.ResourceId -eq $graphSp.Id }).Count -gt 0 } | Select-Object Id, AppId, DisplayName, @{Name="AppRoleId"; Expression={ ($_.AppRoleAssignments).AppRoleId }}

    #Expand permissions for all app role assignments
    $appPermissions = $spRoleAssignments | ForEach-Object { [PSCustomObject]@{ ServicePrincipalId = $_.Id; ScopeType="Application"; Scope = @($graphAppRoles[$_.AppRoleId].Value)}}

    # Switch for all permissions to specific permission
    If ($all.IsPresent) {
        $filteredPermissions = $delegatedPermissions + $appPermissions
    } Else {
        #Filter all permissions by selected permission
        $filteredPermissions = $delegatedPermissions + $appPermissions | Where-Object Scope -CEQ $Permission
    }

    #if applications list is greater that 15, batch requests to ensure filter OR limit is not reached.
    If ($filteredPermissions.Count -gt 15){

        #Initilise variables
        $Filters = @()
        $i=0

        #Loop to generate filter strings (To solve max of 15 OR collection limit for Graph API)
        for($i=0;$i -lt $filteredPermissions.count;$i+=14){
            $filterString = "id in('" + (($filteredPermissions[$i..($i+14)] | Select-Object -ExpandProperty ServicePrincipalId) -join "','") + "')"
            $Filters += $filterString
        }

        #Generate the batch payload
        $batch = [System.Collections.Generic.List[Object]]::new()
        Foreach ($Filter in $Filters){
            $x += 1
            $Obj = [PSCustomObject]@{
                id      = $x
                method  = "GET"
                URL     = "/servicePrincipals?`$filter=$Filter&`$select=id,appId,displayName"
            }
            $batch.add($Obj)
        }
        $BatchBody = [PSCustomObject]@{requests = $batch} | ConvertTo-Json -Depth 4 

        #Send Batch request
        $filteredAppsWithName = (Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/$batch' -Body $BatchBody -ContentType 'application/json' -OutputType PSObject).responses.body.Value

    } Else {
        #Send single request
        $filterString = "id in('" + (($filteredPermissions | Select-Object -ExpandProperty ServicePrincipalId) -join "','") + "')"
        $filteredAppsWithName = Get-MgServicePrincipal -Filter $filterString -Select "id,appId,displayName" -erroraction SilentlyContinue | Select-Object Id, AppId, DisplayName
    }

    #Initialise array
    $Report = [System.Collections.Generic.List[Object]]::new()

    #Loop through the unique name of each app only to avoid duplicate entries
    ForEach ($app in ($filteredAppsWithName | Select -Unique DisplayName, id, Appid)){

        #Get scope types for each app/permission
        $Found = @()
        $Found = $filteredPermissions | Where {$_.ServicePrincipalId -eq $app.Id}

        #Loop through each permission type to add content to report
        Foreach ($perm in $found){
            $obj = [PSCustomObject][ordered]@{
                "DisplayName" = $app.DisplayName
                "AppId" = $app.AppId
                "ID" = $app.Id
                "ScopeType" = $perm.ScopeType
                "Permissions" = ($filteredPermissions | Where {($_.ServicePrincipalId -eq $app.Id) -and $_.ScopeType -eq $perm.ScopeType} | Select -expand Scope) -join ", "
            }
            $report.Add($obj) 
        }
        
    }

    #Check if -All has been defined
    If ($all.IsPresent){
        Write-Host "$(($Report | Select -Unique DisplayName).count) apps found" -ForegroundColor Cyan
    } Else{
        Write-Host "$($report.count) apps found with permission: $Permission `n" -ForegroundColor Cyan
    }

    #Report warning if count equals 0
    If ($Report.count -eq 0){
        Write-Host "Permissions are case senstive, please ensure letter capitalisation is correct `n" -ForegroundColor Yellow
    }

    $Report

}

#----------------End Functions----------------#

#Selector
If ($all.IsPresent){
    Get-AllAppsByPermission -All
} ElseIf ($null -ne $scope){
    Get-AllAppsByPermission -Permission $Scope
}