Public/Get-GkAppRegistrationReport.ps1
|
function Get-GkAppRegistrationReport { <# .SYNOPSIS Report app registrations with credential (secret/certificate) expiry and high-privilege API permissions. .DESCRIPTION Reads GET /applications and, per app, summarizes: * Credentials — passwordCredentials (secrets) and keyCredentials (certificates): counts, the earliest upcoming expiry, and how many are expired or expiring within a threshold. * Permissions — requiredResourceAccess resolved to human names by looking up each resource service principal's appRoles (application permissions, type Role) and oauth2PermissionScopes (delegated, type Scope). Application permissions matching a high-risk set (broad *.ReadWrite.All, RoleManagement.ReadWrite.Directory, Application.ReadWrite.All, full_access_as_app, ...) are flagged. Permission GUIDs are resolved live — never guessed — and cached per run. Use -SkipPermissionResolution to skip the service-principal lookups (faster; permissions are left as GUIDs and high-privilege detection is unavailable). .PARAMETER ExpiringInDays Window in days for the ExpiringSoonCount / -ExpiringOnly filter (default 30). .PARAMETER ExpiringOnly Return only apps with a credential already expired or expiring within ExpiringInDays. .PARAMETER HighPrivilegeOnly Return only apps holding at least one flagged high-privilege application permission. .PARAMETER SkipPermissionResolution Do not resolve permission GUIDs to names (skips per-resource servicePrincipal lookups). .PARAMETER AsReport Flatten HighPrivilegePermissions to a '; '-joined string and add ReportGeneratedUtc. .EXAMPLE Get-GkAppRegistrationReport -ExpiringInDays 30 -ExpiringOnly | Sort-Object DaysUntilExpiry Apps with a secret/cert expired or expiring within 30 days, soonest first. .EXAMPLE Get-GkAppRegistrationReport -HighPrivilegeOnly | Select-Object DisplayName, HighPrivilegePermissions Apps granted high-privilege application permissions. .EXAMPLE Get-GkAppRegistrationReport -SkipPermissionResolution -AsReport | Export-Csv .\apps.csv -NoTypeInformation .OUTPUTS PSGraphKit.AppRegistration #> [CmdletBinding()] [OutputType('PSGraphKit.AppRegistration')] param( [ValidateRange(1, 3650)] [int] $ExpiringInDays = 30, [switch] $ExpiringOnly, [switch] $HighPrivilegeOnly, [switch] $SkipPermissionResolution, [switch] $AsReport ) begin { Test-GkConnection -FunctionName 'Get-GkAppRegistrationReport' | Out-Null $now = [datetime]::UtcNow $highRiskExact = @( 'RoleManagement.ReadWrite.Directory', 'Directory.ReadWrite.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'Domain.ReadWrite.All', 'PrivilegedAccess.ReadWrite.AzureAD', 'full_access_as_app' ) } process { $select = 'id,appId,displayName,signInAudience,passwordCredentials,keyCredentials,requiredResourceAccess' $apps = Invoke-GkGraphRequest -Uri "/applications?`$select=$select&`$top=999" -CallerFunction 'Get-GkAppRegistrationReport' $spCache = @{} # resourceAppId -> @{ Role = @{id=name}; Scope = @{id=name} } foreach ($app in $apps) { # --- Credentials --- $creds = @() foreach ($pc in @(Get-GkDictValue $app 'passwordCredentials')) { $creds += [pscustomobject]@{ Type = 'Secret'; End = (ConvertTo-GkDateTime (Get-GkDictValue $pc 'endDateTime')) } } foreach ($kc in @(Get-GkDictValue $app 'keyCredentials')) { $creds += [pscustomobject]@{ Type = 'Certificate'; End = (ConvertTo-GkDateTime (Get-GkDictValue $kc 'endDateTime')) } } $secretCount = @($creds | Where-Object Type -eq 'Secret').Count $certCount = @($creds | Where-Object Type -eq 'Certificate').Count $withDates = @($creds | Where-Object { $null -ne $_.End }) $earliest = $withDates | Sort-Object End | Select-Object -First 1 $earliestEnd = if ($earliest) { $earliest.End } else { $null } $daysToExpiry = if ($earliestEnd) { [int][math]::Floor(($earliestEnd - $now).TotalDays) } else { $null } $expiredCount = @($withDates | Where-Object { $_.End -lt $now }).Count $expiringSoon = @($withDates | Where-Object { $_.End -ge $now -and ($_.End - $now).TotalDays -le $ExpiringInDays }).Count # --- Permissions --- $appPerms = @(); $delegatedPerms = @(); $highPriv = @() if (-not $SkipPermissionResolution) { foreach ($rra in @(Get-GkDictValue $app 'requiredResourceAccess')) { $resourceAppId = [string](Get-GkDictValue $rra 'resourceAppId') if (-not $resourceAppId) { continue } if (-not $spCache.ContainsKey($resourceAppId)) { $map = @{ Role = @{}; Scope = @{} } try { $sp = Invoke-GkGraphRequest -Raw -CallerFunction 'Get-GkAppRegistrationReport' ` -Uri "/servicePrincipals(appId='$resourceAppId')?`$select=appId,displayName,appRoles,oauth2PermissionScopes" foreach ($ar in @(Get-GkDictValue $sp 'appRoles')) { $map.Role[[string](Get-GkDictValue $ar 'id')] = [string](Get-GkDictValue $ar 'value') } foreach ($os in @(Get-GkDictValue $sp 'oauth2PermissionScopes')) { $map.Scope[[string](Get-GkDictValue $os 'id')] = [string](Get-GkDictValue $os 'value') } } catch { Write-Verbose "PSGraphKit: could not resolve permissions for resource $resourceAppId : $($_.Exception.Message)" } $spCache[$resourceAppId] = $map } $map = $spCache[$resourceAppId] foreach ($ra in @(Get-GkDictValue $rra 'resourceAccess')) { $raId = [string](Get-GkDictValue $ra 'id') $raType = [string](Get-GkDictValue $ra 'type') # 'Role' = application, 'Scope' = delegated if ($raType -eq 'Role') { $name = if ($map.Role.ContainsKey($raId)) { $map.Role[$raId] } else { $raId } $appPerms += $name if ($highRiskExact -contains $name -or $name -like '*.ReadWrite.All') { $highPriv += $name } } else { $name = if ($map.Scope.ContainsKey($raId)) { $map.Scope[$raId] } else { $raId } $delegatedPerms += $name } } } } $highPriv = @($highPriv | Sort-Object -Unique) $isExpiringCandidate = ($expiredCount -gt 0 -or $expiringSoon -gt 0) if ($ExpiringOnly -and -not $isExpiringCandidate) { continue } if ($HighPrivilegeOnly -and $highPriv.Count -eq 0) { continue } $obj = [ordered]@{ PSTypeName = 'PSGraphKit.AppRegistration' DisplayName = [string](Get-GkDictValue $app 'displayName') AppId = [string](Get-GkDictValue $app 'appId') SignInAudience = [string](Get-GkDictValue $app 'signInAudience') SecretCount = $secretCount CertificateCount = $certCount EarliestCredentialExpiry = $earliestEnd DaysUntilExpiry = $daysToExpiry ExpiredCredentialCount = $expiredCount ExpiringSoonCount = $expiringSoon AppPermissionCount = $appPerms.Count DelegatedPermissionCount = $delegatedPerms.Count HighPrivilegePermissions = if ($AsReport) { $highPriv -join '; ' } else { $highPriv } HasHighPrivilege = ($highPriv.Count -gt 0) Id = [string](Get-GkDictValue $app 'id') } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } } } |