Entra/Get-AppRegistrationReport.ps1

<#
.SYNOPSIS
    Lists all Entra ID app registrations with credential expiry information.
.DESCRIPTION
    Queries Microsoft Graph for all application registrations in the tenant and
    reports on credential (password and certificate) status including counts and
    earliest expiry dates. Identifies applications with expired credentials that
    may cause service interruptions. Critical for security assessments and
    certificate lifecycle management.
 
    Requires Microsoft.Graph.Applications module and Application.Read.All permission.
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned
    to the pipeline.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service Graph -Scopes 'Application.Read.All'
    PS> .\Entra\Get-AppRegistrationReport.ps1
 
    Displays all app registrations with credential expiry details.
.EXAMPLE
    PS> .\Entra\Get-AppRegistrationReport.ps1 -OutputPath '.\app-registrations.csv'
 
    Exports app registration details to CSV for review.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

$ErrorActionPreference = 'Stop'

# Verify Graph connection
if (-not (Assert-GraphConnection)) { return }

# Ensure required Graph submodule is loaded (PS 7.x does not auto-import)
Import-Module -Name Microsoft.Graph.Applications -ErrorAction Stop

# Retrieve all app registrations
try {
    Write-Verbose "Retrieving all app registrations..."
    $applications = Get-MgApplication -All -Property 'Id','DisplayName','AppId','CreatedDateTime','SignInAudience','PasswordCredentials','KeyCredentials'
}
catch {
    Write-Error "Failed to retrieve app registrations: $_"
    return
}

$allApps = @($applications)
Write-Verbose "Processing $($allApps.Count) app registrations..."

if ($allApps.Count -eq 0) {
    Write-Verbose "No app registrations found"
    return
}

$now = Get-Date

$report = foreach ($app in $allApps) {
    $passwordCredCount = 0
    $keyCredCount = 0
    $expiredCount = 0
    $allExpiries = @()

    # Process password credentials (client secrets)
    if ($app.PasswordCredentials) {
        $passwordCredCount = $app.PasswordCredentials.Count
        foreach ($cred in $app.PasswordCredentials) {
            if ($cred.EndDateTime) {
                $allExpiries += $cred.EndDateTime
                if ($cred.EndDateTime -lt $now) {
                    $expiredCount++
                }
            }
        }
    }

    # Process key credentials (certificates)
    if ($app.KeyCredentials) {
        $keyCredCount = $app.KeyCredentials.Count
        foreach ($cred in $app.KeyCredentials) {
            if ($cred.EndDateTime) {
                $allExpiries += $cred.EndDateTime
                if ($cred.EndDateTime -lt $now) {
                    $expiredCount++
                }
            }
        }
    }

    # Find earliest expiry across all credentials
    $earliestExpiry = if ($allExpiries.Count -gt 0) {
        ($allExpiries | Sort-Object | Select-Object -First 1).ToString('yyyy-MM-dd HH:mm:ss')
    }
    else {
        ''
    }

    [PSCustomObject]@{
        DisplayName            = $app.DisplayName
        AppId                  = $app.AppId
        CreatedDateTime        = $app.CreatedDateTime
        SignInAudience         = $app.SignInAudience
        PasswordCredentialCount = $passwordCredCount
        KeyCredentialCount     = $keyCredCount
        EarliestExpiry         = $earliestExpiry
        ExpiredCredentials     = $expiredCount
    }
}

$report = @($report) | Sort-Object -Property DisplayName

Write-Verbose "Found $($report.Count) app registrations"

if ($OutputPath) {
    $report | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported app registration report ($($report.Count) apps) to $OutputPath"
}
else {
    Write-Output $report
}