Public/New-PatchManagementReport.ps1
|
function New-PatchManagementReport { <# .SYNOPSIS Generates a self-contained HTML patch management dashboard using PSWriteHTML. .DESCRIPTION Produces a four-tab HTML report: - Overview : KPI cards, donut chart, bar chart, top-10 issues table - PME Issues : Full table of affected devices with conditional row colouring - Error Catalog : Unique PME errors ranked by frequency - All Devices : Complete device list (populated only when -IncludeHealthy used) Requires the PSWriteHTML module. Install with: Install-Module PSWriteHTML -Scope CurrentUser .PARAMETER ReportData Array of PSCustomObject rows from the orchestrator. Each row must contain: DeviceName, CustomerName, SiteName, ServiceState, PMEThresholdStatus, PMEStatus, LastChecked, PatchState .PARAMETER OutputPath Full path for the output HTML file. .PARAMETER ReportTitle Optional custom title. Defaults to 'N-Central Patch Management Report'. .PARAMETER ReportTitle Optional custom title. Defaults to 'Patch Management Analysis'. .EXAMPLE New-PatchManagementReport -ReportData $rows -OutputPath '.\report.html' #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$ReportData, [Parameter(Mandatory)] [string]$OutputPath, [int]$TotalDevicesScanned = 0, [string]$ReportTitle = 'Patch Management Analysis' ) # Verify PSWriteHTML is available if (-not (Get-Module -ListAvailable -Name PSWriteHTML)) { throw "PSWriteHTML module not found. Install it with: Install-Module PSWriteHTML -Scope CurrentUser" } Import-Module PSWriteHTML -ErrorAction Stop # ── Derive report datasets ────────────────────────────────────────────────── $issueRows = @($ReportData | Where-Object { $_.ServiceState -ne 'Normal' }) $allRows = @($ReportData) # KPI counts $total = if ($TotalDevicesScanned -gt 0) { $TotalDevicesScanned } else { $allRows.Count } $issueCount = $issueRows.Count $okCount = $total - $issueCount $pct = if ($total -gt 0) { [Math]::Round(($okCount / $total) * 100, 1) } else { 0 } $failCount = @($issueRows | Where-Object { $_.ServiceState -eq 'Failed' }).Count $warnCount = @($issueRows | Where-Object { $_.ServiceState -eq 'Warning' }).Count $critical = $failCount # Top 10 issues (most recent by LastChecked, then by state severity) $top10 = $issueRows | Sort-Object @{ Expression = { switch ($_.ServiceState) { 'Failed' { 0 } 'Warning' { 1 } default { 2 } } } }, @{ Expression = 'LastChecked'; Descending = $true } | Select-Object -First 10 DeviceName, CustomerName, SiteName, ServiceState, PMEStatus, LastChecked # Error catalog - unique PME errors ranked by count $errorCatalog = $issueRows | Where-Object { $_.PMEStatus -ne 'N/A' -and -not [string]::IsNullOrWhiteSpace($_.PMEStatus) } | Group-Object PMEStatus | Sort-Object Count -Descending | ForEach-Object { [PSCustomObject]@{ PMEStatus = $_.Name Count = $_.Count AffectedDevices = ($_.Group | Select-Object -ExpandProperty DeviceName | Sort-Object | Select-Object -Unique) -join ', ' } } # Issues by customer for bar chart $byCustomer = $issueRows | Group-Object CustomerName | Sort-Object Count -Descending | Select-Object -First 15 # cap at 15 for readability # ── Build the HTML report ────────────────────────────────────────────────── $generatedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' New-HTML -TitleText $ReportTitle -FilePath $OutputPath -ShowHTML:$false { New-HTMLHeader { New-HTMLText -Text $ReportTitle -FontSize 32 -FontWeight bold -Color '#2c3e50' New-HTMLText -Text "Detailed Device Analysis" -FontSize 18 -Color '#34495e' New-HTMLText -Text "Generated: $generatedAt" -FontSize 14 -Color '#7f8c8d' if ($GeneratedBy) { New-HTMLText -Text "Scope: $GeneratedBy" -FontSize 12 -Color '#95a5a6' } } # ── Tab 1: Overview ──────────────────────────────────────────────────── New-HTMLTab -Name 'Overview' -IconSolid 'chart-pie' { # KPI cards New-HTMLSection -Invisible -Density Compact { New-HTMLPanel { New-HTMLInfoCard -Title 'Devices Scanned' -Number $total ` -TitleColor '#7f8c8d' -NumberColor '#3498db' -IconSolid 'server' -IconColor '#3498db' } New-HTMLPanel { New-HTMLInfoCard -Title 'Devices with Issues' -Number $issueCount ` -TitleColor '#7f8c8d' -NumberColor '#e74c3c' -IconSolid 'exclamation-triangle' -IconColor '#e74c3c' } New-HTMLPanel { New-HTMLInfoCard -Title 'Healthy %' -Number "$pct%" ` -TitleColor '#7f8c8d' -NumberColor '#27ae60' -IconSolid 'check-circle' -IconColor '#27ae60' } New-HTMLPanel { New-HTMLInfoCard -Title 'Critical Failures' -Number $critical ` -TitleColor '#7f8c8d' -NumberColor '#9b59b6' -IconSolid 'times-circle' -IconColor '#9b59b6' } } # Charts side by side New-HTMLSection -Density Comfortable { New-HTMLPanel { New-HTMLText -Text "📊 PME Status Distribution" -FontWeight bold New-HTMLChart -Title 'Distribution' -Height 350 { New-ChartToolbar -Download if ($failCount -gt 0) { New-ChartDonut -Name 'Failed' -Value $failCount -Color '#e74c3c' } if ($warnCount -gt 0) { New-ChartDonut -Name 'Warning' -Value $warnCount -Color '#f39c12' } if ($okCount -gt 0) { New-ChartDonut -Name 'Healthy' -Value $okCount -Color '#27ae60' } if ($total -eq 0) { New-ChartDonut -Name 'No Data' -Value 1 -Color '#bdc3c7' } } } New-HTMLPanel { if ($byCustomer -and @($byCustomer).Count -gt 0) { New-HTMLText -Text "🏢 Issues by Customer" -FontWeight bold New-HTMLChart -Title 'Count' -Height 350 { New-ChartToolbar -Download New-ChartAxisX -Name ($byCustomer | Select-Object -ExpandProperty Name) New-ChartBar -Name 'Issue Count' -Value ($byCustomer | Select-Object -ExpandProperty Count) -Color '#2c3e50' } } else { New-HTMLText -Text "🏢 Issues by Customer" -FontWeight bold New-HTMLText -Text 'No customer issue data to chart.' -Color '#bdc3c7' } } } # Top 10 quick-view New-HTMLSection -HeaderText '🔥 Top 10 Most Recent Issues' -CanCollapse { if ($top10 -and @($top10).Count -gt 0) { New-HTMLTable -DataTable $top10 -DisablePaging -DisableSearch -HideFooter { New-TableCondition -Name 'ServiceState' -Value 'Failed' -Operator eq ` -ComparisonType string -BackgroundColor '#e74c3c' -Color white -Row New-TableCondition -Name 'ServiceState' -Value 'Warning' -Operator eq ` -ComparisonType string -BackgroundColor '#f39c12' -Color white -Row } } else { New-HTMLText -Text 'No patch issues found.' -FontWeight bold } } } # ── Tab 2: PME Issues ────────────────────────────────────────────────── New-HTMLTab -Name 'PME Issues' -IconSolid 'exclamation-triangle' { New-HTMLSection -HeaderText "⚠️ Affected Devices ($issueCount)" { if ($issueRows -and @($issueRows).Count -gt 0) { New-HTMLTable -DataTable $issueRows ` -Filtering ` -Buttons @('excelHtml5', 'csvHtml5', 'pdfHtml5', 'copyHtml5') ` -DefaultSortColumn 'ServiceState' { New-TableCondition -Name 'ServiceState' -Value 'Failed' -Operator eq ` -ComparisonType string -BackgroundColor '#fadbd8' -Color '#922b21' -Row New-TableCondition -Name 'ServiceState' -Value 'Warning' -Operator eq ` -ComparisonType string -BackgroundColor '#fdebd0' -Color '#784212' -Row New-TableHeader -Names 'DeviceName', 'CustomerName', 'SiteName', ` 'ServiceState', 'PMEThresholdStatus', ` 'PMEStatus', 'LastChecked' ` -Title 'Patch Management Issues' } } else { New-HTMLText -Text 'No patch issues found - all devices are healthy.' ` -FontWeight bold -FontSize 16 } } } # ── Tab 3: Error Catalog ─────────────────────────────────────────────── New-HTMLTab -Name 'Error Catalog' -IconSolid 'list-alt' { New-HTMLSection -HeaderText '📖 Unique PME Error Messages' { if ($errorCatalog -and @($errorCatalog).Count -gt 0) { New-HTMLTable -DataTable $errorCatalog ` -Filtering ` -DefaultSortColumn 'Count' ` -DefaultSortOrder Descending ` -Buttons @('excelHtml5', 'csvHtml5') { New-TableCondition -Name 'Count' -ComparisonType number -Operator gt ` -Value 5 -BackgroundColor '#f5b7b1' -Row New-TableHeader -Names 'PMEStatus', 'Count', 'AffectedDevices' ` -Title 'Error Frequency' } } else { New-HTMLText -Text 'No PME error messages found.' -FontWeight bold } } } # ── Tab 4: All Devices ───────────────────────────────────────────────── New-HTMLTab -Name 'All Devices' -IconSolid 'server' { New-HTMLSection -HeaderText "📋 Complete Device Status ($total devices)" -CanCollapse { if ($allRows -and @($allRows).Count -gt 0) { $allDeviceRows = $allRows | Select-Object DeviceName, CustomerName, SiteName, PatchState, PMEStatus New-HTMLTable -DataTable $allDeviceRows -Filtering { New-TableCondition -Name 'PatchState' -Value 'Normal' -Operator eq ` -ComparisonType string -BackgroundColor '#d5f5e3' -Row New-TableCondition -Name 'PatchState' -Value 'Failed' -Operator eq ` -ComparisonType string -BackgroundColor '#fadbd8' -Row New-TableCondition -Name 'PatchState' -Value 'Warning' -Operator eq ` -ComparisonType string -BackgroundColor '#fdebd0' -Row } } else { New-HTMLText -Text 'No device data available. Run with -IncludeHealthy to populate this tab.' } } } New-HTMLFooter { New-HTMLText -Text "Application Analysis Report: Patch Management" -Color '#34495e' -FontWeight bold -FontSize 14 New-HTMLText -Text "Generated using PSWriteHTML" -Color '#7f8c8d' -FontSize 12 } } # end New-HTML Write-Verbose "Report written to: $OutputPath" } |