Public/Get-EntraAppPermissionAudit.ps1

function Get-EntraAppPermissionAudit {
    <#
    .SYNOPSIS
        Audits Entra ID app registrations for excessive permissions and expiring credentials.
    .DESCRIPTION
        Reviews all app registrations in the tenant and flags: applications with high-privilege
        API permissions (Mail.ReadWrite, Directory.ReadWrite.All, etc.), apps with expiring or
        expired client secrets/certificates, multi-tenant apps, and apps with no owner assigned.
    .PARAMETER ExpirationWarningDays
        Number of days to warn before credential expiration. Defaults to 30.
    .EXAMPLE
        Get-EntraAppPermissionAudit -ExpirationWarningDays 60
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1, 365)]
        [int]$ExpirationWarningDays = 30
    )

    begin {
        Test-GraphConnection
        $results = [System.Collections.Generic.List[PSObject]]::new()

        # High-privilege permissions to flag
        $dangerousPermissions = @(
            'Directory.ReadWrite.All',
            'Mail.ReadWrite',
            'Mail.Send',
            'Files.ReadWrite.All',
            'Sites.ReadWrite.All',
            'User.ReadWrite.All',
            'RoleManagement.ReadWrite.Directory',
            'Application.ReadWrite.All',
            'AppRoleAssignment.ReadWrite.All',
            'Group.ReadWrite.All'
        )
    }

    process {
        $apps = Get-MgApplication -All -Property @(
            'id', 'displayName', 'appId', 'signInAudience',
            'requiredResourceAccess', 'passwordCredentials',
            'keyCredentials', 'createdDateTime'
        )

        foreach ($app in $apps) {
            $findings = @()

            # Check API permissions
            $highPrivPerms = @()
            foreach ($resource in $app.RequiredResourceAccess) {
                foreach ($perm in $resource.ResourceAccess) {
                    # Resolve permission name via service principal
                    try {
                        $sp = Get-MgServicePrincipal -Filter "appId eq '$($resource.ResourceAppId)'" -ErrorAction Stop
                        $permDef = if ($perm.Type -eq 'Role') {
                            $sp.AppRoles | Where-Object { $_.Id -eq $perm.Id }
                        } else {
                            $sp.Oauth2PermissionScopes | Where-Object { $_.Id -eq $perm.Id }
                        }
                        $permName = $permDef.Value
                        if ($permName -in $dangerousPermissions) {
                            $highPrivPerms += "$permName ($($perm.Type))"
                        }
                    }
                    catch { continue }
                }
            }
            if ($highPrivPerms.Count -gt 0) {
                $findings += "HIGH-PRIVILEGE PERMS: $($highPrivPerms -join '; ')"
            }

            # Check credential expiration
            $credStatus = @()
            $allCreds = @($app.PasswordCredentials) + @($app.KeyCredentials)
            foreach ($cred in ($allCreds | Where-Object { $_ })) {
                if ($cred.EndDateTime -lt (Get-Date)) {
                    $credStatus += "EXPIRED ($($cred.DisplayName))"
                }
                elseif ($cred.EndDateTime -lt (Get-Date).AddDays($ExpirationWarningDays)) {
                    $daysLeft = [math]::Round(($cred.EndDateTime - (Get-Date)).TotalDays)
                    $credStatus += "EXPIRING in $daysLeft days ($($cred.DisplayName))"
                }
            }
            if ($credStatus.Count -gt 0) {
                $findings += $credStatus -join ' | '
            }

            # Check multi-tenant
            if ($app.SignInAudience -in @('AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount')) {
                $findings += 'MULTI-TENANT'
            }

            # Check for owners
            try {
                $owners = Get-MgApplicationOwner -ApplicationId $app.Id -ErrorAction Stop
                if ($owners.Count -eq 0) { $findings += 'NO OWNER' }
            }
            catch { $findings += 'NO OWNER (unable to check)' }

            $results.Add([PSCustomObject]@{
                AppName          = $app.DisplayName
                AppId            = $app.AppId
                Created          = $app.CreatedDateTime
                SignInAudience   = $app.SignInAudience
                SecretCount      = ($app.PasswordCredentials | Measure-Object).Count
                CertCount        = ($app.KeyCredentials | Measure-Object).Count
                HighPrivPerms    = if ($highPrivPerms.Count -gt 0) { $highPrivPerms -join '; ' } else { 'None' }
                Finding          = if ($findings.Count -gt 0) { $findings -join ' | ' } else { 'OK' }
            })
        }
    }

    end {
        $flagged = $results | Where-Object { $_.Finding -ne 'OK' }
        Write-Host " App registrations scanned: $($results.Count) | Findings: $($flagged.Count)" -ForegroundColor Gray
        $results | Sort-Object Finding, AppName
    }
}