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
}

function Get-OAuthPermissionGraph {
<#
.SYNOPSIS
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments) using Microsoft Graph API.
 
.DESCRIPTION
Script to list all delegated permissions and application permissions in Azure AD using Microsoft Graph API
The output will be written to a CSV file.
 
.PARAMETER OutputDir
outputDir is the parameter specifying the output directory.
Default: Output\OAuthPermissions
 
.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
#>


    [CmdletBinding()]
    param(
        [switch] $DelegatedPermissions,
        [switch] $ApplicationPermissions,
        [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
    }

    $requiredScopes = @("Directory.Read.All", "Application.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes

    Write-LogFile -Message "=== Starting OAuth Permissions Collection ===" -Color "Cyan" -Level Minimal

    if (!(Test-Path $OutputDir)) {
        New-Item -ItemType Directory -Force -Path $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
        }
    }

    $script:ObjectCache = @{}
    function Get-CachedObject {
        param($Id, $Type)
        
        if (-not $script:ObjectCache.ContainsKey($Id)) {
            try {
                $object = switch ($Type) {
                    'ServicePrincipal' { Get-MgServicePrincipal -ServicePrincipalId $Id }
                    'User' { Get-MgUser -UserId $Id }
                    'Application' { Get-MgApplication -ApplicationId $Id }
                }
                $script:ObjectCache[$Id] = $object
            }
            catch {
                Write-Verbose "Could not retrieve object $Id : $_"
                return $null
            }
        }
        return $script:ObjectCache[$Id]
    }

    $report = @()
    Write-LogFile -Message "[INFO] Retrieving all ServicePrincipal objects..." -Level Standard
    $allServicePrincipals = Get-MgServicePrincipal -All
    $servicePrincipalCount = $allServicePrincipals.Count
    $summary.ServicePrincipalsProcessed = $servicePrincipalCount

    foreach ($sp in $allServicePrincipals) {
        $script:ObjectCache[$sp.Id] = $sp
    }

    if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
        Write-LogFile -Message "[INFO] Processing delegated permissions..." -Level Standard
        $allDelegatedGrants = Get-MgOauth2PermissionGrant -All

        foreach ($grant in $allDelegatedGrants) {
            $clientSp = Get-CachedObject -Id $grant.ClientId -Type 'ServicePrincipal'
            $resourceSp = Get-CachedObject -Id $grant.ResourceId -Type 'ServicePrincipal'

            if ($grant.Scope) {
                foreach ($scope in $grant.Scope.Split(' ')) {
                    if ($scope) {
                        $summary.DelegatedCount++
                        
                        $principalDisplayName = if ($grant.PrincipalId) {
                            $principal = Get-CachedObject -Id $grant.PrincipalId -Type 'User'
                            $principal.DisplayName
                        } else { "" }

                        $publisherName = if ($clientSp.PublisherName) {
                            $clientSp.PublisherName
                        } else {
                            if ($clientSp.DisplayName -like "Microsoft*") { "Microsoft" } else { "" }
                        }

                        $AccountEnabled = $clientSp.AccountEnabled
                        if ($AccountEnabled -eq $true) {
                        $ApplicationStatus = "Enabled"
                        }
                        else {
                            $ApplicationStatus = "Disabled"
                        }

                        $Tags = $clientSp.Tags
                        if ($Tags -Contains "HideApp") {
                            $ApplicationVisibility = "Hidden"
                        }
                        else {
                            $ApplicationVisibility = "Visible"
                        }

                        if ($Tags -Contains "WindowsAzureActiveDirectoryOnPremApp") {
                            $IsAppProxy = "Yes"
                        }
                        else {
                            $IsAppProxy = "No"
                        }

                        if ($clientSp.AppRoleAssignmentRequired -eq $false) {
                            $AssignmentRequired = "No"
                        }
                        else {
                            $AssignmentRequired = "Yes"
                        }

                        $ServicePrincipalTypes = @()
                        if ($clientSp.AppOwnerOrganizationId -eq "f8cdef31-a31e-4b4a-93e4-5f571e91255a" -or $clientSp.AppOwnerOrganizationId -eq "72f988bf-86f1-41af-91ab-2d7cd011db47") { $ServicePrincipalTypes += "Microsoft Application" }
                        if ($clientSp.ServicePrincipalType -eq "ManagedIdentity") { $ServicePrincipalTypes += "Managed Identity" }
                        if ($clientSp.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp") { $ServicePrincipalTypes += "Enterprise Application" }
                        $ApplicationType = $ServicePrincipalTypes -join " & "
                        
                        $grantDetails = [ordered]@{
                            "PermissionType"         = "Delegated"
                            "AppId"                  = $clientSp.AppId
                            "ClientObjectId"         = $grant.ClientId
                            "AppDisplayName"           = $clientSp.DisplayName
                            "ResourceObjectId"       = $grant.ResourceId
                            "ResourceDisplayName"    = $resourceSp.DisplayName
                            "Permission"             = $scope
                            "ConsentType"            = $grant.ConsentType
                            "PrincipalObjectId"      = $grant.PrincipalId
                            "PrincipalDisplayName"   = $principalDisplayName
                            "Homepage"               = $clientSp.Homepage
                            "PublisherName"          = $publisherName
                            "ReplyUrls"              = ($clientSp.ReplyUrls -join ', ')
                            "ExpiryTime"             = $grant.ExpiryTime
                            #"CreatedDateTime" = $clientSp.AdditionalProperties.CreatedDateTime
                            "CreatedDateTime" = if ($clientSp.AdditionalProperties.ContainsKey('createdDateTime')) {
                                $clientSp.AdditionalProperties['createdDateTime']
                            } else {
                                $null
                            }
                            "AppOwnerOrganizationId" = $clientSp.AppOwnerOrganizationId
                            "ApplicationStatus"      = $ApplicationStatus
                            "ApplicationVisibility"  = $ApplicationVisibility 
                            "AssignmentRequired"     = $AssignmentRequired 
                            "IsAppProxy"             = $IsAppProxy 
                            "PublisherDisplayName"   = $clientSp.VerifiedPublisher.DisplayName #
                            "VerifiedPublisherId"    = $clientSp.VerifiedPublisher.VerifiedPublisherId
                            "AddedDateTime"          = $clientSp.VerifiedPublisher.AddedDateTime 
                            "SignInAudience"         = $clientSp.SignInAudience 
                            "ApplicationType"        = $ApplicationType 
                        }

                        $report += [PSCustomObject]$grantDetails
                    }
                }
            }
        }
    }

    if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
        Write-LogFile -Message "[INFO] Processing application permissions..." -Level Standard
        
        $i = 0
        foreach ($sp in $allServicePrincipals) {
            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving application permissions..." `
                    -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                    -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $appRoleAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All
            foreach ($assignment in $appRoleAssignments) {
                $summary.ApplicationCount++
                
                $resourceSp = Get-CachedObject -Id $assignment.ResourceId -Type 'ServicePrincipal'
                $appRole = $resourceSp.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId }

                $publisherName = if ($sp.PublisherName) {
                    $sp.PublisherName
                } else {
                    if ($sp.DisplayName -like "Microsoft*") { "Microsoft" } else { "" }
                }

                $AccountEnabled = $sp.AccountEnabled # true if the service principal account is enabled; otherwise, false. If set to false, then no users are able to sign in to this app, even if they're assigned to it.
                if ($Tags -eq "True") {
                    $ApplicationStatus = "Enabled"
                }
                else {
                    $ApplicationStatus = "Disabled"
                }

                $Tags = $sp.Tags
                if ($Tags -Contains "HideApp") {
                    $ApplicationVisibility = "Hidden"
                }
                else {
                    $ApplicationVisibility = "Visible"
                }

                if ($Tags -Contains "WindowsAzureActiveDirectoryOnPremApp") {
                    $IsAppProxy = "Yes"
                }
                else {
                    $IsAppProxy = "No"
                }

                if ($sp.AppRoleAssignmentRequired -eq $false) {
                    $AssignmentRequired = "No"
                }
                else {
                    $AssignmentRequired = "Yes"
                }

                $ServicePrincipalTypes = @()
                if ($sp.AppOwnerOrganizationId -eq "f8cdef31-a31e-4b4a-93e4-5f571e91255a" -or $sp.AppOwnerOrganizationId -eq "72f988bf-86f1-41af-91ab-2d7cd011db47") { $ServicePrincipalTypes += "Microsoft Application" }
                if ($sp.ServicePrincipalType -eq "ManagedIdentity") { $ServicePrincipalTypes += "Managed Identity" }
                if ($sp.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp") { $ServicePrincipalTypes += "Enterprise Application" }
                $ApplicationType = $ServicePrincipalTypes -join " & "

                $grantDetails = [ordered]@{
                    "PermissionType"         = "Application"
                    "AppId"                  = $sp.AppId
                    "ClientObjectId"         = $assignment.PrincipalId
                    "AppDisplayName"           = $sp.DisplayName
                    "ResourceObjectId"       = $assignment.ResourceId
                    "ResourceDisplayName"    = $resourceSp.DisplayName
                    "Permission"             = $appRole.Value
                    "ConsentType"            = "AllPrincipals"
                    "PrincipalObjectId"      = $null
                    "PrincipalDisplayName"   = ""
                    "Homepage"               = $sp.Homepage
                    "PublisherName"          = $publisherName
                    "ReplyUrls"              = ($sp.ReplyUrls -join ', ')
                    "IsEnabled"              = $appRole.IsEnabled
                    "Description"            = $appRole.Description
                    "CreationTimestamp"      = $assignment.CreatedDateTime
                    "CreatedDateTime"        = $sp.AdditionalProperties.createdDateTime
                    "AppOwnerOrganizationId" = $sp.AppOwnerOrganizationId
                    "ApplicationStatus"      = $ApplicationStatus
                    "ApplicationVisibility"  = $ApplicationVisibility 
                    "AssignmentRequired"     = $AssignmentRequired
                    "IsAppProxy"             = $IsAppProxy
                    "PublisherDisplayName"   = $sp.VerifiedPublisher.DisplayName
                    "VerifiedPublisherId"    = $sp.VerifiedPublisher.VerifiedPublisherId
                    "AddedDateTime"          = $sp.VerifiedPublisher.AddedDateTime
                    "SignInAudience"         = $sp.SignInAudience 
                    "ApplicationType"        = $ApplicationType
                }

                $report += [PSCustomObject]$grantDetails
            }
        }
    }

    # Export results
    $summary.TotalPermissions = $summary.DelegatedCount + $summary.ApplicationCount
    $summary.ProcessingTime = (Get-Date) - $summary.StartTime

    $outputPath = Join-Path $OutputDir "$($date)-OAuthPermissions.csv"
    $report | Export-CSV -NoTypeInformation -Path $outputPath -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: $outputPath" -Level Standard
    Write-LogFile -Message "Processing Time: $($summary.ProcessingTime.ToString('mm\:ss'))" -Color "Green" -Level Standard
    Write-LogFile -Message "===================================" -Color "Cyan" -Level Standard
}