Public/Get-OCM365AppRegistrationsExpiring.ps1


function Get-OCM365AppRegistrationsExpiring {
    <#
    .SYNOPSIS
        Retrieves Azure AD application registrations with expiring or expired credentials.

    .DESCRIPTION
        Gets all application registrations and identifies client secrets and certificates that are expired or expiring soon.
        Includes application owner information and credential expiry details.

    .PARAMETER ClientSecretsOnly
        If specified, only displays expiring/expired client secrets (excludes certificates).

    .PARAMETER CertificatesOnly
        If specified, only displays expiring/expired certificates (excludes client secrets).

    .PARAMETER SoonToExpireInDays
        Number of days to consider credentials as "soon to expire". Default is 60 days.
        Only credentials expiring within this threshold will be returned.

    .EXAMPLE
        Get-OCM365AppRegistrationsExpiring
        Retrieves all credentials (secrets and certificates) expiring within 60 days.

    .EXAMPLE
        Get-OCM365AppRegistrationsExpiring -ClientSecretsOnly -SoonToExpireInDays 30
        Retrieves only client secrets expiring within 30 days.

    .EXAMPLE
        Get-OCM365AppRegistrationsExpiring -CertificatesOnly -SoonToExpireInDays 90
        Retrieves only certificates expiring within 90 days.

    .NOTES
        Requires Microsoft Graph PowerShell module and an active Graph connection.
        Required Graph API permissions: Application.Read.All, Directory.Read.All
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [Switch]$ClientSecretsOnly,

        [Parameter()]
        [Switch]$CertificatesOnly,

        [Parameter()]
        [int]$SoonToExpireInDays = 60
    )

    begin {
        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Starting function with ClientSecretsOnly: $ClientSecretsOnly, CertificatesOnly: $CertificatesOnly, SoonToExpireInDays: $SoonToExpireInDays"
        
        # Check if connected to Microsoft Graph
        $mgContext = Get-MgContext
        if (-not $mgContext) {
            Write-Error "Not connected to Microsoft Graph. Please run Connect-MgGraph first." -ErrorAction Stop
            throw "Not connected to Microsoft Graph. Please run Connect-MgGraph first."
        }
        
        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Successfully validated Graph connection"

        # Check required permissions using the permission hierarchy function
        $requiredScopes = @('Application.Read.All', 'Directory.Read.All')
        if (-not (Test-OCM365GraphPermission -RequiredPermissions $requiredScopes -Scopes $mgContext.Scopes)) {
            $message = "Missing required permissions: $($requiredScopes -join ', '). Current scopes: $($mgContext.Scopes -join ', ')"
            Write-Error $message -ErrorAction Stop
            throw $message
        }
        
        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Permission validation successful"
        Write-Information "Starting application registration credential expiry scan" -InformationAction $InformationPreference
    }

    process {
        Write-Verbose "Retrieving all application registrations..." -Verbose:$VerbosePreference
        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Fetching applications with properties: DisplayName, AppId, Id, KeyCredentials, PasswordCredentials, CreatedDateTime, SigninAudience"
        
        try {
            $ExportResults = @()
            $AppCount = 0
            $PrintedCount = 0
            $RequiredProperties = @('DisplayName', 'AppId', 'Id', 'KeyCredentials', 'PasswordCredentials', 'CreatedDateTime', 'SigninAudience')
            
            $Applications = Get-MgApplication -All -Property $RequiredProperties -ErrorAction Stop -Verbose:$VerbosePreference -Debug:$DebugPreference
            $TotalApps = $Applications.Count
            
            Write-Information "Found $TotalApps application registrations to process" -InformationAction $InformationPreference
            Write-Verbose "Found $TotalApps applications - processing credentials..." -Verbose:$VerbosePreference
            Write-Debug "[Get-OCM365AppRegistrationsExpiring] Processing $TotalApps applications"

            foreach ($Application in $Applications) {
                $AppCount++
                $AppName = $Application.DisplayName
                $AppId = $Application.Id
                
                Write-Progress -Activity "Processing Application Registrations" `
                    -Status "Processing app $AppCount of $($TotalApps): $AppName" `
                    -PercentComplete (($AppCount / $TotalApps) * 100) `
                    -ProgressAction $ProgressPreference

                Write-Debug "[Get-OCM365AppRegistrationsExpiring] Processing application: $AppName (AppId: $($Application.AppId))"

                # Get application owners
                Write-Debug "[Get-OCM365AppRegistrationsExpiring] Retrieving owners for app: $AppId"
                $Owners = ""
                try {
                    $OwnerList = Get-MgApplicationOwner -ApplicationId $AppId -ErrorAction Stop -Verbose:$VerbosePreference -Debug:$DebugPreference
                    $Owners = ($OwnerList.AdditionalProperties.userPrincipalName) -join ", "
                }
                catch {
                    Write-Warning "Failed to retrieve owners for $AppName : $_"
                    Write-Debug "[Get-OCM365AppRegistrationsExpiring] Failed to get owners for AppId: $AppId"
                    $Owners = "-"
                }

                if ([string]::IsNullOrEmpty($Owners)) {
                    $Owners = "-"
                }

                Write-Debug "[Get-OCM365AppRegistrationsExpiring] App owners: $Owners"

                # Get secrets from the application
                $Secrets = $Application.PasswordCredentials
                $Certificates = $Application.KeyCredentials
                $AppCreationDate = $Application.CreatedDateTime
                $SigninAudience = $Application.SignInAudience

                Write-Debug "[Get-OCM365AppRegistrationsExpiring] Found $($Secrets.Count) client secrets and $($Certificates.Count) certificates"

                #region Process Client Secrets
                if (-not $CertificatesOnly.IsPresent) {
                    Write-Verbose "Processing $($Secrets.Count) client secrets for $AppName" -Verbose:$VerbosePreference
                    Write-Debug "[Get-OCM365AppRegistrationsExpiring] Processing client secrets for: $AppName"
                    
                    foreach ($Secret in $Secrets) {
                        $DisplayName = $Secret.DisplayName
                        $Id = $Secret.KeyId
                        $CreatedTime = $Secret.StartDateTime
                        $ExpiryDate = $Secret.EndDateTime
                        $DaysToExpiry = (New-TimeSpan -Start (Get-Date).Date -End $ExpiryDate).Days

                        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Secret: $DisplayName, Days to expiry: $DaysToExpiry"

                        # Check if credential meets filter criteria
                        $IncludeSecret = $true

                        if ($DaysToExpiry -lt 0) {
                            Write-Verbose "Secret '$DisplayName' has EXPIRED ($DaysToExpiry days)" -Verbose:$VerbosePreference
                        }
                        else {
                            Write-Verbose "Secret '$DisplayName' expires in $DaysToExpiry days" -Verbose:$VerbosePreference
                        }

                        # Filter for soon-to-expire client secrets
                        if ($DaysToExpiry -gt $SoonToExpireInDays) {
                            $IncludeSecret = $false
                            Write-Debug "[Get-OCM365AppRegistrationsExpiring] Secret excluded (expires beyond threshold): $DisplayName"
                        }

                        if ($IncludeSecret) {
                            $PrintedCount++
                            $ExpiryStatus = if ($DaysToExpiry -lt 0) { "Expired" } else { "Active" }
                            $FriendlyExpiryTime = if ($DaysToExpiry -lt 0) { "Expired $([Math]::Abs($DaysToExpiry)) days ago" } else { "Expires in $DaysToExpiry days" }

                            $ExportResult = [PSCustomObject]@{
                                'App Name'           = $AppName
                                'App Owners'         = $Owners
                                'App Creation Time'  = $AppCreationDate
                                'Credential Type'    = "Client Secret"
                                'Name'               = $DisplayName
                                'Id'                 = $Id
                                'Creation Time'      = $CreatedTime
                                'Expiry Date'        = $ExpiryDate
                                'Days to Expiry'     = $DaysToExpiry
                                'Friendly Expiry'    = $FriendlyExpiryTime
                                'App Id'             = $Application.AppId
                                'Status'             = $ExpiryStatus
                            }
                            $ExportResults += $ExportResult
                            Write-Information "Found expiring/expired secret: $AppName - $DisplayName (Days to expiry: $DaysToExpiry)" -InformationAction $InformationPreference
                        }
                    }
                }
                #endregion

                #region Process Certificates
                if (-not $ClientSecretsOnly.IsPresent) {
                    Write-Verbose "Processing $($Certificates.Count) certificates for $AppName" -Verbose:$VerbosePreference
                    Write-Debug "[Get-OCM365AppRegistrationsExpiring] Processing certificates for: $AppName"
                    
                    foreach ($Certificate in $Certificates) {
                        $DisplayName = $Certificate.DisplayName
                        $Id = $Certificate.KeyId
                        $CreatedTime = $Certificate.StartDateTime
                        $ExpiryDate = $Certificate.EndDateTime
                        $DaysToExpiry = (New-TimeSpan -Start (Get-Date).Date -End $ExpiryDate).Days

                        Write-Debug "[Get-OCM365AppRegistrationsExpiring] Certificate: $DisplayName, Days to expiry: $DaysToExpiry"

                        # Check if credential meets filter criteria
                        $IncludeCert = $true

                        if ($DaysToExpiry -lt 0) {
                            Write-Verbose "Certificate '$DisplayName' has EXPIRED ($DaysToExpiry days)" -Verbose:$VerbosePreference
                        }
                        else {
                            Write-Verbose "Certificate '$DisplayName' expires in $DaysToExpiry days" -Verbose:$VerbosePreference
                        }

                        # Filter for soon-to-expire certificates
                        if ($DaysToExpiry -gt $SoonToExpireInDays) {
                            $IncludeCert = $false
                            Write-Debug "[Get-OCM365AppRegistrationsExpiring] Certificate excluded (expires beyond threshold): $DisplayName"
                        }

                        if ($IncludeCert) {
                            $PrintedCount++
                            $ExpiryStatus = if ($DaysToExpiry -lt 0) { "Expired" } else { "Active" }
                            $FriendlyExpiryTime = if ($DaysToExpiry -lt 0) { "Expired $([Math]::Abs($DaysToExpiry)) days ago" } else { "Expires in $DaysToExpiry days" }

                            $ExportResult = [PSCustomObject]@{
                                'App Name'           = $AppName
                                'App Owners'         = $Owners
                                'App Creation Time'  = $AppCreationDate
                                'Credential Type'    = "Certificate"
                                'Name'               = $DisplayName
                                'Id'                 = $Id
                                'Creation Time'      = $CreatedTime
                                'Expiry Date'        = $ExpiryDate
                                'Days to Expiry'     = $DaysToExpiry
                                'Friendly Expiry'    = $FriendlyExpiryTime
                                'App Id'             = $Application.AppId
                                'Status'             = $ExpiryStatus
                            }
                            $ExportResults += $ExportResult
                            Write-Information "Found expiring/expired certificate: $AppName - $DisplayName (Days to expiry: $DaysToExpiry)" -InformationAction $InformationPreference
                        }
                    }
                }
                #endregion
            }

            Write-Progress -Activity "Processing Application Registrations" -Completed -ProgressAction $ProgressPreference
            
            Write-Information "Credential scan complete: Found $PrintedCount expiring/expired credentials across $AppCount applications" -InformationAction $InformationPreference
            Write-Verbose "Credential scan complete!" -Verbose:$VerbosePreference
            Write-Verbose "Processed $TotalApps applications - Found $PrintedCount expiring/expired credentials" -Verbose:$VerbosePreference
            Write-Debug "[Get-OCM365AppRegistrationsExpiring] Scan complete - Total credentials found: $PrintedCount"

            return $ExportResults
        }
        catch {
            Write-Error "Failed to retrieve application registration credentials: $_" -ErrorAction Stop
            throw
        }
    }
}