Public/Get-TntAppRegistrationExpiryReport.ps1
|
function Get-TntAppRegistrationExpiryReport { <# .SYNOPSIS Reports on app registration credential expiry status. .DESCRIPTION Queries Microsoft Graph for all application registrations and checks their password and key (certificate) credentials for expiry. Categorizes each credential as Expired, Expiring Soon (within threshold), or Valid. .PARAMETER TenantId The Azure AD Tenant ID (GUID) to connect to. .PARAMETER ClientId The Application (Client) ID of the app registration created for security reporting. .PARAMETER ClientSecret The client secret for the app registration. Use this for automated scenarios. .PARAMETER CertificateThumbprint The thumbprint of the certificate to use for authentication instead of client secret. .PARAMETER DaysUntilExpiry Number of days threshold for 'Expiring Soon' classification. Defaults to 30. .EXAMPLE Get-TntAppRegistrationExpiryReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret Checks all app registrations for credentials expiring within 30 days. .EXAMPLE Get-TntAppRegistrationExpiryReport -TenantId $tid -ClientId $cid -ClientSecret $secret -DaysUntilExpiry 90 Checks for credentials expiring within 90 days. .OUTPUTS System.Management.Automation.PSCustomObject Returns a structured object containing: - Summary: Total apps, expired/expiring credential counts - Credentials: Detailed per-credential records with expiry info .NOTES Author: Tom de Leeuw Website: https://systom.dev Module: TenantReports Required Permissions: - Application.Read.All (Application) .LINK https://systom.dev #> [CmdletBinding(DefaultParameterSetName = 'ClientSecret')] [OutputType([System.Management.Automation.PSCustomObject])] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')] [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')] [ValidateNotNullOrEmpty()] [Alias('Tenant')] [string]$TenantId, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')] [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')] [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')] [Alias('ApplicationId')] [string]$ClientId, [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Secret', 'ApplicationSecret')] [SecureString]$ClientSecret, [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Thumbprint')] [string]$CertificateThumbprint, [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')] [switch]$Interactive, [Parameter()] [ValidateRange(1, 365)] [int]$DaysUntilExpiry = 30 ) begin { Write-Information 'STARTED : App registration credential expiry analysis...' -InformationAction Continue } process { try { $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters $ConnectionInfo = Connect-TntGraphSession @ConnectionParams $Credentials = [System.Collections.Generic.List[PSCustomObject]]::new() $Now = [DateTime]::Now $ExpiryThreshold = $Now.AddDays($DaysUntilExpiry) # Page through all applications $Uri = 'https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials,keyCredentials&$top=999' Write-Verbose 'Retrieving application registrations...' do { $Response = Invoke-MgGraphRequest -Uri $Uri -Method GET -ErrorAction Stop foreach ($App in $Response.value) { # Process password credentials (client secrets) foreach ($Cred in $App.passwordCredentials) { $EndDate = [DateTime]$Cred.endDateTime $DaysRemaining = [Math]::Round(($EndDate - $Now).TotalDays, 0) $Status = if ($EndDate -lt $Now) { 'Expired' } elseif ($EndDate -le $ExpiryThreshold) { 'ExpiringSoon' } else { 'Valid' } $Credentials.Add([PSCustomObject]@{ AppDisplayName = $App.displayName AppId = $App.appId ObjectId = $App.id CredentialType = 'ClientSecret' CredentialName = $Cred.displayName KeyId = $Cred.keyId StartDate = $Cred.startDateTime EndDate = $EndDate DaysRemaining = $DaysRemaining Status = $Status }) } # Process key credentials (certificates) foreach ($Cred in $App.keyCredentials) { $EndDate = [DateTime]$Cred.endDateTime $DaysRemaining = [Math]::Round(($EndDate - $Now).TotalDays, 0) $Status = if ($EndDate -lt $Now) { 'Expired' } elseif ($EndDate -le $ExpiryThreshold) { 'ExpiringSoon' } else { 'Valid' } $Credentials.Add([PSCustomObject]@{ AppDisplayName = $App.displayName AppId = $App.appId ObjectId = $App.id CredentialType = 'Certificate' CredentialName = $Cred.displayName KeyId = $Cred.keyId StartDate = $Cred.startDateTime EndDate = $EndDate DaysRemaining = $DaysRemaining Status = $Status }) } } $Uri = $Response.'@odata.nextLink' } while ($Uri) $Expired = @($Credentials.Where({ $_.Status -eq 'Expired' })) $ExpiringSoon = @($Credentials.Where({ $_.Status -eq 'ExpiringSoon' })) $Valid = @($Credentials.Where({ $_.Status -eq 'Valid' })) $AppsWithIssues = ($Credentials.Where({ $_.Status -in 'Expired', 'ExpiringSoon' }) | Select-Object -ExpandProperty AppId -Unique).Count $Summary = [PSCustomObject]@{ TenantId = $TenantId ReportGeneratedDate = $Now DaysUntilExpiryThreshold = $DaysUntilExpiry TotalCredentials = $Credentials.Count ExpiredCount = $Expired.Count ExpiringSoonCount = $ExpiringSoon.Count ValidCount = $Valid.Count AppsWithExpiredOrExpiring = $AppsWithIssues } Write-Information "FINISHED : App registration expiry analysis - $($Expired.Count) expired, $($ExpiringSoon.Count) expiring soon." -InformationAction Continue [PSCustomObject][Ordered]@{ Summary = $Summary Credentials = $Credentials.ToArray() } } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Get-TntAppRegistrationExpiryReport failed: $($_.Exception.Message)", $_.Exception), 'GetTntAppRegistrationExpiryReportError', [System.Management.Automation.ErrorCategory]::OperationStopped, $TenantId ) $PSCmdlet.ThrowTerminatingError($errorRecord) } finally { if ($ConnectionInfo.ShouldDisconnect) { Disconnect-TntGraphSession -ConnectionState $ConnectionInfo } } } } |