Public/Get-GkInactiveApp.ps1
|
function Get-GkInactiveApp { <# .SYNOPSIS Report enterprise apps / service principals with no recent sign-in activity (decommission candidates). .DESCRIPTION Reads GET /reports/servicePrincipalSignInActivities (beta) and computes each service principal's most recent activity across its delegated/application client and resource sign-ins. Service principal display names are resolved from /servicePrincipals. This report is on the Microsoft Graph BETA endpoint (no v1.0 equivalent) and is global-cloud only. Requires AuditLog.Read.All. Unavailable data warns and returns nothing. .PARAMETER InactiveDays Staleness threshold in days (default 90). An app is stale when its last activity is at least this many days ago, or it has none recorded. .PARAMETER StaleOnly Return only stale apps (default returns all with the computed fields). .PARAMETER AsReport Add a ReportGeneratedUtc column. .EXAMPLE Get-GkInactiveApp -InactiveDays 180 -StaleOnly | Sort-Object InactiveDays -Descending Enterprise apps unused for 180+ days. .EXAMPLE Get-GkInactiveApp | Where-Object NeverActive Apps with no recorded sign-in activity at all. .EXAMPLE Get-GkInactiveApp -StaleOnly -AsReport | Export-Csv .\inactive-apps.csv -NoTypeInformation .OUTPUTS PSGraphKit.InactiveApp #> [CmdletBinding()] [OutputType('PSGraphKit.InactiveApp')] param( [ValidateRange(1, 3650)] [int] $InactiveDays = 90, [switch] $StaleOnly, [switch] $AsReport ) begin { Test-GkConnection -FunctionName 'Get-GkInactiveApp' | Out-Null $now = [datetime]::UtcNow } process { try { $activities = Invoke-GkGraphRequest -ApiVersion beta -Uri '/reports/servicePrincipalSignInActivities' -CallerFunction 'Get-GkInactiveApp' } catch { Write-Warning "Could not read service principal sign-in activities (beta report, requires AuditLog.Read.All, global cloud only). $($_.Exception.Message)" return } # appId -> displayName map (one call). $nameByAppId = @{} foreach ($sp in (Invoke-GkGraphRequest -Uri '/servicePrincipals?$select=appId,displayName&$top=999' -CallerFunction 'Get-GkInactiveApp')) { $aid = [string](Get-GkDictValue $sp 'appId') if ($aid) { $nameByAppId[$aid] = [string](Get-GkDictValue $sp 'displayName') } } foreach ($a in $activities) { $appId = [string](Get-GkDictValue $a 'appId') $last = $null foreach ($key in 'delegatedClientSignInActivity', 'delegatedResourceSignInActivity', 'applicationAuthenticationClientSignInActivity', 'applicationAuthenticationResourceSignInActivity') { $d = ConvertTo-GkDateTime (Get-GkDictValue (Get-GkDictValue $a $key) 'lastSignInDateTime') if ($null -ne $d -and ($null -eq $last -or $d -gt $last)) { $last = $d } } $never = ($null -eq $last) $inactive = if ($never) { $null } else { [int][math]::Floor(($now - $last).TotalDays) } $isStale = $never -or ($inactive -ge $InactiveDays) if ($StaleOnly -and -not $isStale) { continue } $obj = [ordered]@{ PSTypeName = 'PSGraphKit.InactiveApp' AppDisplayName = if ($nameByAppId.ContainsKey($appId)) { $nameByAppId[$appId] } else { $appId } AppId = $appId LastActivity = $last InactiveDays = $inactive NeverActive = $never IsStale = $isStale Id = [string](Get-GkDictValue $a 'id') } if ($AsReport) { $obj['ReportGeneratedUtc'] = $now } [pscustomobject]$obj } } } |