I often find myself needing to generate reports on Windows updates deployed across our server infrastructure. One particularly challenging aspect has been obtaining the proper "friendly names" for Windows updates based on their KB numbers.
Microsoft's Get-Hotfix cmdlet provides basic information about installed updates, but it lacks the descriptive friendly names that make reporting meaningful to management and provide context about what each update actually contains.
In this post, I'll share how I created a PowerShell script that dynamically queries the Microsoft Update Catalog to retrieve detailed information about Windows updates directly from Microsoft's servers - without relying on hardcoded data that quickly becomes outdated.
Understanding the Problem
When examining Windows updates installed on a server using Get-Hotfix, I get output like:
Source Description HotFixID InstalledBy InstalledOn
------ ----------- -------- ----------- -----------
WrkDevice1 Update KB5054006 NT AUTHORITY\SYSTEM 3/15/2025 12:00:00 AM
WrkDevice1 Security Update KB5052006 NT AUTHORITY\SYSTEM 3/11/2025 12:00:00 AM
WrkDevice1 Update KB5049614 NT AUTHORITY\SYSTEM 2/4/2025 12:00:00 AM
While this tells me the KB numbers and update types, it lacks the detailed titles that Microsoft provides in their Update Catalog, such as "Servicing Stack Update for Windows Server 2016: March 11, 2025 (KB5054006)" - information that's critical for proper documentation and understanding what these updates actually contain.
The Solution: Dynamic Windows KB Information Retrieval
After exploring various approaches, I created a PowerShell script that:
- Accepts a server name as a parameter
- Retrieves installed updates using Get-Hotfix
- Dynamically queries the Microsoft Update Catalog for each KB
- Determines if updates have been superseded
- Compiles all this information into a comprehensive CSV report
The script uses a combination of the MSCatalog PowerShell module (when available) and direct web requests as a fallback method.
Key Components of the Solution
1. Script Parameters and Usage
I designed the script to be flexible with parameters for targeting specific servers:
param (
[Parameter(Mandatory=$true, Position=0)]
[string]$ServerName,
[Parameter(Mandatory=$false)]
[int]$MaxUpdates = 15,
[Parameter(Mandatory=$false)]
[string]$ProxyServer = "proxy.bear.local:3129",
[Parameter(Mandatory=$false)]
[string]$OutputPath = ".\WindowsUpdateDetails.csv"
)
This allows me to run the script with a simple command:
.\Get-WindowsUpdateDetails.ps1 -ServerName "<server>"
Or with more specific options:
.\Get-WindowsUpdateDetails.ps1 -ServerName "<server>" -MaxUpdates 20
-OutputPath "C:\Reports\SERVER01_Updates.csv"
2. Retrieving Installed Updates
Instead of parsing log files, the script now directly queries the target server using Get-Hotfix:
function Get-ServerHotfixes {
param([string]$ServerName, [int]$MaxUpdates)
try {
Write-Host ("Retrieving updates from server: " + $ServerName)
-ForegroundColor Green
# Get hotfixes from server
$hotfixes = Get-HotFix -ComputerName $ServerName |
Where-Object { $_.HotFixID -match "KB\d+" } |
Sort-Object -Property InstalledOn -Descending |
Select-Object -First $MaxUpdates
return $hotfixes
}
catch {
Write-Host ("Error retrieving updates from " + $ServerName + ": " +
$_.Exception.Message) -ForegroundColor Red
return $null
}
}
This function returns the most recent updates installed on the server, sorted by installation date.
3. Setting Up Proxy Configuration
In most corporate environments, direct internet connectivity isn't available. The script configures proxy settings to ensure it can reach Microsoft's servers:
# Configure proxy settings
[System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy
("http://$ProxyServer")
[System.Net.WebRequest]::DefaultWebProxy.Credentials =
[System.Net.CredentialCache]::DefaultNetworkCredentials
# Enable TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
4. Dynamically Retrieving KB Information
The heart of the script is the function that looks up KB information from the Microsoft Update Catalog:
function Get-KBInfoFromCatalog {
param([string]$kbNumber)
try {
Write-Host ("Looking up KB" + $kbNumber + " in Windows Update Catalog...")
-ForegroundColor Green
# Use MSCatalog module if available
if ($usingMSCatalog) {
$searchResults = Get-MSCatalogUpdate -Search "KB$kbNumber" -ErrorAction Stop
if ($searchResults -and $searchResults.Count -gt 0) {
return @{
Title = $searchResults[0].Title
LastUpdated = $searchResults[0].LastUpdated
Classification = $searchResults[0].Classification
Status = "Found"
}
}
}
# Fallback to web scraping if MSCatalog not available
else {
$url = "https://www.catalog.update.microsoft.com/Search.aspx?q=KB$kbNumber"
$webRequest = Invoke-WebRequest -Uri $url -UseBasicParsing
-Proxy "http://$ProxyServer" -ProxyUseDefaultCredentials
if ($webRequest.Content -match '<a[^>]*id="([^"]*)"[^>]*>
([^<]*KB' + $kbNumber + '[^<]*)<\/a>') {
$title = $matches[2]
return @{
Title = $title.Trim()
LastUpdated = "Unknown"
Classification = "Unknown"
Status = "Found"
}
}
}
# Check if update is superseded
if (Test-UpdateSuperseded -kbNumber $kbNumber) {
return @{
Title = "KB$kbNumber (Superseded)"
LastUpdated = "Unknown"
Classification = "Unknown"
Status = "Superseded"
}
}
return @{
Title = "KB$kbNumber (Not Found)"
LastUpdated = "Unknown"
Classification = "Unknown"
Status = "Not Found"
}
}
catch {
# Capture Error Handling
}
}
This approach gives me flexibility - the script first tries to use the MSCatalog module for the best results, but falls back to direct web requests if needed.
5. Detecting Superseded Updates
One of the challenges with Windows updates is that they're frequently superseded by newer versions. The script checks for this:
function Test-UpdateSuperseded {
param([string]$kbNumber)
try {
# Try to search for references to this KB being superseded
$searchUrl = "https://www.catalog.update.microsoft.com/Search.aspx
?q=supersedes+KB$kbNumber"
$webRequest = Invoke-WebRequest -Uri $searchUrl -UseBasicParsing -Proxy
"http://$ProxyServer" -ProxyUseDefaultCredentials
# If we find any results that mention this KB is superseded, return true
if ($webRequest.Content -match "supersedes.*KB$kbNumber"
-or $webRequest.Content -match "KB$kbNumber.*superseded") {
return $true
}
return $false
}
catch {
# If error occurs, assume not superseded
return $false
}
}
This allows my reports to indicate when servers are running outdated updates that have newer replacements available.
6. Main Processing Logic
With all the helper functions defined, the main processing logic becomes straightforward:
# Get the hotfixes from the target server
$hotfixes = Get-ServerHotfixes -ServerName $ServerName -MaxUpdates $MaxUpdates
if ($hotfixes -eq $null -or $hotfixes.Count -eq 0) {
Write-Host "No updates found on server $ServerName. Exiting." -ForegroundColor Yellow
exit
}
Write-Host ("Found " + $hotfixes.Count + " updates on server " + $ServerName)
-ForegroundColor Green
# Process each hotfix
foreach ($hotfix in $hotfixes) {
# Extract KB number (remove "KB" prefix if present)
$kbNumber = $hotfix.HotFixID -replace "KB", ""
# Skip if not a valid KB number
if (-not $kbNumber -or -not ($kbNumber -match "^\d+$")) {
continue
}
# Check if we've already looked up this KB
if (-not $kbCache.ContainsKey($kbNumber)) {
# Get KB info from Update Catalog
$kbInfo = Get-KBInfoFromCatalog -kbNumber $kbNumber
$kbCache[$kbNumber] = $kbInfo
Write-Host ("KB" + $kbNumber + " Info: " + $kbInfo.Title) -ForegroundColor Cyan
} else {
$kbInfo = $kbCache[$kbNumber]
Write-Host ("Using cached info for KB" + $kbNumber) -ForegroundColor Cyan
}
# Add to results
$results += [PSCustomObject]@{
ServerName = $ServerName
HotFixID = $hotfix.HotFixID
Description = $hotfix.Description
InstalledOn = $hotfix.InstalledOn
InstalledBy = $hotfix.InstalledBy
FriendlyName = $kbInfo.Title
Status = $kbInfo.Status
LastUpdated = $kbInfo.LastUpdated
Classification = $kbInfo.Classification
}
}
Benefits of This Approach
- Server-specific targeting - I can easily get details for any specific server without
needing to parse log files. - Future-proof - The script doesn't rely on hardcoded data that becomes outdated.
It will work with any Windows update, past or future. - Accurate information - By querying Microsoft directly, I get the most accurate and up-to-date information about each update.
- Comprehensive reporting - The resulting CSV includes not just the KB numbers, but friendly names, classification, status, and other useful data.
- Flexibility - The script works in various environments through its adaptable design, falling back to different methods as needed.
Performance Considerations
To minimize the load on external resources and improve script performance, I implemented a caching mechanism:
# Cache for KB lookups to avoid duplicate web requests
$kbCache = @{}
# Later in the code...
if (-not $kbCache.ContainsKey($kbNumber)) {
# Get KB info from Update Catalog
$kbInfo = Get-KBInfoFromCatalog -kbNumber $kbNumber
$kbCache[$kbNumber] = $kbInfo
} else {
$kbInfo = $kbCache[$kbNumber]
}
This ensures that if the same KB appears multiple times across different servers (which is common), I only query Microsoft's servers once for each unique KB number.
Real-World Example
I recently used this script to audit updates on a critical domain controller:
.\Get-WindowsUpdateDetails.ps1 -ServerName “claws.bear.local"
-MaxUpdates 10 -OutputPath "C:\Reports\DC01_Updates.csv"
The script retrieved the 10 most recent updates, queried the Microsoft Update Catalog for detailed information, and produced a CSV that included:
- The original HotFixID (KB number)
- When and by whom it was installed
- The full friendly name from Microsoft's Update Catalog
- Whether the update has been superseded
- Classification and other metadata
This detailed information helped me quickly understand what each update did and whether any had been superseded by newer versions.
Conclusion
This approach to Windows update reporting has saved me countless hours of manual lookup work and helped provide more accurate and detailed information to management. By dynamically querying the Microsoft Update Catalog, I've created a solution that will continue to work as new updates are released, without requiring constant maintenance.
The ability to target any server directly with a simple parameter makes this script incredibly versatile for both ad-hoc queries and scheduled reporting tasks.
The next time you're faced with understanding what updates are installed on your servers and what they actually mean, consider this approach for automating the lookup process.