├── 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 | [](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 | }
--------------------------------------------------------------------------------