Public/Get-IOExpiringSecrets.ps1

function Get-IOExpiringSecrets {
    <#
    .SYNOPSIS
        Finds app registrations with client secrets or certificates expiring within a specified number of days.
    .EXAMPLE
        Get-IOExpiringSecrets -DaysUntilExpiry 30
    .EXAMPLE
        Get-IOExpiringSecrets -DaysUntilExpiry 90 -IncludeExpired -ToCsv "expiring-secrets.csv"
    #>

    [CmdletBinding()]
    param(
        [ValidateRange(1, 3650)]
        [int]$DaysUntilExpiry = 30,

        [switch]$IncludeExpired,

        [string]$ToCsv
    )

    $cmdName = $MyInvocation.MyCommand.Name
    Write-IOLog "Scanning app registrations for secrets/certs expiring within $DaysUntilExpiry days..." -Level Info -Component $cmdName

    $now      = [datetime]::UtcNow
    $cutoff   = $now.AddDays($DaysUntilExpiry)
    $results  = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Fetch all app registrations with credential fields
    $apps = Invoke-IOGraphRequest -Uri 'v1.0/applications?$select=id,displayName,appId,passwordCredentials,keyCredentials'

    $total = ($apps | Measure-Object).Count
    Write-IOLog "Evaluating $total app registration(s)..." -Level Verbose -Component $cmdName
    if ($total -eq 0) {
        Export-IOResult -Data @() -ToCsv $ToCsv -CommandName $cmdName
        return
    }
    $counter = 0

    try {
    foreach ($app in $apps) {
        $counter++
        if ($counter % 50 -eq 0) {
            Write-Progress -Activity 'Scanning secrets & certificates' -Status "$counter / $total apps" -PercentComplete (($counter / $total) * 100)
        }

        # ── Password credentials (client secrets) ─────────────────────────
        if ($app.passwordCredentials) {
            foreach ($cred in $app.passwordCredentials) {
                if (-not $cred.endDateTime) { continue }
                $endDate = [datetime]::Parse($cred.endDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal)

                $isExpired  = $endDate -lt $now
                $isExpiring = $endDate -le $cutoff

                if ($isExpiring -or ($IncludeExpired -and $isExpired)) {
                    $results.Add([PSCustomObject]@{
                        ApplicationName   = $app.displayName
                        ApplicationId     = $app.appId
                        ObjectId          = $app.id
                        CredentialType    = 'ClientSecret'
                        CredentialName    = $cred.displayName
                        KeyId             = $cred.keyId
                        ExpiryDate        = $endDate.ToString('yyyy-MM-dd')
                        DaysRemaining     = [math]::Floor(($endDate - $now).TotalDays)
                        Status            = if ($isExpired) { 'EXPIRED' } else { 'EXPIRING' }
                    })
                }
            }
        }

        # ── Key credentials (certificates) ────────────────────────────────
        if ($app.keyCredentials) {
            foreach ($cred in $app.keyCredentials) {
                if (-not $cred.endDateTime) { continue }
                $endDate = [datetime]::Parse($cred.endDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal)

                $isExpired  = $endDate -lt $now
                $isExpiring = $endDate -le $cutoff

                if ($isExpiring -or ($IncludeExpired -and $isExpired)) {
                    $results.Add([PSCustomObject]@{
                        ApplicationName   = $app.displayName
                        ApplicationId     = $app.appId
                        ObjectId          = $app.id
                        CredentialType    = 'Certificate'
                        CredentialName    = $cred.displayName
                        KeyId             = $cred.keyId
                        ExpiryDate        = $endDate.ToString('yyyy-MM-dd')
                        DaysRemaining     = [math]::Floor(($endDate - $now).TotalDays)
                        Status            = if ($isExpired) { 'EXPIRED' } else { 'EXPIRING' }
                    })
                }
            }
        }
    }
    }
    finally {
        Write-Progress -Activity 'Scanning secrets & certificates' -Completed
    }

    $sorted = $results | Sort-Object DaysRemaining
    Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName
}