Scripts/Get-OAuthPermissions.ps1

function CacheObject ($Object) {
    if ($Object) {
        if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) {
            $script:ObjectByObjectClassId[$Object.ObjectType] = @{}
        }
        $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object
        $script:ObjectByObjectId[$Object.ObjectId] = $Object
    }
}

# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not).
function GetObjectByObjectId ($ObjectId) {
    if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) {
        Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId)
        try {
            $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId
            CacheObject -Object $object
        } catch {
            Write-Verbose "Object not found."
        }
    }
    return $script:ObjectByObjectId[$ObjectId]
}

# Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode)
# or by iterating over all ServicePrincipal objects. The latter is required if there are more than
# 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD.
function GetOAuth2PermissionGrants ([switch]$FastMode) {
    if ($FastMode) {
        Get-AzureADOAuth2PermissionGrant -All $true
    } else {
        $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving delegated permissions..." `
                               -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                               -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $client = $_.Value
            Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId
        }
    }
}

function GetAzureADServicePrincipal ($ObjectId) {
    Get-AzureADServicePrincipal -ObjectId $ObjectId | ForEach-Object {
        $Output = $_
        $script:homepage = $Output.Homepage
        $script:PublisherName = $Output.PublisherName
        $script:ReplyUrls = $Output.ReplyUrls
        $script:AppDisplayName = $Output.AppDisplayName
        $script:AppId = $Output.AppId
    }
}

function Get-OAuthPermissions
{
<#
.SYNOPSIS
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
Script inspired by: https://gist.github.com/psignoret/41793f8c6211d2df5051d77ca3728c09
 
.DESCRIPTION
Script to list all delegated permissions and application permissions in Azure AD
The output will be written to a CSV file.
 
.PARAMETER OutputDir
outputDir is the parameter specifying the output directory.
Default: Output\OAuthPermissions
 
.PARAMETER ShowProgress
Switch parameter to show progress bars during processing.
Default: $true
 
.PARAMETER Encoding
Encoding is the parameter specifying the encoding of the CSV output file.
Default: UTF8
 
.PARAMETER LogLevel
Specifies the level of logging:
None: No logging
Minimal: Critical errors only
Standard: Normal operational logging
Default: Standard
 
.EXAMPLE
Get-OAuthPermissions
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
 
#>


    [CmdletBinding()]
    param(
        [switch] $DelegatedPermissions,
        [switch] $ApplicationPermissions,
        [string[]] $UserProperties = @("DisplayName"),
        [string[]] $ServicePrincipalProperties = @("DisplayName"),
        [switch] $ShowProgress = $true,
        [int] $PrecacheSize = 999,
        [string] $OutputDir = "Output\OAuthPermissions",
        [string] $Encoding = "UTF8",
        [ValidateSet('None', 'Minimal', 'Standard')]
        [string]$LogLevel = 'Standard'
    )

    Set-LogLevel -Level ([LogLevel]::$LogLevel)
    $date = Get-Date -Format "ddMMyyyyHHmmss" 
    $summary = @{
        TotalPermissions = 0
        DelegatedCount = 0
        ApplicationCount = 0
        ServicePrincipalsProcessed = 0
        StartTime = Get-Date
        ProcessingTime = $null
    }
        
    try {
        $tenant_details = Get-AzureADTenantDetail -ErrorAction stop
    } catch {
        write-logFile -Message "[INFO] Ensure you are connected to Azure by running the Connect-Azure command before executing this script" -Color "Yellow" -Level Minimal
        Write-logFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" -Level Minimal
        throw
    }

    Write-LogFile -Message "=== Starting OAuth Permissions Collection ===" -Color "Cyan" -Level Minimal
    
    if (!(test-path $OutputDir)) {
        New-Item -ItemType Directory -Force -Name $OutputDir > $null
    }
    else {
        if (!(Test-Path -Path $OutputDir)) {
            Write-Error "[Error] Custom directory invalid: $OutputDir exiting script" -ErrorAction Stop
            Write-LogFile -Message "[Error] Custom directory invalid: $OutputDir exiting script" -Level Minimal
        }
    }
    
    $report = @(
    Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f `
                    $tenant_details.ObjectId, `
                    ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name)

    $script:ObjectByObjectId = @{}
    $script:ObjectByObjectClassId = @{}
    $empty = @{} #

    Write-LogFile -Message "[INFO] Retrieving all ServicePrincipal objects..." -Level Standard
    Get-AzureADServicePrincipal -All $true | ForEach-Object {
        CacheObject -Object $_
        $summary.ServicePrincipalsProcessed++
    }
    $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count

    if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
        Get-AzureADUser -Top $PrecacheSize | Where-Object {
            CacheObject -Object $_
        }

        $fastQueryMode = $false
        try {
            $null = Get-AzureADOAuth2PermissionGrant -Top 999
            $fastQueryMode = $true
        } catch {
            if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) {
                Write-LogFile -Message "[ERROR] Fast query for delegated permissions failed, using slow method" -Level Minimal -Color "Red"
            } else {
                throw $_
            }
        }

        GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object {
            $grant = $_
            GetAzureADServicePrincipal($grant.ClientId)
            if ($grant.Scope) {
                $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {
                    $summary.DelegatedCount++
                    $grantDetails =  [ordered]@{
                        "PermissionType" = "Delegated"
                        "AppId" = $script:AppId
                        "ClientObjectId" = $grant.ClientId
                        "ResourceObjectId" = $grant.ResourceId
                        "Permission" = $_
                        "ConsentType" = $grant.ConsentType
                        "PrincipalObjectId" = $grant.PrincipalId
                        "Homepage" = $script:homepage
                        "PublisherName" = $script:PublisherName
                        "ReplyUrls" = $null
                        "ExpiryTime" = $grant.ExpiryTime
                    }

                    if ($null -ne $ReplyUrls) {
                        $grantDetails["ReplyUrls"] = $script:ReplyUrls -join ', '
                    }

                    if ($ServicePrincipalProperties.Count -gt 0) {
                        $client = GetObjectByObjectId -ObjectId $grant.ClientId
                        $resource = GetObjectByObjectId -ObjectId $grant.ResourceId
                        $insertAtClient = 2
                        $insertAtResource = 3
                        foreach ($propertyName in $ServicePrincipalProperties) {
                            $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                            $insertAtResource++
                            $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                            $insertAtResource ++
                        }
                    }

                    if ($UserProperties.Count -gt 0) {
                        $principal = $empty
                        if ($grant.PrincipalId) {
                            $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId
                        }
                        foreach ($propertyName in $UserProperties) {
                            $grantDetails["Principal$propertyName"] = $principal.$propertyName
                        }
                    }
                    New-Object PSObject -Property $grantDetails
                }
            }
        }
    }

    if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
        $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving application permissions..." `
                            -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                            -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $sp = $_.Value
            GetAzureADServicePrincipal($sp.ObjectId)

            Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true `
            | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object {
                $summary.ApplicationCount++
                $assignment = $_

                $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId
                $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id }

                $grantDetails =  [ordered]@{
                    "PermissionType" = "Application"
                    "AppId" = $null
                    "ClientObjectId" = $assignment.PrincipalId
                    "ResourceObjectId" = $assignment.ResourceId
                    "Permission" = $appRole.Value
                    "IsEnabled" = $null
                    "Description" = $null
                    "CreationTimestamp" = $null
                    "Homepage" = $script:homepage
                    "PublisherName" = $script:PublisherName
                    "ReplyUrls" = $null
                }

                if ($null -ne $sp -and $sp.AppId) {
                    $grantDetails["AppId"] = $sp.AppId
                }

                if ($null -ne $ReplyUrls) {
                    $grantDetails["ReplyUrls"] = $script:ReplyUrls -join ', '
                }
            
                if ($null -ne $appRole -and $appRole.IsEnabled) {
                    $grantDetails["IsEnabled"] = $appRole.IsEnabled
                }
            
                if ($null -ne $appRole -and $appRole.Description) {
                    $grantDetails["Description"] = $appRole.Description
                }
            
                if ($null -ne $assignment -and $assignment.CreationTimestamp) {
                    $grantDetails["CreationTimestamp"] = $assignment.CreationTimestamp
                }

                if ($ServicePrincipalProperties.Count -gt 0) {
                    $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId                 

                    $insertAtClient = 2
                    $insertAtResource = 3
                    foreach ($propertyName in $ServicePrincipalProperties) {
                        $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                        $insertAtResource++
                        $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                        $insertAtResource ++
                    }
                }
                
                New-Object PSObject -Property $grantDetails
            }
        }
    }
)
    $summary.TotalPermissions = $summary.DelegatedCount + $summary.ApplicationCount
    $summary.ProcessingTime = (Get-Date) - $summary.StartTime
    $report | ConvertTo-Csv | Format-Table > $null
    $prop = $report.ForEach{ $_.PSObject.Properties.Name } | Select-Object -Unique
    $report | Select-Object $prop | Export-CSV -NoTypeInformation -Path "$OutputDir\$($date)-OAuthPermissions.csv" -Encoding $Encoding

    Write-LogFile -Message "`n=== OAuth Permissions Analysis Summary ===" -Color "Cyan" -Level Standard
    Write-LogFile -Message "Service Principals Processed: $($summary.ServicePrincipalsProcessed)" -Level Standard
    Write-LogFile -Message "Total Permissions Found: $($summary.TotalPermissions)" -Level Standard
    Write-LogFile -Message " - Delegated Permissions: $($summary.DelegatedCount)" -Level Standard
    Write-LogFile -Message " - Application Permissions: $($summary.ApplicationCount)" -Level Standard
    Write-LogFile -Message "`nOutput File: $OutputDir\$($date)-OAuthPermissions.csv" -Level Standard
    Write-LogFile -Message "Processing Time: $($summary.ProcessingTime.ToString('mm\:ss'))" -Color "Green" -Level Standard
    Write-LogFile -Message "===================================" -Color "Cyan" -Level Standard
}