Files
windows-builder/includes/utils/get-latest-win-update.ps1
2026-06-02 03:37:09 -07:00

304 lines
10 KiB
PowerShell

#Requires -Version 3.0
<#
.SYNOPSIS
Downloads the latest Windows Update for Windows 10 or 11
.DESCRIPTION
Fetches and downloads the latest cumulative update from Microsoft Update Catalog
for manual injection into Windows images
.PARAMETER WindowsVersion
Windows version: "10" or "11"
.PARAMETER Architecture
System architecture: "x64" or "ARM64"
.PARAMETER Edition
Windows edition (optional): "Home", "Pro", "Enterprise", etc.
.PARAMETER OutputPath
Download destination path
.PARAMETER Build
Specific build number (optional, e.g., "22H2", "23H2", "24H2")
.EXAMPLE
.\Get-LatestWindowsUpdate.ps1 -WindowsVersion 11 -Architecture x64 -OutputPath "C:\Updates"
#>
param(
[Parameter(Mandatory=$true)]
[ValidateSet("10", "11")]
[string]$WindowsVersion,
[Parameter(Mandatory=$true)]
[ValidateSet("x64", "ARM64")]
[string]$Architecture,
[Parameter(Mandatory=$false)]
[string]$Edition,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[Parameter(Mandatory=$false)]
[string]$Build
)
# Create output directory if it doesn't exist
if (-not (Test-Path $OutputPath)) {
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
}
# Microsoft Update Catalog URL
$catalogUrl = "https://www.catalog.update.microsoft.com"
# Build search query based on Windows version
if ($WindowsVersion -eq "11") {
if ($Build) {
$searchQuery = "Windows 11 $Build $Architecture Cumulative Update"
} else {
$searchQuery = "Windows 11 $Architecture Cumulative Update"
}
} else {
if ($Build) {
$searchQuery = "Windows 10 $Build $Architecture Cumulative Update"
} else {
$searchQuery = "Windows 10 $Architecture Cumulative Update"
}
}
Write-Host "Searching for: $searchQuery" -ForegroundColor Cyan
# Function to parse Microsoft Update Catalog
function Search-UpdateCatalog {
param([string]$Query)
$searchUrl = "$catalogUrl/Search.aspx?q=$([System.Uri]::EscapeDataString($Query))"
try {
$response = Invoke-WebRequest -Uri $searchUrl -UseBasicParsing
$html = $response.Content
# Parse the results table
$updates = @()
# The catalog structure: each row has an ID like "GUID_R0", "GUID_R1", etc.
# We need to extract GUIDs from row IDs and then find the corresponding title/product/classification
# Find all table rows with GUID pattern IDs
$rowPattern = '(<tr id="([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})_R\d+"[^>]*>.*?</tr>)'
$rowMatches = [regex]::Matches($html, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
foreach ($rowMatch in $rowMatches) {
$updateId = $rowMatch.Groups[2].Value
$rowHtml = $rowMatch.Groups[1].Value
# Extract title from the link
$title = ""
if ($rowHtml -match 'class="contentTextItemSpacerNoBreakLink">([^<]+)</a>') {
$title = $Matches[1].Trim()
}
# Extract all TD contents by finding text between > and <
# Pattern: extract content from TD cells more carefully
$tdPattern = '<td[^>]*id="[^"]*_C(\d+)_R\d+"[^>]*>(.*?)</td>'
$tdMatches = [regex]::Matches($rowHtml, $tdPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
$products = ""
$classification = ""
# C1 = title, C2 = products, C3 = classification
foreach ($td in $tdMatches) {
$colNum = $td.Groups[1].Value
$content = $td.Groups[2].Value
# Strip HTML tags and get text
$text = $content -replace '<[^>]+>', '' -replace '\s+', ' '
$text = $text.Trim()
if ($colNum -eq "2") {
$products = $text
}
elseif ($colNum -eq "3") {
$classification = $text
}
}
if ($title) {
$updates += [PSCustomObject]@{
UpdateId = $updateId
Title = $title
Products = $products
Classification = $classification
}
}
}
return $updates
}
catch {
Write-Error "Failed to search catalog: $_"
return @()
}
}
# Function to get download link for an update
function Get-UpdateDownloadLink {
param([string]$UpdateId)
$downloadUrl = "$catalogUrl/DownloadDialog.aspx"
try {
# Format the POST body as the catalog expects
$body = @{
updateIDs = "[{`"size`":0,`"languages`":`"`",`"uidInfo`":`"$UpdateId`",`"updateID`":`"$UpdateId`"}]"
}
$response = Invoke-WebRequest -Uri $downloadUrl -Method Post -Body $body -UseBasicParsing -ContentType "application/x-www-form-urlencoded"
$html = $response.Content
# Try multiple patterns to extract the download URL
# Pattern 1: JavaScript variable assignment
if ($html -match "downloadInformation\[\d+\]\.files\[\d+\]\.url\s*=\s*'([^']+)'") {
return $Matches[1]
}
# Pattern 2: Direct URL in quotes
if ($html -match "'(https?://[^']+\.msu)'") {
return $Matches[1]
}
# Pattern 3: URL without quotes
if ($html -match '(https?://[^\s"<>]+\.msu)') {
return $Matches[0]
}
# If we still haven't found it, try to extract any .msu URL
if ($html -match '(https?://[^\s"<>]+catalog[^\s"<>]+\.msu)') {
return $Matches[0]
}
Write-Host "Debug: Could not find download URL in response" -ForegroundColor Red
return $null
}
catch {
Write-Error "Failed to get download link: $_"
return $null
}
}
# Search for updates
Write-Host "Searching Microsoft Update Catalog..." -ForegroundColor Yellow
$updates = Search-UpdateCatalog -Query $searchQuery
if ($updates.Count -eq 0) {
Write-Error "No updates found matching criteria"
exit 1
}
Write-Host "Found $($updates.Count) total updates" -ForegroundColor Cyan
Write-Host "`nAll updates found:" -ForegroundColor Yellow
foreach ($update in $updates) {
Write-Host " - $($update.Title)" -ForegroundColor Gray
Write-Host " Classification: $($update.Classification)" -ForegroundColor DarkGray
}
# Filter for cumulative updates only and exclude preview builds
$cumulativeUpdates = $updates | Where-Object {
$_.Title -match "Cumulative Update" -and
$_.Title -notmatch "Preview" -and
$_.Title -notmatch "Dynamic" -and
$_.Title -notmatch "\.NET Framework" -and
$_.Title -notmatch "Internet Explorer" -and
$_.Title -notmatch "Server 2012" -and
$_.Title -notmatch "Server 2016" -and
$_.Title -notmatch "Server 2019" -and
$_.Title -notmatch "Server 2022" -and
($_.Title -match "Windows $WindowsVersion" -or $_.Title -match "Windows $WindowsVersion,") -and
($_.Classification -eq "Updates" -or $_.Classification -eq "Security Updates")
}
Write-Host "`nFiltered to $($cumulativeUpdates.Count) cumulative updates" -ForegroundColor Cyan
if ($Edition) {
$cumulativeUpdates = $cumulativeUpdates | Where-Object {
$_.Products -match $Edition
}
}
# Sort by KB number (higher KB = newer) and get the latest
$latestUpdate = $cumulativeUpdates | Sort-Object {
if ($_.Title -match 'KB(\d+)') {
[int]$Matches[1]
} else {
0
}
} -Descending | Select-Object -First 1
if (-not $latestUpdate) {
Write-Error "No suitable cumulative update found"
exit 1
}
Write-Host "`nLatest Update Found:" -ForegroundColor Green
Write-Host " Title: $($latestUpdate.Title)" -ForegroundColor White
Write-Host " Products: $($latestUpdate.Products)" -ForegroundColor White
Write-Host " Update ID: $($latestUpdate.UpdateId)" -ForegroundColor White
# Extract KB number for filename
$kbNumber = ""
if ($latestUpdate.Title -match '(KB\d+)') {
$kbNumber = $Matches[1]
}
# Get download link
Write-Host "`nRetrieving download link..." -ForegroundColor Yellow
$downloadLink = Get-UpdateDownloadLink -UpdateId $latestUpdate.UpdateId
if (-not $downloadLink) {
Write-Error "Failed to retrieve download link"
exit 1
}
Write-Host "Download URL: $downloadLink" -ForegroundColor Cyan
# Generate filename
$fileName = "Windows$($WindowsVersion)_${Architecture}_${kbNumber}_CumulativeUpdate.msu"
$outputFile = Join-Path $OutputPath $fileName
# Download the update
Write-Host "`nDownloading update to: $outputFile" -ForegroundColor Yellow
try {
$ProgressPreference = 'SilentlyContinue'
# Use BITS transfer for large files with resume capability
Import-Module BitsTransfer -ErrorAction SilentlyContinue
if (Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue) {
Write-Host "Using BITS transfer..." -ForegroundColor Cyan
Start-BitsTransfer -Source $downloadLink -Destination $outputFile -DisplayName "Windows Update Download" -Description $latestUpdate.Title
}
else {
Write-Host "Using web request..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $downloadLink -OutFile $outputFile -UseBasicParsing
}
Write-Host "`nDownload completed successfully!" -ForegroundColor Green
Write-Host "File location: $outputFile" -ForegroundColor White
# Display file info
$fileInfo = Get-Item $outputFile
Write-Host "File size: $([math]::Round($fileInfo.Length / 1MB, 2)) MB" -ForegroundColor White
Write-Host "SHA256: $((Get-FileHash -Path $outputFile -Algorithm SHA256).Hash)" -ForegroundColor White
# Display injection instructions
Write-Host "`n=== Injection Instructions ===" -ForegroundColor Cyan
Write-Host "To inject this update into a WIM file, use DISM:" -ForegroundColor Yellow
Write-Host " 1. Mount the WIM: " -NoNewline -ForegroundColor White
Write-Host "dism /Mount-Wim /WimFile:install.wim /Index:1 /MountDir:C:\Mount" -ForegroundColor Gray
Write-Host " 2. Add update: " -NoNewline -ForegroundColor White
Write-Host "dism /Image:C:\Mount /Add-Package /PackagePath:`"$outputFile`"" -ForegroundColor Gray
Write-Host " 3. Commit changes: " -NoNewline -ForegroundColor White
Write-Host "dism /Unmount-Wim /MountDir:C:\Mount /Commit" -ForegroundColor Gray
}
catch {
Write-Error "Download failed: $_"
exit 1
}