Retrieve Hotfixes and query the Windows Update Catalog with PowerShell


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:

  1. Accepts a server name as a parameter
  2. Retrieves installed updates using Get-Hotfix
  3. Dynamically queries the Microsoft Update Catalog for each KB
  4. Determines if updates have been superseded
  5. 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

  1. Server-specific targeting - I can easily get details for any specific server without
    needing to parse log files.
  2. Future-proof - The script doesn't rely on hardcoded data that becomes outdated.
    It will work with any Windows update, past or future.
  3. Accurate information - By querying Microsoft directly, I get the most accurate and up-to-date information about each update.
  4. Comprehensive reporting - The resulting CSV includes not just the KB numbers, but friendly names, classification, status, and other useful data.
  5. 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:

  1. The original HotFixID (KB number)
  2. When and by whom it was installed
  3. The full friendly name from Microsoft's Update Catalog
  4. Whether the update has been superseded
  5. 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.

Previous Post Next Post

نموذج الاتصال