Public/Get-M365EnterpriseApp.ps1

function Get-M365EnterpriseApp {
    <#
    .SYNOPSIS
        Returns enterprise apps and their consented (delegated) permissions, flagging risky scopes.
    .DESCRIPTION
        Reads OAuth2 permission grants via Microsoft Graph, aggregates the consented scopes per app,
        resolves the app name and marks risky scopes (e.g. Mail.ReadWrite, Directory.ReadWrite.All,
        full_access_as_user). Read-only. Uses Directory.Read.All.
    .PARAMETER RiskyOnly
        Return only apps that hold at least one risky scope.
    .EXAMPLE
        Get-M365EnterpriseApp -RiskyOnly
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param([switch]$RiskyOnly)

    Assert-TTGraph
    Write-TTLog -Level INFO -Message "Reading OAuth2 permission grants (enterprise apps) ..."

    $risky = @(
        'Mail.ReadWrite', 'Mail.Send', 'MailboxSettings.ReadWrite', 'Files.ReadWrite.All',
        'Sites.ReadWrite.All', 'Directory.ReadWrite.All', 'User.ReadWrite.All', 'Group.ReadWrite.All',
        'full_access_as_user', 'Application.ReadWrite.All', 'RoleManagement.ReadWrite.Directory',
        'AppRoleAssignment.ReadWrite.All', 'Mail.Read', 'Files.Read.All'
    )

    try { $grants = Get-TTGraphCollection 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$top=100' }
    catch { Write-Warning "Could not read oauth2PermissionGrants: $_"; return }

    $spCache = @{}
    function Resolve-Sp { param($Id)
        if ($spCache.ContainsKey($Id)) { return $spCache[$Id] }
        $n = $Id
        try { $n = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$Id`?`$select=displayName" -OutputType PSObject -ErrorAction Stop).displayName } catch { }
        $spCache[$Id] = $n; return $n
    }

    foreach ($g in ($grants | Group-Object clientId)) {
        $scopes = @($g.Group | ForEach-Object { ($_.scope -split '\s+') } | Where-Object { $_ } | Select-Object -Unique)
        $riskyScopes = @($scopes | Where-Object { $_ -in $risky })
        if ($RiskyOnly -and -not $riskyScopes) { continue }
        $tenantWide = @($g.Group | Where-Object { $_.consentType -eq 'AllPrincipals' }).Count -gt 0

        [pscustomobject]@{
            App         = Resolve-Sp $g.Name
            TenantWide  = $tenantWide
            ScopeCount  = $scopes.Count
            Scopes      = $scopes
            RiskyScopes = $riskyScopes
            ClientId    = $g.Name
        }
    }
}