Skip to content

Harden Get-Win11CumulativeUpdates.ps1 (locale, signatures, timeouts)#3

Merged
jm2 merged 1 commit into
mainfrom
unify-catalog-driver-scrapers
Jun 15, 2026
Merged

Harden Get-Win11CumulativeUpdates.ps1 (locale, signatures, timeouts)#3
jm2 merged 1 commit into
mainfrom
unify-catalog-driver-scrapers

Conversation

@jm2

@jm2 jm2 commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Holistic review + surgical hardening of Get-Win11CumulativeUpdates.ps1 — the one catalog scraper deliberately kept out of the shared engine (it has CU-specific orchestration: warn-open signatures, multi-.msu checkpoint chain, KB-based target id, SHA256 cache).

The core machinery was verified correct against the live catalog: the 25H2 CU query selects the true newest CU, and its DownloadDialog returns the full checkpoint chain (target + KB5043080). These are hardening fixes, not a rewrite.

Fixes

  • Locale date bug (medium)[datetime]::Parse is CurrentCulture-bound; on a non-US host a CU dated after the 12th throws inside the -Parallel try/catch and is silently dropped (possibly the newest one). → ParseExact + InvariantCulture. Proven under de-DE: the old code throws on 6/15/2026 (dropped) and misreads 6/9/2026 as Sep 6.
  • Authenticode trust gap (medium) — the SHA256-sidecar cache-hit path reused a file without re-running Get-AuthenticodeSignature (the sidecar is self-generated → byte-stability, not authenticity). Now re-verified on cache reuse.
  • Timeouts-TimeoutSec on every request (60s metadata, 3600s for the large .msu) + a browser UA.
  • KB target match anchored so a shorter KB can't prefix-match a longer one.
  • Cleanup — removed dead $downloadedCount; corrected the inaccurate "checkpoint last" comment (position varies: first for 25H2, last for 24H2).

Intentionally not changed

  • The SSU section is a no-op for 24H2/25H2 (SSUs are folded into the checkpoint chain) but degrades correctly — left as-is.
  • The 25-row cap is not a real risk here: the catalog sorts CU queries date-descending, so it only truncates the oldest entries.
  • Transport-only adoption of the shared CatalogScrape.psm1 (timeouts/ConvertTo-CsDate/parse helpers) is a reasonable future cleanup but kept out of this surgical PR.

Validation

  • pwsh syntax-clean; engine unit tests still 68/68; the locale fix demonstrated live under a non-US culture.

🤖 Generated with Claude Code

Surgical fixes from a holistic review of the CU scraper (the one catalog
script intentionally not folded into the new shared engine, since it has
CU-specific orchestration: warn-open signatures, multi-.msu checkpoint
chain, KB-based target id, SHA256 cache).

- Locale date bug that could silently drop the newest CU: [datetime]::Parse
  is CurrentCulture-bound, so on a non-US host a CU dated after the 12th
  throws inside the -Parallel try/catch (which only warns) and is dropped
  from results. Use ParseExact + InvariantCulture (proven: under de-DE the
  old code drops 6/15/2026 and misreads 6/9/2026 as Sep 6).
- Authenticode trust gap: the SHA256-sidecar cache-hit reused a file without
  re-running Get-AuthenticodeSignature (the sidecar is self-generated, so it
  proves byte-stability, not Microsoft authenticity). Re-check on cache reuse.
- Add per-request -TimeoutSec (60s metadata, 3600s for the large .msu) + UA
  so a stalled connection can't hang the run.
- Anchor the KB target-id match so a shorter KB can't prefix-match a longer one.
- Remove dead $downloadedCount; correct the now-inaccurate "checkpoint last"
  comment (checkpoint position varies: first for 25H2, last for 24H2).

Checkpoint-chain logic and CU-query selection verified correct live (target +
KB5043080 checkpoint returned by DownloadDialog; newest CU surfaced in the
top-25 by the catalog's date-descending default sort).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jm2 jm2 merged commit 6be4115 into main Jun 15, 2026
6 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the robustness and reliability of the Get-Win11CumulativeUpdates.ps1 script by introducing bounded timeouts, a custom user agent, locale-independent date parsing, refined KB matching, and Authenticode signature verification for cached files. The review feedback suggests guarding the Windows-only Get-AuthenticodeSignature cmdlet with $IsWindows to ensure cross-platform compatibility, suppressing the Invoke-WebRequest progress bar to improve download speeds, and expanding the date parsing formats to align with the shared engine.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +201 to +204
$cachedSig = Get-AuthenticodeSignature -FilePath $OutFile
if ($cachedSig.Status -ne 'Valid') {
Write-Warning " [!] Authenticode signature for cached $FileName is '$($cachedSig.Status)' (not Valid) — treat this package as UNVERIFIED before servicing."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Get-AuthenticodeSignature is a Windows-only cmdlet and is not available in PowerShell Core on Linux or macOS. Since this script is used in a LAN iPXE environment (as indicated by the user agent lan-ipxe-catalog-scrape), it is highly likely to be run on Linux-based pre-staging or PXE servers.

To prevent the script from throwing a CommandNotFoundException and failing on non-Windows platforms, guard the Get-AuthenticodeSignature calls with the $IsWindows automatic variable.

Note: You should also apply this guard to the fresh-download path's signature check (around line 235), which is outside this diff hunk.

                    if ($IsWindows) {
                        $cachedSig = Get-AuthenticodeSignature -FilePath $OutFile
                        if ($cachedSig.Status -ne 'Valid') {
                            Write-Warning "   [!] Authenticode signature for cached $FileName is '$($cachedSig.Status)' (not Valid) — treat this package as UNVERIFIED before servicing." 
                        }
                    }

Comment on lines +45 to +48
# Bounded timeout + browser UA so a stalled catalog connection can't hang the run
# (callers can override TimeoutSec, e.g. the large .msu download).
if (-not $Params.ContainsKey('TimeoutSec')) { $Params['TimeoutSec'] = 60 }
if (-not $Params.ContainsKey('UserAgent')) { $Params['UserAgent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) lan-ipxe-catalog-scrape' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PowerShell's Invoke-WebRequest renders a progress bar by default, which can severely degrade download performance (often by 10x or more) and cause high CPU usage, especially when downloading large files like the cumulative update .msu packages (which can be 1.5 GB or more).

Setting $ProgressPreference = 'SilentlyContinue' locally within Invoke-CatalogRequest will suppress the progress bar and significantly speed up the downloads without polluting the caller's scope.

    # Bounded timeout + browser UA so a stalled catalog connection can't hang the run
    # (callers can override TimeoutSec, e.g. the large .msu download).
    $ProgressPreference = 'SilentlyContinue'
    if (-not $Params.ContainsKey('TimeoutSec')) { $Params['TimeoutSec'] = 60 }
    if (-not $Params.ContainsKey('UserAgent')) { $Params['UserAgent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) lan-ipxe-catalog-scrape' }

# Locale-independent parse: the catalog serves US M/d/yyyy. Plain [datetime]::Parse
# is CurrentCulture-bound, so on a non-US host a day>12 throws (caught below -> the
# CU is silently dropped, possibly the newest) and day<=12 silently swaps day/month.
$DateObj = [datetime]::ParseExact($DateString, [string[]]@('M/d/yyyy', 'MM/dd/yyyy'), [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure robustness and consistency with the shared engine (CatalogScrape.psm1), consider including the same set of date formats when parsing. The shared engine uses:
@('M/d/yyyy', 'MM/dd/yyyy', 'yyyy-MM-dd', 'M/d/yyyy h:mm:ss tt')

This prevents potential parsing failures if the catalog format ever changes or includes a time component.

                $DateObj = [datetime]::ParseExact($DateString, [string[]]@('M/d/yyyy', 'MM/dd/yyyy', 'yyyy-MM-dd', 'M/d/yyyy h:mm:ss tt'), [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None)

@jm2 jm2 deleted the unify-catalog-driver-scrapers branch June 18, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant