Public/Get-MSCatalogUpdate.ps1
|
function Get-MSCatalogUpdate { [CmdletBinding(DefaultParameterSetName = 'Search')] [OutputType([MSCatalogUpdate[]])] param ( #region Parameters [Parameter(Mandatory = $false, HelpMessage = "Filter updates by architecture")] [ValidateSet("All", "x64", "x86", "arm64")] [string] $Architecture = "All", [Parameter(Mandatory = $false, HelpMessage = "Sort in descending order")] [switch] $Descending, [Parameter(Mandatory = $false, HelpMessage = "Exclude .NET Framework updates")] [switch] $ExcludeFramework, [Parameter(Mandatory = $false, HelpMessage = "Filter updates from this date")] [DateTime] $FromDate, [Parameter(Mandatory = $false, HelpMessage = "Format for the results")] [ValidateSet("Default", "CSV", "JSON", "XML")] [string] $Format = "Default", [Parameter(Mandatory = $false, HelpMessage = "Only show .NET Framework updates")] [switch] $GetFramework, [Parameter(Mandatory = $false, HelpMessage = "Search through all available pages")] [switch] $AllPages, [Parameter(Mandatory = $false, HelpMessage = "Include dynamic updates")] [switch] $IncludeDynamic, [Parameter(Mandatory = $false, HelpMessage = "Include file names in the results")] [switch] $IncludeFileNames, [Parameter(Mandatory = $false, HelpMessage = "Include preview updates")] [switch] $IncludePreview, [Parameter(Mandatory = $false, HelpMessage = "Filter updates from the last N days")] [int] $LastDays, [Parameter(Mandatory = $false, HelpMessage = "Filter updates with maximum size")] [double] $MaxSize, [Parameter(Mandatory = $false, HelpMessage = "Filter updates with minimum size")] [double] $MinSize, [Parameter(Mandatory = $true, ParameterSetName = 'OS', HelpMessage = "Operating System to search updates for")] [ValidateSet("Windows 11", "Windows 10", "Windows Server")] [string] $OperatingSystem, [Parameter(Mandatory = $false, HelpMessage = "Select specific properties to display")] [string[]] $Properties, [Parameter(Mandatory = $true, ParameterSetName = 'Search', Position = 0, HelpMessage = "Search query for Microsoft Update Catalog")] [string] $Search, [Parameter(Mandatory = $false, HelpMessage = "Unit for size filtering (MB or GB)")] [ValidateSet("MB", "GB")] [string] $SizeUnit = "MB", [Parameter(Mandatory = $false, HelpMessage = "Sort results by specified field")] [ValidateSet("Date", "Size", "Title", "Classification", "Product")] [string] $SortBy = "Date", [Parameter(Mandatory = $false, HelpMessage = "Use strict search with exact phrase matching")] [switch] $Strict, [Parameter(Mandatory = $false, HelpMessage = "Filter updates until this date")] [DateTime] $ToDate, [Parameter(Mandatory = $false, HelpMessage = "Filter by update type")] [ValidateSet( "Security Updates", "Updates", "Critical Updates", "Feature Packs", "Service Packs", "Tools", "Update Rollups", "Cumulative Updates", "Security Quality Updates", "Driver Updates" )] [string[]] $UpdateType, [Parameter(Mandatory = $false, ParameterSetName = 'OS', HelpMessage = "OS Version/Release (e.g., 22H2, 21H2, 23H2, 2019, 2016, 2012 R2)")] [string] $Version, [Parameter(Mandatory = $false, HelpMessage = "Include Microsoft Edge Stable channel updates")] [switch] $IsStable, [Parameter(Mandatory = $false, HelpMessage = "Include Microsoft Edge Extended Stable channel updates")] [switch] $IsExtendedStable, [Parameter(Mandatory = $false, HelpMessage = "Include Microsoft Edge Dev channel updates")] [switch] $IsDev, [Parameter(Mandatory = $false, HelpMessage = "Export results to JSON file")] [string] $ExportJson, [Parameter(Mandatory = $false, HelpMessage = "Export results to CSV file")] [string] $ExportCsv, [Parameter(Mandatory = $false, HelpMessage = "Export results to XML file")] [string] $ExportXml, [Parameter(Mandatory = $false, HelpMessage = "Append to existing export file instead of overwriting")] [switch] $Append #endregion Parameters ) begin { #region Debug Preference Override if ($PSBoundParameters.ContainsKey('Debug') -and $PSBoundParameters['Debug']) { $DebugPreference = 'Continue' Write-Debug "Debug mode enabled - DebugPreference set to 'Continue'" } #endregion Debug Preference Override #region Parameter Validation if ($PSCmdlet.ParameterSetName -eq 'Search' -and $PSBoundParameters.ContainsKey('Version')) { $errorMessage = "The -Version parameter can only be used with -OperatingSystem parameter, not with -Search. " + "Either use: -Search 'Windows 10 22H2' OR -OperatingSystem 'Windows 10' -Version '22H2'" if ($DebugPreference -ne 'SilentlyContinue') { Write-Error $errorMessage -ErrorAction Stop } else { Write-Host "Error: " -ForegroundColor Red -NoNewline Write-Host "Invalid parameter combination. Use -Search 'Windows 10 22H2' format or separate -OperatingSystem and -Version parameters." return } } #endregion Parameter Validation #region Initialization if (-not ('MSCatalogUpdate' -as [type])) { $classPath = Join-Path $PSScriptRoot '..\Classes\MSCatalogUpdate.Class.ps1' if (Test-Path $classPath) { . $classPath } else { throw "MSCatalogUpdate class file not found at: $classPath" } } $ProgressPreference = "SilentlyContinue" $Updates = @() $MaxResults = 1000 $UseOSBehavior = $false $ShouldAbort = $false #endregion Initialization #region Smart Search Parser if ($PSCmdlet.ParameterSetName -eq 'Search' -and $Search) { # Apply default descending sort if (-not $PSBoundParameters.ContainsKey('Descending')) { $Descending = $true Write-Verbose "Applied default Descending=$Descending for Search parameter" } # Check for update type prefix in search (Servicing Stack, Dynamic Update, etc.) # BUT NOT .NET Framework - that's a product name, not an update type $updateTypePrefix = "" if ($Search -match '^(Servicing Stack|Dynamic Update|Security Update|Cumulative Update|Feature Update)\s+(.+)$' -and $Search -notmatch '\.NET') { $updateTypePrefix = $matches[1] $remainingSearch = $matches[2] Write-Debug "Detected update type prefix: '$updateTypePrefix', remaining search: '$remainingSearch'" $script:UpdateTypePrefix = $updateTypePrefix $Search = $remainingSearch } else { $script:UpdateTypePrefix = $null } # Special handling for .NET Framework searches $isDotNetFrameworkSearch = $Search -match '^\.NET Framework\s+(.+)$' if ($isDotNetFrameworkSearch) { $remainingAfterDotNet = $matches[1] Write-Debug "Detected .NET Framework search, remaining: '$remainingAfterDotNet'" # Check if there's a Windows version in the remaining part if ($remainingAfterDotNet -match '(Windows Server|Windows 10|Windows 11)\s+(?:Version\s+)?(\d{2}H[12]|\d{4})(\s+R2)?(?:\s+(x64|x86|arm64))?') { $OperatingSystem = $matches[1] $Version = $matches[2] $R2Suffix = $matches[3] $UseOSBehavior = $true $script:IsDotNetFrameworkSearch = $true Write-Verbose "Parsed .NET Framework search: OS='$OperatingSystem', Version='$Version'" Write-Debug ".NET Framework search with OS: OS='$($matches[1])', Version='$($matches[2])', R2='$($matches[3])', Arch='$($matches[4])'" # Handle R2 suffix if ($R2Suffix) { $script:IsR2Version = $true $Version = "$Version R2" Write-Debug "R2 version detected, updated Version to: $Version" } else { $script:IsR2Version = $false } # Handle architecture if ($matches[4]) { if (-not $PSBoundParameters.ContainsKey('Architecture')) { $Architecture = $matches[4] Write-Verbose "Detected architecture from search string: $Architecture" Write-Debug "Architecture parameter updated to: $Architecture" } else { Write-Debug "Architecture parameter was explicitly set to: $Architecture (ignoring detected: $($matches[4]))" } } $script:DotNetParsed = $true } } else { $script:IsDotNetFrameworkSearch = $false $script:DotNetParsed = $false } # Check if search contains OS name without version if (-not $script:DotNetParsed -and $Search -match '^(Windows Server|Windows 10|Windows 11)(?:\s+(x64|x86|arm64))?\s*$') { $detectedOS = $matches[1] Write-Host "Error: " -ForegroundColor Red -NoNewline Write-Host "Version is required when searching for $detectedOS updates." Write-Host "" Write-Host "Example usage:" -ForegroundColor Cyan switch ($detectedOS) { "Windows 11" { Write-Host " Get-MSCatalogUpdate -Search 'Windows 11 23H2 x64'" -ForegroundColor Yellow Write-Host " Get-MSCatalogUpdate -Search 'Windows 11 24H2 x64'" -ForegroundColor Yellow } "Windows 10" { Write-Host " Get-MSCatalogUpdate -Search 'Windows 10 1809 x64'" -ForegroundColor Yellow Write-Host " Get-MSCatalogUpdate -Search 'Windows 10 22H2 x64'" -ForegroundColor Yellow } "Windows Server" { Write-Host " Get-MSCatalogUpdate -Search 'Windows Server 2012 R2 x64'" -ForegroundColor Yellow Write-Host " Get-MSCatalogUpdate -Search 'Windows Server 2025 x64'" -ForegroundColor Yellow } } Write-Host "" $ShouldAbort = $true return } # Pattern matching: OS + Version + R2 (optional) + Architecture (optional) if (-not $script:DotNetParsed -and $Search -match '(Windows Server|Windows 10|Windows 11)\s+(?:Version\s+)?(\d{2}H[12]|\d{4})(\s+R2)?(?:\s+(x64|x86|arm64))?') { $OperatingSystem = $matches[1] $Version = $matches[2] $R2Suffix = $matches[3] $UseOSBehavior = $true Write-Verbose "Parsed Search: OperatingSystem='$OperatingSystem', Version='$Version', R2='$R2Suffix'" Write-Debug "Regex matches: OS='$($matches[1])', Version='$($matches[2])', R2='$($matches[3])', Arch='$($matches[4])'" # Handle R2 suffix if ($R2Suffix) { $script:IsR2Version = $true $Version = "$Version R2" Write-Debug "R2 version detected, updated Version to: $Version" } else { $script:IsR2Version = $false } # Handle architecture if ($matches[4]) { if (-not $PSBoundParameters.ContainsKey('Architecture')) { $Architecture = $matches[4] Write-Verbose "Detected architecture from search string: $Architecture" Write-Debug "Architecture parameter updated to: $Architecture" } else { Write-Debug "Architecture parameter was explicitly set to: $Architecture (ignoring detected: $($matches[4]))" } } } } # OS parameter set if ($PSCmdlet.ParameterSetName -eq 'OS') { $UseOSBehavior = $true $script:IsR2Version = $false $script:UpdateTypePrefix = $null $script:IsDotNetFrameworkSearch = $false if (-not $PSBoundParameters.ContainsKey('Descending')) { $Descending = $true Write-Verbose "Applied default Descending=$Descending for OS parameter set" } } Write-Debug "After Smart Search Parser: Architecture='$Architecture', UseOSBehavior='$UseOSBehavior', IsR2='$script:IsR2Version', UpdateTypePrefix='$script:UpdateTypePrefix', IsDotNetFramework='$script:IsDotNetFrameworkSearch'" #endregion Smart Search Parser #region Query Building if (-not $ShouldAbort) { Write-Debug "Query Building: Architecture parameter = '$Architecture'" $searchQuery = if ($UseOSBehavior) { # Map Windows Server versions $mappedVersion = $Version if ($OperatingSystem -eq "Windows Server" -and $Version -notlike "*R2") { $mappedVersion = switch ($Version) { "2025" { "24H2" } "2022" { "21H2" } default { $Version } } Write-Debug "Windows Server version mapping: $Version -> $mappedVersion" } $script:VersionForFiltering = $mappedVersion # Special handling for .NET Framework searches if ($script:IsDotNetFrameworkSearch) { $osPhrase = switch ($OperatingSystem) { "Windows 10" { "Windows 10, version $mappedVersion" } "Windows 11" { "Windows 11, version $mappedVersion" } "Windows Server" { "Windows Server $mappedVersion" } default { "$OperatingSystem $mappedVersion" } } $archSuffix = if ($Architecture -ne "All") { " for $Architecture" } else { "" } $query = ".NET Framework $osPhrase$archSuffix" Write-Debug ".NET Framework search query: '$query'" $query } else { # Regular OS query building $osPhrase = switch ($OperatingSystem) { "Windows 10" { "Windows 10 Version $mappedVersion" } "Windows 11" { "Windows 11 Version $mappedVersion" } "Windows Server" { if ($mappedVersion -match '^\d{2}H[12]$') { "Microsoft Server Operating System version $mappedVersion" } else { "Windows Server $mappedVersion" } } default { "$OperatingSystem $mappedVersion" } } Write-Debug "OS Phrase: '$osPhrase'" # Build architecture suffix $archSuffix = if ($Architecture -ne "All") { $suffix = switch ($Architecture) { "x64" { " for x64-based Systems" } "x86" { " for x86-based Systems" } "arm64" { " for ARM64-based Systems" } } Write-Debug "Architecture suffix: '$suffix'" $suffix } else { Write-Debug "Architecture is 'All', no suffix added" "" } # Determine update prefix $updatePrefix = "" if ($UpdateType -and $UpdateType.Count -gt 0) { if ($UpdateType -contains "Cumulative Updates" -or $UpdateType -contains "Updates" -or $UpdateType -contains "Security Updates") { $updatePrefix = "Cumulative Update" } elseif ($UpdateType -contains "Critical Updates" -or $UpdateType -contains "Feature Packs" -or $UpdateType -contains "Service Packs" -or $UpdateType -contains "Tools" -or $UpdateType -contains "Update Rollups" -or $UpdateType -contains "Driver Updates") { $updatePrefix = "" } else { $updatePrefix = "Update" } } else { if ($OperatingSystem -eq "Windows Server" -and $mappedVersion -match '^\d{4}') { $updatePrefix = "Update" } else { $updatePrefix = "Cumulative Update" } } Write-Debug "Update prefix: '$updatePrefix'" if ($script:UpdateTypePrefix) { Write-Debug "UpdateTypePrefix '$script:UpdateTypePrefix' will be used for POST-FILTERING only" } # Construct full query if ($Version) { if ($updatePrefix) { "$updatePrefix for $osPhrase$archSuffix" } else { "$osPhrase$archSuffix" } } else { if ($updatePrefix) { "$updatePrefix for $OperatingSystem$archSuffix" } else { "$OperatingSystem$archSuffix" } } } } else { $script:VersionForFiltering = $null $script:IsR2Version = $false if (-not $Strict) { $cleanSearch = $Search -replace '\s+for\s+(x64|x86|arm64).*$', '' ` -replace '\s+\((x64|x86|arm64)\).*$', '' ` -replace '\s+(x64|x86|arm64)\s*-.*$', '' $cleanSearch } else { $Search } } Write-Verbose "Search query: $searchQuery" Write-Debug "Full search query being sent to catalog: $searchQuery" Write-Debug "UseOSBehavior: $UseOSBehavior" Write-Debug "UpdateType specified: $($UpdateType -join ', ')" Write-Debug "VersionForFiltering: $script:VersionForFiltering" Write-Debug "IsR2Version: $script:IsR2Version" Write-Debug "UpdateTypePrefix for filtering: $script:UpdateTypePrefix" Write-Debug "IsDotNetFrameworkSearch: $script:IsDotNetFrameworkSearch" Write-Debug "Will use strict search: $(($PSCmdlet.ParameterSetName -eq 'OS' -and $Version) -or $Strict)" } #endregion Query Building } process { if ($ShouldAbort) { return } try { #region Search Preparation # Auto-enable strict search if "Windows" is in the search query $autoStrictEnabled = $searchQuery -match 'Windows' $isNetSearch = $searchQuery -match '^\.NET' $useStrictSearch = $false if (-not $isNetSearch) { $useStrictSearch = $Strict -or ($PSCmdlet.ParameterSetName -eq 'OS' -and $Version) -or $autoStrictEnabled } if ($isNetSearch -and $Strict) { Write-Debug "Strict search disabled for .NET queries (not supported by Microsoft Catalog)" } if ($autoStrictEnabled -and -not $Strict) { Write-Debug "Auto-enabled strict search (detected 'Windows' in query)" } $EncodedSearch = switch ($true) { $useStrictSearch { [uri]::EscapeDataString('"' + $searchQuery + '"') } $GetFramework { [uri]::EscapeDataString("*$searchQuery*") } default { [uri]::EscapeDataString($searchQuery) } } $Uri = "https://www.catalog.update.microsoft.com/Search.aspx?q=$EncodedSearch" Write-Debug "Request URI: $Uri" Write-Debug "Using strict search: $useStrictSearch $(if ($autoStrictEnabled -and -not $Strict) { '(auto-enabled)' })" $Res = Invoke-CatalogRequest -Uri $Uri $Rows = $Res.Rows Write-Debug "Initial rows returned: $($Rows.Count)" #endregion Search Preparation #region Pagination if ($AllPages) { $PageCount = 0 while ($Res.NextPage -and $PageCount -lt 39) { $PageCount++ $PageUri = "$Uri&p=$PageCount" Write-Debug "Fetching page $PageCount" $Res = Invoke-CatalogRequest -Uri $PageUri $Rows += $Res.Rows } Write-Debug "Total rows after pagination: $($Rows.Count)" } #endregion Pagination #region Base Filtering $Rows = $Rows.Where({ $title = $_.SelectNodes("td")[1].InnerText.Trim() $classification = $_.SelectNodes("td")[3].InnerText.Trim() $include = $true Write-Debug "Evaluating: $title | Classification: $classification" # Basic exclusions if (-not $IncludeDynamic -and $title -like "*Dynamic*") { Write-Debug "Excluded (Dynamic): $title" $include = $false } if (-not $IncludePreview -and $title -like "*Preview*") { Write-Debug "Excluded (Preview): $title" $include = $false } # Framework filtering if ($GetFramework) { if (-not ($title -like "*Framework*")) { Write-Debug "Excluded (Not Framework): $title" $include = $false } } elseif ($ExcludeFramework) { if ($title -like "*Framework*") { Write-Debug "Excluded (Framework): $title" $include = $false } } # Edge channel filtering if ($IsStable -or $IsExtendedStable -or $IsDev) { $isEdgeSearch = $title -match "Microsoft Edge" if ($isEdgeSearch) { $includeByChannel = $false if ($IsStable -and $title -like "*Edge-Stable Channel*" -and $title -notlike "*Extended*") { $includeByChannel = $true Write-Debug "Included (Stable channel): $title" } if ($IsExtendedStable -and $title -like "*Extended Stable Channel*") { $includeByChannel = $true Write-Debug "Included (Extended Stable channel): $title" } if ($IsDev -and $title -like "*Edge-Dev Channel*") { $includeByChannel = $true Write-Debug "Included (Dev channel): $title" } if (-not $includeByChannel) { Write-Debug "Excluded (Edge channel filter - looking for: Stable=$IsStable, Extended=$IsExtendedStable, Dev=$IsDev): $title" $include = $false } } } # Update type prefix filtering if ($script:UpdateTypePrefix) { if (-not ($title -like "*$script:UpdateTypePrefix*")) { Write-Debug "Excluded (Update type prefix '$script:UpdateTypePrefix' not found): $title" $include = $false } } # OS and Version filtering if ($UseOSBehavior) { if ($OperatingSystem -eq "Windows Server") { if (-not ($title -like "*Microsoft*Server*" -or $title -like "*Server Operating System*" -or $title -like "*Windows Server*")) { Write-Debug "Excluded (Not Windows Server): $title" $include = $false } } else { if (-not ($title -like "*$OperatingSystem*")) { Write-Debug "Excluded (Not $OperatingSystem): $title" $include = $false } } $versionToCheck = if ($script:VersionForFiltering) { $script:VersionForFiltering } else { $Version } if ($versionToCheck -and -not ($title -like "*$versionToCheck*")) { Write-Debug "Excluded (Not version $versionToCheck): $title" $include = $false } # R2 filtering if ($script:IsR2Version) { $baseVersion = $versionToCheck -replace '\s+R2$', '' if (($title -like "*$baseVersion*") -and -not ($title -like "*R2*")) { Write-Debug "Excluded (R2 required but not found): $title" $include = $false } } elseif ($versionToCheck -match '^\d{4}$' -and $OperatingSystem -eq "Windows Server") { if ($title -like "*$versionToCheck R2*") { Write-Debug "Excluded (Non-R2 required but R2 found): $title" $include = $false } } } # UpdateType filtering if ($UpdateType) { $hasMatchingType = $false foreach ($type in $UpdateType) { switch ($type) { "Security Updates" { if ($classification -eq "Security Updates") { $hasMatchingType = $true } } "Cumulative Updates" { if ($title -like "*Cumulative Update*") { $hasMatchingType = $true } } "Critical Updates" { if ($classification -eq "Critical Updates") { $hasMatchingType = $true } } "Updates" { if ($classification -eq "Updates") { $hasMatchingType = $true } } "Feature Packs" { if ($classification -eq "Feature Packs") { $hasMatchingType = $true } } "Service Packs" { if ($classification -eq "Service Packs") { $hasMatchingType = $true } } "Tools" { if ($classification -eq "Tools") { $hasMatchingType = $true } } "Update Rollups" { if ($classification -eq "Update Rollups") { $hasMatchingType = $true } } "Security Quality Updates" { if (($classification -eq "Security Updates") -and ($title -like "*Quality Update*")) { $hasMatchingType = $true } } "Driver Updates" { if ($title -like "*Driver*") { $hasMatchingType = $true } } default { if ($title -like "*$type*") { $hasMatchingType = $true } } } if ($hasMatchingType) { break } } if (-not $hasMatchingType) { Write-Debug "Excluded (UpdateType mismatch - looking for: $($UpdateType -join ', ')): $title" $include = $false } } if ($include) { Write-Debug "Included: $title" } $include }) Write-Debug "Rows after base filtering: $($Rows.Count)" #endregion Base Filtering #region Architecture Filtering $skipArchFilter = ($PSCmdlet.ParameterSetName -eq 'OS' -and $Architecture -ne "All" -and $Version) -or ($UseOSBehavior -and $Architecture -ne "All" -and $Version) if ($Architecture -ne "All" -and -not $skipArchFilter) { $preFilterCount = $Rows.Count $Rows = $Rows.Where({ $title = $_.SelectNodes("td")[1].InnerText.Trim() $match = switch ($Architecture) { "x64" { $title -match "x64|64.?bit|64.?based" -and -not ($title -match "x86|32.?bit|arm64") } "x86" { $title -match "x86|32.?bit|32.?based" -and -not ($title -match "64.?bit|arm64") } "arm64" { $title -match "arm64|ARM.?based" } } if (-not $match) { Write-Debug "Excluded (Architecture $Architecture): $title" } $match }) Write-Debug "Rows after architecture filtering ($Architecture): $($Rows.Count) (was: $preFilterCount)" } elseif ($skipArchFilter) { Write-Debug "Skipping architecture filter (already in search query)" } #endregion Architecture Filtering #region Create Update Objects $Updates = $Rows.Where({ $_.Id -ne "headerRow" }).ForEach({ try { [MSCatalogUpdate]::new($_, $IncludeFileNames) } catch { if ($DebugPreference -ne 'SilentlyContinue') { Write-Warning "Failed to process update: $($_.Exception.Message)" } $null } }) | Where-Object { $null -ne $_ } Write-Debug "Updates created: $($Updates.Count)" #endregion Create Update Objects #region Apply Filters if ($FromDate) { $preCount = $Updates.Count $Updates = $Updates.Where({ $_.LastUpdated -ge $FromDate }) Write-Debug "After FromDate filter: $($Updates.Count) (was: $preCount)" } if ($ToDate) { $preCount = $Updates.Count $Updates = $Updates.Where({ $_.LastUpdated -le $ToDate }) Write-Debug "After ToDate filter: $($Updates.Count) (was: $preCount)" } if ($LastDays) { $CutoffDate = (Get-Date).AddDays(-$LastDays) $preCount = $Updates.Count $Updates = $Updates.Where({ $_.LastUpdated -ge $CutoffDate }) Write-Debug "After LastDays filter ($LastDays days, cutoff: $CutoffDate): $($Updates.Count) (was: $preCount)" } if ($MinSize -or $MaxSize) { $Multiplier = if ($SizeUnit -eq "GB") { 1024 } else { 1 } $preCount = $Updates.Count $Updates = $Updates.Where({ $size = [double]($_.Size -replace ' MB$','') $meetsMin = -not $MinSize -or $size -ge ($MinSize * $Multiplier) $meetsMax = -not $MaxSize -or $size -le ($MaxSize * $Multiplier) $meetsMin -and $meetsMax }) Write-Debug "After size filter: $($Updates.Count) (was: $preCount)" } #endregion Apply Filters #region Sorting and Output # Apply sorting $Updates = switch ($SortBy) { "Date" { $Updates | Sort-Object LastUpdated -Descending:$Descending } "Size" { $Updates | Sort-Object { [double]($_.Size -replace ' MB$','') } -Descending:$Descending } "Title" { $Updates | Sort-Object Title -Descending:$Descending } "Classification" { $Updates | Sort-Object Classification -Descending:$Descending } "Product" { $Updates | Sort-Object Products -Descending:$Descending } default { $Updates } } # Handle exports $exportPerformed = $false if ($ExportJson) { try { $jsonContent = if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Json -Depth 10 } else { $Updates | ConvertTo-Json -Depth 10 } if ($Append -and (Test-Path $ExportJson)) { $existingJson = Get-Content $ExportJson -Raw | ConvertFrom-Json $newJson = $jsonContent | ConvertFrom-Json $existingArray = @($existingJson) $newArray = @($newJson) $existingGuids = @{} foreach ($item in $existingArray) { if ($item.Guid) { $existingGuids[$item.Guid] = $true } } $uniqueNewItems = @() $duplicateCount = 0 foreach ($item in $newArray) { if ($item.Guid -and $existingGuids.ContainsKey($item.Guid)) { $duplicateCount++ Write-Debug "Skipping duplicate: $($item.Title) (GUID: $($item.Guid))" } else { $uniqueNewItems += $item if ($item.Guid) { $existingGuids[$item.Guid] = $true } } } $combined = $existingArray + $uniqueNewItems $combined | ConvertTo-Json -Depth 10 | Out-File $ExportJson -Encoding UTF8 $message = "Appended $($uniqueNewItems.Count) updates to JSON file: $ExportJson" if ($duplicateCount -gt 0) { $message += " (skipped $duplicateCount duplicate$(if($duplicateCount -gt 1){'s'}))" } Write-Verbose $message if ($duplicateCount -gt 0) { Write-Host $message -ForegroundColor Yellow } } else { $jsonContent | Out-File $ExportJson -Encoding UTF8 Write-Verbose "Exported $($Updates.Count) updates to JSON file: $ExportJson" } $exportPerformed = $true } catch { Write-Warning "Failed to export to JSON: $($_.Exception.Message)" } } if ($ExportCsv) { try { if ($Append -and (Test-Path $ExportCsv)) { $existingCsv = Import-Csv $ExportCsv $existingGuids = @{} foreach ($item in $existingCsv) { if ($item.Guid) { $existingGuids[$item.Guid] = $true } } $uniqueNewItems = @() $duplicateCount = 0 foreach ($item in $Updates) { if ($item.Guid -and $existingGuids.ContainsKey($item.Guid)) { $duplicateCount++ Write-Debug "Skipping duplicate: $($item.Title) (GUID: $($item.Guid))" } else { $uniqueNewItems += $item } } if ($uniqueNewItems.Count -gt 0) { if ($Properties) { $uniqueNewItems | Select-Object $Properties | Export-Csv -Path $ExportCsv -NoTypeInformation -Encoding UTF8 -Append } else { $uniqueNewItems | Export-Csv -Path $ExportCsv -NoTypeInformation -Encoding UTF8 -Append } } $message = "Appended $($uniqueNewItems.Count) updates to CSV file: $ExportCsv" if ($duplicateCount -gt 0) { $message += " (skipped $duplicateCount duplicate$(if($duplicateCount -gt 1){'s'}))" } Write-Verbose $message if ($duplicateCount -gt 0) { Write-Host $message -ForegroundColor Yellow } } else { if ($Properties) { $Updates | Select-Object $Properties | Export-Csv -Path $ExportCsv -NoTypeInformation -Encoding UTF8 } else { $Updates | Export-Csv -Path $ExportCsv -NoTypeInformation -Encoding UTF8 } Write-Verbose "Exported $($Updates.Count) updates to CSV file: $ExportCsv" } $exportPerformed = $true } catch { Write-Warning "Failed to export to CSV: $($_.Exception.Message)" } } if ($ExportXml) { try { if ($Append -and (Test-Path $ExportXml)) { [xml]$existingXml = Get-Content $ExportXml $existingObjects = $existingXml.Objects.Object $existingGuids = @{} foreach ($obj in $existingObjects) { $guidProp = $obj.Property | Where-Object { $_.Name -eq 'Guid' } if ($guidProp) { $existingGuids[$guidProp.'#text'] = $true } } $uniqueNewItems = @() $duplicateCount = 0 foreach ($item in $Updates) { if ($item.Guid -and $existingGuids.ContainsKey($item.Guid)) { $duplicateCount++ Write-Debug "Skipping duplicate: $($item.Title) (GUID: $($item.Guid))" } else { $uniqueNewItems += $item } } if ($uniqueNewItems.Count -gt 0) { $xmlContent = if ($Properties) { $uniqueNewItems | Select-Object $Properties | ConvertTo-Xml -As String -Depth 10 } else { $uniqueNewItems | ConvertTo-Xml -As String -Depth 10 } Add-Content -Path $ExportXml -Value $xmlContent -Encoding UTF8 } $message = "Appended $($uniqueNewItems.Count) updates to XML file: $ExportXml" if ($duplicateCount -gt 0) { $message += " (skipped $duplicateCount duplicate$(if($duplicateCount -gt 1){'s'}))" } Write-Verbose $message if ($duplicateCount -gt 0) { Write-Host $message -ForegroundColor Yellow } } else { $xmlContent = if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Xml -As String -Depth 10 } else { $Updates | ConvertTo-Xml -As String -Depth 10 } $xmlContent | Out-File $ExportXml -Encoding UTF8 Write-Verbose "Exported $($Updates.Count) updates to XML file: $ExportXml" } $exportPerformed = $true } catch { Write-Warning "Failed to export to XML: $($_.Exception.Message)" } } # Display result summary $IsUpdate = ($MyInvocation.Line -match '^\s*\$update\s*=') $IsPiped = ($PSCmdlet.MyInvocation.PipelineLength -gt 1) if (-not $IsUpdate -and -not $IsPiped) { Write-Host "`nSearch completed for: $searchQuery" Write-Host "Found $($Updates.Count) updates" if ($exportPerformed) { Write-Host "" if ($ExportJson) { Write-Host "Exported to JSON: $ExportJson" -ForegroundColor Green } if ($ExportCsv) { Write-Host "Exported to CSV: $ExportCsv" -ForegroundColor Green } if ($ExportXml) { Write-Host "Exported to XML: $ExportXml" -ForegroundColor Green } } if ($Updates.Count -eq 0) { Write-Host "`nNo updates found matching the criteria." -ForegroundColor Yellow if ($DebugPreference -ne 'SilentlyContinue') { Write-Host "Try:" -ForegroundColor Cyan Write-Host " - Removing or adjusting filters (-UpdateType, -Architecture, -LastDays)" -ForegroundColor Cyan Write-Host " - Using -AllPages to search more results" -ForegroundColor Cyan Write-Host " - Using -Verbose or -Debug for more information" -ForegroundColor Cyan } } } if ($Updates.Count -ge $MaxResults) { Write-Warning "Result limit of $MaxResults reached. Please refine your search criteria." } # Return results to pipeline if (-not $exportPerformed -or $IsUpdate -or $IsPiped) { switch ($Format) { "Default" { if ($Properties) { $Updates | Select-Object $Properties } else { $Updates } } "CSV" { if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Csv -NoTypeInformation } else { $Updates | ConvertTo-Csv -NoTypeInformation } } "JSON" { if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Json } else { $Updates | ConvertTo-Json } } "XML" { if ($Properties) { $Updates | Select-Object $Properties | ConvertTo-Xml -As String } else { $Updates | ConvertTo-Xml -As String } } } } #endregion Sorting and Output } catch { if ($DebugPreference -ne 'SilentlyContinue') { Write-Warning "Error processing search request: $($_.Exception.Message)" Write-Debug "Full error: $($_.Exception | Format-List * -Force | Out-String)" } } } end { $ProgressPreference = "Continue" } } |