Scripts/Get-AppRegLastUsedDate.ps1
|
<#
.SYNOPSIS Returns all App Registrations with their last used date from the current tenant. .DESCRIPTION Queries Microsoft Graph for all App Registrations and their corresponding Service Principals to extract the signInActivity (ALL-TIME last sign-in dates). Returns a simple PSObject list with app name, appId, created date, owner, and the most recent sign-in date from signInActivity. Requires an active Graph session (Connect-MgGraph). .PARAMETER IncludeMicrosoftApps (Optional) Include Microsoft 1st-party apps in the output. By default they are excluded to reduce noise. .EXAMPLE .\Get-AppRegLastUsedDate.ps1 # Returns all non-Microsoft app registrations with last used dates. .EXAMPLE .\Get-AppRegLastUsedDate.ps1 | Sort-Object LastUsedDate | Format-Table -AutoSize # Sorted by last used date ascending (oldest/never-used first). .EXAMPLE .\Get-AppRegLastUsedDate.ps1 | Where-Object { $_.LastUsedDate -eq "Never" } | Export-Csv -Path "C:\unused_apps.csv" -NoTypeInformation # Export apps that have never been used. .EXAMPLE .\Get-AppRegLastUsedDate.ps1 -IncludeMicrosoftApps # Include Microsoft 1st-party apps in the output. .NOTES Author: Lakshmanachari Panuganti Created: 8th March 2026 Version: 1.0 Prerequisites: PowerShell 7.2+, Microsoft.Graph.Authentication, Microsoft.Graph.Applications Required Scopes: Application.Read.All, Directory.Read.All #> #Requires -Version 7.2 [CmdletBinding()] param ( [switch]$IncludeMicrosoftApps ) $ErrorActionPreference = "Stop" $MicrosoftTenantId = "f8cdef31-a31e-4b4a-93e4-5f571e91255a" # ── Validate Graph session ── $mgContext = Get-MgContext if ($null -eq $mgContext) { throw "No Graph session. Run: Connect-MgGraph -Scopes 'Application.Read.All','Directory.Read.All'" } try { $null = Get-MgOrganization -ErrorAction Stop | Select-Object -First 1 } catch { throw "Graph token invalid or expired. Reconnect with: Connect-MgGraph -Scopes 'Application.Read.All','Directory.Read.All'" } $tenantId = $mgContext.TenantId Write-Host "Tenant: $tenantId | Account: $($mgContext.Account ?? $mgContext.AppName)" -ForegroundColor Cyan # ── Load App Registrations ── Write-Host "Loading App Registrations..." -ForegroundColor White $allApps = Get-MgApplication -All -ExpandProperty "owners" -Property @( "id", "appId", "displayName", "createdDateTime", "signInAudience", "passwordCredentials", "keyCredentials" ) Write-Host " $($allApps.Count) App Registrations loaded" -ForegroundColor Green # ── Load Service Principals (for signInActivity) ── Write-Host "Loading Service Principals (signInActivity)..." -ForegroundColor White $allSPs = Get-MgServicePrincipal -All -Property @( "appId", "displayName", "accountEnabled", "appOwnerOrganizationId", "servicePrincipalType", "signInActivity" ) Write-Host " $($allSPs.Count) Service Principals loaded" -ForegroundColor Green # Build SP lookup by AppId $spLookup = @{} foreach ($sp in $allSPs) { $spLookup[$sp.AppId] = $sp } # ── Build result ── Write-Host "Processing..." -ForegroundColor White $results = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($app in $allApps) { $sp = $spLookup[$app.AppId] # Skip Microsoft 1st-party unless requested if (-not $IncludeMicrosoftApps -and $null -ne $sp -and $sp.AppOwnerOrganizationId -eq $MicrosoftTenantId) { continue } # Best sign-in date from signInActivity (ALL-TIME, no retention limit) $lastInteractive = $sp.SignInActivity.LastSignInDateTime $lastNonInteractive = $sp.SignInActivity.LastNonInteractiveSignInDateTime $bestDate = $null if ($null -ne $lastInteractive) { $bestDate = $lastInteractive } if ($null -ne $lastNonInteractive -and ($null -eq $bestDate -or $lastNonInteractive -gt $bestDate)) { $bestDate = $lastNonInteractive } $daysSinceUse = if ($null -ne $bestDate) { [int]((Get-Date) - $bestDate).TotalDays } else { -1 } # Owner $ownerUPN = if ($app.Owners.Count -gt 0) { ($app.Owners | ForEach-Object { $_.AdditionalProperties.userPrincipalName ?? $_.AdditionalProperties.displayName ?? $_.Id }) -join ";" } else { "NoOwner" } # Credential status $now = Get-Date $secretCount = $app.PasswordCredentials.Count $hasActiveSecret = ($app.PasswordCredentials | Where-Object { $_.EndDateTime -ge $now }).Count -gt 0 $certCount = $app.KeyCredentials.Count $results.Add([PSCustomObject]@{ AppName = $app.DisplayName AppId = $app.AppId ObjectId = $app.Id CreatedDate = $app.CreatedDateTime SignInAudience = $app.SignInAudience Owner = $ownerUPN HasServicePrincipal = ($null -ne $sp) SPEnabled = if ($null -ne $sp) { $sp.AccountEnabled } else { "N/A" } LastInteractiveSignIn = if ($null -ne $lastInteractive) { $lastInteractive } else { "Never" } LastDaemonSignIn = if ($null -ne $lastNonInteractive) { $lastNonInteractive } else { "Never" } LastUsedDate = if ($null -ne $bestDate) { $bestDate } else { "Never" } DaysSinceLastUse = $daysSinceUse SecretCount = $secretCount HasActiveSecret = $hasActiveSecret CertCount = $certCount }) } Write-Host " $($results.Count) apps processed" -ForegroundColor Green Write-Host "" # Return the object for pipeline use $results |