Public/eventlog/Get-WindowsUpdateFailure.ps1
|
#Requires -Version 5.1 function Get-WindowsUpdateFailure { <# .SYNOPSIS Report Windows Update failures and restart requirements from the System log .DESCRIPTION Queries the Microsoft-Windows-WindowsUpdateClient provider in the System event log for failed installations, required restarts, and optionally successful installations. Named XML EventData fields are parsed so localized event message text does not drive classification or extraction. Results include the update identity, KB article, raw and hexadecimal error codes, restart status, and event message. Results are newest first, and an error on one computer does not stop the remaining computers from being processed. .PARAMETER ComputerName One or more computer names to target. Defaults to the local computer. Accepts pipeline input by value and by property name. .PARAMETER Days Look-back window in days. Valid values are 1 through 3650. Defaults to 30. .PARAMETER MaxEvents Maximum number of Windows Update events read per machine. Valid values are 1 through 10000. Defaults to 200. The most recent events are prioritized when the log is larger. .PARAMETER KBArticle Optional case-insensitive filter for the extracted KB article, such as KB5030211. .PARAMETER UpdateTitle Optional case-insensitive substring filter for the update title. .PARAMETER IncludeSuccess Includes event ID 19 successful installations in addition to failures and required restart events. .PARAMETER Credential Optional PSCredential for authenticating to remote machines. Ignored for local machine queries. .EXAMPLE Get-WindowsUpdateFailure Returns failed Windows Update installations and required restarts from the local computer over the last 30 days. .EXAMPLE Get-WindowsUpdateFailure -ComputerName 'SRV01' -Days 90 -KBArticle 'KB5030211' Returns matching Windows Update events from SRV01 over the last 90 days. .EXAMPLE 'SRV01', 'SRV02' | Get-WindowsUpdateFailure -MaxEvents 500 -IncludeSuccess Returns failures, required restarts, and successful installations from multiple computers through pipeline input. .OUTPUTS PSWinOps.WindowsUpdateFailure One object per recognized Windows Update event, newest first, with status, update identity, error information, restart state, and the raw event message. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-09-04 Requires: PowerShell 5.1+ / Windows only Requires: Event Log Readers membership for remote or protected System log access Requires: WinRM enabled on target machines for remote queries Event IDs 20, 21, and 19 represent installation failure, restart required, and installation success respectively. EventData XML is used for extraction; localized Message text is retained for context only and is never used for classification. .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.diagnostics/get-winevent #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')] [OutputType('PSWinOps.WindowsUpdateFailure')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateRange(1, 3650)] [int]$Days = 30, [Parameter(Mandatory = $false)] [ValidateRange(1, 10000)] [int]$MaxEvents = 200, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string]$KBArticle = '', [Parameter(Mandatory = $false)] [AllowEmptyString()] [string]$UpdateTitle = '', [Parameter(Mandatory = $false)] [switch]$IncludeSuccess, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting Windows Update failure query" $startTime = (Get-Date).AddDays(-$Days) $scriptBlock = { param( [datetime]$ScanStartTime, [int]$MaxEvts, [bool]$IncludeSuccessFilter, [string]$KBArticleFilter, [string]$UpdateTitleFilter ) $eventIds = @(20, 21) if ($IncludeSuccessFilter) { $eventIds += 19 } $filter = @{ LogName = 'System' ProviderName = 'Microsoft-Windows-WindowsUpdateClient' Id = $eventIds StartTime = $ScanStartTime } $eventRecords = @() try { $eventRecords = @(Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvts -ErrorAction Stop) } catch { if ($_.Exception.Message -notmatch 'No events were found') { throw } } if ($eventRecords.Count -eq 0) { return } function Get-EventDataMap { param( [Parameter(Mandatory = $true)] [object]$Event ) $map = @{} try { $xml = [xml]$Event.ToXml() foreach ($node in @($xml.SelectNodes("//*[local-name()='Data']"))) { $name = [string]$node.GetAttribute('Name') if ([string]::IsNullOrWhiteSpace($name)) { continue } $map[$name] = [string]$node.InnerText } } catch { Write-Verbose "Could not parse Windows Update event $($Event.Id) as XML: $_" } return $map } function Get-EventField { param( [Parameter(Mandatory = $true)] [hashtable]$Data, [Parameter(Mandatory = $true)] [string[]]$Names ) foreach ($name in $Names) { if ($Data.ContainsKey($name) -and -not [string]::IsNullOrWhiteSpace([string]$Data[$name])) { return [string]$Data[$name] } } return '' } function ConvertTo-ErrorCodeHex { param( [Parameter(Mandatory = $false)] [string]$Value ) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } $rawValue = $Value.Trim() $signedNumber = [int64]0 if ([int64]::TryParse( $rawValue, [Globalization.NumberStyles]::Integer, [Globalization.CultureInfo]::InvariantCulture, [ref]$signedNumber)) { $unsignedValue = $signedNumber if ($signedNumber -lt 0) { $unsignedValue += 4294967296 } return ('0x{0:X8}' -f $unsignedValue) } $hexValue = $rawValue -replace '^0x', '' $unsignedNumber = [uint32]0 if ([uint32]::TryParse( $hexValue, [Globalization.NumberStyles]::AllowHexSpecifier, [Globalization.CultureInfo]::InvariantCulture, [ref]$unsignedNumber)) { return ('0x{0:X8}' -f $unsignedNumber) } return '' } $rows = [System.Collections.Generic.List[psobject]]::new() foreach ($eventRecord in @($eventRecords | Sort-Object -Property TimeCreated -Descending)) { $eventId = [int]$eventRecord.Id if ($eventId -eq 19 -and -not $IncludeSuccessFilter) { continue } $status = switch ($eventId) { 19 { 'Succeeded' } 20 { 'Failed' } 21 { 'RebootRequired' } default { continue } } $data = Get-EventDataMap -Event $eventRecord $updateTitleValue = Get-EventField -Data $data -Names @('UpdateTitle', 'Title', 'UpdateName') $updateIdValue = Get-EventField -Data $data -Names @('UpdateId', 'UpdateGUID', 'UpdateGuid', 'UpdateIdentity') $kbArticleValue = Get-EventField -Data $data -Names @('KBArticle', 'KBNumber', 'KB') if ([string]::IsNullOrWhiteSpace($kbArticleValue) -and -not [string]::IsNullOrWhiteSpace($updateTitleValue)) { $kbMatch = [regex]::Match($updateTitleValue, '(?i)\bKB\d+\b') if ($kbMatch.Success) { $kbArticleValue = $kbMatch.Value } } $errorCodeValue = Get-EventField -Data $data -Names @('ErrorCode', 'HResult', 'HRESULT', 'ResultCode', 'Error') $failureReasonValue = Get-EventField -Data $data -Names @('FailureReason', 'ErrorDescription', 'Reason') if ([string]::IsNullOrWhiteSpace($failureReasonValue)) { $failureReasonValue = switch ($eventId) { 20 { 'InstallationFailure' } 21 { 'RestartRequired' } default { '' } } } $rows.Add([PSCustomObject]@{ EventTime = $eventRecord.TimeCreated EventId = $eventId Status = $status KBArticle = $kbArticleValue UpdateTitle = $updateTitleValue UpdateId = $updateIdValue ErrorCode = $errorCodeValue ErrorCodeHex = ConvertTo-ErrorCodeHex -Value $errorCodeValue RebootRequired = ($eventId -eq 21) FailureReason = $failureReasonValue Message = [string]$eventRecord.Message }) } foreach ($row in ($rows | Sort-Object -Property EventTime -Descending)) { if (-not [string]::IsNullOrWhiteSpace($KBArticleFilter) -and ([string]$row.KBArticle).IndexOf($KBArticleFilter, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { continue } if (-not [string]::IsNullOrWhiteSpace($UpdateTitleFilter) -and ([string]$row.UpdateTitle).IndexOf($UpdateTitleFilter, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { continue } [PSCustomObject]@{ PSTypeName = 'PSWinOps.WindowsUpdateFailure' ComputerName = $env:COMPUTERNAME EventTime = $row.EventTime.ToString('o') EventId = $row.EventId Status = $row.Status KBArticle = $row.KBArticle UpdateTitle = $row.UpdateTitle UpdateId = $row.UpdateId ErrorCode = $row.ErrorCode ErrorCodeHex = $row.ErrorCodeHex RebootRequired = $row.RebootRequired FailureReason = $row.FailureReason Message = $row.Message Timestamp = Get-Date -Format 'o' } } } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Querying Windows Update failures on '$targetComputer'" Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($startTime, $MaxEvents, $IncludeSuccess.IsPresent, $KBArticle, $UpdateTitle) } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed Windows Update failure query" } } |