
かつて山市良と呼ばれたおじさんのブログ
セイテクエンジニアのブログ かつて山市良と呼ばれたおじさんのブログ メモ. Microsoft Updateカタログからのダウンロードを自動化したい!
2025年09月02日配信
2025年09月02日更新
執筆者:山内 和朗
Windowsの更新プログラムの大部分は、Microsoft Updateカタログ(https://www.catalog.update.microsoft.com/)から誰でもダウンロードすることができます。KB番号で識別される更新プログラムのMicrosoft Updateカタログからのダウンロードを、スクリプトで自動化できれば、更新プログラムの展開の自動化に役立つと考えました。そこで、その実現可能性を検証するべく、サンプルスクリプトを作成してみました。なお、ここで紹介するPowerShellスクリプトは、この記事の執筆時点のMicrosoft Updateカタログサイトの仕様に基づいています。サイトに変更があると、スクリプトは機能しなくなる可能性があるため、その点に留意してください。
連載「どうする!?残る閉域網の更新管理」では、共有フォルダーを介して、オフラインスキャン用のカタログファイル(wsusscn2.cab")を、クローズドネットワーク(閉域網)にあるWindows Serverマシンに提供し、オフラインスキャンを実施して、必要な更新プログラムをCSV形式でリスト化しました。そして、管理者がCSVファイルを確認し、Microsoft Updateカタログから必要な更新プログラム(.MSU)をダウンロードして共有フォルダーに配置することで、閉域網での更新プログラムのインストールを実現しました。この人手(管理者)を介したダウンロード作業まで自動化できれば、更新作業の負担は各段に削減できるでしょう。
どうする!?残る閉域網の更新管理(2025年7月31日~全6回、連載目次)
Windows UpdateやMicrosoft Updateカタログサイトで提供される更新プログラムには、「UpdateID」という固有の属性値があります。Microsoft Updateカタログサイトでの検索結果のページ(Search.aspx)には表示されませんが、ページのソースを確認すると、UpdateIDを知ることができます。また、「ダウンロード」ボタンをクリックして表示されるダウンロードダイアログ(DownloadDialog.aspx)ではUpdateIDの確認とクリップボードへのコピーが可能になっています(画面1)。
画面1 Microsoft Updateカタログで更新プログラムを検索すると、検索ページ(Search.aspx)のページソースやダウンロードダイアログ(DownloadDialog.aspx)でUpdateIDを確認できる
次のPowerShellスクリプト「get-updateids.ps1」は、Webスクレイピングの方法で、Microsoft Updateカタログサイトを検索し、検索結果から更新プログラムのUpdateIDと更新プログラムの名称を一覧表示するものです。なお、他のスクリプトも同様ですが、正規表現による値の取得についてはあまり得意ではないので、生成AIにお任せしました。-KBパラメーターにはKB番号を指定することを想定していますが、任意のキーワードを使用できます(画面2)。また、結果はオブジェクトで返されるため、更新プログラム名の一部(Microsoft server operating system version 24H2など)やアーキテクチャ(更新プログラム名に含まれるx64やarm64)に基づいて、結果をさらに絞り込むことができます。
画面2 WebスクレイピングでMicrosoft Updateカタログサイトを検索し、更新プログラムのUpdateIDを取得する
[get-updateids.ps1](プレーンテキスト《Get-Help対応版》で表示)
Param($KB)
if ($PSBoundParameters.Count -ne 1) { Write-host "Error: -KB KB999999" ;exit 1}
$searchstr = "https://www.catalog.update.microsoft.com/Search.aspx?q=$($KB)"
$ret = Invoke-WebRequest -Uri $searchstr
if ($ret.StatusCode -eq "200"){
$pattern = @'
(?is)<a[^>]*onclick\s*=\s*(["']?)goToDetails\(\s*"([0-9A-Fa-f-]{36})"\s*\)\s*;?\s*\1[^>]*>\s*(.*?)\s*</a>
'@
$items =
[regex]::Matches($ret.Content, $pattern) |
ForEach-Object {
$guid = $_.Groups[2].Value
$raw = $_.Groups[3].Value
$text = ($raw -replace '<[^>]+>','')
$text = [System.Net.WebUtility]::HtmlDecode($text)
$title = ($text -replace '\s+',' ').Trim()
[pscustomobject]@{ UpdateId = $guid; Title = $title }
} | Sort-Object UpdateId -Unique
return $items
} else {
Write-Error -Message "HTTP Status Code: $($ret.StatusCode)"
return $null
}
更新プログラムのUpdateIDは、Windows Update Agent(WUA) APIによるオンラインスクキャンやオフラインスキャンの結果から取得することもできます。
Windows Update エージェント API の使用|Windows アプリ開発(Microsoft Learn)
次のPowerShellスクリプト「get-updateidbyscan.ps1」は、オンラインまたはオフラインスキャン(wsusscn2.cabのパスが有効な場合)を実行して、対象のデバイスで利用可能な(不足している)更新プログラムを検索し、そのタイトルとUpdateIDを表示します(画面3)。ただし、WUA APIが意図しないUpdateIDを返すことがありました。さらなる検証が必要であるため、この方法で取得したUpdateIDは参考程度に扱ってください。
なお、このスクリプトは管理者として実行する必要がありました。WUA APIを使用したオフラインスキャンには、通常のオンラインスキャンとは異なり、管理者権限が必要なようです。また、ユーザープロファイルのダウンロードフォルダーが暗号化されている場合、管理者に昇格して実行するとwsusscn2.cabの読み取りができなくなるため、$scancabPathの値を別のパスに変更してください。
画面3 WUA APIを使用して、利用可能な(不足している)更新プログラムのUpdateIDを取得する
※セキュリティインテリジェンスの更新プログラムの検出はオンラインスキャンのみ。Microsoft CDN(Windows Update)で提供されるセキュリティインテリジェンスの更新プログラムのすべてがMicrosoft Updateカタログでダウンロード提供されるわけではないことにも注意(UpdateIDの定でMicrosoft Updateカタログサイトからダウンロードできないこともある)。
[get-updateidsbyscan.ps1](プレーンテキスト《Get-Help対応版》で表示)
#https://catalog.s.download.windowsupdate.com/microsoftupdate/v6/wsusscan/wsusscn2.cab
$scancabPath = "$env:userprofile\Downloads\wsusscn2.cab"
$UpdateSession = New-Object -ComObject Microsoft.Update.Session
if (Test-Path $scancabPath) {
$UpdateServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
$UpdateService = $UpdateServiceManager.AddScanPackageService("Offline Sync Service", $scancabPath)
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$UpdateSearcher.ServerSelection = 3 # ssOthers
$UpdateSearcher.ServiceID = [string] $UpdateService.ServiceID
} else {
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
}
$searchResult = $updateSearcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")
if ($searchResult.Updates.Count -eq 0) {
Write-Host "There are no applicable updates."
} else {
$i = 0
foreach ($update in $searchResult.Updates){
$i++
Write-Host $i">" $update.Title
Write-Host " Suport URL:" $update.SupportUrl
Write-Host " Update ID :" $update.Identity.UpdateID
Write-Host ""
}
}
更新プログラムのダウンロードリンクは、Microsoft Updateカタログサイトのダウンロードダイアログ(DownloadDialog.aspx)で確認することができます。次のPowerShellスクリプト「get-downloadlinks.ps1」は、-UpdateIDパラメーターに指定したUpdateIDに紐づいた更新プログラムのダウンロードリンク(URL)とファイル名(FILENAME)を取得します(画面4)。
画面4 更新プログラムのUpdateIDを指定して、ダウンロードリンクとファイル名を取得する
[get-downloadlinks.ps1](プレーンテキスト《Get-Help対応版》で表示)
Param($UpdateId)
if ($PSBoundParameters.Count -ne 1) { Write-host "Error: -UpdateId <GUID>" ;exit 1}
$PostJson = @{size = 0; UpdateID = $UpdateId; UpdateIDInfo = $UpdateId} | ConvertTo-Json -Compress
$Body = @{UpdateIDs = "[$PostJson]"}
$ret = Invoke-WebRequest `
-Uri 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' `
-Method Post `
-ContentType 'application/x-www-form-urlencoded' `
-Body $body
$pattern = "downloadInformation\[\d+\]\.files\[\d+\]\.url\s*=\s*'([^']+)'"
$items = [regex]::Matches($ret.Content, $pattern) |
ForEach-Object {
$url = $_.Groups[1].Value
[pscustomobject]@{
URL = $url
FileName = Split-Path $url -Leaf
}
} | Sort-Object URL -Unique
$items
※このスクリプトは、PowerShell Galleryで公開されている「MSCatalogLTS」(オーナーMarco-online氏)のアイデアを参考に作成しました。 MSCatalogLTS|PowerShell Gallery
更新プログラムのダウンロードリンクを取得できれば、あとはcurl.exeコマンドまたはInvoke-WebRequestコマンドレットを使用して、ダウンロードを実施することができます。
次のPowerShellスクリプト「download-updates.ps1」は、必要な更新プログラムのタイトル(Title列)、KB番号(KBId列)、およびUpdateID(UpateID列)がUpdateID列に書き込まれたCSVファイルを読み込んでcurl.exe($env:WINDIR¥System32¥curl.exeが存在する場合)またはInvoke-WebRequestコマンドレットを使用して、更新プログラムをダウンロードします(画面5)。
.\download-updates.ps1 -CsvPath <CSVファイルのパス> -PackageDir <ダウンロード先ディレクトリ> -LogDir <ログの出力先ディレクトリ> |
[download-updates.ps1](プレーンテキストで表示)
※このスクリプトと同じディレクトリに get-updateids.ps1 と get-downloadlinks.ps1 が存在する必要があります。
Param($CsvPath, $PackageDir, $LogDir)
if ($PSBoundParameters.Count -ne 3) { Write-host "Error: -CsvPath <path> -PackageDir <path> -LogDir <Path>" ;exit 1}
If ( -not (Test-Path $CsvPath)) { Write-host "Error: -CsvPath <path> does not exist." ;exit 1}
If ( -not (Test-Path $packagedir)) { Write-host "Error: -PackageDir <path> does not exist." ;exit 1}
If ( -not (Test-Path $LogDir)) { Write-host "Error: -LogDir <path> does not exist." ;exit 1}
# Keywords that are not downloaded based on part of the file name. 'KB9999999' is dummy (you can delete it).
# 'KB5043080' is a checkpoint cumulative update for Windows 11 24H2 and Windows Server 2025 or later, and does not require installation.
# Detail: https://learn.microsoft.com/ja-jp/windows/deployment/update/catalog-checkpoint-cumulative-updates
$ExcludeLists = 'KB5043080','KB9999999'
function Write-Log {
param (
[string]$Message,
[string]$LogPath = "$env:TEMP\onlinepatch.log"
)
#Output to Console
Write-Host $Message
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$entry = "[$timestamp] $Message"
Add-Content -Path $LogPath -Value $entry
}
# for .\get-updateids.ps1
# for .\get-downloadLinks {
Set-Location -Path $PSScriptRoot
function Start-Download {
param (
[string]$downloaduri,
[string]$PackageDir
)
$filename = Split-Path $downloaduri -Leaf
if (Test-Path (Join-Path $PackageDir $filename)) {
Write-Log -Message "File $($filename) has already beed downloaded. Skip download." -LogPath $LogPath
} else {
Write-Log -Message "Downloading $($filename) ..." -LogPath $LogPath
if (Test-Path "$env:WINDIR\System32\curl.exe") {
& curl.exe -L --fail --retry 3 --retry-delay 2 -o (Join-Path $PackageDir $filename) $downloaduri
} else {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $downloaduri -OutFile (Join-Path $PackageDir $filename)
$ProgressPreference = 'Continue'
}
if (Test-Path (Join-Path $PackageDir $filename)) {
Write-Log -Message "Download completed." -LogPath $LogPath
} else {
Write-Log -Message "Download failed." -LogPath $LogPath
}
}
}
$LogPath = "$LogDir\downloadupdates.log"
if (Test-Path $logPath) {
$logTime = (get-item $logPath).LastWriteTime
} else {
$logTime = get-Date("1900/1/1")
}
Write-Log -Message "Start download-updates.ps1" -LogPath $LogPath
Write-Log -Message "Import CSV from $($CsvPath)" -LogPath $LogPath
$csvrows = Import-Csv -Path $CsvPath
Write-Log -Message "Found $($csvrows.Count) udpates." -LogPath $LogPath
foreach ($csvrow in $csvrows) {
$KBID = $csvrow.KBId
$TITLE = $csvrow.TITLE
$HINTID = $csvrow.UpdateId
$HINT = "NOTMATCHTITLE"
if ($TITLE -match "Server") {
$HINT = "Server"
} elseif ($TITLE -match "x64-based") {
$HINT = "x64-based"
} elseif ($TITLE -match "arm64-based") {
$HINT = "arm64-based"
}
$UpdateIds = .\Get-UpdateIds.ps1 -KB $KBID
if ($UpdateIds.Count -eq 0) {
Write-Log -Message "UpdateID corresponding to $($KBID) not found." -LogPath $LogPath
} else {
foreach ($UpdateId in $UpdateIds) {
$DownloadID = ""
if($UpdateId.UpdateId -eq $HINTID) {
$DownloadId = $UpdateId.UpdateId
} elseif($UpdateId.TITLE -match $HINT) {
$DownloadId = $UpdateId.UpdateId
}
if ($DownloadId -ne "") {
$downloadlinks = .\Get-DownloadLinks.ps1 -UpdateId $DownloadId
if ($downloadlinks.Count -eq 0) {
Write-Log -Message "DownloadLinks corresponding to $($KBID) : $($UpdateId.UpdateId) not found." -LogPath $LogPath
} else {
foreach ($downloadlink in $downloadLinks) {
if ($downloadlink.FileName | Select-String -Pattern $ExcludeLists -SimpleMatch -Quiet) {
Write-Log -Message "Skip UpdateID: $($UpdateId.UpdateId) FileName $($downloadlink.FileName -replace '^(?s)(.{0,25}).*','$1') ... based on the ExcludeLists." -LogPath $LogPath
} else {
Start-Download -downloaduri $downloadlink.URL -PackageDir $PackageDir
}
}
}
} else {
Write-Log -Message "Skip UpdateID: $($UpdateId.UpdateId) based on the hint (ID or Platform)." -LogPath $LogPath
}
}
}
}
画面5 CSVから更新プログラムのUpdateIDを読み込んで、ダウンロードするスクリプト
CSVファイルの自動生成につついては、「vol.127 オフラインスキャンの自動化に挑む」で紹介した「wuscanoffline.ps1」に修正を加え、UpdateID列を追加で出力するようにしました(赤字部分)。
[wuscanoffline.ps1(修正版)](プレーンテキストで表示、共有フォルダーのパス¥¥192.168.0.1¥WUPackages¥・・・ については環境に合わせて変更してください。)
$logPath = "\\192.168.0.1\WUPackages\logs\wuscan_$(hostname).csv"
$scancabPath = "\\192.168.0.1\WUPackages\wsusscn2.cab"
if (Test-Path $scancabPath) {
Write-Host "Scanning start using $scancabPath ..."
Copy-Item $scancabPath "$env:Temp\mywsusscn2.cab"
$scancabPath = "$env:Temp\mywsusscn2.cab"
#pause
} else {
Write-Host $scancabPath "not found. Nothing to do."
#pause
exit 1
}
$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$UpdateServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
$UpdateService = $UpdateServiceManager.AddScanPackageService("Offline Sync Service", $scancabPath)
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
Write-Host "Searching for updates..."
$UpdateSearcher.ServerSelection = 3 # ssOthers
$UpdateSearcher.ServiceID = [string] $UpdateService.ServiceID
$SearchResult = $UpdateSearcher.Search("IsInstalled=0")
$Updates = $SearchResult.Updates
Remove-Item $scancabPath
If ($SearchResult.Updates.Count -eq 0) {
Write-Host "There are no applicable updates."
#pause
exit 0
}
Write-Host "List of applicable items on the machine when using wssuscan.cab:"
$results = @()
For ($i = 0; $i -lt $SearchResult.Updates.Count; $i++) {
$update = $SearchResult.Updates.Item($i)
Write-Host ($i + 1) "> " $update.Title
$kbID = ""
if ($update.Title -match 'KB\d{6,7}') {
$kbID = $matches[0]
}
$results += [PSCustomObject]@{
Title = $update.Title
KBId = $kbID
UpdateId = $update.Identity.UpdateID
Support = "https://support.microsoft.com/kb/" + $kbID.Substring(2)
Catalog = "https://catalog.update.microsoft.com/Search.aspx?q=" + $kbID
}
}
$results | Export-Csv -Path $logPath -NoTypeInformation -Encoding UTF8
#pause
exit 0
CSVファイルのUpdateID列にはWUA APIで取得したUpdateIDが含まれますが、「download-updates.ps1」はこの値を参考値として扱い、KB番号に基づいて別途取得したUpdateIDと突き合わせ、両者のIDが同じ場合はそれを採用、異なる場合は更新プログラムのタイトル(Title列)に含まれるキーワード(Windows Server向けは"Server"を含む、Windowsクライアント向けは"x64-based"または"arm64-based"を含む)によってダウンロードするかしないかを判断します。また、Microsoft Updateカタログで公開されているWindows 11バージョン24H2およびWindows Server 2025以降の品質更新プログラムのダウンロードリンク一覧に含まれるチェックポイント累積更新プログラム(Checkpoint cumulative updates、現時点ではKB5043080のみ)については、インストール不要であるためダウンロードをスキップします。チェックポイント累積更新プログラムは除外リストExcludeLists変数で定義しています。このリストに、キーワード(ファイル名の一部)を追加することで、さらに特定の更新プログラムを除外することができます。
チェックポイント累積更新プログラムと Microsoft Update Catalog の使用状況|Windows(Microsoft Learn)
「download-updates.ps1」と修正版「wuscanoffline.ps1」は、2025年7月および8月のWindows Server 2022およびWindows Server 2025の更新プログラムでテストし、期待通りに動くことを確認しました。Windows ServerおよびWindows 10/11(x64またはarm64)向けの毎月の品質更新プログラム(MSU)と悪意のあるソフトウェアの削除ツール(EXE)のダウンロードを想定していますが、十分にテストしたわけではありません。テストした環境で期待通りに動くように調整した結果をスクリプトに反映したまでですそのため、システムの環境やリリース月によっは、すべての更新プログラムをカバーできるなかったり、不要な更新プログラムを無駄にダウンロードしたりする可能性があります。例えば、1つのUpdateIDに多数の更新パッケージ《EXE形式》が紐づく.NET Updateの更新プログラム(例、KB5064838)のダウンロードにはうまく対応できないことが分かっています。*1 また、最初に指摘しましたが、今回紹介したPowerShellスクリプトは、この記事の執筆時点のMicrosoft Updateカタログサイトの仕様に基づいています。サイトに変更があると、スクリプトは機能しなくなる可能性があります。また、スクリプトの動作は十分にテストされていません。コンセプト実証(PoC)のためのサンプルと考えてください。*2
*1 「どうする!?残る閉域網の更新管理」連載シリーズの一連のスクリプトは、wsusscn2.cabを使用したオフラインスキャンで検索可能なWindows向け更新プログラムの検索とダウンロード、インストールを想定しています。
*2 閉域網にあるサーバーの更新管理を想定したPoCですが、インターネット接続可能なデバイスの更新プログラムの確認、ダウンロード、インストールにも応用できると思います。