Public/windowsupdate/Get-WindowsUpdateHistory.ps1

#Requires -Version 5.1
function Get-WindowsUpdateHistory {
    <#
        .SYNOPSIS
            Retrieves Windows Update installation history from local or remote computers
 
        .DESCRIPTION
            Queries the Windows Update Agent COM API (Microsoft.Update.Session) to retrieve
            the installation history of Windows Updates. Results include update title, KB article,
            operation type, result status, classification, products, client application, HResult
            error code, update source, date, description, and support URL.
            Output is sorted by date descending (most recent first) and limited by MaxResults.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local computer.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not used for local queries.
 
        .PARAMETER MaxResults
            Maximum number of history entries to return per computer.
            Valid range is 1 to 1000. Defaults to 50.
 
        .EXAMPLE
            Get-WindowsUpdateHistory
 
            Retrieves the 50 most recent Windows Update history entries from the local computer.
 
        .EXAMPLE
            Get-WindowsUpdateHistory -ComputerName 'SRV01' -Credential (Get-Credential) -MaxResults 100
 
            Retrieves the 100 most recent update history entries from SRV01 using alternate credentials.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-WindowsUpdateHistory -MaxResults 10
 
            Retrieves the 10 most recent update history entries from SRV01 and SRV02 via pipeline.
 
        .OUTPUTS
            PSWinOps.WindowsUpdateHistory
            Returns objects with ComputerName, Title, KBArticle, Operation, Result, HResult,
            Classification, Products, ClientApplicationID, ServerSelection, ServiceID, Date,
            Description, SupportUrl, UpdateId, RevisionNumber, and Timestamp properties.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.1.0
            Last Modified: 2026-04-08
            Requires: PowerShell 5.1+ / Windows only
            Requires: Windows Update service must be accessible on target machines
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdatesession
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.WindowsUpdateHistory')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 1000)]
        [int]$MaxResults = 50
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $resultCodeMap = @{
            0 = 'NotStarted'
            1 = 'InProgress'
            2 = 'Succeeded'
            3 = 'SucceededWithErrors'
            4 = 'Failed'
            5 = 'Aborted'
        }

        $operationMap = @{
            1 = 'Installation'
            2 = 'Uninstallation'
        }

        $serverSelectionMap = @{
            0 = 'Default'
            1 = 'WSUS'
            2 = 'WindowsUpdate'
            3 = 'Other'
        }

        $wuScriptBlock = {
            param(
                [int]$Limit
            )

            try {
                $session = New-Object -ComObject 'Microsoft.Update.Session'
                $searcher = $session.CreateUpdateSearcher()
                $totalCount = $searcher.GetTotalHistoryCount()

                if ($totalCount -eq 0) {
                    return @()
                }

                $queryCount = [System.Math]::Min($Limit, $totalCount)
                $history = $searcher.QueryHistory(0, $queryCount)

                $entries = [System.Collections.Generic.List[object]]::new()
                foreach ($entry in $history) {
                    $classification = $null
                    $products = [System.Collections.Generic.List[string]]::new()

                    try {
                        if ($entry.Categories) {
                            foreach ($category in $entry.Categories) {
                                if ($category.Type -eq 'UpdateClassification') {
                                    if ($null -eq $classification) {
                                        $classification = [string]$category.Name
                                    }
                                } else {
                                    $products.Add([string]$category.Name)
                                }
                            }
                        }
                    } catch {
                        Write-Verbose -Message "Could not retrieve categories for '$($entry.Title)': $_"
                    }

                    $entries.Add([PSCustomObject]@{
                            Title               = [string]$entry.Title
                            Operation           = [int]$entry.Operation
                            ResultCode          = [int]$entry.ResultCode
                            HResult             = [int]$entry.HResult
                            Classification      = $classification
                            Products            = @($products)
                            ClientApplicationID = [string]$entry.ClientApplicationID
                            ServerSelection     = [int]$entry.ServerSelection
                            ServiceID           = [string]$entry.ServiceID
                            Date                = [datetime]$entry.Date
                            Description         = [string]$entry.Description
                            SupportUrl          = [string]$entry.SupportUrl
                            UpdateId            = [string]$entry.UpdateIdentity.UpdateID
                            RevisionNumber      = [int]$entry.UpdateIdentity.RevisionNumber
                        })
                }

                return $entries
            } catch {
                throw "Failed to query Windows Update history: $_"
            }
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying update history on $computer"

            try {
                $invokeParams = @{
                    ComputerName = $computer
                    ScriptBlock  = $wuScriptBlock
                    ArgumentList = @($MaxResults)
                }

                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $invokeParams['Credential'] = $Credential
                }

                $rawEntries = Invoke-RemoteOrLocal @invokeParams

                if (-not $rawEntries -or @($rawEntries).Count -eq 0) {
                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] No update history found on $computer"
                    continue
                }

                $sortedEntries = $rawEntries | Sort-Object -Property 'Date' -Descending

                foreach ($entry in $sortedEntries) {
                    $kbMatch = [regex]::Match($entry.Title, 'KB\d+')
                    $kbArticle = if ($kbMatch.Success) {
                        $kbMatch.Value
                    } else {
                        ''
                    }

                    $operationString = if ($operationMap.ContainsKey($entry.Operation)) {
                        $operationMap[$entry.Operation]
                    } else {
                        'Unknown'
                    }

                    $resultString = if ($resultCodeMap.ContainsKey($entry.ResultCode)) {
                        $resultCodeMap[$entry.ResultCode]
                    } else {
                        'Unknown'
                    }

                    $serverSelectionString = if ($serverSelectionMap.ContainsKey($entry.ServerSelection)) {
                        $serverSelectionMap[$entry.ServerSelection]
                    } else {
                        'Unknown'
                    }

                    # Format HResult as hex for readability (e.g. 0x80070005)
                    $hResultHex = if ($entry.HResult -ne 0) {
                        '0x{0:X8}' -f $entry.HResult
                    } else {
                        '0x00000000'
                    }

                    [PSCustomObject]@{
                        PSTypeName          = 'PSWinOps.WindowsUpdateHistory'
                        ComputerName        = $computer
                        Title               = $entry.Title
                        KBArticle           = $kbArticle
                        Operation           = $operationString
                        Result              = $resultString
                        HResult             = $hResultHex
                        Classification      = $entry.Classification
                        Products            = $entry.Products
                        ClientApplicationID = $entry.ClientApplicationID
                        ServerSelection     = $serverSelectionString
                        ServiceID           = $entry.ServiceID
                        Date                = $entry.Date
                        Description         = $entry.Description
                        SupportUrl          = $entry.SupportUrl
                        UpdateId            = $entry.UpdateId
                        RevisionNumber      = $entry.RevisionNumber
                        Timestamp           = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                    }
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to retrieve update history from ${computer}: $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}