Public/windowsupdate/Get-WindowsUpdate.ps1
|
#Requires -Version 5.1 function Get-WindowsUpdate { <# .SYNOPSIS Lists available Windows Updates on local or remote computers .DESCRIPTION Scans for available (not yet installed) Windows Updates using the COM API (Microsoft.Update.Session). Returns each pending update with its classification, product categories, download status, size, reboot requirement, MSRC severity, CVE identifiers, EULA status, and more. By default all classifications and products are returned. Use the Classification, Product, and KBArticleID parameters to filter results. Hidden updates are excluded unless the IncludeHidden switch is specified. By default, the machine's configured update source is used (WSUS, WUFB, or Windows Update). Use the MicrosoftUpdate switch to query the full Microsoft Update catalog instead. .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 required for local queries. .PARAMETER Classification Optional filter to return only updates matching the specified classifications. Common values: 'Critical Updates', 'Security Updates', 'Update Rollups', 'Feature Packs', 'Service Packs', 'Definition Updates', 'Tools', 'Drivers', 'Updates'. When not specified, all classifications are returned. .PARAMETER Product Optional filter to return only updates matching the specified product names. Common values: 'Windows Server 2022', 'Windows 11', 'Microsoft Office', 'Windows Defender'. When not specified, all products are returned. .PARAMETER KBArticleID Optional filter to return only updates matching the specified KB article IDs. Accepts one or more KB identifiers with or without the 'KB' prefix (e.g., 'KB5034441' or '5034441'). When not specified, all updates are returned. .PARAMETER MicrosoftUpdate When specified, queries the full Microsoft Update catalog instead of the machine's configured source (WSUS, WUFB, or Windows Update). This provides access to all Microsoft products including Office, SQL Server, etc. .PARAMETER IncludeHidden When specified, includes updates that have been hidden (declined). By default hidden updates are excluded from results. .EXAMPLE Get-WindowsUpdate Lists all available updates on the local computer using the configured source. .EXAMPLE Get-WindowsUpdate -ComputerName 'SRV01' -KBArticleID 'KB5034441' Checks if a specific KB is available on SRV01. .EXAMPLE 'SRV01', 'SRV02' | Get-WindowsUpdate -MicrosoftUpdate -Classification 'Security Updates' Lists security updates from the full Microsoft Update catalog on SRV01 and SRV02. .OUTPUTS PSWinOps.WindowsUpdate Returns objects with ComputerName, Title, KBArticle, Classification, Products, IsDownloaded, IsHidden, IsInstalled, IsMandatory, IsUninstallable, RebootRequired, MsrcSeverity, Description, ReleaseNotes, CveIDs, EulaAccepted, Deadline, SizeMB, 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-iupdatesearcher #> [CmdletBinding()] [OutputType('PSWinOps.WindowsUpdate')] 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)] [switch]$MicrosoftUpdate, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$KBArticleID, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$Classification, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$Product, [Parameter(Mandatory = $false)] [switch]$IncludeHidden ) begin { $sourceLabel = if ($MicrosoftUpdate) { 'Microsoft Update' } else { 'Default (machine config)' } Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting — Source: $sourceLabel" # Normalize KBArticleID — strip 'KB' prefix for consistent matching $normalizedKBIds = $null if ($KBArticleID) { $normalizedKBIds = $KBArticleID | ForEach-Object -Process { $_ -replace '^KB', '' } Write-Verbose -Message "[$($MyInvocation.MyCommand)] KB filter: $($KBArticleID -join ', ')" } if ($Classification) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Classification filter: $($Classification -join ', ')" } if ($Product) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Product filter: $($Product -join ', ')" } if ($IncludeHidden) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Including hidden updates" } $wuScriptBlock = { param( [bool]$SearchHidden, [bool]$UseMicrosoftUpdate ) try { # Detect machine's configured update source from registry $wuRegPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' $auRegPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' $wuReg = Get-ItemProperty -Path $wuRegPath -ErrorAction SilentlyContinue $auReg = Get-ItemProperty -Path $auRegPath -ErrorAction SilentlyContinue $configuredSource = 'Windows Update' $configuredUrl = $null $configuredTargetGroup = $null if ($auReg.UseWUServer -eq 1 -and -not [string]::IsNullOrEmpty($wuReg.WUServer)) { $configuredSource = 'WSUS' $configuredUrl = $wuReg.WUServer $configuredTargetGroup = $wuReg.TargetGroup } elseif ($wuReg.DeferFeatureUpdates -eq 1 -or $wuReg.DeferQualityUpdates -eq 1) { $configuredSource = 'WUFB' } $session = New-Object -ComObject 'Microsoft.Update.Session' $searcher = $session.CreateUpdateSearcher() $effectiveSource = $configuredSource if ($UseMicrosoftUpdate) { $serviceManager = New-Object -ComObject 'Microsoft.Update.ServiceManager' $serviceManager.ClientApplicationID = 'PSWinOps' $service = $serviceManager.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '') $searcher.ServerSelection = 3 $searcher.ServiceID = $service.ServiceID $effectiveSource = 'Microsoft Update' } $criteria = 'IsInstalled=0' if (-not $SearchHidden) { $criteria += ' AND IsHidden=0' } $searchResult = $searcher.Search($criteria) # Build metadata object for verbose output $metadata = [PSCustomObject]@{ ConfiguredSource = $configuredSource ConfiguredUrl = $configuredUrl TargetGroup = $configuredTargetGroup EffectiveSource = $effectiveSource TotalCount = $searchResult.Updates.Count } if ($searchResult.Updates.Count -eq 0) { return [PSCustomObject]@{ Metadata = $metadata Entries = @() } } $entries = [System.Collections.Generic.List[object]]::new() foreach ($update in $searchResult.Updates) { $classification = $null $products = [System.Collections.Generic.List[string]]::new() foreach ($category in $update.Categories) { if ($category.Type -eq 'UpdateClassification') { if ($null -eq $classification) { $classification = $category.Name } } else { $products.Add($category.Name) } } $kbArticle = '' if ($update.KBArticleIDs.Count -gt 0) { $kbArticle = "KB$($update.KBArticleIDs.Item(0))" } $cveIds = @() if ($update.CveIDs) { foreach ($cve in $update.CveIDs) { $cveIds += [string]$cve } } $allKBs = @() if ($update.KBArticleIDs.Count -gt 0) { foreach ($kb in $update.KBArticleIDs) { $allKBs += [string]$kb } } $entries.Add([PSCustomObject]@{ Title = [string]$update.Title KBArticle = $kbArticle KBArticleIDs = $allKBs Classification = [string]$classification Products = @($products) Description = [string]$update.Description ReleaseNotes = [string]$update.ReleaseNotes MsrcSeverity = [string]$update.MsrcSeverity CveIDs = $cveIds IsDownloaded = [bool]$update.IsDownloaded IsHidden = [bool]$update.IsHidden IsInstalled = [bool]$update.IsInstalled IsMandatory = [bool]$update.IsMandatory IsUninstallable = [bool]$update.IsUninstallable EulaAccepted = [bool]$update.EulaAccepted Deadline = $update.Deadline RebootRequired = ($update.RebootBehavior -ne 0) MaxSizeBytes = [long]$update.MaxDownloadSize UpdateId = [string]$update.Identity.UpdateID RevisionNumber = [int]$update.Identity.RevisionNumber }) } return [PSCustomObject]@{ Metadata = $metadata Entries = $entries } } catch { throw "Failed to search for Windows Updates: $_" } } } process { foreach ($computer in $ComputerName) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Scanning '$computer' (Source: $sourceLabel, Criteria: IsInstalled=0$(if (-not $IncludeHidden) { ' AND IsHidden=0' }))" try { $invokeParams = @{ ComputerName = $computer ScriptBlock = $wuScriptBlock ArgumentList = @([bool]$IncludeHidden, [bool]$MicrosoftUpdate) } if ($PSBoundParameters.ContainsKey('Credential')) { $invokeParams['Credential'] = $Credential } $scanTimer = [System.Diagnostics.Stopwatch]::StartNew() $scanResult = Invoke-RemoteOrLocal @invokeParams $scanTimer.Stop() # Unpack metadata and entries $metadata = $scanResult.Metadata $rawEntries = $scanResult.Entries # Build verbose source info $configInfo = $metadata.ConfiguredSource if ($metadata.ConfiguredUrl) { $configInfo += " ($($metadata.ConfiguredUrl))" } if ($metadata.TargetGroup) { $configInfo += " [Group: $($metadata.TargetGroup)]" } if ($metadata.EffectiveSource -ne $metadata.ConfiguredSource) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] '$computer' configured: $configInfo — Overriding to: $($metadata.EffectiveSource)" } else { Write-Verbose -Message "[$($MyInvocation.MyCommand)] '$computer' source: $configInfo" } $totalFound = $metadata.TotalCount Write-Verbose -Message "[$($MyInvocation.MyCommand)] Scan completed on '$computer' in $($scanTimer.Elapsed.TotalSeconds.ToString('F1'))s — $totalFound update(s) found" if ($totalFound -eq 0) { continue } # Filter by KBArticleID if specified if ($normalizedKBIds) { $rawEntries = @($rawEntries | Where-Object -FilterScript { $entryKBs = $_.KBArticleIDs $null -ne ($normalizedKBIds | Where-Object -FilterScript { $_ -in $entryKBs } | Select-Object -First 1) }) } # Filter by Classification if specified if ($Classification) { $rawEntries = @($rawEntries | Where-Object -FilterScript { $_.Classification -in $Classification }) } # Filter by Product if specified if ($Product) { $rawEntries = @($rawEntries | Where-Object -FilterScript { $entryProducts = $_.Products $null -ne ($Product | Where-Object -FilterScript { $_ -in $entryProducts } | Select-Object -First 1) }) } $filteredCount = @($rawEntries).Count if ($filteredCount -ne $totalFound) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] After filtering: $filteredCount of $totalFound update(s) match criteria on '$computer'" } foreach ($entry in $rawEntries) { [PSCustomObject]@{ PSTypeName = 'PSWinOps.WindowsUpdate' ComputerName = $computer Title = $entry.Title KBArticle = $entry.KBArticle Classification = $entry.Classification Products = $entry.Products Description = $entry.Description ReleaseNotes = $entry.ReleaseNotes MsrcSeverity = $entry.MsrcSeverity CveIDs = $entry.CveIDs IsDownloaded = $entry.IsDownloaded IsHidden = $entry.IsHidden IsInstalled = $entry.IsInstalled IsMandatory = $entry.IsMandatory IsUninstallable = $entry.IsUninstallable EulaAccepted = $entry.EulaAccepted Deadline = $entry.Deadline RebootRequired = $entry.RebootRequired SizeMB = [math]::Round($entry.MaxSizeBytes / 1MB, 2) 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 updates from ${computer}: $_" continue } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |