232 lines
10 KiB
PowerShell
232 lines
10 KiB
PowerShell
#Requires -RunAsAdministrator
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# ****************************** MODULE IMPORT & INITIALIZATION ******************************
|
||
# HTML reporting module
|
||
Import-Module -Name PSWriteHTML -ErrorAction Stop
|
||
|
||
Write-Host "Starting WSUS report generation..."
|
||
|
||
# Load WSUS admin assembly explicitly (no deprecated LoadWithPartialName)
|
||
$wsusDll = "$env:ProgramFiles\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"
|
||
if (Test-Path $wsusDll) { Add-Type -Path $wsusDll } else { throw "WSUS Admin DLL not found: $wsusDll" }
|
||
|
||
# Connect to local WSUS and pull data
|
||
$wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
|
||
$updates = $wsusServer.GetUpdates()
|
||
$computers = $wsusServer.GetComputerTargets()
|
||
$targetGroups = $wsusServer.GetComputerTargetGroups()
|
||
|
||
# Map groupId -> groupName for quick lookups
|
||
$groupHashTable = @{}
|
||
$targetGroups | ForEach-Object { $groupHashTable[$_.ID] = $_.Name }
|
||
|
||
Write-Host "Retrieved WSUS data: Updates, Computers, and Target Groups."
|
||
|
||
# Ensure output folder exists
|
||
$outFile = "C:\exploit\report\default.htm"
|
||
$outDir = Split-Path $outFile -Parent
|
||
if (-not (Test-Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null }
|
||
|
||
# ****************************** REPORT GENERATION START ******************************
|
||
New-HTML -TitleText 'WSUS Report' {
|
||
|
||
# ****************************** COMPUTERS BY TARGET GROUP ******************************
|
||
# Build a simple dataset: group name + computer count (skip 'All Computers')
|
||
$DataTable = New-Object System.Collections.Generic.List[Object]
|
||
foreach ($group in $targetGroups) {
|
||
$computersCount = $group.GetComputerTargets().Count
|
||
if ($computersCount -gt 0 -and $group.Name -ne 'All Computers') {
|
||
$DataTable.Add([PSCustomObject]@{
|
||
TargetName = $group.Name
|
||
Total = $computersCount
|
||
})
|
||
}
|
||
}
|
||
Write-Host "Populated DataTable with computer counts per Target Group."
|
||
|
||
New-HTMLSection -HeaderText 'Computers By TargetGroup' {
|
||
# Table
|
||
New-HTMLPanel {
|
||
New-HTMLTable -DataTable $DataTable -HideFooter -DataTableID 'IDtargetGroup' {
|
||
# Bind events to the correct table ID
|
||
New-TableEvent -ID 'IDtargetGroup' -SourceColumnID 0 -TargetColumnId 0
|
||
}
|
||
}
|
||
# Pie chart per group
|
||
New-HTMLPanel {
|
||
New-HTMLChart {
|
||
New-ChartToolbar -Download
|
||
foreach ($row in $DataTable) { New-ChartPie -Name $row.TargetName -Value $row.Total }
|
||
}
|
||
}
|
||
}
|
||
|
||
# ****************************** COMPUTER LIST (HIDDEN) ******************************
|
||
$ListeComputersWSUS = @()
|
||
foreach ($computer in $computers) {
|
||
foreach ($tg in $computer.GetComputerTargetGroups()) {
|
||
if ($tg.Name -ne 'All Computers') {
|
||
$ListeComputersWSUS += [PSCustomObject]@{
|
||
TargetName = $tg.Name
|
||
ComputerName = $computer.FullDomainName
|
||
}
|
||
}
|
||
}
|
||
}
|
||
New-HTMLSection -HeaderText 'Computers' -Invisible {
|
||
New-HTMLTable -DataTable $ListeComputersWSUS -DataTableID 'AllComputertargetGroup' -HideFooter
|
||
}
|
||
|
||
# ****************************** DEPLOYMENT STATUS OF KB UPDATES ******************************
|
||
$DateNow = Get-Date
|
||
$ListeKB = @()
|
||
$MaxDaysReport = 90 # (kept for future filtering if needed)
|
||
|
||
foreach ($update in $updates) {
|
||
foreach ($approval in $update.GetUpdateApprovals()) {
|
||
foreach ($tg in $targetGroups) {
|
||
if ($tg.Id -eq $approval.ComputerTargetGroupId) {
|
||
$DateKB = $approval.GoLiveTime
|
||
$LastChange = New-TimeSpan -Start $DateKB -End $DateNow
|
||
$ActionType = $approval.Action
|
||
|
||
if ($ActionType -eq 'Install') {
|
||
$KBTitre = $update.Title
|
||
$installKB = 0; $MissKB = 0
|
||
|
||
# Summaries per group for this update
|
||
foreach ($sum in $update.GetSummaryPerComputerTargetGroup()) {
|
||
$groupName = $groupHashTable[$sum.ComputerTargetGroupId]
|
||
if ($tg.Name -eq $groupName) {
|
||
$installKB = $sum.InstalledCount + $sum.InstalledPendingRebootCount
|
||
$MissKB = $sum.UnknownCount + $sum.NotInstalledCount + $sum.DownloadedCount + $sum.FailedCount
|
||
}
|
||
}
|
||
|
||
# Keep only meaningful KBs (exclude ARM64, SSU, Flash, specific KBs)
|
||
if (($installKB -ne 0 -or $MissKB -ne 0) -and
|
||
($KBTitre -notmatch 'ARM64') -and
|
||
($KBTitre -notmatch 'Servicing Stack Update') -and
|
||
($KBTitre -notmatch 'Flash Player') -and
|
||
($KBTitre -notlike '*KB4470788*') -and
|
||
($KBTitre -notlike '*KB4499728*')) {
|
||
$ListeKB += [PSCustomObject]@{
|
||
KBTitre = $KBTitre
|
||
KBGroup = $tg.Name
|
||
KBChang = $LastChange.Days
|
||
ActionType = $ActionType
|
||
installKB = $installKB
|
||
MissKB = $MissKB
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# Sort by age then title; get unique titles for chart grouping
|
||
$ListeKB = $ListeKB | Sort-Object KBChang, KBTitre
|
||
$groupedByTitle = @{}
|
||
foreach ($o in $ListeKB) {
|
||
if (-not $groupedByTitle.ContainsKey($o.KBTitre)) { $groupedByTitle[$o.KBTitre] = @() }
|
||
$groupedByTitle[$o.KBTitre] += $o
|
||
}
|
||
|
||
foreach ($Title in $groupedByTitle.Keys) {
|
||
# Timeline per group (how many days since approval)
|
||
New-HTMLSection -HeaderText $Title {
|
||
New-HTMLChart -Title $Title -TitleAlignment center {
|
||
foreach ($o in $groupedByTitle[$Title]) {
|
||
New-ChartTimeline -DateFrom (Get-Date).AddDays(-$o.KBChang - 1) -DateTo (Get-Date) -Name $o.KBGroup
|
||
}
|
||
}
|
||
}
|
||
|
||
# Donut charts (Installed vs Missing) per group – hidden
|
||
New-HTMLSection -HeaderText 'Computers' -Invisible {
|
||
foreach ($o in $groupedByTitle[$Title]) {
|
||
New-HTMLPanel {
|
||
New-HTMLChart -Title $o.KBGroup {
|
||
New-ChartLegend -Name "Installed","Missing" -Color Green,Red
|
||
New-ChartDonut -Name "Installed" -Value $o.installKB
|
||
New-ChartDonut -Name "Missing" -Value $o.MissKB
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# ------------------------- KB and Computers status table (hidden) -------------------------
|
||
$updateScope2 = New-Object Microsoft.UpdateServices.Administration.UpdateScope
|
||
$updateScope2.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
|
||
$updateScope2.UpdateApprovalActions = [Microsoft.UpdateServices.Administration.UpdateApprovalActions]::Install
|
||
$updateScope2.UpdateSources = [Microsoft.UpdateServices.Administration.UpdateSources]::MicrosoftUpdate
|
||
$updateScope2.ExcludedInstallationStates = @('NotApplicable','Installed','InstalledPendingReboot')
|
||
|
||
$allComputers = $wsusServer.GetComputerTargets()
|
||
$resultsComputerStatus = @()
|
||
|
||
foreach ($pc in $allComputers) {
|
||
$updatelist = $pc.GetUpdateInstallationInfoPerUpdate($updateScope2)
|
||
foreach ($ui in $updatelist) {
|
||
$updInfo = $ui.GetUpdate()
|
||
$approvalGroup = $ui.GetUpdateApprovalTargetGroup().Name
|
||
if ($approvalGroup -ne 'All Computers' -and $updInfo.IsApproved) {
|
||
$resultsComputerStatus += [pscustomobject][Ordered]@{
|
||
ComputerName = $pc.FullDomainName
|
||
Status = $ui.UpdateInstallationState
|
||
ApprovalTargetGroup= $approvalGroup
|
||
Approved = $updInfo.IsApproved
|
||
Title = $updInfo.Title
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
New-HTMLSection -HeaderText 'Computers' -Invisible {
|
||
foreach ($o in $groupedByTitle[$Title]) {
|
||
New-HTMLPanel {
|
||
$specific = $resultsComputerStatus |
|
||
Where-Object { $_.Title -like "*$Title*" -and $_.ApprovalTargetGroup -like "*$($o.KBGroup)*" } |
|
||
Select-Object ComputerName, Status
|
||
New-HTMLTable -DataTable $specific -HideFooter -HideButtons
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# ****************************** COMPUTERS IN ERROR ******************************
|
||
$computersInError = @()
|
||
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
|
||
$updateScope.IncludedInstallationStates = 'Failed'
|
||
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
|
||
$computerScope.IncludedInstallationStates = 'Failed'
|
||
|
||
$computersInError = $wsusServer.GetComputerTargets($computerScope) | ForEach-Object {
|
||
$compName = $_.FullDomainName
|
||
$failedTitles = ($_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach-Object {
|
||
$_.GetUpdate().Title
|
||
}) -join ', '
|
||
if ($failedTitles) {
|
||
[pscustomobject]@{
|
||
Computername = $compName
|
||
TargetGroups = ($_.GetComputerTargetGroups() | Select-Object -ExpandProperty Name) -join ', '
|
||
Updates = $failedTitles
|
||
}
|
||
}
|
||
} | Sort-Object Computername
|
||
|
||
New-HTMLSection -HeaderText 'Computers in ERROR' {
|
||
New-HTMLTable -DataTable $computersInError -DataTableID 'NewIDtoSearchInChartERR' -HideFooter
|
||
}
|
||
|
||
# ****************************** FOOTER ******************************
|
||
New-HTMLFooter {
|
||
New-HTMLText -Text ("Report generated (GMT): {0:u}" -f (Get-Date).ToUniversalTime()) -Color Blue -Alignment center
|
||
}
|
||
|
||
} -FilePath $outFile -Online
|
||
|
||
Write-Host "WSUS report has been generated successfully at $outFile"
|