Dargslan.WsusHealth.psm1

<#
.SYNOPSIS
    Audit WSUS server health: client compliance, missing critical updates, sync state. JSON / HTML report.

.DESCRIPTION
    Part of the Dargslan Windows Admin Tools collection.
    Free Cheat Sheet: https://dargslan.com/cheat-sheets/wsus-server-health-audit-2026
    Full Guide: https://dargslan.com/blog/wsus-server-health-audit-powershell-2026
    More tools: https://dargslan.com

.LINK
    https://dargslan.com

.LINK
    https://github.com/Dargslan/powershell-admin-scripts
#>


$script:Banner = @"
+----------------------------------------------------------+
| Dargslan WSUS Health Audit
| https://dargslan.com - Free cheat sheets & eBooks |
+----------------------------------------------------------+
"@


function Get-DargslanWsusServerStatus {
    <#
    .SYNOPSIS
        Connect to WSUS and return basic server status + last sync.
    #>

    [CmdletBinding()]
    param([string]$Server = $env:COMPUTERNAME, [int]$Port = 8530, [bool]$UseSsl = $false)
    [void][reflection.assembly]::LoadWithPartialName('Microsoft.UpdateServices.Administration')
    $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($Server, $UseSsl, $Port)
    $st   = $wsus.GetStatus()
    $sync = $wsus.GetSubscription()
    [pscustomobject]@{
        Server                  = $Server
        Computers               = $st.ComputerTargetCount
        UpdateCount             = $st.UpdateCount
        DeclinedCount           = $st.DeclinedUpdateCount
        ApprovedCount           = $st.ApprovedUpdateCount
        UpdatesNeedingFiles     = $st.UpdatesWithStaleUpdateApprovalsCount
        LastSync                = $sync.GetLastSynchronizationInfo().StartTime
        LastSyncResult          = $sync.GetLastSynchronizationInfo().Result
    }
}

function Get-DargslanWsusClientCompliance {
    <#
    .SYNOPSIS
        Bucket clients by status (UpToDate, NeedingUpdates, Failed, Unknown).
    #>

    [CmdletBinding()]
    param([string]$Server = $env:COMPUTERNAME, [int]$Port = 8530, [bool]$UseSsl = $false)
    [void][reflection.assembly]::LoadWithPartialName('Microsoft.UpdateServices.Administration')
    $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($Server, $UseSsl, $Port)
    $summary = $wsus.GetSummariesPerComputerTarget(
        (New-Object Microsoft.UpdateServices.Administration.UpdateScope),
        (New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope))
    $buckets = [pscustomobject]@{
        Server         = $Server
        UpToDate       = ($summary | Where-Object { $_.NotInstalledCount + $_.DownloadedCount + $_.FailedCount -eq 0 }).Count
        NeedingUpdates = ($summary | Where-Object { $_.NotInstalledCount + $_.DownloadedCount -gt 0 -and $_.FailedCount -eq 0 }).Count
        Failed         = ($summary | Where-Object FailedCount -gt 0).Count
        Unknown        = ($summary | Where-Object UnknownCount -gt 0).Count
        Total          = $summary.Count
    }
    $buckets
}

function Get-DargslanWsusMissingCritical {
    <#
    .SYNOPSIS
        Return needed critical / security updates per client.
    #>

    [CmdletBinding()]
    param([string]$Server = $env:COMPUTERNAME, [int]$Port = 8530, [bool]$UseSsl = $false, [int]$Top = 50)
    [void][reflection.assembly]::LoadWithPartialName('Microsoft.UpdateServices.Administration')
    $wsus  = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($Server, $UseSsl, $Port)
    $scope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
    $scope.Classifications.Add(($wsus.GetUpdateClassifications() | Where-Object Title -in 'Critical Updates','Security Updates')) | Out-Null
    $scope.IncludedInstallationStates = 'NotInstalled'
    $wsus.GetSummariesPerComputerTarget($scope, (New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope)) |
        Where-Object NotInstalledCount -gt 0 |
        Sort-Object NotInstalledCount -Descending |
        Select-Object -First $Top -Property @{N='Computer';E={($wsus.GetComputerTarget($_.ComputerTargetId)).FullDomainName}}, NotInstalledCount, DownloadedCount, FailedCount
}

function Get-DargslanWsusHealthReport {
    <#
    .SYNOPSIS
        Combined WSUS health report with PASS / WARN / FAIL verdict.
    #>

    [CmdletBinding()]
    param([string]$Server = $env:COMPUTERNAME, [int]$Port = 8530, [bool]$UseSsl = $false)
    $st  = Get-DargslanWsusServerStatus -Server $Server -Port $Port -UseSsl $UseSsl
    $cli = Get-DargslanWsusClientCompliance -Server $Server -Port $Port -UseSsl $UseSsl
    $pct = if ($cli.Total) { [math]::Round(($cli.UpToDate / $cli.Total) * 100, 1) } else { 0 }
    $score = 0
    if ($st.LastSync -gt (Get-Date).AddDays(-2))     { $score++ }
    if ($st.LastSyncResult -eq 'Succeeded')           { $score++ }
    if ($pct -ge 90)                                  { $score++ }
    if ($cli.Failed -le [math]::Max(1, $cli.Total * 0.05)) { $score++ }
    $verdict = if ($score -ge 4) { 'PASS' } elseif ($score -ge 2) { 'WARN' } else { 'FAIL' }
    [pscustomobject]@{
        Server      = $Server
        Status      = $st
        Compliance  = $cli
        UpToDatePct = $pct
        Score       = $score
        Verdict     = $verdict
        TimeStamp   = (Get-Date).ToString('s')
    }
}

function Export-DargslanWsusHealthReport {
    <#
    .SYNOPSIS
        Export the WSUS audit to HTML and JSON.
    #>

    [CmdletBinding()]
    param([string]$Server = $env:COMPUTERNAME, [int]$Port = 8530, [bool]$UseSsl = $false, [string]$OutDir = (Join-Path $env:TEMP 'DargslanWsusAudit'))
    if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null }
    $r = Get-DargslanWsusHealthReport -Server $Server -Port $Port -UseSsl $UseSsl
    $json = Join-Path $OutDir ('wsus-' + $Server + '.json')
    $html = Join-Path $OutDir ('wsus-' + $Server + '.html')
    $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8
    $body  = "<h1>WSUS Health - $($r.Server)</h1>"
    $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/4) - $($r.UpToDatePct)% up to date</p>"
    $body += '<h2>Status</h2>'     + ($r.Status     | ConvertTo-Html -Fragment)
    $body += '<h2>Compliance</h2>' + ($r.Compliance | ConvertTo-Html -Fragment)
    ConvertTo-Html -Body $body -Title 'WSUS Health' | Set-Content $html -Encoding UTF8
    [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict }
}