<# .SYNOPSIS Inventories and monitors Azure App Service certificates across all enabled subscriptions. .DESCRIPTION This script performs a comprehensive audit of all Azure App Service certificates across all enabled subscriptions in your Azure tenant. It extracts certificate details including expiration dates, thumbprints, subject names, and calculates the remaining days until expiration. The script is designed for: - Certificate lifecycle management and monitoring - Proactive identification of expiring certificates - Compliance auditing and reporting - Security assessments of certificate inventory - Planning certificate renewal activities The script processes all enabled subscriptions automatically and exports results to a timestamped CSV file, making it suitable for automated monitoring and reporting workflows. Key features: - Multi-subscription certificate discovery - Expiration date calculation with days remaining - Error handling for inaccessible or invalid certificates - Detailed logging and progress reporting - CSV export for further analysis and alerting .PARAMETER None This script does not accept parameters and processes all enabled subscriptions automatically. .EXAMPLE .\Certificates.ps1 Runs the certificate inventory across all enabled subscriptions and exports results to a timestamped CSV file in the current directory. .EXAMPLE # Schedule for automated monitoring $scriptPath = "C:\Scripts\Certificates.ps1" & $scriptPath Executes the script from a scheduled task or automation workflow for regular certificate monitoring. .EXAMPLE # Run and immediately view results .\Certificates.ps1 Get-Content ".\$(Get-Date -Format 'yyyy-MM-dd HHmm') azure_appservice_certificates.csv" Runs the script and displays the generated CSV content for immediate review. .NOTES Author: Cloud Engineering Team Version: 1.0 Prerequisites: - Azure PowerShell module (Az) must be installed - User must be authenticated to Azure (Connect-AzAccount) - User must have at least 'Reader' permissions across target subscriptions - Access to Microsoft.Web/certificates resources Required Permissions: - Reader access to subscriptions containing App Service certificates - Web App Certificate Reader or App Service Certificate Reader permissions - Resource Group Reader permissions for certificate resource groups Output File: - Format: "YYYY-MM-DD HHMM azure_appservice_certificates.csv" - Location: Current directory - Content: Certificate inventory with expiration analysis Certificate Status Analysis: - TotalDays > 30: Certificate is healthy - TotalDays 7-30: Certificate expires soon (warning) - TotalDays < 7: Certificate expires very soon (critical) - TotalDays < 0: Certificate has already expired (urgent action required) Performance Considerations: - Processing time depends on the number of subscriptions and certificates - Large tenants with many certificates may require extended execution time - Network latency affects certificate detail retrieval Security and Compliance: - Certificate thumbprints and subject names are included in output - Ensure proper access controls on generated CSV files - Consider encryption for sensitive certificate inventory data - Regular execution recommended for proactive certificate management Common Use Cases: - Monthly certificate expiration reports - Pre-renewal planning and notifications - Compliance audits requiring certificate inventory - Security assessments of certificate lifecycle management .LINK https://docs.microsoft.com/en-us/azure/app-service/configure-ssl-certificate https://docs.microsoft.com/en-us/powershell/module/az.websites/ #> # Ensure user is authenticated to Azure # Uncomment the following line if authentication is needed: # Connect-AzAccount # Display script header and configuration Write-Host "======================================================================================================================================================================" Write-Host "Azure App Service Certificate Inventory and Monitoring" Write-Host "======================================================================================================================================================================" Write-Host "Starting certificate discovery across all enabled subscriptions..." Write-Host "Script execution started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Host "" # Generate timestamped filename for export [string] $date = Get-Date -Format "yyyy-MM-dd HHmm" $fileName = ".\$date azure_appservice_certificates.csv" Write-Host "Export file: $fileName" Write-Host "" # Get all enabled subscriptions for processing Write-Host "Retrieving enabled subscriptions..." try { $subscriptions = Get-AzSubscription | Where-Object State -eq "Enabled" Write-Host "✓ Found $($subscriptions.Count) enabled subscription(s) to process:" foreach ($sub in $subscriptions) { Write-Host " - $($sub.Name) ($($sub.Id))" } } catch { Write-Error "Failed to retrieve Azure subscriptions. Please ensure you are authenticated (Connect-AzAccount)" throw $_ } Write-Host "" # Define certificate information class for structured data collection class CertificateCheck { # Azure subscription identifier containing the certificate [string] $SubscriptionId = "" # Full Azure resource ID of the certificate [string] $CertificateId = "" # Resource group name where the certificate is deployed [string] $ResourceGroupName = "" # Certificate subject name (Common Name and additional fields) [string] $SubjectName = "" # Certificate thumbprint (SHA-1 hash identifier) [string] $ThumbPrint = "" # Certificate expiration date and time [DateTime] $ExpirationDate # Number of days remaining until expiration (negative if expired) [double] $TotalDays # Certificate health status (Expired, Critical, Warning, Healthy, Error) [string] $Health = "" # Error messages or status comments for problematic certificates [string] $Comment = "" } # Initialize result collection and processing variables [CertificateCheck[]]$Result = @() $StartDate = (Get-Date) $totalCertificates = 0 $processedSubscriptions = 0 $certificatesWithIssues = 0 Write-Host "======================================================================================================================================================================" Write-Host "Processing Certificates by Subscription" Write-Host "======================================================================================================================================================================" # Process each enabled subscription foreach ($subscription in $subscriptions) { Write-Host "" Write-Host "Processing subscription: $($subscription.Name) ($($subscription.Id))" try { # Set Azure context to current subscription Set-AzContext -SubscriptionId $subscription.Id -ErrorAction Stop | Out-Null Write-Host "✓ Successfully connected to subscription" # Retrieve all App Service certificates in the subscription Write-Host "Discovering App Service certificates..." $certs = Get-AzResource -ResourceType Microsoft.Web/certificates -ApiVersion "2018-02-01" -ExpandProperties | Select-Object * -ExpandProperty Properties if ($certs) { Write-Host "✓ Found $($certs.Count) certificate(s) in subscription" $subscriptionCertCount = 0 # Process each certificate found foreach ($cert in $certs) { $id = $cert.Id Write-Host " Processing certificate: $($cert.Name)" # Create new certificate check instance [CertificateCheck] $certificateCheck = [CertificateCheck]::new() # Populate basic certificate information $certificateCheck.SubscriptionId = $subscription.Id $certificateCheck.CertificateId = $id $certificateCheck.ThumbPrint = $cert.Properties.thumbprint $certificateCheck.ResourceGroupName = $cert.ResourceGroupName try { $thumbprint = $certificateCheck.ThumbPrint # Retrieve detailed certificate information $certificate = Get-AzWebAppCertificate -ResourceGroupName $certificateCheck.ResourceGroupName -Thumbprint $thumbprint -ErrorAction Stop if ($null -eq $certificate) { $certificateCheck.Health = "Error" $certificateCheck.Comment = "Could not find certificate details" $certificatesWithIssues++ Write-Host " ⚠ Warning: Certificate details not accessible" } else { try { # Extract certificate subject name and expiration details $subjectname = $certificate.SubjectName $certificateCheck.SubjectName = $subjectname Write-Host " ✓ Subject: $subjectname" # Calculate expiration and days remaining $EndDate = [datetime]$certificate.ExpirationDate $certificateCheck.ExpirationDate = $EndDate $span = New-TimeSpan -Start $StartDate -End $EndDate $certificateCheck.TotalDays = [Math]::Round($span.TotalDays, 1) # Determine and assign health status based on expiration if ($certificateCheck.TotalDays -lt 0) { $certificateCheck.Health = "Expired" Write-Host " 🔴 EXPIRED: $([Math]::Abs($certificateCheck.TotalDays)) days ago" -ForegroundColor Red $certificatesWithIssues++ } elseif ($certificateCheck.TotalDays -lt 7) { $certificateCheck.Health = "Critical" Write-Host " 🟠 CRITICAL: Expires in $($certificateCheck.TotalDays) days" -ForegroundColor Yellow $certificatesWithIssues++ } elseif ($certificateCheck.TotalDays -lt 30) { $certificateCheck.Health = "Warning" Write-Host " 🟡 WARNING: Expires in $($certificateCheck.TotalDays) days" -ForegroundColor Yellow } else { $certificateCheck.Health = "Healthy" Write-Host " ✓ Healthy: Expires in $($certificateCheck.TotalDays) days" } } catch { $certificateCheck.Health = "Error" $certificateCheck.Comment = "Could not determine expiration date" $certificatesWithIssues++ Write-Host " ⚠ Warning: Could not determine expiration date" } } } catch { $certificateCheck.Health = "Error" $certificateCheck.Comment = "Could not load certificate details" $certificatesWithIssues++ Write-Host " ❌ Error: Could not load certificate details" } # Add certificate to results collection $Result += $certificateCheck $totalCertificates++ $subscriptionCertCount++ } Write-Host " ✓ Processed $subscriptionCertCount certificate(s) in subscription" } else { Write-Host " ℹ No App Service certificates found in this subscription" } $processedSubscriptions++ } catch { Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red Write-Host " Please verify permissions and subscription access" } } Write-Host "" Write-Host "======================================================================================================================================================================" Write-Host "Exporting Results and Analysis" Write-Host "======================================================================================================================================================================" # Export results to CSV file Write-Host "Exporting certificate inventory to CSV file..." try { $Result | Export-Csv -Path $fileName -NoTypeInformation -Force Write-Host "✓ Successfully exported $($Result.Count) certificate records to: $fileName" } catch { Write-Error "Failed to export results to CSV file: $($_.Exception.Message)" throw $_ } # Display results summary table Write-Host "" Write-Host "Certificate Inventory Summary:" Write-Host "======================================================================================================================================================================" $Result | Format-Table -AutoSize # Generate detailed analysis and statistics Write-Host "" Write-Host "======================================================================================================================================================================" Write-Host "Certificate Analysis Summary" Write-Host "======================================================================================================================================================================" Write-Host "Execution completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Host "" Write-Host "Processing Statistics:" Write-Host " Subscriptions processed: $processedSubscriptions" Write-Host " Total certificates discovered: $totalCertificates" Write-Host " Certificates with issues: $certificatesWithIssues" Write-Host "" # Analyze certificate expiration status using Health property if ($Result.Count -gt 0) { $expiredCerts = $Result | Where-Object { $_.Health -eq "Expired" } $criticalCerts = $Result | Where-Object { $_.Health -eq "Critical" } $warnCerts = $Result | Where-Object { $_.Health -eq "Warning" } $healthyCerts = $Result | Where-Object { $_.Health -eq "Healthy" } $errorCerts = $Result | Where-Object { $_.Health -eq "Error" } Write-Host "Certificate Status Analysis:" Write-Host " 🔴 Expired certificates: $($expiredCerts.Count)" Write-Host " 🟠 Critical (< 7 days): $($criticalCerts.Count)" Write-Host " 🟡 Warning (7-30 days): $($warnCerts.Count)" Write-Host " ✓ Healthy (> 30 days): $($healthyCerts.Count)" Write-Host " ❌ Error/Inaccessible: $($errorCerts.Count)" Write-Host "" # Display urgent action items if ($expiredCerts.Count -gt 0 -or $criticalCerts.Count -gt 0) { Write-Host "🚨 URGENT ACTION REQUIRED:" if ($expiredCerts.Count -gt 0) { Write-Host " - $($expiredCerts.Count) certificate(s) have already expired" } if ($criticalCerts.Count -gt 0) { Write-Host " - $($criticalCerts.Count) certificate(s) expire within 7 days" } Write-Host " Review the CSV file for detailed certificate information" } if ($warnCerts.Count -gt 0) { Write-Host "⚠ RENEWAL PLANNING NEEDED:" Write-Host " - $($warnCerts.Count) certificate(s) expire within 30 days" } } else { Write-Host "ℹ No certificates found across all processed subscriptions" } Write-Host "" Write-Host "Output File Information:" Write-Host " File Path: $fileName" Write-Host " File Size: $([Math]::Round((Get-Item $fileName).Length / 1KB, 2)) KB" Write-Host "" Write-Host "Recommendations:" Write-Host " - Schedule regular execution for proactive certificate monitoring" Write-Host " - Set up alerts for certificates expiring within 30 days" Write-Host " - Implement automated renewal processes where possible" Write-Host " - Review and resolve any certificates with error status" Write-Host "======================================================================================================================================================================"