Public/Get-GkStaleAppCredential.ps1

function Get-GkStaleAppCredential {
    <#
    .SYNOPSIS
        Report app credentials (secrets/certificates) that have never been used or are long unused.

    .DESCRIPTION
        Reads GET /reports/appCredentialSignInActivities (beta) and reports the last time each app
        credential authenticated, so unused secrets/certs can be removed. App display names are
        resolved from /servicePrincipals.

        BETA endpoint (no v1.0 equivalent), global-cloud only. Requires AuditLog.Read.All.
        Unavailable data warns and returns nothing.

    .PARAMETER InactiveDays
        Staleness threshold in days (default 90).

    .PARAMETER StaleOnly
        Return only credentials that are stale (unused >= InactiveDays or never used).

    .PARAMETER AsReport
        Add a ReportGeneratedUtc column.

    .EXAMPLE
        Get-GkStaleAppCredential | Where-Object NeverUsed

        App credentials that have never authenticated — prime removal candidates.

    .EXAMPLE
        Get-GkStaleAppCredential -InactiveDays 180 -StaleOnly

        Credentials unused for 180+ days.

    .EXAMPLE
        Get-GkStaleAppCredential -AsReport | Export-Csv .\stale-credentials.csv -NoTypeInformation

    .OUTPUTS
        PSGraphKit.StaleAppCredential
    #>

    [CmdletBinding()]
    [OutputType('PSGraphKit.StaleAppCredential')]
    param(
        [ValidateRange(1, 3650)]
        [int] $InactiveDays = 90,

        [switch] $StaleOnly,

        [switch] $AsReport
    )

    begin {
        Test-GkConnection -FunctionName 'Get-GkStaleAppCredential' | Out-Null
        $now = [datetime]::UtcNow
    }

    process {
        try {
            $creds = Invoke-GkGraphRequest -ApiVersion beta -Uri '/reports/appCredentialSignInActivities' -CallerFunction 'Get-GkStaleAppCredential'
        }
        catch {
            Write-Warning "Could not read app credential sign-in activities (beta report, requires AuditLog.Read.All, global cloud only). $($_.Exception.Message)"
            return
        }

        $nameByAppId = @{}
        foreach ($sp in (Invoke-GkGraphRequest -Uri '/servicePrincipals?$select=appId,displayName&$top=999' -CallerFunction 'Get-GkStaleAppCredential')) {
            $aid = [string](Get-GkDictValue $sp 'appId')
            if ($aid) { $nameByAppId[$aid] = [string](Get-GkDictValue $sp 'displayName') }
        }

        foreach ($c in $creds) {
            $appId = [string](Get-GkDictValue $c 'appId')
            $last  = ConvertTo-GkDateTime (Get-GkDictValue (Get-GkDictValue $c 'signInActivity') 'lastSignInDateTime')
            $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.StaleAppCredential'
                AppDisplayName   = if ($nameByAppId.ContainsKey($appId)) { $nameByAppId[$appId] } else { $appId }
                AppId            = $appId
                KeyId            = [string](Get-GkDictValue $c 'keyId')
                KeyType          = [string](Get-GkDictValue $c 'keyType')
                CredentialOrigin = [string](Get-GkDictValue $c 'credentialOrigin')
                LastUsed         = $last
                InactiveDays     = $inactive
                NeverUsed        = $never
                IsStale          = $isStale
            }
            if ($AsReport) { $obj['ReportGeneratedUtc'] = $now }
            [pscustomobject]$obj
        }
    }
}