AzureAppGWCertificateMetaData.ps1
|
<#PSScriptInfo
.VERSION 1.0.0 .GUID 1d923b03-23e2-4bb9-91d2-b9501cedb742 .AUTHOR Brandyn Koehler .COMPANYNAME b-rito .COPYRIGHT (c) 2025 Brandyn Koehler .TAGS Azure Az ApplicationGateway AzNetworking Certificates Metadata .LICENSEURI https://opensource.org/licenses/MIT .PROJECTURI https://github.com/b-rito/PowerShell/Azure/ApplicationGateway/AzureAppGWCertificateMetaData.ps1 .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.0.0 - Gather certificate metadata for Application Gateway resources - Retrieves certificate chain details for listener (Key Vault–backed when associated) .PRIVATEDATA #> #Requires -Module Az.Network #Requires -Module Az.Resources #Requires -Module Az.Accounts <# .SYNOPSIS Collects Azure Application Gateway listener certificate metadata (thumbprints, CN, SANs, issuer, validity). .DESCRIPTION This single-file script queries one or more Azure Application Gateways and returns formatted certificate metadata for listener certificates, including full chain information when available. Supports both targeting a specific gateway by ResourceId and scanning gateways across a subscription (optionally limited to a resource group). Output can be written to console or saved to a file. Transcript logging is available for troubleshooting. .PARAMETER ResourceId The Application Gateway resource ID. Example: "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.Network/applicationGateways/<appgw-name>" .PARAMETER SubscriptionId The Azure subscription ID to scan for Application Gateways when not using -ResourceId. .PARAMETER ResourceGroupName (Optional) The resource group name to limit scanning when using -SubscriptionId. .PARAMETER OutputFile (Optional) File path for saving formatted certificate metadata (UTF-8). If omitted, writes to console. .PARAMETER TranscriptLog (Optional) Switch. When provided, starts a transcript in the current directory and stops it on completion. .EXAMPLE .\AppGWCertificateMetadata.ps1 -ResourceId "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.Network/applicationGateways/<AppGw>" .EXAMPLE .\AppGWCertificateMetadata.ps1 -SubscriptionId "<sub-id>" -OutputFile ".\AppGWCertMetaData.txt" .EXAMPLE .\AppGWCertificateMetadata.ps1 -SubscriptionId "<sub-id>" -ResourceGroupName "<rg>" -OutputFile ".\AppGWCertMetaData.txt" -TranscriptLog .INPUTS String .OUTPUTS String .NOTES Requires Az context permissions to read Application Gateway resources and invoke ARM REST API operations. Non-interactive environments (e.g., CI) are auto-accepted for bulk operations. .LINK Project: https://github.com/b-rito/PowerShell/Azure/ApplicationGateway/AzureAppGWCertificateMetaData.ps1 Docs: https://github.com/b-rito/PowerShell/Azure/ApplicationGateway/AzureAppGWCertificateMetaData.ps1 #> #Requires -Version 5.1 Param( [Parameter(ParameterSetName = "One", Mandatory=$True, Position = 0)][string] $ResourceId, [Parameter(ParameterSetName = "Many", Mandatory=$True, Position = 0)][string] $SubscriptionId, [Parameter(ParameterSetName = "Many", Mandatory=$False)][string] $ResourceGroupName, [Parameter(Mandatory=$False)][string] $OutputFile, [Parameter(Mandatory=$False)][switch] $TranscriptLog ) # For additional output of all done if ($TranscriptLog) { $transcriptPath = "AppGWCertMetaData_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" Start-Transcript -Path $transcriptPath } # Validate Modules installed if (!(Get-Module -ListAvailable -Name Az.Network)) { Write-Error "You need 'Az' module to proceed. Az is a new cross-platform PowerShell module that will replace AzureRM. You can install this module by running 'Install-Module Az' in an elevated PowerShell prompt." Write-Warning "If you see error 'AzureRM.Profile already loaded. Az and AzureRM modules cannot be imported in the same session', You would need to close the current session and start new one." exit } # Validate resourceId if ($PSCmdlet.ParameterSetName -eq "One") { $matchResponse = $resourceId -match "/subscriptions/(.*?)/resourceGroups/" if(!$matchResponse) { Write-Warning "Invalid ResourceId format $resourceId." exit } $subscriptionId = $matches[1] } # Validating Set-Context success, suppress common warnings Set-AzContext -Subscription $subscriptionId -ErrorVariable contextFailure -WarningAction SilentlyContinue | Out-Null if ($contextFailure) { Write-Warning "Unable to set subscription $subscriptionId in context. Please retry again." exit } ### Script Scoped Functions and Variables Function Build-Cert { param( [string]$Thumbprint, [string]$Sans, [string]$CNname, [string]$IssuerName, [string]$ValidityUpto ) [PSCustomObject]@{ Thumbprint = $Thumbprint Sans = $Sans CNname = $CNname IssuerName = $IssuerName ValidityUpto = $ValidityUpto } } Function Build-CertContainer { param( [string]$CertName, [System.Collections.Generic.List[object]]$Certificates ) [PSCustomObject]@{ CertName = $CertName Certificates = $Certificates } } Function Build-GatewayContainer { param( [string]$ResourceId, [System.Collections.Generic.List[object]]$CertificateList ) [PSCustomObject]@{ ResourceId = $ResourceId CertificateList = $CertificateList } } Function GetAllApplicationGateways { $Script:RunningGateways = @() $Private:StoppedGateways = @() if ($ResourceGroupName) { $Private:AllGateways = (Get-AzApplicationGateway -ResourceGroupName $ResourceGroupName | Select-Object Id, Sku, OperationalState, ProvisioningState) } else { $Private:AllGateways = (Get-AzApplicationGateway | Select-Object Id, Sku, OperationalState, ProvisioningState) } $Private:SupportedGateways = $Private:AllGateways | Where-Object { $_.Sku.Name -like "*v2" -or $_.Sku.Name -like "basic" } $Private:SupportedGateways | Foreach-Object { if ($_.OperationalState -eq "Running") { $Script:RunningGateways += $_ } else { $Private:StoppedGateways += $_.Id } } if ($Script:RunningGateways.Count -eq 0) { Write-Warning "No supported Application Gateways in 'Running' state were found in subscription $subscriptionId. Exiting script." exit } if ($Private:StoppedGateways.Count -gt 0) { Write-Warning "There are $($Private:StoppedGateways.Count) Application Gateways not in 'Running' state and will be skipped." $Private:StoppedGatewaysFile = "StoppedAppGateways_$(Get-Date -Format 'yyyyMMdd').txt" $Private:StoppedGateways | Out-File -FilePath $Private:StoppedGatewaysFile Write-Warning "The list of stopped Application Gateways has been saved to $Private:StoppedGatewaysFile" } } ### Private Scoped Functions Function Private:ValidateUserContinuation() { if ($env:CI -or -not $Host.UI.RawUI.KeyAvailable) { Write-Output "Non-interactive environment detected — Continuing." Continue } GetAllApplicationGateways $Count = $Script:RunningGateways.Count if ($Count -eq 1) { $Question = "There was $Count supported Application Gateway V2 found in the Subscription. Do you want to continue?" } else { $Question = "There were $Count supported Application Gateway V2s found. Do you want to continue?" } $Choices = [System.Management.Automation.Host.ChoiceDescription[]]@( (New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue the operation."), (New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Cancel the operation.") ) $Decision = $Host.UI.PromptForChoice($null, $Question, $Choices, 1) if ($Decision -eq 0) { Write-Output "Continuing..." } else { Write-Output "Cancelled by user." exit } } Function Private:GetListenerCertificateMetadata { param( [Parameter(Mandatory=$true)]$Resource ) $Path = "$($resource)/getListenerCertificateMetadata?api-version=2025-03-01" $Request = Invoke-AzRestMethod -Path $Path -Method POST $Content = $Request.Content | ConvertFrom-Json if ($Request.StatusCode -eq 400) { Write-Error "Invoke-AzRestMethod returned an 400. Details: $($Content.error.code)" Return } if ($Request.StatusCode -ne 202) { Write-Error "Invoke-AzRestMethod returned an unexpected status code. Expected 202, received $($Request.StatusCode). Details: $($Request.Content)" Return } Do { Write-Debug "Executing GetListenerCertificateMetadata for $resource" $Count++ Write-Debug "Retrieval attempt $Count" Start-Sleep -Seconds 1 $Response = Invoke-AzRestMethod -Path $Request.Headers.Location.PathAndQuery -Method GET $Status = $Response.StatusCode } While ($Status -ne "200") $listenerCertificateJson = ($Response.Content | ConvertFrom-Json).listenerCertificateDetails foreach ($name in $listenerCertificateJson.psobject.Properties.Name) { $listenerCertificateJson.$name = @($listenerCertificateJson.$name) } Return $listenerCertificateJson } Function Private:CleanCertificateMetadata { param( [Parameter(Mandatory=$true)]$ResourceId, [Parameter(Mandatory=$true)][PSCustomObject]$CertMetadata ) $CertificateList = [System.Collections.Generic.List[object]]::new() foreach ($certGroup in $CertMetadata.psobject.Properties) { $FullChain = [System.Collections.Generic.List[object]]::new() $certName = $certGroup.Name foreach ($cert in $certGroup.Value) { $temp = Build-Cert -Thumbprint $cert.thumbprint -Sans $cert.saNs -CNname $cert.cnName -IssuerName $cert.issuerName -ValidityUpto $cert.validityUpto $FullChain.Add($temp) } $CertificateContainer = Build-CertContainer -CertName $certName -Certificates $FullChain $CertificateList.Add($CertificateContainer) } Return Build-GatewayContainer -ResourceId $ResourceId -CertificateList $CertificateList } Function Private:FormatCertificateOutput { param( [Parameter(Mandatory = $true)]$CertContainer, [string]$OutputFile = $null ) $outputLines = @() $outputLines += "==================== Application Gateway Certificate Metadata ====================" $outputLines += "Resource: $($CertContainer.ResourceId)" $outputLines += "`tListener Certificate Metadata" foreach ($chain in $CertContainer.CertificateList) { $outputLines += "`t========================================" $outputLines += "`tCert Chain: $($chain.CertName)" # Loop through each certificate in this chain foreach ($cert in $chain.Certificates) { $line = "`t`tThumbprint: $($cert.Thumbprint)`n" $line += "`t`tCN: $($cert.CNname)`n" if ($cert.Sans -ne "") { $line += "`t`tSANs: $($cert.Sans -join ', ')`n" } $line += "`t`tIssuer: $($cert.IssuerName)`n" $line += "`t`tValid Until: $($cert.ValidityUpto)`n" $outputLines += $line } } # Output to console $outputLines | ForEach-Object { Write-Output $_ } # Optionally output to file if ($OutputFile) { $outputLines | Out-File -Append -FilePath $OutputFile -Encoding UTF8 } } $sw = [Diagnostics.Stopwatch]::StartNew() try{ # Validate incase there are more than 1 AppGW found switch ($PSCmdlet.ParameterSetName) { 'One' { $CertificateMetadata = GetListenerCertificateMetadata -Resource $ResourceId # Clean Listner Metadata $CleanedCertificates = CleanCertificateMetadata -ResourceId $ResourceId -CertMetadata $CertificateMetadata # Output and format FormatCertificateOutput -CertContainer $CleanedCertificates -OutputFile $OutputFile } 'Many'{ ValidateUserContinuation ForEach ($ResourceId in $Script:RunningGateways.Id) { # Get Listener Metadata by AppGW Resource ID $CertificateMetadata = GetListenerCertificateMetadata -Resource $ResourceId # Clean Listner Metadata from 1 AppGW $CleanedCertificates = CleanCertificateMetadata -ResourceId $ResourceId -CertMetadata $CertificateMetadata # Output and format FormatCertificateOutput -CertContainer $CleanedCertificates -OutputFile $OutputFile } } } } catch [Exception] { Write-Output $_.Exception | Format-List -Force } finally { $completionTime = "Application Gateway Certificate Metadata completed. TimeTaken : $($sw.Elapsed.TotalSeconds) seconds" Write-Output $completionTime if ($OutputFile) { Add-Content -Path $OutputFile -Value $completionTime } if ($TranscriptLog) { Stop-Transcript } } |