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 } } |