Public/Export-AzRetirementReport.ps1
|
function Export-AzRetirementReport { <# .SYNOPSIS Exports retirement recommendations to CSV, JSON, or HTML #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param( [Parameter(Mandatory, ValueFromPipeline)] [object[]]$Recommendations, [Parameter(Mandatory)] [string]$OutputPath, [ValidateSet("CSV", "JSON", "HTML")] [string]$Format = "CSV" ) begin { $allRecs = @() } process { $allRecs += $Recommendations } end { if (-not $PSCmdlet.ShouldProcess($OutputPath, "Export $($allRecs.Count) retirement recommendation(s) as $Format")) { return } switch ($Format) { "CSV" { $allRecs | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8 } "JSON" { $allRecs | ConvertTo-Json -Depth 10 | Out-File $OutputPath -Encoding utf8 } "HTML" { # Helper function to escape HTML to prevent XSS function ConvertTo-HtmlEncoded { param([string]$Text) if ([string]::IsNullOrEmpty($Text)) { return $Text } return [System.Net.WebUtility]::HtmlEncode($Text) } $generatedTime = (Get-Date -AsUTC).ToString("yyyy-MM-dd HH:mm:ss 'UTC'") $totalCount = $allRecs.Count # Define CSS for professional styling $css = @" <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; background-color: #f5f5f5; color: #333; } .container { max-width: 1400px; margin: 0 auto; background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #0078d4; border-bottom: 3px solid #0078d4; padding-bottom: 10px; margin-bottom: 20px; } .metadata { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 25px; border-left: 4px solid #0078d4; } .metadata p { margin: 5px 0; font-size: 14px; } .metadata strong { color: #0078d4; } table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 14px; } th { background-color: #0078d4; color: white; padding: 12px 8px; text-align: left; font-weight: 600; position: sticky; top: 0; } td { padding: 10px 8px; border-bottom: 1px solid #e0e0e0; vertical-align: top; } tr:hover { background-color: #f8f9fa; } .impact-high { color: #d13438; font-weight: bold; } .impact-medium { color: #ff8c00; font-weight: bold; } .impact-low { color: #107c10; font-weight: bold; } .resource-name { font-weight: 600; color: #0078d4; } .timestamp { color: #666; font-size: 12px; } a { color: #0078d4; text-decoration: none; } a:hover { text-decoration: underline; } .recommendation-id { font-family: 'Courier New', monospace; font-size: 12px; color: #666; } .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #e0e0e0; text-align: center; color: #666; font-size: 12px; } </style> "@ # Build HTML manually for better control over formatting $htmlContent = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Azure Service Retirement Report</title> $css </head> <body> <div class="container"> <h1>Azure Service Retirement Report</h1> <div class="metadata"> <p><strong>Generated:</strong> $generatedTime</p> <p><strong>Total Recommendations:</strong> $totalCount</p> <p><strong>Report Type:</strong> Service Retirement and Upgrade Recommendations</p> <p><strong>Impact Levels:</strong> Recommendations are categorized as High (critical, address immediately), Medium (important, moderate timeline), or Low (beneficial, lower priority). <a href="https://learn.microsoft.com/azure/advisor/advisor-overview" target="_blank" rel="noopener noreferrer">Learn more about Azure Advisor impact levels</a></p> </div> <table> <thead> <tr> <th>Impact</th> <th>Resource Name</th> <th>Resource Type</th> <th>Problem</th> <th>Solution</th> <th>Resource Group</th> <th>Subscription ID</th> <th>Resource Link</th> <th>Learn More</th> </tr> </thead> <tbody> "@ # Add table rows - collect in array for better performance $tableRows = foreach ($rec in $allRecs) { # HTML encode all user-provided data to prevent XSS $encodedResourceName = ConvertTo-HtmlEncoded $rec.ResourceName $encodedResourceType = ConvertTo-HtmlEncoded $rec.ResourceType $encodedResourceGroup = ConvertTo-HtmlEncoded $rec.ResourceGroup $encodedImpact = ConvertTo-HtmlEncoded $rec.Impact $encodedProblem = ConvertTo-HtmlEncoded $rec.Problem $encodedSolution = ConvertTo-HtmlEncoded $rec.Solution $encodedSubscriptionId = ConvertTo-HtmlEncoded $rec.SubscriptionId # Validate and sanitize CSS class name to prevent CSS injection $impactClass = switch ($rec.Impact) { "High" { "impact-high" } "Medium" { "impact-medium" } "Low" { "impact-low" } default { "" } } # Build learn more link with proper encoding and validation $encodedLearnMoreLink = if ($rec.LearnMoreLink) { $url = $rec.LearnMoreLink # Validate URL starts with http:// or https:// to prevent javascript: protocol injection if ($url -imatch '^https?://') { $encodedUrl = ConvertTo-HtmlEncoded $url "<a href='$encodedUrl' target='_blank' rel='noopener noreferrer'>Documentation</a>" } else { ConvertTo-HtmlEncoded "Invalid URL" } } else { "N/A" } # Build Resource link with proper encoding and validation $encodedResourceLink = if ($rec.ResourceLink) { $url = $rec.ResourceLink # Validate URL starts with http:// or https:// to prevent javascript: protocol injection if ($url -imatch '^https?://') { $encodedUrl = ConvertTo-HtmlEncoded $url "<a href='$encodedUrl' target='_blank' rel='noopener noreferrer'>View Resource</a>" } else { ConvertTo-HtmlEncoded "Invalid URL" } } else { "N/A" } # Output row HTML @" <tr> <td class="$impactClass">$encodedImpact</td> <td class="resource-name">$encodedResourceName</td> <td>$encodedResourceType</td> <td>$encodedProblem</td> <td>$encodedSolution</td> <td>$encodedResourceGroup</td> <td><span class="recommendation-id">$encodedSubscriptionId</span></td> <td>$encodedResourceLink</td> <td>$encodedLearnMoreLink</td> </tr> "@ } # Join all rows efficiently $htmlContent += ($tableRows -join "") # Close HTML $htmlContent += @" </tbody> </table> <div class="footer"> <p>Generated by AzRetirementMonitor | Azure Service Retirement Monitoring Tool<br> <a href="https://github.com/cocallaw/AzRetirementMonitor" target="_blank" rel="noopener noreferrer">View on GitHub</a></p> </div> </div> </body> </html> "@ $htmlContent | Out-File $OutputPath -Encoding utf8 } } Write-Verbose "Report exported to $OutputPath" } } |