Public/Get-GkServicePrincipalReport.ps1

function Get-GkServicePrincipalReport {
    <#
    .SYNOPSIS
        Report service principals (enterprise apps) with type, state, and optionally their
        tenant-wide OAuth2 consent grants.

    .DESCRIPTION
        Lists GET /servicePrincipals with the properties that matter for an app-security review:
        type, whether the app is enabled, whether app-role assignment is required, and tags. With
        -IncludeConsentGrants it also pulls GET /oauth2PermissionGrants (one tenant-wide call) and
        annotates each service principal with the number of delegated grants and whether any is
        consented for ALL users (consentType=AllPrincipals) — the over-privilege signal to flag.

        Requires Application.Read.All (and Directory.Read.All for the consent grants).

    .PARAMETER IncludeConsentGrants
        Annotate each service principal with DelegatedGrantCount and HasTenantWideConsent from
        /oauth2PermissionGrants.

    .PARAMETER Type
        Filter by servicePrincipalType (e.g. Application, ManagedIdentity, Legacy). Default all.

    .PARAMETER AsReport
        Flatten Tags to a '; '-joined string and add ReportGeneratedUtc.

    .EXAMPLE
        Get-GkServicePrincipalReport -IncludeConsentGrants | Where-Object HasTenantWideConsent

        Enterprise apps with a tenant-wide (all-users) delegated consent.

    .EXAMPLE
        Get-GkServicePrincipalReport -Type ManagedIdentity

        List managed identity service principals.

    .EXAMPLE
        Get-GkServicePrincipalReport -AsReport | Export-Csv .\service-principals.csv -NoTypeInformation

    .OUTPUTS
        PSGraphKit.ServicePrincipal
    #>

    [CmdletBinding()]
    [OutputType('PSGraphKit.ServicePrincipal')]
    param(
        [switch] $IncludeConsentGrants,
        [string] $Type,
        [switch] $AsReport
    )

    begin {
        Test-GkConnection -FunctionName 'Get-GkServicePrincipalReport' | Out-Null
        $now = [datetime]::UtcNow
    }

    process {
        # Build a clientId -> grants map once, if requested.
        $grantsByClient = @{}
        if ($IncludeConsentGrants) {
            foreach ($g in (Invoke-GkGraphRequest -Uri '/oauth2PermissionGrants' -CallerFunction 'Get-GkServicePrincipalReport')) {
                $client = [string](Get-GkDictValue $g 'clientId')
                if (-not $client) { continue }
                if (-not $grantsByClient.ContainsKey($client)) { $grantsByClient[$client] = [System.Collections.Generic.List[object]]::new() }
                $grantsByClient[$client].Add($g)
            }
        }

        $select = 'id,appId,displayName,accountEnabled,servicePrincipalType,appRoleAssignmentRequired,tags,signInAudience'
        $sps = Invoke-GkGraphRequest -Uri "/servicePrincipals?`$select=$select&`$top=999" -CallerFunction 'Get-GkServicePrincipalReport'

        foreach ($sp in $sps) {
            $spType = [string](Get-GkDictValue $sp 'servicePrincipalType')
            if ($Type -and $spType -ne $Type) { continue }

            $tags = @(Get-GkDictValue $sp 'tags')
            $id = [string](Get-GkDictValue $sp 'id')

            $obj = [ordered]@{
                PSTypeName                = 'PSGraphKit.ServicePrincipal'
                DisplayName               = [string](Get-GkDictValue $sp 'displayName')
                AppId                     = [string](Get-GkDictValue $sp 'appId')
                ServicePrincipalType      = $spType
                AccountEnabled            = [bool](Get-GkDictValue $sp 'accountEnabled')
                AppRoleAssignmentRequired = [bool](Get-GkDictValue $sp 'appRoleAssignmentRequired')
                SignInAudience            = [string](Get-GkDictValue $sp 'signInAudience')
                Tags                      = if ($AsReport) { $tags -join '; ' } else { $tags }
                Id                        = $id
            }

            if ($IncludeConsentGrants) {
                $grants = @()
                if ($grantsByClient.ContainsKey($id)) { $grants = @($grantsByClient[$id]) }
                $obj['DelegatedGrantCount'] = $grants.Count
                $obj['HasTenantWideConsent'] = [bool](@($grants | Where-Object { [string](Get-GkDictValue $_ 'consentType') -eq 'AllPrincipals' }).Count)
            }

            if ($AsReport) { $obj['ReportGeneratedUtc'] = $now }
            [pscustomobject]$obj
        }
    }
}