├── Types ├── Net45 │ └── HtmlAgilityPack.dll └── netstandard2.0 │ └── HtmlAgilityPack.dll ├── Private ├── Invoke-ParseDate.ps1 ├── Set-TempSecurityProtocol.ps1 ├── Get-UpdateLinks.ps1 └── Invoke-CatalogRequest.ps1 ├── Classes ├── MSCatalogResponse.Class.ps1 └── MSCatalogUpdate.Class.ps1 ├── LICENSE ├── MSCatalogLTS.psm1 ├── Public ├── Save-MSCatalogOutput.ps1 ├── Save-MSCatalogUpdate.ps1 └── Get-MSCatalogUpdate.ps1 ├── Format └── MSCatalogUpdate.Format.ps1xml ├── README.md ├── MSCatalogLTS.psd1 └── Tests └── Test-GetMSCatalogUpdate.ps1 /Types/Net45/HtmlAgilityPack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marco-online/MSCatalogLTS/HEAD/Types/Net45/HtmlAgilityPack.dll -------------------------------------------------------------------------------- /Types/netstandard2.0/HtmlAgilityPack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marco-online/MSCatalogLTS/HEAD/Types/netstandard2.0/HtmlAgilityPack.dll -------------------------------------------------------------------------------- /Private/Invoke-ParseDate.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ParseDate { 2 | param ( 3 | [String] $DateString 4 | ) 5 | 6 | $Array = $DateString.Split("/") 7 | Get-Date -Year $Array[2] -Month $Array[0] -Day $Array[1] 8 | } -------------------------------------------------------------------------------- /Private/Set-TempSecurityProtocol.ps1: -------------------------------------------------------------------------------- 1 | function Set-TempSecurityProtocol { 2 | [CmdletBinding()] 3 | param ( 4 | [switch] $ResetToDefault 5 | ) 6 | 7 | if (($null -ne $Script:MSCatalogSecProt) -and $ResetToDefault) { 8 | [Net.ServicePointManager]::SecurityProtocol = $Script:MSCatalogSecProt 9 | } else { 10 | if ($null -eq $Script:MSCatalogSecProt) { 11 | $Script:MSCatalogSecProt = [Net.ServicePointManager]::SecurityProtocol 12 | } 13 | $Tls11 = [System.Net.SecurityProtocolType]::Tls11 14 | $Tls12 = [System.Net.SecurityProtocolType]::Tls12 15 | $CurrentProtocol = [Net.ServicePointManager]::SecurityProtocol 16 | $NewProtocol = $CurrentProtocol -bor $Tls11 -bor $Tls12 17 | [Net.ServicePointManager]::SecurityProtocol = $NewProtocol 18 | } 19 | } -------------------------------------------------------------------------------- /Classes/MSCatalogResponse.Class.ps1: -------------------------------------------------------------------------------- 1 | class MSCatalogResponse { 2 | [HtmlAgilityPack.HtmlNode[]] $Rows 3 | [string] $EventArgument 4 | [string] $EventValidation 5 | [string] $ViewState 6 | [string] $ViewStateGenerator 7 | [string] $NextPage 8 | 9 | MSCatalogResponse($HtmlDoc) { 10 | $Table = $HtmlDoc.GetElementbyId("ctl00_catalogBody_updateMatches") 11 | $this.Rows = $Table.SelectNodes("tr") | Where-Object { $_.Id -ne "headerRow" } 12 | $this.EventArgument = $HtmlDoc.GetElementbyId("__EVENTARGUMENT").Attributes["value"].Value 13 | $this.EventValidation = $HtmlDoc.GetElementbyId("__EVENTVALIDATION").Attributes["value"].Value 14 | $this.ViewState = $HtmlDoc.GetElementbyId("__VIEWSTATE").Attributes["value"].Value 15 | $this.ViewStateGenerator = $HtmlDoc.GetElementbyId("__VIEWSTATEGENERATOR").Attributes["value"].Value 16 | $NextPageNode = $HtmlDoc.GetElementbyId("ctl00_catalogBody_nextPageLink") 17 | $this.NextPage = if ($null -ne $NextPageNode) { $NextPageNode.InnerText.Trim() } else { $null } 18 | } 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marco-online 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MSCatalogLTS.psm1: -------------------------------------------------------------------------------- 1 | try { 2 | if (!([System.Management.Automation.PSTypeName]'HtmlAgilityPack.HtmlDocument').Type) { 3 | if ($PSVersionTable.PSEdition -eq "Desktop") { 4 | Add-Type -Path "$PSScriptRoot\Types\Net45\HtmlAgilityPack.dll" 5 | } else { 6 | Add-Type -Path "$PSScriptRoot\Types\netstandard2.0\HtmlAgilityPack.dll" 7 | } 8 | } 9 | } catch { 10 | Write-Error -Message "Failed to load HtmlAgilityPack: $_" 11 | throw 12 | } 13 | 14 | $Classes = @(Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1) 15 | $Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1) 16 | $Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1) 17 | 18 | foreach ($ClassFile in $Classes) { 19 | try { 20 | . $ClassFile.FullName 21 | } catch { 22 | Write-Error -Message "Failed to import class $($ClassFile.FullName): $_" 23 | throw 24 | } 25 | } 26 | 27 | foreach ($Module in ($Private + $Public)) { 28 | try { 29 | . $Module.FullName 30 | } catch { 31 | Write-Error -Message "Failed to import function $($Module.FullName): $_" 32 | throw 33 | } 34 | } 35 | 36 | Export-ModuleMember -Function $Public.BaseName 37 | -------------------------------------------------------------------------------- /Classes/MSCatalogUpdate.Class.ps1: -------------------------------------------------------------------------------- 1 | class MSCatalogUpdate { 2 | [string] $Title 3 | [string] $Products 4 | [string] $Classification 5 | [datetime] $LastUpdated 6 | [string] $Version 7 | [string] $Size 8 | [string] $SizeInBytes 9 | [string] $Guid 10 | [string[]] $FileNames 11 | 12 | MSCatalogUpdate() {} 13 | 14 | MSCatalogUpdate($Row, $IncludeFileNames) { 15 | $Cells = $Row.SelectNodes("td") 16 | $this.Title = $Cells[1].innerText.Trim() 17 | $this.Products = $Cells[2].innerText.Trim() 18 | $this.Classification = $Cells[3].innerText.Trim() 19 | $this.LastUpdated = (Invoke-ParseDate -DateString $Cells[4].innerText.Trim()) 20 | $this.Version = $Cells[5].innerText.Trim() 21 | $this.Size = $Cells[6].SelectNodes("span")[0].InnerText 22 | $this.SizeInBytes = [Int64] $Cells[6].SelectNodes("span")[1].InnerText 23 | $this.Guid = $Cells[7].SelectNodes("input")[0].Id 24 | $this.FileNames = if ($IncludeFileNames) { 25 | $Links = Get-UpdateLinks -Guid $Cells[7].SelectNodes("input")[0].Id 26 | foreach ($Link in $Links.Matches) { 27 | $Link.Value.Split('/')[-1] 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Private/Get-UpdateLinks.ps1: -------------------------------------------------------------------------------- 1 | function Get-UpdateLinks { 2 | [CmdletBinding()] 3 | param ( 4 | [Parameter( 5 | Mandatory = $true, 6 | Position = 0 7 | )] 8 | [String] $Guid 9 | ) 10 | 11 | $Post = @{size = 0; UpdateID = $Guid; UpdateIDInfo = $Guid} | ConvertTo-Json -Compress 12 | $Body = @{UpdateIDs = "[$Post]"} 13 | 14 | $Params = @{ 15 | Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx" 16 | Body = $Body 17 | ContentType = "application/x-www-form-urlencoded" 18 | UseBasicParsing = $true 19 | } 20 | 21 | $DownloadDialog = Invoke-WebRequest @Params 22 | $Links = $DownloadDialog.Content -replace "www.download.windowsupdate", "download.windowsupdate" 23 | 24 | $Regex = "downloadInformation\[0\]\.files\[\d+\]\.url\s*=\s*'([^']*kb(\d+)[^']*)'" 25 | $DownloadMatches = [regex]::Matches($Links, $Regex) 26 | 27 | if ($DownloadMatches.Count -eq 0) { 28 | $RegexFallback = "downloadInformation\[0\]\.files\[0\]\.url\s*=\s*'([^']*)'" 29 | $DownloadMatches = [regex]::Matches($Links, $RegexFallback) 30 | } 31 | 32 | if ($DownloadMatches.Count -eq 0) { 33 | return $null 34 | } 35 | 36 | $KbLinks = foreach ($Match in $DownloadMatches) { 37 | [PSCustomObject]@{ 38 | URL = $Match.Groups[1].Value 39 | KB = if ($Match.Groups.Count -gt 2 -and $Match.Groups[2].Success) { [int]$Match.Groups[2].Value } else { 0 } 40 | } 41 | } 42 | 43 | return $KbLinks | Sort-Object KB -Descending 44 | } -------------------------------------------------------------------------------- /Public/Save-MSCatalogOutput.ps1: -------------------------------------------------------------------------------- 1 | function Save-MSCatalogOutput { 2 | param ( 3 | [Parameter( 4 | Mandatory = $true, 5 | Position = 0, 6 | ParameterSetName = "ByObject" 7 | )] 8 | [Object] $Update, 9 | 10 | [Parameter(Mandatory = $true)] 11 | [string] $Destination, 12 | 13 | [string] $WorksheetName = "Updates" 14 | ) 15 | 16 | if (-not (Get-Module -Name ImportExcel -ListAvailable)) { 17 | try { 18 | Import-Module ImportExcel -ErrorAction Stop 19 | } 20 | catch { 21 | Write-Warning "Unable to Import the Excel Module" 22 | return 23 | } 24 | } 25 | 26 | if ($Update.Count -gt 1) { 27 | $Update = $Update | Select-Object -First 1 28 | } 29 | 30 | $data = [PSCustomObject]@{ 31 | Title = $Update.Title 32 | Products = $Update.Products 33 | Classification = $Update.Classification 34 | LastUpdated = $Update.LastUpdated.ToString('yyyy/MM/dd') 35 | Guid = $Update.Guid 36 | } 37 | 38 | $filePath = $Destination 39 | if (Test-Path -Path $filePath) { 40 | $existingData = Import-Excel -Path $filePath -WorksheetName $WorksheetName 41 | if ($existingData.Guid -contains $Update.Guid) { 42 | return 43 | } 44 | $data | Export-Excel -Path $filePath -WorksheetName $WorksheetName -Append -AutoSize -TableStyle Light1 45 | } else { 46 | $data | Export-Excel -Path $filePath -WorksheetName $WorksheetName -AutoSize -TableStyle Light1 47 | } 48 | } -------------------------------------------------------------------------------- /Private/Invoke-CatalogRequest.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-CatalogRequest { 2 | param ( 3 | [Parameter(Mandatory = $true)] 4 | [string] $Uri, 5 | 6 | [Parameter(Mandatory = $false)] 7 | [string] $Method = "Get" 8 | ) 9 | 10 | try { 11 | Set-TempSecurityProtocol 12 | 13 | $Headers = @{ 14 | "Cache-Control" = "no-cache" 15 | "Pragma" = "no-cache" 16 | } 17 | 18 | $Params = @{ 19 | Uri = $Uri 20 | UseBasicParsing = $true 21 | ErrorAction = "Stop" 22 | Headers = $Headers 23 | } 24 | 25 | $Results = Invoke-WebRequest @Params 26 | $HtmlDoc = [HtmlAgilityPack.HtmlDocument]::new() 27 | $HtmlDoc.LoadHtml($Results.RawContent.ToString()) 28 | $NoResults = $HtmlDoc.GetElementbyId("ctl00_catalogBody_noResultText") 29 | $ErrorText = $HtmlDoc.GetElementbyId("errorPageDisplayedError") 30 | 31 | if ($null -eq $NoResults -and $null -eq $ErrorText) { 32 | return [MSCatalogResponse]::new($HtmlDoc) 33 | } elseif ($ErrorText) { 34 | if ($ErrorText.InnerText -match '8DDD0010') { 35 | throw "The catalog.microsoft.com site has encountered an error with code 8DDD0010. Please try again later." 36 | } else { 37 | throw "The catalog.microsoft.com site has encountered an error: $($ErrorText.InnerText)" 38 | } 39 | } else { 40 | Write-Warning "We did not find any results for $Uri" 41 | } 42 | } catch { 43 | Write-Warning "$_" 44 | } finally { 45 | Set-TempSecurityProtocol -ResetToDefault 46 | } 47 | } -------------------------------------------------------------------------------- /Format/MSCatalogUpdate.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MSCatalogUpdate 6 | 7 | MSCatalogUpdate 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Title 32 | 33 | 34 | Products 35 | 36 | 37 | Classification 38 | 39 | 40 | Get-Date -Date $_.LastUpdated -Format "yyyy/MM/dd" 41 | 42 | 43 | Size 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSCatalogLTS 2 | 3 | MSCatalogLTS is a Long-term support module for searching and downloading updates from https://www.catalog.update.microsoft.com. 4 | It is cross-platform and runs on both Desktop and Core versions of PowerShell. 5 | 6 | [![psgallery](https://img.shields.io/powershellgallery/v/mscataloglts?style=flat-square&logo=powershell)](https://www.powershellgallery.com/packages/MSCatalogLTS) 7 | 8 | ## Quick Install 9 | 10 | ``` powershell 11 | Install-Module -Name MSCatalogLTS 12 | ``` 13 | 14 | Update to the latest version: 15 | 16 | ```powershell 17 | Update-Module -Name MSCatalogLTS 18 | ``` 19 | 20 | ```powershell 21 | Get-MSCatalogUpdate -Search "Cumulative Update for Windows 11 Version 24H2 for x64" -Strict -LastDays 60 -Descending 22 | 23 | Search completed for: Cumulative Update for Windows 11 Version 24H2 for x64 24 | Found 2 updates 25 | 26 | Title Products Classification LastUpdated Size 27 | ----- -------- -------------- ----------- ---- 28 | 2025-09 Cumulative Update for Windows 11 Version 24H2 for x64-based Systems (KB5065426) (26100.6584) Windows 11 Security Updates 2025/09/09 3811.1 MB 29 | 2025-08 Cumulative Update for Windows 11 Version 24H2 for x64-based Systems (KB5063878) (26100.4946) Windows 11 Security Updates 2025/08/12 3054.9 MB 30 | ``` 31 | 32 | 33 | 34 | --- 35 | 36 | ## 📚 Full Documentation 37 | 38 | Looking for usage examples, parameter reference, or advanced filtering? 39 | 40 | 👉 Visit the [MSCatalogLTS Wiki](https://github.com/Marco-online/MSCatalogLTS/wiki/MSCatalogLTS) 41 | 42 | --- 43 | 44 | ## Credits 45 | 46 | Inspired by [MSCatalog](https://github.com/ryan-jan/MSCatalog) — thanks to the original author! 47 | 48 | 49 | -------------------------------------------------------------------------------- /Public/Save-MSCatalogUpdate.ps1: -------------------------------------------------------------------------------- 1 | function Save-MSCatalogUpdate { 2 | param ( 3 | [Parameter(Position = 0, ValueFromPipeline = $true)] 4 | [MSCatalogUpdate] $Update, 5 | 6 | [Parameter(Position = 1)] 7 | [String[]] $Guid, 8 | 9 | [Parameter(Position = 2)] 10 | [String] $Destination = (Get-Location).Path, 11 | 12 | [switch] $DownloadAll 13 | ) 14 | 15 | begin { 16 | $ProgressPreference = 'SilentlyContinue' 17 | $GuidsToProcess = @() 18 | $AllUpdates = @() 19 | 20 | # Check if destination directory exists and create it if needed 21 | if (-not (Test-Path -Path $Destination -PathType Container)) { 22 | try { 23 | New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop | Out-Null 24 | Write-Output "Created destination directory: $Destination" 25 | } 26 | catch { 27 | Write-Error "Failed to create destination directory '$Destination': $_" 28 | return 29 | } 30 | } 31 | } 32 | 33 | process { 34 | if ($Update -and $Update.Guid) { 35 | $AllUpdates += $Update 36 | } 37 | if ($Guid) { 38 | $GuidsToProcess += $Guid 39 | } 40 | } 41 | 42 | end { 43 | # Filter out updates with valid GUIDs 44 | $ValidUpdates = $AllUpdates | Where-Object { $_.Guid -and $_.Guid -ne '' } 45 | 46 | # Sort by Title to find the latest update 47 | $LatestUpdate = $ValidUpdates | 48 | Sort-Object -Property Title -Descending | 49 | Select-Object -First 1 50 | 51 | if ($LatestUpdate -and $LatestUpdate.Guid) { 52 | Write-Output "Selected latest update: $($LatestUpdate.Title)" 53 | $GuidsToProcess = @($LatestUpdate.Guid) 54 | } else { 55 | Write-Warning "No valid update found with a GUID." 56 | return 57 | } 58 | 59 | foreach ($GuidItem in $GuidsToProcess) { 60 | if ([string]::IsNullOrWhiteSpace($GuidItem)) { 61 | Write-Warning "Skipped empty GUID." 62 | continue 63 | } 64 | 65 | $Links = Get-UpdateLinks -Guid $GuidItem 66 | if (-not $Links) { 67 | Write-Warning "No valid download links found for GUID '$GuidItem'." 68 | continue 69 | } 70 | 71 | $TotalCount = if ($DownloadAll) { $Links.Count } else { 1 } 72 | Write-Output "Found $($Links.Count) download links for GUID '$GuidItem'. $(if (-not $DownloadAll -and $Links.Count -gt 1) {"Only downloading the first file. Use -DownloadAll to download all files."})" 73 | 74 | $LinksToProcess = if ($DownloadAll) { $Links } else { $Links | Select-Object -First 1 } 75 | $SuccessCount = 0 76 | 77 | foreach ($Link in $LinksToProcess) { 78 | $url = $Link.URL 79 | $name = $url.Split('/')[-1] 80 | $cleanname = $name.Split('_')[0] 81 | # Determine extension based on URL or use .msu as default 82 | $extension = if ($url -match '\.(cab|exe|msi|msp|msu)$') { 83 | ".$($matches[1])" 84 | } else { 85 | ".msu" 86 | } 87 | 88 | $CleanOutFile = $cleanname + $extension 89 | $OutFile = Join-Path -Path $Destination -ChildPath $CleanOutFile 90 | 91 | if (Test-Path -Path $OutFile) { 92 | Write-Warning "File already exists: $CleanOutFile. Skipping download." 93 | continue 94 | } 95 | 96 | try { 97 | Write-Output "Downloading $CleanOutFile..." 98 | Set-TempSecurityProtocol 99 | Invoke-WebRequest -Uri $url -OutFile $OutFile -ErrorAction Stop 100 | Set-TempSecurityProtocol -ResetToDefault 101 | 102 | if (Test-Path -Path $OutFile) { 103 | Write-Output "Successfully downloaded file $CleanOutFile to $Destination" 104 | $SuccessCount++ 105 | } else { 106 | Write-Warning "Downloading file $CleanOutFile failed." 107 | } 108 | } 109 | catch { 110 | Write-Warning "Error downloading $CleanOutFile : $_" 111 | } 112 | } 113 | 114 | Write-Output "Download complete: $SuccessCount of $TotalCount files downloaded successfully." 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /MSCatalogLTS.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'MSCatalogLTS' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'MSCatalogLTS.psm1' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '1.0.8' 12 | 13 | # Supported PSEditions 14 | # CompatiblePSEditions = @() 15 | 16 | # ID used to uniquely identify this module 17 | GUID = '721ac2a2-e4b6-4948-9c22-6ad2a52c0de6' 18 | 19 | # Author of this module 20 | Author = 'Marco-online' 21 | 22 | # Company or vendor of this module 23 | CompanyName = '' 24 | 25 | # Copyright statement for this module 26 | Copyright = '(c) 2025 Marco-online All rights reserved.' 27 | 28 | # Description of the functionality provided by this module 29 | Description = 'MSCatalogLTS is a Long-term support module for searching and downloading Windows updates' 30 | 31 | # Minimum version of the Windows PowerShell engine required by this module 32 | # PowerShellVersion = '' 33 | 34 | # Name of the Windows PowerShell host required by this module 35 | # PowerShellHostName = '' 36 | 37 | # Minimum version of the Windows PowerShell host required by this module 38 | # PowerShellHostVersion = '' 39 | 40 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 41 | # DotNetFrameworkVersion = '' 42 | 43 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 44 | # CLRVersion = '' 45 | 46 | # Processor architecture (None, X86, Amd64) required by this module 47 | # ProcessorArchitecture = '' 48 | 49 | # Modules that must be imported into the global environment prior to importing this module 50 | # RequiredModules = @() 51 | 52 | # Assemblies that must be loaded prior to importing this module 53 | # RequiredAssemblies = @() 54 | 55 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 56 | # ScriptsToProcess = @() 57 | 58 | # Type files (.ps1xml) to be loaded when importing this module 59 | # TypesToProcess = @() 60 | 61 | # Format files (.ps1xml) to be loaded when importing this module 62 | FormatsToProcess = @( 63 | '.\Format\MSCatalogUpdate.Format.ps1xml' 64 | ) 65 | 66 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 67 | # NestedModules = @() 68 | 69 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 70 | FunctionsToExport = @( 71 | 'Get-MSCatalogUpdate', 72 | 'Save-MSCatalogUpdate', 73 | 'Save-MSCatalogOutput' 74 | ) 75 | 76 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 77 | #CmdletsToExport = '*' 78 | 79 | # Variables to export from this module 80 | #VariablesToExport = '*' 81 | 82 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 83 | #AliasesToExport = '*' 84 | 85 | # DSC resources to export from this module 86 | # DscResourcesToExport = @() 87 | 88 | # List of all modules packaged with this module 89 | # ModuleList = @() 90 | 91 | # List of all files packaged with this module 92 | # FileList = @() 93 | 94 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 95 | PrivateData = @{ 96 | 97 | PSData = @{ 98 | 99 | # Tags applied to this module. These help with module discovery in online galleries. 100 | # Tags = @() 101 | 102 | # A URL to the license for this module. 103 | # LicenseUri = '' 104 | 105 | # A URL to the main website for this project. 106 | ProjectUri = 'https://github.com/Marco-online/MSCatalogLTS' 107 | 108 | # A URL to an icon representing this module. 109 | # IconUri = '' 110 | 111 | # ReleaseNotes of this module 112 | # ReleaseNotes = '' 113 | 114 | } # End of PSData hashtable 115 | 116 | } # End of PrivateData hashtable 117 | 118 | # HelpInfo URI of this module 119 | # HelpInfoURI = '' 120 | 121 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 122 | # DefaultCommandPrefix = '' 123 | } 124 | -------------------------------------------------------------------------------- /Tests/Test-GetMSCatalogUpdate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Tests the functionality of the Get-MSCatalogUpdate cmdlet. 4 | 5 | .DESCRIPTION 6 | This script contains test cases for verifying the behavior and functionality 7 | of the Get-MSCatalogUpdate cmdlet. It ensures that the cmdlet correctly 8 | retrieves updates from the Microsoft Update Catalog based on various criteria 9 | and parameters. 10 | 11 | .NOTES 12 | Name: Test-GetMSCatalogUpdate.ps1 13 | Author: Mickaël CHAVE 14 | Created: 03/21/2025 15 | Version: 1.0.0 16 | Repository: https://github.com/Marco-online/MSCatalogLTS 17 | License: MIT License 18 | 19 | .LINK 20 | https://github.com/Marco-online/MSCatalogLTS 21 | 22 | .EXAMPLE 23 | .\Test-GetMSCatalogUpdate.ps1 24 | #> 25 | 26 | Clear-Host 27 | 28 | #region Module Import 29 | # Import the module 30 | $ModulePath = Split-Path $PSScriptRoot -Parent 31 | Import-Module "$ModulePath\MSCatalogLTS.psd1" -Force 32 | #endregion 33 | 34 | #region Helper Functions 35 | # Helper function to run tests and display results 36 | function Test-Scenario { 37 | param ( 38 | [string]$Name, 39 | [hashtable]$Parameters, 40 | [scriptblock]$Validation = { param($result) $result.Count -gt 0 } 41 | ) 42 | 43 | Write-Host "`n======================================================" 44 | Write-Host "Testing Scenario: $Name" -ForegroundColor Cyan 45 | Write-Host "======================================================" 46 | 47 | # Build search query for display 48 | $searchText = if ($Parameters.ContainsKey('Search')) { 49 | $Parameters.Search 50 | } elseif ($Parameters.ContainsKey('OperatingSystem')) { 51 | $os = $Parameters.OperatingSystem 52 | $version = if ($Parameters.ContainsKey('Version')) { " Version $($Parameters.Version)" } else { "" } 53 | $updateType = if ($Parameters.ContainsKey('UpdateType')) { 54 | if ($Parameters.UpdateType -is [array]) { 55 | " $($Parameters.UpdateType -join ' or ')" 56 | } else { 57 | " $($Parameters.UpdateType)" 58 | } 59 | } else { "" } 60 | "$os$version$updateType" 61 | } else { 62 | "custom search" 63 | } 64 | 65 | try { 66 | # Display parameter details 67 | Write-Host "Parameters:" -ForegroundColor Yellow 68 | $Parameters.GetEnumerator() | ForEach-Object { 69 | Write-Host " $($_.Key): $($_.Value)" -ForegroundColor Gray 70 | } 71 | 72 | Write-Host "`nExecuting search for: $searchText..." -ForegroundColor Yellow 73 | 74 | # Execute the command and measure execution time 75 | $startTime = Get-Date 76 | $result = Get-MSCatalogUpdate @Parameters -ErrorAction Stop 77 | $executionTime = ((Get-Date) - $startTime).TotalSeconds 78 | 79 | if ($null -eq $result -or $result.Count -eq 0) { 80 | Write-Host "No results found for this query." -ForegroundColor Yellow 81 | $returnObj = New-Object PSObject -Property @{ 82 | Success = $false 83 | Result = $null 84 | Message = "No results found" 85 | ExecutionTime = $executionTime 86 | SearchText = $searchText 87 | } 88 | return $returnObj 89 | } 90 | 91 | Write-Host "Found $($result.Count) updates in $([math]::Round($executionTime, 2)) seconds" -ForegroundColor Green 92 | 93 | # Display first few results 94 | if ($result.Count -gt 0) { 95 | Write-Host "`nSample Results:" -ForegroundColor Yellow 96 | $sampleSize = [Math]::Min(3, $result.Count) 97 | $result | Select-Object -First $sampleSize | ForEach-Object { 98 | Write-Host "- $($_.Title)" -ForegroundColor Gray 99 | Write-Host " Classification: $($_.Classification)" -ForegroundColor Gray 100 | } 101 | 102 | if ($result.Count -gt $sampleSize) { 103 | Write-Host "- ... and $($result.Count - $sampleSize) more" -ForegroundColor Gray 104 | } 105 | } 106 | 107 | # Execute validation 108 | $validationResult = & $Validation $result 109 | if ($validationResult -eq $true) { 110 | Write-Host "`n[PASS] Test PASSED" -ForegroundColor Green 111 | $returnObj = New-Object PSObject -Property @{ 112 | Success = $true 113 | Result = $result 114 | Message = "Test passed" 115 | ExecutionTime = $executionTime 116 | SearchText = $searchText 117 | } 118 | return $returnObj 119 | } 120 | else { 121 | Write-Host "`n[FAIL] Test FAILED: Validation criteria not met" -ForegroundColor Red 122 | $returnObj = New-Object PSObject -Property @{ 123 | Success = $false 124 | Result = $result 125 | Message = "Validation failed" 126 | ExecutionTime = $executionTime 127 | SearchText = $searchText 128 | } 129 | return $returnObj 130 | } 131 | } 132 | catch { 133 | Write-Host "`n[ERROR] ERROR: $($_.Exception.Message)" -ForegroundColor Red 134 | $returnObj = New-Object PSObject -Property @{ 135 | Success = $false 136 | Result = $null 137 | Message = $_.Exception.Message 138 | ExecutionTime = 0 139 | SearchText = $searchText 140 | } 141 | return $returnObj 142 | } 143 | } 144 | 145 | # Results tracking 146 | $successCount = 0 147 | $failureCount = 0 148 | $testResults = @() 149 | 150 | # Run test and record result 151 | function Invoke-Test { 152 | param ( 153 | [string]$Name, 154 | [hashtable]$Parameters, 155 | [scriptblock]$Validation = { param($result) $result.Count -gt 0 } 156 | ) 157 | 158 | $result = Test-Scenario -Name $Name -Parameters $Parameters -Validation $Validation 159 | $testResult = New-Object PSObject -Property @{ 160 | Name = $Name 161 | Parameters = ($Parameters.Keys -join ", ") 162 | Success = $result.Success 163 | ResultCount = if ($result.Result) { $result.Result.Count } else { 0 } 164 | ExecutionTime = [math]::Round($result.ExecutionTime, 2) 165 | Message = $result.Message 166 | SearchText = $result.SearchText 167 | } 168 | 169 | $script:testResults += $testResult 170 | 171 | if ($result.Success) { 172 | $script:successCount++ 173 | } else { 174 | $script:failureCount++ 175 | } 176 | 177 | return $result.Result 178 | } 179 | #endregion 180 | 181 | #region Test Initialization 182 | # Record start time for overall execution time 183 | $scriptStartTime = Get-Date 184 | #endregion 185 | 186 | #region Test Group 1: Basic Search Parameter 187 | # ====================================================== 188 | # TEST GROUP 1: Basic Search Parameter 189 | # ====================================================== 190 | Write-Host "`n### TEST GROUP: BASIC SEARCH PARAMETER ###" -ForegroundColor Blue -BackgroundColor White 191 | 192 | # Test 1.1: Basic KB number search 193 | Invoke-Test -Name "KB Number Search" -Parameters @{ 194 | Search = "KB5035853" 195 | } 196 | 197 | # Test 1.2: Basic Windows 10 search 198 | Invoke-Test -Name "Windows 10 Search" -Parameters @{ 199 | Search = "Cumulative Update for Windows 10 Version 22H2" 200 | } 201 | 202 | # Test 1.3: Basic Windows 11 search 203 | Invoke-Test -Name "Windows 11 Search" -Parameters @{ 204 | Search = "Cumulative Update for Windows 11 Version 24H2" 205 | } 206 | 207 | # Test 1.4: Basic Windows Server search 208 | Invoke-Test -Name "Windows Server Search" -Parameters @{ 209 | Search = "Cumulative Update for Microsoft server operating system" 210 | } 211 | #endregion 212 | 213 | #region Test Group 2: OperatingSystem Parameter 214 | # ====================================================== 215 | # TEST GROUP 2: OperatingSystem Parameter 216 | # ====================================================== 217 | Write-Host "`n### TEST GROUP: OPERATING SYSTEM PARAMETER ###" -ForegroundColor Blue -BackgroundColor White 218 | 219 | # Test 2.1: Windows 10 OS parameter with UpdateType 220 | Invoke-Test -Name "Windows 10 OS Parameter" -Parameters @{ 221 | OperatingSystem = "Windows 10" 222 | UpdateType = "Cumulative Updates" 223 | Strict = $true 224 | } -Validation { 225 | param($result) 226 | ($result | Where-Object { $_.Title -match "Windows 10" }).Count -gt 0 227 | } 228 | 229 | # Test 2.2: Windows 11 OS parameter with UpdateType 230 | Invoke-Test -Name "Windows 11 OS Parameter" -Parameters @{ 231 | OperatingSystem = "Windows 11" 232 | UpdateType = "Cumulative Updates" 233 | Strict = $true 234 | } -Validation { 235 | param($result) 236 | ($result | Where-Object { $_.Title -match "Windows 11" }).Count -gt 0 237 | } 238 | 239 | # Test 2.3: Windows Server OS parameter with UpdateType 240 | Invoke-Test -Name "Windows Server OS Parameter" -Parameters @{ 241 | OperatingSystem = "Windows Server" 242 | UpdateType = "Cumulative Updates" 243 | Strict = $true 244 | } -Validation { 245 | param($result) 246 | ($result | Where-Object { $_.Title -match "Microsoft server operating system" }).Count -gt 0 247 | } 248 | #endregion 249 | 250 | #region Test Group 3: OS + Version Parameter 251 | # ====================================================== 252 | # TEST GROUP 3: OS + Version Parameter 253 | # ====================================================== 254 | Write-Host "`n### TEST GROUP: OS + VERSION PARAMETER ###" -ForegroundColor Blue -BackgroundColor White 255 | 256 | # Test 3.1: Windows 11 Version 24H2 with UpdateType 257 | Invoke-Test -Name "Windows 11 + Version 24H2" -Parameters @{ 258 | OperatingSystem = "Windows 11" 259 | Version = "24H2" 260 | UpdateType = "Cumulative Updates" 261 | } -Validation { 262 | param($result) 263 | ($result | Where-Object { $_.Title -match "Windows 11.*(24H2|Version 24H2)" }).Count -gt 0 264 | } 265 | 266 | # Test 3.2: Windows 10 Version 22H2 with UpdateType 267 | Invoke-Test -Name "Windows 10 + Version 22H2" -Parameters @{ 268 | OperatingSystem = "Windows 10" 269 | Version = "22H2" 270 | UpdateType = "Cumulative Updates" 271 | } -Validation { 272 | param($result) 273 | ($result | Where-Object { $_.Title -match "Windows 10.*(22H2|Version 22H2)" }).Count -gt 0 274 | } 275 | 276 | # Test 3.3: Windows Server Version 22H2 with UpdateType 277 | Invoke-Test -Name "Windows Server + Version 22H2" -Parameters @{ 278 | OperatingSystem = "Windows Server" 279 | Version = "22H2" 280 | UpdateType = "Cumulative Updates" 281 | } -Validation { 282 | param($result) 283 | ($result | Where-Object { $_.Title -match "Microsoft server operating system.*(22H2|Version 22H2)" }).Count -gt 0 284 | } 285 | #endregion 286 | 287 | #region Test Group 4: OS + UpdateType Parameter 288 | # ====================================================== 289 | # TEST GROUP 4: OS + UpdateType Parameter 290 | # ====================================================== 291 | Write-Host "`n### TEST GROUP: OS + UPDATE TYPE PARAMETER ###" -ForegroundColor Blue -BackgroundColor White 292 | 293 | # Test 4.1: Windows 11 + Cumulative Updates 294 | Invoke-Test -Name "Windows 11 + Cumulative Updates" -Parameters @{ 295 | OperatingSystem = "Windows 11" 296 | UpdateType = "Cumulative Updates" 297 | } -Validation { 298 | param($result) 299 | ($result | Where-Object { 300 | $_.Title -match "Windows 11" -and $_.Title -match "Cumulative Update" 301 | }).Count -gt 0 302 | } 303 | 304 | # Test 4.2: Windows 11 + Security Updates 305 | # Add Cumulative Updates also to ensure results 306 | Invoke-Test -Name "Windows 11 + Security Updates" -Parameters @{ 307 | OperatingSystem = "Windows 11" 308 | UpdateType = @("Security Updates", "Cumulative Updates") 309 | } -Validation { 310 | param($result) 311 | ($result | Where-Object { 312 | $_.Title -match "Windows 11" -and 313 | ($_.Title -match "Security Update|Cumulative Update" -or 314 | $_.Classification -match "Security Updates|Cumulative Updates") 315 | }).Count -gt 0 316 | } 317 | 318 | # Test 4.3: Multiple update types 319 | Invoke-Test -Name "Windows 11 + Multiple Update Types" -Parameters @{ 320 | OperatingSystem = "Windows 11" 321 | UpdateType = @("Security Updates", "Cumulative Updates") 322 | } -Validation { 323 | param($result) 324 | ($result | Where-Object { 325 | $_.Title -match "Windows 11" -and 326 | ($_.Title -match "Cumulative Update|Security Update" -or 327 | $_.Classification -match "Security Updates|Cumulative Updates") 328 | }).Count -gt 0 329 | } 330 | #endregion 331 | 332 | #region Test Group 5: OS + Architecture Parameter 333 | # ====================================================== 334 | # TEST GROUP 5: OS + Architecture Parameter 335 | # ====================================================== 336 | Write-Host "`n### TEST GROUP: OS + ARCHITECTURE PARAMETER ###" -ForegroundColor Blue -BackgroundColor White 337 | 338 | # Test 5.1: Windows 11 + x64 with UpdateType 339 | Invoke-Test -Name "Windows 11 + x64 Architecture" -Parameters @{ 340 | OperatingSystem = "Windows 11" 341 | Architecture = "x64" 342 | UpdateType = "Cumulative Updates" 343 | } -Validation { 344 | param($result) 345 | ($result | Where-Object { 346 | $_.Title -match "Windows 11" -and 347 | $_.Title -match "x64-based|64-bit" -and -not ($_.Title -match "arm64|x86-based|32-bit") 348 | }).Count -gt 0 349 | } 350 | 351 | # Test 5.2: Windows 11 + ARM64 with UpdateType 352 | Invoke-Test -Name "Windows 11 + ARM64 Architecture" -Parameters @{ 353 | OperatingSystem = "Windows 11" 354 | Architecture = "arm64" 355 | UpdateType = "Cumulative Updates" 356 | } -Validation { 357 | param($result) 358 | ($result | Where-Object { 359 | $_.Title -match "Windows 11" -and 360 | $_.Title -match "arm64|ARM-based" 361 | }).Count -gt 0 362 | } 363 | #endregion 364 | 365 | #region Test Group 6: OS + Version + UpdateType 366 | # ====================================================== 367 | # TEST GROUP 6: OS + Version + UpdateType 368 | # ====================================================== 369 | Write-Host "`n### TEST GROUP: OS + VERSION + UPDATE TYPE ###" -ForegroundColor Blue -BackgroundColor White 370 | 371 | # Test 6.1: Windows 11 + 22H2 + Cumulative Updates 372 | Invoke-Test -Name "Windows 11 + 22H2 + Cumulative Updates" -Parameters @{ 373 | OperatingSystem = "Windows 11" 374 | Version = "22H2" 375 | UpdateType = "Cumulative Updates" 376 | } -Validation { 377 | param($result) 378 | ($result | Where-Object { 379 | $_.Title -match "Windows 11.*22H2.*Cumulative Update" -or 380 | $_.Title -match "Cumulative Update.*Windows 11.*22H2" 381 | }).Count -gt 0 382 | } 383 | #endregion 384 | 385 | #region Test Group 7: All Parameters Combined 386 | # ====================================================== 387 | # TEST GROUP 7: All Parameters Combined 388 | # ====================================================== 389 | Write-Host "`n### TEST GROUP: ALL PARAMETERS COMBINED ###" -ForegroundColor Blue -BackgroundColor White 390 | 391 | # Test 7.1: OS + Version + UpdateType + Architecture 392 | Invoke-Test -Name "All Parameters Combined" -Parameters @{ 393 | OperatingSystem = "Windows 11" 394 | Version = "22H2" 395 | UpdateType = "Cumulative Updates" 396 | Architecture = "x64" 397 | } -Validation { 398 | param($result) 399 | ($result | Where-Object { 400 | ($_.Title -match "Windows 11.*22H2.*Cumulative Update.*x64-based" -or 401 | $_.Title -match "Cumulative Update.*Windows 11.*22H2.*x64-based") -and 402 | -not ($_.Title -match "arm64|x86-based|32-bit") 403 | }).Count -gt 0 404 | } 405 | #endregion 406 | 407 | #region Test Summary 408 | # Calculate script execution time 409 | $scriptEndTime = Get-Date 410 | $totalExecutionTime = ($scriptEndTime - $scriptStartTime).TotalSeconds 411 | 412 | # Display test summary 413 | $totalTests = $successCount + $failureCount 414 | $successRate = if ($totalTests -gt 0) { [math]::Round(($successCount / $totalTests) * 100, 2) } else { 0 } 415 | 416 | Write-Host "`n======================================================" 417 | Write-Host "TEST SUMMARY" -ForegroundColor Green 418 | Write-Host "======================================================" 419 | Write-Host "Total Tests: $totalTests" -ForegroundColor Cyan 420 | Write-Host "Successful: $successCount" -ForegroundColor Green 421 | Write-Host "Failed: $failureCount" -ForegroundColor Red 422 | Write-Host "Success Rate: $successRate%" -ForegroundColor Cyan 423 | Write-Host "Total Execution Time: $([math]::Round($totalExecutionTime, 2)) seconds" -ForegroundColor Cyan 424 | 425 | Write-Host "`nDetailed Test Results:" -ForegroundColor Cyan 426 | $testResults | Format-Table Name, Parameters, Success, ResultCount, ExecutionTime, Message -AutoSize 427 | 428 | Write-Host "`n======================================================" 429 | Write-Host "TEST COMPLETION" -ForegroundColor Green 430 | Write-Host "======================================================" 431 | #endregion 432 | -------------------------------------------------------------------------------- /Public/Get-MSCatalogUpdate.ps1: -------------------------------------------------------------------------------- 1 | function Get-MSCatalogUpdate { 2 | [CmdletBinding(DefaultParameterSetName = 'Search')] 3 | [OutputType([MSCatalogUpdate[]])] 4 | param ( 5 | #region Parameters 6 | [Parameter(Mandatory = $false, 7 | HelpMessage = "Filter updates by architecture")] 8 | [ValidateSet("All", "x64", "x86", "arm64")] 9 | [string] $Architecture = "All", 10 | 11 | [Parameter(Mandatory = $false, 12 | HelpMessage = "Sort in descending order")] 13 | [switch] $Descending, 14 | 15 | [Parameter(Mandatory = $false, 16 | HelpMessage = "Exclude .NET Framework updates")] 17 | [switch] $ExcludeFramework, 18 | 19 | [Parameter(Mandatory = $false, 20 | HelpMessage = "Filter updates from this date")] 21 | [DateTime] $FromDate, 22 | 23 | [Parameter(Mandatory = $false, 24 | HelpMessage = "Format for the results")] 25 | [ValidateSet("Default", "CSV", "JSON", "XML")] 26 | [string] $Format = "Default", 27 | 28 | [Parameter(Mandatory = $false, 29 | HelpMessage = "Only show .NET Framework updates")] 30 | [switch] $GetFramework, 31 | 32 | [Parameter(Mandatory = $false, 33 | HelpMessage = "Search through all available pages")] 34 | [switch] $AllPages, 35 | 36 | [Parameter(Mandatory = $false, 37 | HelpMessage = "Include dynamic updates")] 38 | [switch] $IncludeDynamic, 39 | 40 | [Parameter(Mandatory = $false, 41 | HelpMessage = "Include file names in the results")] 42 | [switch] $IncludeFileNames, 43 | 44 | [Parameter(Mandatory = $false, 45 | HelpMessage = "Include preview updates")] 46 | [switch] $IncludePreview, 47 | 48 | [Parameter(Mandatory = $false, 49 | HelpMessage = "Filter updates from the last N days")] 50 | [int] $LastDays, 51 | 52 | [Parameter(Mandatory = $false, 53 | HelpMessage = "Filter updates with maximum size")] 54 | [double] $MaxSize, 55 | 56 | [Parameter(Mandatory = $false, 57 | HelpMessage = "Filter updates with minimum size")] 58 | [double] $MinSize, 59 | 60 | [Parameter(Mandatory = $true, ParameterSetName = 'OS', 61 | HelpMessage = "Operating System to search updates for")] 62 | [ValidateSet("Windows 11", "Windows 10", "Windows Server")] 63 | [string] $OperatingSystem, 64 | 65 | [Parameter(Mandatory = $false, 66 | HelpMessage = "Select specific properties to display")] 67 | [string[]] $Properties, 68 | 69 | [Parameter(Mandatory = $true, ParameterSetName = 'Search', 70 | Position = 0, 71 | HelpMessage = "Search query for Microsoft Update Catalog")] 72 | [string] $Search, 73 | 74 | [Parameter(Mandatory = $false, 75 | HelpMessage = "Unit for size filtering (MB or GB)")] 76 | [ValidateSet("MB", "GB")] 77 | [string] $SizeUnit = "MB", 78 | 79 | [Parameter(Mandatory = $false, 80 | HelpMessage = "Sort results by specified field")] 81 | [ValidateSet("Date", "Size", "Title", "Classification", "Product")] 82 | [string] $SortBy = "Date", 83 | 84 | [Parameter(Mandatory = $false, 85 | HelpMessage = "Use strict search with exact phrase matching")] 86 | [switch] $Strict, 87 | 88 | [Parameter(Mandatory = $false, 89 | HelpMessage = "Filter updates until this date")] 90 | [DateTime] $ToDate, 91 | 92 | [Parameter(Mandatory = $false, 93 | HelpMessage = "Filter by update type")] 94 | [ValidateSet( 95 | "Security Updates", 96 | "Updates", 97 | "Critical Updates", 98 | "Feature Packs", 99 | "Service Packs", 100 | "Tools", 101 | "Update Rollups", 102 | "Cumulative Updates", 103 | "Security Quality Updates", 104 | "Driver Updates" 105 | )] 106 | [string[]] $UpdateType, 107 | 108 | [Parameter(Mandatory = $false, ParameterSetName = 'OS', 109 | HelpMessage = "OS Version/Release (e.g., 22H2, 21H2, 23H2)")] 110 | [string] $Version 111 | #endregion Parameters 112 | ) 113 | 114 | begin { 115 | #region Initialization 116 | # Ensure MSCatalogUpdate class is available 117 | if (-not ('MSCatalogUpdate' -as [type])) { 118 | $classPath = Join-Path $PSScriptRoot '..\Classes\MSCatalogUpdate.Class.ps1' 119 | if (Test-Path $classPath) { 120 | . $classPath 121 | } else { 122 | throw "MSCatalogUpdate class file not found at: $classPath" 123 | } 124 | } 125 | 126 | $ProgressPreference = "SilentlyContinue" 127 | $Updates = @() 128 | $MaxResults = 1000 129 | #endregion Initialization 130 | 131 | #region Query Building 132 | # Build search query based on parameters 133 | $searchQuery = if ($PSCmdlet.ParameterSetName -eq 'OS') { 134 | switch ($OperatingSystem) { 135 | "Windows 10" { 136 | if ($Version) { 137 | if ($UpdateType -contains "Cumulative Updates") { 138 | "Cumulative Update for Windows 10 Version $Version" 139 | } else { 140 | "Windows 10 Version $Version" 141 | } 142 | } else { 143 | if ($UpdateType -contains "Cumulative Updates") { 144 | "Cumulative Update for Windows 10" 145 | } else { 146 | "Windows 10" 147 | } 148 | } 149 | } 150 | "Windows 11" { 151 | if ($Version) { 152 | if ($UpdateType -contains "Cumulative Updates") { 153 | "Cumulative Update for Windows 11 Version $Version" 154 | } else { 155 | "Windows 11 Version $Version" 156 | } 157 | } else { 158 | if ($UpdateType -contains "Cumulative Updates") { 159 | "Cumulative Update for Windows 11" 160 | } else { 161 | "Windows 11" 162 | } 163 | } 164 | } 165 | "Windows Server" { 166 | if ($Version) { 167 | if ($UpdateType -contains "Cumulative Updates") { 168 | "Cumulative Update for Microsoft Server Operating System, Version $Version" 169 | } else { 170 | "Microsoft Server Operating System, Version $Version" 171 | } 172 | } else { 173 | if ($UpdateType -contains "Cumulative Updates") { 174 | "Cumulative Update for Microsoft Server Operating System" 175 | } else { 176 | "Microsoft Server Operating System" 177 | } 178 | } 179 | } 180 | default { 181 | if ($Version) { 182 | if ($UpdateType -contains "Cumulative Updates") { 183 | "Cumulative Update for $OperatingSystem $Version" 184 | } else { 185 | "$OperatingSystem $Version" 186 | } 187 | } else { 188 | if ($UpdateType -contains "Cumulative Updates") { 189 | "Cumulative Update for $OperatingSystem" 190 | } else { 191 | "$OperatingSystem" 192 | } 193 | } 194 | } 195 | } 196 | } else { 197 | $Search 198 | } 199 | 200 | Write-Verbose "Search query: $searchQuery" 201 | #endregion Query Building 202 | } 203 | 204 | process { 205 | try { 206 | #region Search Preparation 207 | # Prepare search query 208 | $EncodedSearch = switch ($true) { 209 | $Strict { [uri]::EscapeDataString('"' + $searchQuery + '"') } 210 | $GetFramework { [uri]::EscapeDataString("*$searchQuery*") } 211 | default { [uri]::EscapeDataString($searchQuery) } 212 | } 213 | 214 | # Initialize catalog request 215 | $Uri = "https://www.catalog.update.microsoft.com/Search.aspx?q=$EncodedSearch" 216 | $Res = Invoke-CatalogRequest -Uri $Uri 217 | $Rows = $Res.Rows 218 | #endregion Search Preparation 219 | 220 | #region Pagination 221 | # Handle pagination 222 | if ($AllPages) { 223 | $PageCount = 0 224 | while ($Res.NextPage -and $PageCount -lt 39) { # Microsoft Catalog limit is 40 pages 225 | $PageCount++ 226 | $PageUri = "$Uri&p=$PageCount" 227 | $Res = Invoke-CatalogRequest -Uri $PageUri 228 | $Rows += $Res.Rows 229 | } 230 | } 231 | #endregion Pagination 232 | 233 | #region Base Filtering 234 | # Apply base filters with improved logic 235 | $Rows = $Rows.Where({ 236 | $title = $_.SelectNodes("td")[1].InnerText.Trim() 237 | $classification = $_.SelectNodes("td")[3].InnerText.Trim() 238 | $include = $true 239 | 240 | # Basic exclusion filters 241 | if (-not $IncludeDynamic -and $title -like "*Dynamic*") { $include = $false } 242 | if (-not $IncludePreview -and $title -like "*Preview*") { $include = $false } 243 | 244 | # Framework filtering: handle GetFramework and ExcludeFramework parameters 245 | if ($GetFramework) { 246 | # If GetFramework is specified, only keep Framework updates 247 | if (-not ($title -like "*Framework*")) { $include = $false } 248 | } elseif ($ExcludeFramework) { 249 | # If ExcludeFramework is specified, exclude Framework updates 250 | if ($title -like "*Framework*") { $include = $false } 251 | } 252 | 253 | # OS and Version specific filtering 254 | if ($PSCmdlet.ParameterSetName -eq 'OS') { 255 | if ($OperatingSystem -eq "Windows Server") { 256 | # For Server, look for "Microsoft server" or similar patterns 257 | if (-not ($title -like "*Microsoft*Server*" -or $title -like "*Server Operating System*")) { $include = $false } 258 | } 259 | else { 260 | # For other OS types, use the standard pattern 261 | if (-not ($title -like "*$OperatingSystem*")) { $include = $false } 262 | } 263 | if ($Version -and -not ($title -like "*$Version*")) { $include = $false } 264 | } 265 | 266 | # Update type filtering 267 | if ($UpdateType) { 268 | $hasMatchingType = $false 269 | foreach ($type in $UpdateType) { 270 | switch ($type) { 271 | "Security Updates" { 272 | # In the Classification column 273 | if ($classification -eq "Security Updates") { 274 | $hasMatchingType = $true 275 | } 276 | } 277 | "Cumulative Updates" { 278 | # In the title, look for "Cumulative Update" 279 | if ($title -like "*Cumulative Update*") { 280 | $hasMatchingType = $true 281 | } 282 | } 283 | "Critical Updates" { 284 | # In the Classification column 285 | if ($classification -eq "Critical Updates") { 286 | $hasMatchingType = $true 287 | } 288 | } 289 | "Updates" { 290 | # In the Classification column 291 | if ($classification -eq "Updates") { 292 | $hasMatchingType = $true 293 | } 294 | } 295 | "Feature Packs" { 296 | # In the Classification column 297 | if ($classification -eq "Feature Packs") { 298 | $hasMatchingType = $true 299 | } 300 | } 301 | "Service Packs" { 302 | # In the Classification column 303 | if ($classification -eq "Service Packs") { 304 | $hasMatchingType = $true 305 | } 306 | } 307 | "Tools" { 308 | # In the Classification column 309 | if ($classification -eq "Tools") { 310 | $hasMatchingType = $true 311 | } 312 | } 313 | "Update Rollups" { 314 | # In the Classification column 315 | if ($classification -eq "Update Rollups") { 316 | $hasMatchingType = $true 317 | } 318 | } 319 | "Security Quality Updates" { 320 | # Combines security and quality 321 | if (($classification -eq "Security Updates") -and 322 | ($title -like "*Quality Update*")) { 323 | $hasMatchingType = $true 324 | } 325 | } 326 | "Driver Updates" { 327 | # For drivers 328 | if ($title -like "*Driver*") { 329 | $hasMatchingType = $true 330 | } 331 | } 332 | default { 333 | if ($title -like "*$type*") { 334 | $hasMatchingType = $true 335 | } 336 | } 337 | } 338 | if ($hasMatchingType) { break } 339 | } 340 | if (-not $hasMatchingType) { $include = $false } 341 | } 342 | 343 | $include 344 | }) 345 | #endregion Base Filtering 346 | 347 | #region Architecture Filtering 348 | # Apply architecture filter with improved logic 349 | if ($Architecture -ne "all") { 350 | $Rows = $Rows.Where({ 351 | $title = $_.SelectNodes("td")[1].InnerText.Trim() 352 | switch ($Architecture) { 353 | "x64" { $title -match "x64|64.?bit|64.?based" -and -not ($title -match "x86|32.?bit|arm64") } 354 | "x86" { $title -match "x86|32.?bit|32.?based" -and -not ($title -match "64.?bit|arm64") } 355 | "arm64" { $title -match "arm64|ARM.?based" } 356 | } 357 | }) 358 | } 359 | #endregion Architecture Filtering 360 | 361 | #region Create Update Objects 362 | # Create MSCatalogUpdate objects with improved error handling 363 | $Updates = $Rows.Where({ $_.Id -ne "headerRow" }).ForEach({ 364 | try { 365 | [MSCatalogUpdate]::new($_, $IncludeFileNames) 366 | } catch { 367 | Write-Warning "Failed to process update: $($_.Exception.Message)" 368 | $null 369 | } 370 | }) | Where-Object { $null -ne $_ } 371 | #endregion Create Update Objects 372 | 373 | #region Apply Filters 374 | # Apply date filters 375 | if ($FromDate) { $Updates = $Updates.Where({ $_.LastUpdated -ge $FromDate }) } 376 | if ($ToDate) { $Updates = $Updates.Where({ $_.LastUpdated -le $ToDate }) } 377 | if ($LastDays) { 378 | $CutoffDate = (Get-Date).AddDays(-$LastDays) 379 | $Updates = $Updates.Where({ $_.LastUpdated -ge $CutoffDate }) 380 | } 381 | 382 | # Apply size filters 383 | if ($MinSize -or $MaxSize) { 384 | $Multiplier = if ($SizeUnit -eq "GB") { 1024 } else { 1 } 385 | $Updates = $Updates.Where({ 386 | $size = [double]($_.Size -replace ' MB$','') 387 | $meetsMin = -not $MinSize -or $size -ge ($MinSize * $Multiplier) 388 | $meetsMax = -not $MaxSize -or $size -le ($MaxSize * $Multiplier) 389 | $meetsMin -and $meetsMax 390 | }) 391 | } 392 | #endregion Apply Filters 393 | 394 | #region Sorting and Output 395 | # Apply sorting 396 | $Updates = switch ($SortBy) { 397 | "Date" { $Updates | Sort-Object LastUpdated -Descending:$Descending } 398 | "Size" { $Updates | Sort-Object { [double]($_.Size -replace ' MB$','') } -Descending:$Descending } 399 | "Title" { $Updates | Sort-Object Title -Descending:$Descending } 400 | "Classification" { $Updates | Sort-Object Classification -Descending:$Descending } 401 | "Product" { $Updates | Sort-Object Products -Descending:$Descending } 402 | default { $Updates } 403 | } 404 | 405 | # Display result summary 406 | Write-Host "`nSearch completed for: $searchQuery" 407 | Write-Host "Found $($Updates.Count) updates" 408 | if ($Updates.Count -ge $MaxResults) { 409 | Write-Warning "Result limit of $MaxResults reached. Please refine your search criteria." 410 | } 411 | 412 | # Format and return results 413 | switch ($Format) { 414 | "Default" { 415 | if ($Properties) { $Updates | Select-Object $Properties } 416 | else { $Updates } 417 | } 418 | "CSV" { 419 | if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Csv -NoTypeInformation } 420 | else { $Updates | ConvertTo-Csv -NoTypeInformation } 421 | } 422 | "JSON" { 423 | if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Json } 424 | else { $Updates | ConvertTo-Json } 425 | } 426 | "XML" { 427 | if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Xml -As String } 428 | else { $Updates | ConvertTo-Xml -As String } 429 | } 430 | } 431 | #endregion Sorting and Output 432 | } 433 | catch { 434 | Write-Warning "Error processing search request: $($_.Exception.Message)" 435 | } 436 | } 437 | 438 | end { 439 | $ProgressPreference = "Continue" 440 | } 441 | } --------------------------------------------------------------------------------