Public/Get-ExcessiveAppPermissions.ps1

<#
.SYNOPSIS
    Identifies enterprise applications with excessive Microsoft Graph API permissions.

.DESCRIPTION
    Audits service principals for overprivileged Graph API permissions that could pose security risks.
    Checks for high-risk application permissions like Directory.ReadWrite.All, Mail.ReadWrite.All, etc.

.PARAMETER IncludeMicrosoftApps
    Include Microsoft first-party applications in the audit. Default is $false.

.PARAMETER ExportPath
    Optional path to export results to CSV.

.EXAMPLE
    Get-ExcessiveAppPermissions

    Returns all third-party apps with excessive permissions.

.EXAMPLE
    Get-ExcessiveAppPermissions | Where-Object { $_.RiskLevel -eq 'HIGH' }

    Shows only high-risk apps (more than 3 risky permissions).

.NOTES
    Author: Kent Agent (kentagent-ai)
    Created: 2026-03-11
    Updated: 2026-03-12 (v2.2.5 - fixed Graph API filtering)
    Requires: Microsoft.Graph PowerShell module
    Permissions: Application.Read.All, Directory.Read.All

.LINK
    https://github.com/kentagent-ai/EntraIDSecurityScripts
#>

function Get-ExcessiveAppPermissions {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [bool]$IncludeMicrosoftApps = $false,

        [Parameter(Mandatory = $false)]
        [string]$ExportPath
    )

    begin {
        # Verify Graph connection
        $context = Get-MgContext
        if (-not $context) {
            throw "Not connected to Microsoft Graph. Run: Connect-MgGraph -Scopes 'Application.Read.All', 'Directory.Read.All'"
        }

        # Microsoft's tenant ID
        $microsoftTenantId = '72f988bf-86f1-41af-91ab-2d7cd011db47'

        # High-risk application (not delegated) permissions
        $highRiskPerms = @(
            'Mail.ReadWrite.All'
            'Mail.Read.All'
            'Mail.Send'
            'Directory.ReadWrite.All'
            'RoleManagement.ReadWrite.Directory'
            'Application.ReadWrite.All'
            'AppRoleAssignment.ReadWrite.All'
            'User.ReadWrite.All'
            'Group.ReadWrite.All'
            'Domain.ReadWrite.All'
            'IdentityRiskEvent.ReadWrite.All'
            'Policy.ReadWrite.ConditionalAccess'
            'UserAuthenticationMethod.ReadWrite.All'
            'Files.ReadWrite.All'
            'Sites.ReadWrite.All'
        )

        $results = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    process {
        Write-Verbose "Retrieving service principals..."

        try {
            # Get all service principals - filter client-side to avoid Graph API limitations
            $sps = Get-MgServicePrincipal -All -Property Id, DisplayName, AppId, AppOwnerOrganizationId, ServicePrincipalType -ErrorAction Stop
        }
        catch {
            throw "Failed to retrieve service principals: $_"
        }

        Write-Host "Analyzing permissions for $($sps.Count) service principals..." -ForegroundColor Cyan

        # Get Microsoft Graph service principal for permission lookups
        $graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" -Property Id, AppRoles -ErrorAction SilentlyContinue

        if (-not $graphSp) {
            Write-Warning "Could not find Microsoft Graph service principal for permission lookups"
        }

        $processedCount = 0
        $skippedMicrosoft = 0

        foreach ($sp in $sps) {
            $processedCount++
            if ($processedCount % 100 -eq 0) {
                Write-Progress -Activity "Analyzing app permissions" -Status "$processedCount of $($sps.Count)" -PercentComplete (($processedCount / $sps.Count) * 100)
            }

            # Filter Microsoft apps client-side
            $isMicrosoftApp = $sp.AppOwnerOrganizationId -eq $microsoftTenantId
            if ($isMicrosoftApp -and -not $IncludeMicrosoftApps) {
                $skippedMicrosoft++
                continue
            }

            # Skip managed identities
            if ($sp.ServicePrincipalType -eq 'ManagedIdentity') {
                continue
            }

            # Get app role assignments (application permissions granted to this SP)
            $appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -ErrorAction SilentlyContinue

            if (-not $appRoles) {
                continue
            }

            $riskyPerms = @()

            foreach ($assignment in $appRoles) {
                # Look up the permission name from the resource's app roles
                $permName = $null

                if ($graphSp -and $assignment.ResourceId -eq $graphSp.Id) {
                    # This is a Graph API permission
                    $permName = ($graphSp.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId }).Value
                }
                else {
                    # For other resources, use the display name
                    $permName = $assignment.AppRoleId
                }

                if ($permName -and $permName -in $highRiskPerms) {
                    $riskyPerms += $permName
                }
            }

            if ($riskyPerms.Count -gt 0) {
                $riskLevel = if ($riskyPerms.Count -gt 3) { 'HIGH' } 
                             elseif ($riskyPerms.Count -gt 1) { 'MEDIUM' } 
                             else { 'LOW' }

                $results.Add([PSCustomObject]@{
                    DisplayName          = $sp.DisplayName
                    AppId                = $sp.AppId
                    ServicePrincipalId   = $sp.Id
                    ServicePrincipalType = $sp.ServicePrincipalType
                    IsMicrosoftApp       = $isMicrosoftApp
                    RiskyPermissions     = ($riskyPerms -join ', ')
                    PermissionCount      = $riskyPerms.Count
                    RiskLevel            = $riskLevel
                    Recommendation       = switch ($riskLevel) {
                        'HIGH'   { 'Review immediately - excessive high-risk permissions' }
                        'MEDIUM' { 'Review permissions and apply least privilege' }
                        'LOW'    { 'Monitor for unusual activity' }
                    }
                })
            }
        }

        Write-Progress -Activity "Analyzing app permissions" -Completed
    }

    end {
        Write-Verbose "Found $($results.Count) apps with excessive permissions"

        # Summary
        $high = ($results | Where-Object { $_.RiskLevel -eq 'HIGH' }).Count
        $medium = ($results | Where-Object { $_.RiskLevel -eq 'MEDIUM' }).Count
        $low = ($results | Where-Object { $_.RiskLevel -eq 'LOW' }).Count

        Write-Host "`n=== Excessive App Permissions ===" -ForegroundColor Yellow
        Write-Host "Apps with risky permissions: $($results.Count)" -ForegroundColor White
        Write-Host "Skipped Microsoft apps: $skippedMicrosoft" -ForegroundColor Gray
        Write-Host "HIGH risk (>3 perms): $high" -ForegroundColor $(if ($high -gt 0) { 'Red' } else { 'Green' })
        Write-Host "MEDIUM risk (2-3 perms): $medium" -ForegroundColor $(if ($medium -gt 0) { 'Yellow' } else { 'Green' })
        Write-Host "LOW risk (1 perm): $low" -ForegroundColor $(if ($low -gt 0) { 'Yellow' } else { 'Green' })
        Write-Host "=================================" -ForegroundColor Yellow

        # Export if requested
        if ($ExportPath) {
            try {
                $results | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
                Write-Host "Results exported to: $ExportPath" -ForegroundColor Green
            }
            catch {
                Write-Error "Failed to export results: $_"
            }
        }

        return $results | Sort-Object RiskLevel, PermissionCount -Descending
    }
}

Export-ModuleMember -Function Get-ExcessiveAppPermissions -ErrorAction SilentlyContinue