Public/Get-IOOrphanedApps.ps1
|
function Get-IOOrphanedApps { <# .SYNOPSIS Finds app registrations and enterprise apps with no owners assigned. .EXAMPLE Get-IOOrphanedApps .EXAMPLE Get-IOOrphanedApps -IncludeEnterpriseApps -ToCsv "orphaned-apps.csv" #> [CmdletBinding()] param( [switch]$IncludeEnterpriseApps, [string]$ToCsv ) $cmdName = $MyInvocation.MyCommand.Name Write-IOLog 'Scanning for app registrations with no owners...' -Level Info -Component $cmdName $results = [System.Collections.Generic.List[PSCustomObject]]::new() # ── App Registrations ────────────────────────────────────────────────── $apps = Invoke-IOGraphRequest -Uri 'v1.0/applications?$select=id,displayName,appId,createdDateTime' $total = ($apps | Measure-Object).Count $counter = 0 if ($total -gt 0) { try { foreach ($app in $apps) { $counter++ if ($counter % 50 -eq 0) { Write-Progress -Activity 'Checking app registration owners' -Status "$counter / $total" -PercentComplete (($counter / $total) * 100) } try { $owners = Invoke-IOGraphRequest -Uri "v1.0/applications/$($app.id)/owners?`$select=id,displayName,userPrincipalName" -NoPagination -SkipConnectionCheck } catch { $owners = @() } if (-not $owners -or $owners.Count -eq 0) { $created = if ($app.createdDateTime) { [datetime]::Parse($app.createdDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal).ToString('yyyy-MM-dd') } else { 'Unknown' } $results.Add([PSCustomObject]@{ DisplayName = $app.displayName AppId = $app.appId ObjectId = $app.id Type = 'AppRegistration' CreatedDate = $created OwnerCount = 0 }) } } } finally { Write-Progress -Activity 'Checking app registration owners' -Completed } } # end if total > 0 # ── Enterprise Apps (Service Principals) ─────────────────────────────── if ($IncludeEnterpriseApps) { Write-IOLog 'Also scanning enterprise apps (service principals)...' -Level Info -Component $cmdName $sps = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals?`$filter=servicePrincipalType eq 'Application'&`$select=id,displayName,appId,createdDateTime" $total = ($sps | Measure-Object).Count $counter = 0 try { foreach ($sp in $sps) { $counter++ if ($counter % 50 -eq 0 -and $total -gt 0) { Write-Progress -Activity 'Checking enterprise app owners' -Status "$counter / $total" -PercentComplete (($counter / $total) * 100) } try { $owners = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals/$($sp.id)/owners?`$select=id,displayName" -NoPagination -SkipConnectionCheck } catch { $owners = @() } if (-not $owners -or $owners.Count -eq 0) { $created = if ($sp.createdDateTime) { [datetime]::Parse($sp.createdDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal).ToString('yyyy-MM-dd') } else { 'Unknown' } $results.Add([PSCustomObject]@{ DisplayName = $sp.displayName AppId = $sp.appId ObjectId = $sp.id Type = 'EnterpriseApp' CreatedDate = $created OwnerCount = 0 }) } } } finally { Write-Progress -Activity 'Checking enterprise app owners' -Completed } } $sorted = $results | Sort-Object Type, DisplayName Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName } |