PSPatchManager.psm1
# Helper function for Windows Update error handling function Get-WindowsUpdateErrorMessage { param([int]$ErrorCode) $hexError = "0x{0:X8}" -f $ErrorCode switch ($hexError) { "0x80240032" { return "No updates were found to install. ($hexError)" } "0x80240022" { return "The update is not applicable to this computer. ($hexError)" } "0x80240006" { return "The search may have been interrupted. ($hexError)" } "0x8024000B" { return "Service registration is missing or corrupt. ($hexError)" } "0x8024000C" { return "Windows Update Agent is missing or corrupt. ($hexError)" } "0x8024000D" { return "Windows Update Agent is missing or corrupt. ($hexError)" } "0x8024000E" { return "Windows Update Agent is missing or corrupt. ($hexError)" } "0x8024001E" { return "Service registration is missing or corrupt. ($hexError)" } "0x8024002E" { return "Windows Update Agent was unable to provide the service. ($hexError)" } default { return "Windows Update error: $hexError" } } } # Helper function to translate update operation result codes function Get-UpdateOperationResult { param( [Parameter(Mandatory)] [string]$ResultCode, [string]$Operation ) $status = switch ($ResultCode) { "0" { "Failed" } "1" { "In Progress" } "2" { "Succeeded" } "3" { "Succeeded with Errors" } "4" { "Cancelled" } "5" { "Aborted" } default { "Unknown ($ResultCode)" } } return "$Operation $status" } # Common helper function for error handling and verbose output function Write-PMLog { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [ValidateSet('Info', 'Warning', 'Error')] [string]$Type = 'Info' ) switch ($Type) { 'Info' { if ($Message -match '^(Processing|Found|Status:|Download Summary|Total updates|Starting download)') { Write-Verbose "`n$Message" } elseif ($Message -match '^(Adding update|Downloading)') { Write-Verbose " $Message" } else { Write-Verbose $Message } } 'Warning' { Write-Warning "`n$Message" } 'Error' { Write-Error "`n$Message" } } } function Get-PMWinUpdate { <# .SYNOPSIS Gets available Windows updates with advanced filtering options. .DESCRIPTION Retrieves a list of available Windows updates with the ability to filter by various criteria including severity, type, reboot requirement, date range, size, and title search. .PARAMETER Title Filter updates by title. Supports wildcard patterns unless -ExactMatch is specified. .PARAMETER ExactMatch When used with -Title, requires an exact match instead of wildcard pattern matching. .PARAMETER Hidden If specified, returns hidden updates instead of visible ones. .PARAMETER NotInstalled If specified, returns only updates that are not installed. .PARAMETER Severity Filters updates by severity level. Valid values are: - Critical - Important - Moderate - Low - Unspecified .PARAMETER Type Filters updates by type. Valid values are: - Software - Driver - Security - FeaturePack .PARAMETER RebootRequired If specified, returns only updates that require a reboot. .PARAMETER FromDate If specified, returns updates released on or after this date. .PARAMETER ToDate If specified, returns updates released on or before this date. .PARAMETER MaxSize If specified, returns updates with size less than or equal to this value (in MB). .PARAMETER SortBy Specifies the property to sort results by. Valid values are: - Size - ReleaseDate - Title - Severity .PARAMETER Descending If specified, sorts the results in descending order. .PARAMETER ExcludeBeta If specified, excludes beta updates from the results. .PARAMETER OnlyDownloaded If specified, returns only updates that have been downloaded. .PARAMETER CategoryFilter If specified, filters updates by category name (supports wildcards). .PARAMETER Repository Specifies the repository to use for updates. Valid values are: - WindowsUpdate - MicrosoftUpdate .EXAMPLE Get-PMWinUpdate -Title "Security Update for Windows" Returns all updates containing "Security Update for Windows" in their title. .EXAMPLE Get-PMWinUpdate -Title "KB5025221" -ExactMatch Returns the update with exact title match for KB5025221. .EXAMPLE Get-PMWinUpdate -Severity Critical, Important Returns all critical and important updates. #> [CmdletBinding()] param( [Parameter(ParameterSetName='Title')] [string]$Title, [switch]$Hidden, [switch]$NotInstalled, [ValidateSet('Critical', 'Important', 'Moderate', 'Low', 'Unspecified')] [string[]]$Severity, [ValidateSet('Software', 'Driver', 'Security', 'FeaturePack')] [string[]]$Type, [switch]$RebootRequired, [DateTime]$FromDate, [DateTime]$ToDate, [int]$MaxSize, # Size in MB [ValidateSet('Size', 'ReleaseDate', 'Title', 'Severity')] [string]$SortBy = 'ReleaseDate', [switch]$Descending, [switch]$ExcludeBeta, [switch]$OnlyDownloaded, [string]$CategoryFilter, [ValidateSet('WindowsUpdate', 'MicrosoftUpdate')] [string]$Repository = 'WindowsUpdate' ) begin { Write-PMLog -Message 'Initializing Windows Update session...' -Type Info try { $UpdateSession = New-Object -ComObject Microsoft.Update.Session $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() # Configure repository if ($Repository -eq 'MicrosoftUpdate') { $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d" # Check if Microsoft Update is registered $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID } if (-not $Service) { Write-PMLog -Message "Microsoft Update Service is not registered. Use Register-PMMicrosoftUpdate to enable it." -Type Warning return } Write-PMLog -Message "Using Microsoft Update Service..." -Type Info $UpdateSearcher.ServerSelection = 3 # ssOthers $UpdateSearcher.ServiceID = $ServiceID } else { Write-PMLog -Message "Using Windows Update Service..." -Type Info $UpdateSearcher.ServerSelection = 2 # ssWindowsUpdate } } catch { $errorMessage = 'Failed to initialize Windows Update session: ' + $_.Exception.Message Write-PMLog -Message $errorMessage -Type Error return } } end { try { Write-PMLog -Message 'Searching for Windows Updates...' -Type Info $searchCriteria = 'IsInstalled=0' if ($Hidden) { $searchCriteria = $searchCriteria + ' AND IsHidden=1' } elseif ($NotInstalled) { $searchCriteria = $searchCriteria + ' AND IsHidden=0' } $SearchResult = $UpdateSearcher.Search($searchCriteria) $updates = $SearchResult.Updates | ForEach-Object { # Convert size to readable format $sizeString = if ($_.MaxDownloadSize -ge 1GB) { '{0:N2} GB' -f ($_.MaxDownloadSize / 1GB) } elseif ($_.MaxDownloadSize -ge 1MB) { '{0:N2} MB' -f ($_.MaxDownloadSize / 1MB) } elseif ($_.MaxDownloadSize -ge 1KB) { '{0:N2} KB' -f ($_.MaxDownloadSize / 1KB) } else { '{0} B' -f $_.MaxDownloadSize } [PSCustomObject]@{ PSTypeName = 'PSPatchManager.WindowsUpdate' Title = $_.Title KB = $_.KBArticleIDs -join ',' Size = $sizeString SizeBytes = $_.MaxDownloadSize IsHidden = $_.IsHidden Categories = ($_.Categories | Select-Object -ExpandProperty Name) -join '; ' Severity = $_.MsrcSeverity Type = if ($_.Type -eq 1) { 'Software' } elseif ($_.Type -eq 2) { 'Driver' } elseif ($_.Categories.Name -contains 'Security Updates') { 'Security' } elseif ($_.Categories.Name -contains 'Feature Packs') { 'FeaturePack' } else { 'Other' } RebootRequired = $_.RebootRequired ReleaseDate = $_.LastDeploymentChangeTime Description = $_.Description SupportUrl = $_.SupportUrl IsBeta = $_.IsBeta IsDownloaded = $_.IsDownloaded Impact = $_.MsrcImpact } } # Apply additional filters if ($Severity) { $updates = $updates | Where-Object { $_.Severity -in $Severity } } if ($Type) { $updates = $updates | Where-Object { $_.Type -in $Type } } if ($RebootRequired) { $updates = $updates | Where-Object { $_.RebootRequired -eq $true } } if ($FromDate) { $updates = $updates | Where-Object { $_.ReleaseDate -ge $FromDate } } if ($ToDate) { $updates = $updates | Where-Object { $_.ReleaseDate -le $ToDate } } if ($MaxSize) { $maxBytes = $MaxSize * 1MB $updates = $updates | Where-Object { $_.SizeBytes -le $maxBytes } } # Apply new filters if ($ExcludeBeta) { $updates = $updates | Where-Object { -not $_.IsBeta } } if ($OnlyDownloaded) { $updates = $updates | Where-Object { $_.IsDownloaded } } if ($CategoryFilter) { $updates = $updates | Where-Object { $_.Categories -like "*$CategoryFilter*" } } # Add title filtering if ($Title) { # Check if title contains wildcard characters if ($Title -match '[*?]') { $updates = $updates | Where-Object { $_.Title -like $Title } } else { $updates = $updates | Where-Object { $_.Title -eq $Title } } } # Sort the results $updates = switch ($SortBy) { 'Size' { $updates | Sort-Object { $_.SizeBytes } } 'ReleaseDate' { $updates | Sort-Object ReleaseDate } 'Title' { $updates | Sort-Object Title } 'Severity' { $updates | Sort-Object { switch ($_.Severity) { 'Critical' { 1 } 'Important' { 2 } 'Moderate' { 3 } 'Low' { 4 } default { 5 } } } } } if ($Descending) { $updates = [array]($updates | Sort-Object -Descending) } # Add summary information if ($updates) { $totalSize = ($updates | Measure-Object -Property SizeBytes -Sum).Sum $totalSizeString = if ($totalSize -ge 1GB) { '{0:N2} GB' -f ($totalSize / 1GB) } elseif ($totalSize -ge 1MB) { '{0:N2} MB' -f ($totalSize / 1MB) } elseif ($totalSize -ge 1KB) { '{0:N2} KB' -f ($totalSize / 1KB) } else { '{0} B' -f $totalSize } Write-PMLog -Message ('Found {0} update(s). Total size: {1}' -f $updates.Count, $totalSizeString) -Type Info } else { Write-PMLog -Message 'No updates found matching the specified criteria.' -Type Info } return $updates } catch { $errorMessage = 'Error searching for updates: ' + $_.Exception.Message Write-PMLog -Message $errorMessage -Type Error } } } function Install-PMWinUpdate { <# .SYNOPSIS Installs specified Windows updates with advanced reboot control. .DESCRIPTION Downloads and installs Windows updates with granular control over reboot behavior. Updates can be specified by KB number, title search, or installed automatically. Supports recursive update cycles and custom reboot actions. .PARAMETER KB One or more KB article IDs to install specific updates. .PARAMETER Title Filter and install updates by title. Supports wildcard patterns unless -ExactMatch is specified. .PARAMETER ExactMatch When used with -Title, requires an exact match instead of wildcard pattern matching. .PARAMETER AcceptAll If specified, automatically accepts and installs all available updates. .PARAMETER RebootBehavior Controls how reboots are handled: - Never: Never reboot automatically (default) - IfRequired: Reboot only if required by updates - Always: Always reboot after installing updates .PARAMETER Repository Specifies the repository to use for updates. Valid values are: - WindowsUpdate - MicrosoftUpdate .EXAMPLE Install-PMWinUpdate -Title "Security Update for Windows" Installs all updates containing "Security Update for Windows" in their title. .EXAMPLE Install-PMWinUpdate -Title "KB5025221" -ExactMatch Installs the specific update matching KB5025221. .EXAMPLE Get-PMWinUpdate -Severity Critical | Install-PMWinUpdate -RebootBehavior IfRequired Installs all critical updates, rebooting if required. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ParameterSetName='KB')] [string[]]$KB, [Parameter(ParameterSetName='Title')] [string]$Title, [switch]$AcceptAll = $true, [ValidateSet('Never', 'IfRequired', 'Always')] [string]$RebootBehavior = 'Never', [switch]$ContinueAfterReboot, [int]$MaxCycles = 3, [int]$RebootTimeout = 300, [scriptblock]$PreRebootAction, [scriptblock]$PostRebootAction, [DateTime]$ScheduledReboot, [ValidateSet('WindowsUpdate', 'MicrosoftUpdate')] [string]$Repository = 'WindowsUpdate' ) begin { $script:cycleCount = 0 $script:pendingReboot = $false Write-PMLog -Message 'Initializing Windows Update installation session...' -Type Info try { $script:updateSession = New-Object -ComObject Microsoft.Update.Session $script:updateSearcher = $updateSession.CreateUpdateSearcher() # Configure repository if ($Repository -eq 'MicrosoftUpdate') { $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d" # Check if Microsoft Update is registered $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID } if (-not $Service) { Write-PMLog -Message "Microsoft Update Service is not registered. Use Register-PMMicrosoftUpdate to enable it." -Type Warning return } Write-PMLog -Message "Using Microsoft Update Service..." -Type Info $script:updateSearcher.ServerSelection = 3 # ssOthers $script:updateSearcher.ServiceID = $ServiceID } else { Write-PMLog -Message "Using Windows Update Service..." -Type Info $script:updateSearcher.ServerSelection = 2 # ssWindowsUpdate } $script:updateDownloader = $updateSession.CreateUpdateDownloader() $script:updateInstaller = $updateSession.CreateUpdateInstaller() } catch { Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error return } function Test-PendingReboot { $regKeys = @( @{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\'; Value='RebootPending'}, @{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\'; Value='RebootRequired'} ) foreach ($key in $regKeys) { if (Test-Path $key.Path) { if (Get-ItemProperty -Path $key.Path -Name $key.Value -ErrorAction SilentlyContinue) { return $true } } } return $false } function Invoke-SystemReboot { if ($PreRebootAction) { Write-PMLog -Message 'Executing pre-reboot action...' -Type Info & $PreRebootAction } if ($ScheduledReboot) { $waitSeconds = [math]::Max(0, [int]($ScheduledReboot - (Get-Date)).TotalSeconds) if ($waitSeconds -gt 0) { Write-PMLog -Message "System will reboot at $($ScheduledReboot.ToString('yyyy-MM-dd HH:mm:ss'))" -Type Warning $rebootJob = Start-Job -ScriptBlock { param($timeout) Start-Sleep -Seconds $timeout Restart-Computer -Force } -ArgumentList $waitSeconds } else { Write-PMLog -Message "Scheduled reboot time has already passed, rebooting now..." -Type Warning $rebootJob = Start-Job -ScriptBlock { param($timeout) Start-Sleep -Seconds $timeout Restart-Computer -Force } -ArgumentList $RebootTimeout } } else { Write-PMLog -Message "System will reboot in $RebootTimeout seconds..." -Type Warning $rebootJob = Start-Job -ScriptBlock { param($timeout) Start-Sleep -Seconds $timeout Restart-Computer -Force } -ArgumentList $RebootTimeout } if ($ContinueAfterReboot) { $taskName = 'PSPatchManager_ContinueUpdates' $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-Command `"Import-Module PSPatchManager; Install-PMWinUpdate -AcceptAll -RebootBehavior $RebootBehavior -ContinueAfterReboot -MaxCycles $MaxCycles -RebootTimeout $RebootTimeout`"" $trigger = New-ScheduledTaskTrigger -AtStartup Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Force | Out-Null } $rebootJob | Wait-Job if ($PostRebootAction) { Write-PMLog -Message 'Executing post-reboot action...' -Type Info & $PostRebootAction } } } end { try { $script:cycleCount++ if ($script:cycleCount -gt $MaxCycles) { Write-PMLog -Message 'Maximum update cycles reached.' -Type Warning return } Write-PMLog -Message "Retrieving available updates..." -Type Info try { $SearchResult = $script:updateSearcher.Search("IsInstalled=0 AND IsHidden=0") Write-PMLog -Message "Found $($SearchResult.Updates.Count) total available updates." -Type Info if ($Title) { Write-PMLog -Message "Filtering updates with title pattern: $Title" -Type Info } elseif ($KB) { Write-PMLog -Message "Filtering updates for KB(s): $($KB -join ', ')" -Type Info } } catch { $errorMsg = Get-WindowsUpdateErrorMessage $_.Exception.HResult Write-PMLog -Message "Error searching for updates: $errorMsg" -Type Error throw } if ($SearchResult.Updates.Count -eq 0) { Write-PMLog -Message "No updates available." -Type Info return } $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl $totalUpdates = $SearchResult.Updates.Count $currentUpdate = 0 foreach ($Update in $SearchResult.Updates) { $currentUpdate++ Write-Progress -Activity 'Processing Windows Updates' -Status "Processing update $currentUpdate of $totalUpdates" -PercentComplete ($currentUpdate / $totalUpdates * 100) try { $updateKB = $Update.KBArticleIDs | Select-Object -First 1 $shouldInstall = $false if ($KB) { $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' } $shouldInstall = $updateKB -in $kbNumbers } elseif ($Title) { # Check if title contains wildcard characters if ($Title -match '[*?]') { $shouldInstall = $Update.Title -like $Title } else { $shouldInstall = $Update.Title -eq $Title } } else { $shouldInstall = $true } if ($shouldInstall) { if (-not $Update.EulaAccepted) { $Update.AcceptEula() } if ($AcceptAll -or $PSCmdlet.ShouldProcess($Update.Title, 'Install update')) { Write-PMLog -Message "Processing update: $($Update.Title) (KB$updateKB)" -Type Info $UpdatesToInstall.Add($Update) | Out-Null if (-not $Update.IsDownloaded) { $UpdatesToDownload.Add($Update) | Out-Null } } } } catch { Write-PMLog -Message "Error processing update $($Update.Title): $($_.Exception.Message)" -Type Warning continue } } Write-Progress -Activity 'Processing Windows Updates' -Completed Write-PMLog -Message "$($UpdatesToInstall.Count) update(s) matched the specified criteria." -Type Info if ($UpdatesToInstall.Count -gt 0) { # Download updates if needed if ($UpdatesToDownload.Count -gt 0) { Write-PMLog -Message "Downloading $($UpdatesToDownload.Count) updates..." -Type Info $script:updateDownloader.Updates = $UpdatesToDownload try { $DownloadResult = $script:updateDownloader.Download() switch ($DownloadResult.ResultCode) { 0 { Write-PMLog -Message "Download failed." -Type Error; throw "Download failed" } 1 { Write-PMLog -Message "Download in progress..." -Type Info } 2 { Write-PMLog -Message "Download completed successfully." -Type Info } 3 { Write-PMLog -Message "Download completed with errors." -Type Warning } 4 { Write-PMLog -Message "Download cancelled." -Type Warning; throw "Download cancelled" } 5 { Write-PMLog -Message "Download aborted." -Type Error; throw "Download aborted" } default { Write-PMLog -Message "Unknown download status: $($DownloadResult.ResultCode)" -Type Warning } } if ($DownloadResult.ResultCode -ne 2) { throw "Download failed with status: $($DownloadResult.ResultCode)" } } catch { Write-PMLog -Message "Error during download: $($_.Exception.Message)" -Type Error throw } } else { Write-PMLog -Message "All updates are already downloaded." -Type Info } Write-PMLog -Message 'Installing updates...' -Type Info try { $script:updateInstaller.Updates = $UpdatesToInstall $InstallResult = $script:updateInstaller.Install() if ($InstallResult.ResultCode -ne 2) { Write-PMLog -Message "Installation failed with status: $($InstallResult.ResultCode)" -Type Error throw "Installation operation failed with status: $($InstallResult.ResultCode)" } # Get the results immediately to avoid COM object issues $resultCode = $InstallResult.ResultCode.ToString() $rebootRequired = $InstallResult.RebootRequired $updateResults = @() # Process each update result immediately for ($i = 0; $i -lt $UpdatesToInstall.Count; $i++) { $update = $UpdatesToInstall.Item($i) $updateResult = $InstallResult.GetUpdateResult($i) $updateResults += [PSCustomObject]@{ Title = $update.Title KB = $update.KBArticleIDs -join ',' Result = $updateResult.ResultCode.ToString() Status = switch ($updateResult.ResultCode.ToString()) { "0" { "Failed" } "1" { "In Progress" } "2" { "Success" } "3" { "Partial Success" } "4" { "Cancelled" } "5" { "Aborted" } default { "Unknown" } } HResult = if ($updateResult.HResult -ne 0) { "0x{0:X8}" -f $updateResult.HResult } else { "Success" } } } Write-PMLog -Message "Installation Summary:" -Type Info Write-PMLog -Message "Status: $(Get-UpdateOperationResult -ResultCode $resultCode -Operation 'Installation')" -Type Info Write-PMLog -Message "Updates Processed: $($UpdatesToInstall.Count)" -Type Info Write-PMLog -Message "Reboot Required: $rebootRequired" -Type Info $script:pendingReboot = $rebootRequired -or (Test-PendingReboot) if ($script:pendingReboot) { Write-PMLog -Message 'System requires a reboot.' -Type Warning switch ($RebootBehavior) { 'Always' { Invoke-SystemReboot } 'IfRequired' { if ($script:pendingReboot) { Invoke-SystemReboot } } } } return $updateResults } catch { throw "Installation failed: $($_.Exception.Message)" } } else { Write-PMLog -Message "No updates matched the specified criteria." -Type Info } } catch { $errorCode = if ($_.Exception.HResult) { "0x{0:X8}" -f $_.Exception.HResult } else { $_.Exception.Message } Write-PMLog -Message "Error during update installation (Error code: $errorCode): $($_.Exception.Message)" -Type Error throw } finally { Write-Progress -Activity 'Processing Windows Updates' -Completed Write-Progress -Activity 'Downloading Updates' -Completed } } } function Set-PMWinUpdate { <# .SYNOPSIS Modifies Windows update properties such as hidden status. .DESCRIPTION Allows modification of Windows update properties including: - Hiding/Unhiding updates - Additional properties can be added as needed Updates can be specified by KB number or title search. .PARAMETER KB One or more KB article IDs of the updates to modify. .PARAMETER Title Filter updates by title. Supports wildcard patterns. .PARAMETER Hidden If specified, sets the hidden status of the matching updates. Use $true to hide updates, $false to unhide them. .EXAMPLE Set-PMWinUpdate -KB 'KB5025221' -Hidden $true Hides the specified update. .EXAMPLE Set-PMWinUpdate -KB 'KB5025221', 'KB5025222' -Hidden $false Unhides multiple updates. .EXAMPLE Set-PMWinUpdate -Title "Security Update for Windows*" -Hidden $true Hides all updates matching the title pattern. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ParameterSetName='KB')] [string[]]$KB, [Parameter(ParameterSetName='Title')] [string]$Title, [Parameter(Mandatory)] [bool]$Hidden ) begin { Write-PMLog -Message 'Initializing Windows Update session...' -Type Info try { $updateSession = New-Object -ComObject Microsoft.Update.Session $updateSearcher = $updateSession.CreateUpdateSearcher() } catch { Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error return } } process { try { $searchCriteria = "" if ($KB) { $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' } $kbFilter = $kbNumbers | ForEach-Object { "KBArticleID='$_'" } $searchCriteria = "($($kbFilter -join ' OR '))" } elseif ($Title) { if ($Title -match '[*?]') { # Need to search all and filter later for wildcard $searchCriteria = "IsInstalled=0" } else { $searchCriteria = "Title='$Title'" } } Write-PMLog -Message 'Searching for updates to modify...' -Type Info $searchResult = $updateSearcher.Search($searchCriteria) # Apply title filter for wildcards $updates = if ($Title -and $Title -match '[*?]') { $searchResult.Updates | Where-Object { $_.Title -like $Title } } else { $searchResult.Updates } if ($updates.Count -eq 0) { $criteria = if ($KB) { "KB(s): $($KB -join ', ')" } else { "Title: $Title" } Write-PMLog -Message "No updates found matching $criteria" -Type Warning return } Write-PMLog -Message "Found $($updates.Count) update(s) to process." -Type Info $action = if ($Hidden) { "hide" } else { "unhide" } $results = @() foreach ($update in $updates) { $updateKB = $update.KBArticleIDs -join ',' if ($PSCmdlet.ShouldProcess($update.Title, "$action update")) { if ($update.IsHidden -eq $Hidden) { $status = if ($Hidden) { "already hidden" } else { "already visible" } Write-PMLog -Message "Update $($update.Title) (KB$updateKB) is $status" -Type Info } else { Write-PMLog -Message "$(if ($Hidden) { 'Hiding' } else { 'Unhiding' }) update: $($update.Title) (KB$updateKB)" -Type Info $update.IsHidden = $Hidden } $results += [PSCustomObject]@{ Title = $update.Title KB = $updateKB Result = "2" # Success code for consistency Status = if ($Hidden) { "Hidden" } else { "Unhidden" } HResult = "Success" } } } Write-PMLog -Message "Successfully processed $action requests." -Type Info return $results } catch { if ($_.Exception.HResult -eq 0x80240032) { $criteria = if ($KB) { "KB(s): $($KB -join ', ')" } else { "Title: $Title" } Write-PMLog -Message "No updates found matching $criteria" -Type Warning return } Write-PMLog -Message "Error modifying updates: $($_.Exception.Message)" -Type Error throw } } } function Invoke-PMWinUpdate { <# .SYNOPSIS Manages Windows updates on remote computers without requiring module installation on remote systems. .DESCRIPTION Installs Windows updates on one or more remote computers with detailed progress tracking and reporting. Does not require PSPatchManager to be installed on remote systems as all necessary code is included in the remote execution context. .PARAMETER ComputerName One or more computer names to manage updates on. .PARAMETER Credential Optional credentials to use for remote connections. .EXAMPLE Invoke-PMWinUpdate -ComputerName 'Server1' Installs updates on a single remote computer. .EXAMPLE Invoke-PMWinUpdate -ComputerName 'Server1', 'Server2' -Credential (Get-Credential) Installs updates on multiple servers using specified credentials. .NOTES - Does not require PSPatchManager to be installed on remote computers - Provides detailed progress and error reporting - Returns detailed update installation results - Requires WinRM to be enabled on remote systems #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$ComputerName, [System.Management.Automation.PSCredential] $Credential ) end { foreach ($Computer in $ComputerName) { try { Write-PMLog -Message "Connecting to $Computer..." -Type Info # Define the remote scriptblock with embedded functions $remoteScript = { function Write-RemoteLog { param([string]$Message, [string]$Type = 'Info') switch ($Type) { 'Info' { Write-Verbose $Message -Verbose } 'Warning' { Write-Warning $Message } 'Error' { Write-Error $Message } } } try { Write-RemoteLog "Initializing Windows Update session..." $UpdateSession = New-Object -ComObject Microsoft.Update.Session $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() Write-RemoteLog "Searching for updates..." $SearchResult = $UpdateSearcher.Search("IsInstalled=0") if ($SearchResult.Updates.Count -gt 0) { $UpdateDownloader = $UpdateSession.CreateUpdateDownloader() $UpdateInstaller = $UpdateSession.CreateUpdateInstaller() $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl Write-RemoteLog "Processing $($SearchResult.Updates.Count) updates..." foreach ($Update in $SearchResult.Updates) { if (-not $Update.EulaAccepted) { $Update.AcceptEula() } Write-RemoteLog "Adding update: $($Update.Title)" $UpdatesToInstall.Add($Update) | Out-Null } if ($UpdatesToInstall.Count -gt 0) { Write-RemoteLog "Downloading updates..." $UpdateDownloader.Updates = $UpdatesToInstall $DownloadResult = $UpdateDownloader.Download() if ($DownloadResult.ResultCode -eq 2) { # Success Write-RemoteLog "Installing updates..." $UpdateInstaller.Updates = $UpdatesToInstall $InstallResult = $UpdateInstaller.Install() # Get the results immediately to avoid COM object issues $resultCode = $InstallResult.ResultCode.ToString() $rebootRequired = $InstallResult.RebootRequired $updateResults = @() # Process each update result immediately for ($i = 0; $i -lt $UpdatesToInstall.Count; $i++) { $update = $UpdatesToInstall.Item($i) $updateResult = $InstallResult.GetUpdateResult($i) $updateResults += @{ Title = $update.Title KB = $update.KBArticleIDs -join ',' Result = Get-UpdateOperationResult -ResultCode $updateResult.ResultCode.ToString() -Operation "Update" HResult = if ($updateResult.HResult -ne 0) { "0x{0:X8}" -f $updateResult.HResult } else { "Success" } } } $result = @{ ComputerName = $env:COMPUTERNAME UpdateCount = $SearchResult.Updates.Count Result = Get-UpdateOperationResult -ResultCode $resultCode -Operation "Installation" RebootRequired = $rebootRequired Updates = $updateResults } } else { Write-RemoteLog "Download failed with status: $($DownloadResult.ResultCode)" -Type Error throw "Download operation failed with status: $($DownloadResult.ResultCode)" } } } else { $result = @{ ComputerName = $env:COMPUTERNAME UpdateCount = 0 Result = "No updates found" RebootRequired = $false Updates = @() } } return $result } catch { Write-RemoteLog "Error: $_" -Type Error throw $_ } } $params = @{ ComputerName = $Computer ScriptBlock = $remoteScript } if ($Credential) { $params['Credential'] = $Credential } $result = Invoke-Command @params Write-PMLog -Message "Completed update process on $Computer" -Type Info # Format and output the result [PSCustomObject]@{ ComputerName = $result.ComputerName UpdateCount = $result.UpdateCount Result = $result.Result RebootRequired = $result.RebootRequired Updates = $result.Updates } } catch { Write-PMLog -Message "Error processing updates on $Computer`: $($_.Exception.Message)" -Type Error } } } } function Download-PMWinUpdate { <# .SYNOPSIS Downloads Windows updates without installing them. .DESCRIPTION Downloads Windows updates to prestage them on the system without installation. Updates can be specified by KB number, title search, or downloaded automatically. .PARAMETER KB One or more KB article IDs to download specific updates. .PARAMETER Title Filter and download updates by title. Supports wildcard patterns unless -ExactMatch is specified. .PARAMETER ExactMatch When used with -Title, requires an exact match instead of wildcard pattern matching. .PARAMETER Type Filter updates by type (Software, Driver, Security, FeaturePack). .PARAMETER Severity Filter updates by severity (Critical, Important, Moderate, Low). .PARAMETER RebootRequired Filter updates by reboot requirement. .PARAMETER ReleaseDate Filter updates by release date. .PARAMETER AcceptAll If specified, automatically accepts and downloads all available updates. .EXAMPLE Download-PMWinUpdate -Title "Security Update for Windows" Downloads all updates containing "Security Update for Windows" in their title. .EXAMPLE Download-PMWinUpdate -Title "KB5025221" -ExactMatch Downloads the specific update matching KB5025221. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ParameterSetName='KB')] [string[]]$KB, [Parameter(ParameterSetName='Title')] [string]$Title, [ValidateSet('Software', 'Driver', 'Security', 'FeaturePack')] [string]$Type = '', [ValidateSet('Critical', 'Important', 'Moderate', 'Low', 'Unspecified')] [string]$Severity = '', [bool]$RebootRequired, [DateTime]$ReleaseDate, [switch]$AcceptAll = $true ) begin { $script:updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl Write-PMLog -Message 'Initializing Windows Update download session...' -Type Info try { $script:updateSession = New-Object -ComObject Microsoft.Update.Session $script:updateSearcher = $updateSession.CreateUpdateSearcher() $script:updateDownloader = $updateSession.CreateUpdateDownloader() } catch { Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error return } } end { try { Write-PMLog -Message "Retrieving available updates..." -Type Info $SearchResult = $script:updateSearcher.Search("IsInstalled=0 AND IsHidden=0") Write-PMLog -Message "Found $($SearchResult.Updates.Count) total available updates." -Type Info # Display filter criteria if any are set $activeFilters = @() if ($Title) { $activeFilters += "Title: '$Title'" + $(if ($Title -match '[*?]') { " (Wildcard)" } else { " (Exact Match)" }) } if ($KB) { $activeFilters += "KB: $($KB -join ', ')" } if ($Type) { $activeFilters += "Type: $Type" } if ($Severity) { $activeFilters += "Severity: $Severity" } if ($PSBoundParameters.ContainsKey('RebootRequired')) { $activeFilters += "RebootRequired: $RebootRequired" } if ($ReleaseDate) { $activeFilters += "ReleaseDate: >= $($ReleaseDate.ToString('yyyy-MM-dd'))" } if ($activeFilters.Count -gt 0) { Write-PMLog -Message "Active filters:`n $($activeFilters -join "`n ")" -Type Info } foreach ($Update in $SearchResult.Updates) { $updateKB = $Update.KBArticleIDs | Select-Object -First 1 $shouldDownload = $false # Apply filters $matchesFilters = (!$Type -or $Update.Type -eq $Type) -and (!$Severity -or $Update.MsrcSeverity -eq $Severity) -and (!$PSBoundParameters.ContainsKey('RebootRequired') -or $Update.RebootRequired -eq $RebootRequired) -and (!$ReleaseDate -or $Update.LastDeploymentChangeTime -ge $ReleaseDate) if ($KB) { $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' } $shouldDownload = $updateKB -in $kbNumbers -and $matchesFilters } elseif ($Title) { # Check if title contains wildcard characters if ($Title -match '[*?]') { $shouldDownload = $Update.Title -like $Title -and $matchesFilters } else { $shouldDownload = $Update.Title -eq $Title -and $matchesFilters } } else { $shouldDownload = $matchesFilters } if ($shouldDownload) { if ($Update.IsDownloaded) { Write-PMLog -Message "Update already downloaded: $($Update.Title)" -Type Info continue } if (-not $Update.EulaAccepted) { $Update.AcceptEula() } if ($AcceptAll -or $PSCmdlet.ShouldProcess($Update.Title, 'Download update')) { Write-PMLog -Message "Adding update for download: $($Update.Title)" -Type Info $script:updatesToDownload.Add($Update) | Out-Null } } } $alreadyDownloaded = $SearchResult.Updates | Where-Object { $_.IsDownloaded -and ( ($KB -and ($_.KBArticleIDs | Where-Object { $KB -contains "KB$_" })) -or ($Title -and $(if ($Title -match '[*?]') { $_.Title -like $Title } else { $_.Title -eq $Title })) ) } if ($alreadyDownloaded) { Write-PMLog -Message "`nAlready downloaded updates:" -Type Info foreach ($update in $alreadyDownloaded) { Write-PMLog -Message " $($update.Title) (KB$($update.KBArticleIDs -join ','))" -Type Info } } Write-PMLog -Message "`n$($script:updatesToDownload.Count) update(s) require download." -Type Info if ($script:updatesToDownload.Count -gt 0) { Write-PMLog -Message "`nDownload Summary:" -Type Info Write-PMLog -Message "Total updates to download: $($script:updatesToDownload.Count)" -Type Info Write-PMLog -Message "Starting download process...`n" -Type Info try { $script:updateDownloader.Updates = $script:updatesToDownload $DownloadResult = $script:updateDownloader.Download() $resultCode = $DownloadResult.ResultCode.ToString() switch ($resultCode) { "0" { Write-PMLog -Message "Download failed." -Type Error; throw "Download failed" } "1" { Write-PMLog -Message "Download in progress..." -Type Info } "2" { Write-PMLog -Message "Download completed successfully." -Type Info } "3" { Write-PMLog -Message "Download completed with errors." -Type Warning } "4" { Write-PMLog -Message "Download cancelled." -Type Warning; throw "Download cancelled" } "5" { Write-PMLog -Message "Download aborted." -Type Error; throw "Download aborted" } default { Write-PMLog -Message "Unknown download result: $resultCode" -Type Warning } } # Return download results $script:updatesToDownload | ForEach-Object { [PSCustomObject]@{ Title = $_.Title KB = $_.KBArticleIDs -join ',' Result = $resultCode Status = switch ($resultCode) { "0" { "Failed" } "1" { "In Progress" } "2" { "Success" } "3" { "Partial Success" } "4" { "Cancelled" } "5" { "Aborted" } default { "Unknown" } } } } } catch { Write-PMLog -Message "Error during update download: $($_.Exception.Message)" -Type Error throw } } else { Write-PMLog -Message "No updates matched the specified criteria." -Type Info } } catch { Write-PMLog -Message "Download failed: $($_.Exception.Message)" -Type Error throw } finally { Write-Progress -Activity 'Processing Windows Updates' -Completed Write-Progress -Activity 'Downloading Updates' -Completed } } } function Register-PMMicrosoftUpdate { <# .SYNOPSIS Registers the system with Microsoft Update Service. .DESCRIPTION Enables Microsoft Update Service on the system, allowing access to both Windows Updates and updates for other Microsoft products. .EXAMPLE Register-PMMicrosoftUpdate Registers the system with Microsoft Update Service. #> [CmdletBinding(SupportsShouldProcess)] param() begin { Write-PMLog -Message 'Initializing Microsoft Update registration...' -Type Info } process { try { $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d" if ($PSCmdlet.ShouldProcess("Microsoft Update Service", "Register")) { # Check if already registered $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID } if ($Service) { Write-PMLog -Message "System is already registered with Microsoft Update Service." -Type Info return $true } Write-PMLog -Message "Registering system with Microsoft Update Service..." -Type Info $AddServiceResult = $ServiceManager.AddService2($ServiceID, 7, "") Write-PMLog -Message "Successfully registered with Microsoft Update Service." -Type Info return $true } } catch { Write-PMLog -Message "Failed to register Microsoft Update Service: $($_.Exception.Message)" -Type Error return $false } } } # Export functions Export-ModuleMember -Function Get-PMWinUpdate, Install-PMWinUpdate, Set-PMWinUpdate, Invoke-PMWinUpdate, Download-PMWinUpdate, Register-PMMicrosoftUpdate |