Public/Get-ServerUpdateStatus.ps1

function Get-WindowsUpdateStatus {
<#
.SYNOPSIS
    Internal helper returning the update status of a single computer.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$ComputerName
)

$result = [PSCustomObject]@{
    LastInstalledUpdate  = $null
    LastInstalledOn      = $null
    AvailableUpdateCount = $null
    Status               = 'unavailable'
}

try {
    $hotfix = Get-CimInstance -ComputerName $ComputerName -ClassName Win32_QuickFixEngineering -ErrorAction Stop |
        Sort-Object InstalledOn -Descending |
        Select-Object -First 1
} catch {
    Write-Verbose "Failed to retrieve Windows update status from '$ComputerName': $($_.Exception.Message)"
    return $result
}

if ($hotfix) {
    $result.LastInstalledUpdate = $hotfix.HotFixID
    $result.LastInstalledOn = $hotfix.InstalledOn

    $installed = '{0} installed {1:yyyy-MM-dd}' -f $hotfix.HotFixID, $hotfix.InstalledOn
} else {
    $installed = 'no updates installed'
}

try {
    $pending = Invoke-Command -ComputerName $ComputerName -ErrorAction Stop -ScriptBlock {
        $searcher = (New-Object -ComObject 'Microsoft.Update.Session').CreateUpdateSearcher()
        @($searcher.Search('IsInstalled=0 and IsHidden=0').Updates).Count
    }

    $count = [int](@($pending)[0])
    $result.AvailableUpdateCount = $count

    $available = switch ($count) {
        0 { 'no updates available' }
        1 { '1 update available' }
        default { "$count updates available" }
    }
} catch {
    Write-Verbose "Failed to check for available Windows updates on '$ComputerName': $($_.Exception.Message)"
    $available = 'available updates unknown'
}

$result.Status = '{0}; {1}' -f $installed, $available
$result
}

function Get-WindowsUpdateStatusColor {
<#
.SYNOPSIS
    Internal helper mapping an update status string to a console color.
#>

param(
    [string]$Status
)

if ($Status -match 'unavailable|unknown') {
    'Red'
} elseif ($Status -match 'not running') {
    'DarkGray'
} elseif ($Status -match 'no updates available') {
    'Green'
} else {
    'Orange'
}
}

function ConvertTo-UpdateStatusObject {
<#
.SYNOPSIS
    Internal helper flattening an update status into an output object.
#>

param(
    [string]$ComputerName,
    $HyperVServer,
    $State,
    $Status
)

[PSCustomObject]@{
    ComputerName         = $ComputerName
    HyperVServer         = $HyperVServer
    State                = $State
    LastInstalledUpdate  = $Status.LastInstalledUpdate
    LastInstalledOn      = $Status.LastInstalledOn
    AvailableUpdateCount = $Status.AvailableUpdateCount
    UpdateStatus         = $Status.Status
}
}

function Write-ColorLine {
<#
.SYNOPSIS
    Internal helper writing a colored console line.
 
.DESCRIPTION
    ConsoleColor has no orange, so 'Orange' is rendered with a 24-bit ANSI
    escape sequence and falls back to DarkYellow on hosts without virtual
    terminal support.
#>

param(
    [string]$Text,
    [string]$Color,
    [switch]$NoNewline
)

if ($Color -eq 'Orange') {
    if ($Host.UI.SupportsVirtualTerminal) {
        $esc = [char]27
        Write-Host "$esc[38;2;255;140;0m$Text$esc[0m" -NoNewline:$NoNewline
        return
    }

    $Color = 'DarkYellow'
}

Write-Host $Text -ForegroundColor $Color -NoNewline:$NoNewline
}

function Get-ServerUpdateStatus {
<#
.SYNOPSIS
    Reports the Windows update status of one or more servers, and optionally of
    the VMs they host.
 
.DESCRIPTION
    For each computer, Get-ServerUpdateStatus reports two things: the most
    recently installed update (from Win32_QuickFixEngineering) and how many
    updates are still available (from a Windows Update Agent search for updates
    that are neither installed nor hidden). Any Windows computer can be
    queried; Hyper-V is only needed for -IncludeVM.
 
    By default the result is written to the console in color:
 
        HV01
          Windows Update: KB5126043 installed 2026-09-17; no updates available
          APP01 - Running
            Windows Update: KB5126043 installed 2026-08-13; 3 updates available
          TEST01 - Off
            Windows Update: not running
 
    Computer names are white and VM state is colored by state (green running,
    gray off, yellow otherwise). The update status is green when a computer is
    current, orange when updates are available, and red when the status could
    not be retrieved.
 
    Because a single query can take several seconds, progress is reported per
    computer and each line is printed as soon as that computer has been
    checked, rather than after the whole server has been processed.
 
    Querying a computer requires CIM access (for the installed update) and
    PowerShell remoting (for the update search). Failures are not treated as
    errors: the computer is reported as 'unavailable', or as
    'available updates unknown' when only the update search failed, and
    -Verbose explains the underlying reason.
 
.PARAMETER ComputerName
    One or more computer names to query. Accepts pipeline input, including
    objects with a Name or ComputerName property.
 
.PARAMETER IncludeVM
    Also reports each VM hosted by the queried computers. Requires the Hyper-V
    module locally. Computers that do not run Hyper-V are still reported and
    show '(VM list unavailable)'; hosts without VMs show '(no VMs)'. VMs that
    are not running are listed without being queried.
 
.PARAMETER AsObject
    Returns an object per computer (and per VM with -IncludeVM) instead of
    writing colored output. Each object has these properties:
 
        ComputerName Name of the computer or VM.
        HyperVServer Host name for VM entries, otherwise null.
        State VM state for VM entries, otherwise null.
        LastInstalledUpdate Hotfix ID of the newest installed update.
        LastInstalledOn Date that update was installed.
        AvailableUpdateCount Number of updates available, or null if unknown.
        UpdateStatus The status text shown in the console output.
 
.INPUTS
    System.String[]. Computer names can be piped to this command.
 
.OUTPUTS
    None by default; the status is written to the host. With -AsObject, one
    PSCustomObject per computer and per VM.
 
.EXAMPLE
    Get-ServerUpdateStatus -ComputerName 'APP01', 'SQL01'
 
    Shows the update status of two servers.
 
.EXAMPLE
    Get-ServerUpdateStatus -ComputerName 'HV01', 'HV02' -IncludeVM
 
    Shows the update status of two Hyper-V hosts followed by their VMs.
 
.EXAMPLE
    Get-ServerUpdateStatus -ComputerName 'HV01' -IncludeVM -AsObject |
        Where-Object AvailableUpdateCount -gt 0
 
    Lists only the machines that have updates waiting to be installed.
 
.EXAMPLE
    Get-Content .\servers.txt | Get-ServerUpdateStatus -AsObject |
        Export-Csv .\update-status.csv -NoTypeInformation
 
    Builds a report from a list of servers.
 
.EXAMPLE
    Get-ServerUpdateStatus -ComputerName 'APP01' -Verbose
 
    Shows why a computer could not be queried.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [Alias('Name')]
    [string[]]$ComputerName,

    [switch]$IncludeVM,

    [switch]$AsObject
)

process {
    $serverNumber = 0

    foreach ($server in $ComputerName) {
        $serverNumber++

        Write-Progress -Id 1 -Activity 'Checking Windows update status' -Status "$server ($serverNumber of $($ComputerName.Count))" -PercentComplete (100 * ($serverNumber - 1) / $ComputerName.Count)

        $serverStatus = Get-WindowsUpdateStatus -ComputerName $server

        if ($AsObject) {
            ConvertTo-UpdateStatusObject -ComputerName $server -Status $serverStatus
        } else {
            Write-ColorLine -Text $server -Color White
            Write-ColorLine -Text (' Windows Update: {0}' -f $serverStatus.Status) -Color (Get-WindowsUpdateStatusColor -Status $serverStatus.Status)
        }

        if (-not $IncludeVM) {
            continue
        }

        $vms = @()
        $vmListFailed = $false

        if (-not (Get-Command -Name Get-VM -ErrorAction SilentlyContinue)) {
            Write-Verbose "The Hyper-V module is not available, so VMs on '$server' cannot be enumerated."
            $vmListFailed = $true
        } else {
            try {
                $vms = @(Get-VM -ComputerName $server -ErrorAction Stop)
            } catch {
                Write-Verbose "Failed to retrieve VMs from '$server': $($_.Exception.Message)"
                $vmListFailed = $true
            }
        }

        if ($vmListFailed) {
            if (-not $AsObject) {
                Write-ColorLine -Text ' (VM list unavailable)' -Color Red
            }

            continue
        }

        if ($vms.Count -eq 0) {
            if (-not $AsObject) {
                Write-ColorLine -Text ' (no VMs)' -Color DarkGray
            }

            continue
        }

        $vmNumber = 0

        foreach ($vm in $vms) {
            $vmNumber++
            $isRunning = "$($vm.State)" -eq 'Running'

            Write-Progress -Id 2 -ParentId 1 -Activity "Checking Windows update status on VMs of $server" -Status "$($vm.Name) ($vmNumber of $($vms.Count))" -PercentComplete (100 * ($vmNumber - 1) / $vms.Count)

            $vmStatus = if ($isRunning) {
                Get-WindowsUpdateStatus -ComputerName $vm.Name
            } else {
                [PSCustomObject]@{
                    LastInstalledUpdate  = $null
                    LastInstalledOn      = $null
                    AvailableUpdateCount = $null
                    Status               = 'not running'
                }
            }

            if ($AsObject) {
                ConvertTo-UpdateStatusObject -ComputerName $vm.Name -HyperVServer $server -State "$($vm.State)" -Status $vmStatus
                continue
            }

            $stateColor = if ($isRunning) {
                'Green'
            } elseif ("$($vm.State)" -eq 'Off') {
                'DarkGray'
            } else {
                'Yellow'
            }

            Write-ColorLine -Text (' {0} - ' -f $vm.Name) -Color White -NoNewline
            Write-ColorLine -Text "$($vm.State)" -Color $stateColor
            Write-ColorLine -Text (' Windows Update: {0}' -f $vmStatus.Status) -Color (Get-WindowsUpdateStatusColor -Status $vmStatus.Status)
        }

        Write-Progress -Id 2 -ParentId 1 -Activity "Checking Windows update status on VMs of $server" -Completed
    }

    Write-Progress -Id 1 -Activity 'Checking Windows update status' -Completed
}
}