Payton Flint's Tech Blog
Menu
  • Home
  • Blog
  • Categories
  • Resources
  • About
  • Contact
Menu

PowerShell – Save Updates And Import to WSUS

Posted on November 27, 2022June 4, 2023 by paytonflint

Since Internet Explorer has now been deprecated, there is a need to import updates to WSUS natively without the installation of additional browsers. I’ve found that some of the functionality can be accomplished with Microsoft MVP Chrissy Lemaire’s “KBUpdate” PowerShell module, but it merely facilitated the process by downloading the update, and thus, I did not find it an elegant solution. And, of course, yet again, it required the installation of third party software – even if it is just a module, I find it distasteful.

I found this guide to be particularly helpful in outlining the process: https://4sysops.com/archives/import-updates-manually-into-wsus-with-ie-or-powershell/. However, that still leaves the problem of getting the downloads from the Microsoft Update Catalog, which appears to be purpose-built to make this rather difficult to script.

I reverse-engineered the “KBUpdate” module to figure out how to get the downloads from the Microsoft Update Catalog. This involves building a JSON request form to submit to Microsoft’s site. I have built my own WSUS Import script that runs you step-by-step through selecting individual updates from the Microsoft Update Catalog, saves them to a downloads folder at the root of the script, and gives you the option to import them if you so choose. Check out the screenshots below to get a feel for the procedure.

# Written by Payton Flint
# See https://paytonflint.com/powershell-import-kbid-to-wsus/

# This script is a modification of Chrissy LeMaire's (potatoqualitee) KBUpdate module
# The original source can be found here: https://github.com/potatoqualitee/kbupdate.git

# Clear variables for repeatability
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0

# Import UpdateServices module
#Import-Module -Name UpdateServices -ErrorAction Stop
 
# Identify location of script
$ScriptPath = Split-Path ($MyInvocation.MyCommand.Path) -Parent
 
# Set systems list location
$KBList = Get-Content "$ScriptPath\KBList.txt"
 
# Specify registry key info
$regPath = 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319'
$regName = 'SchUseStrongCrypto'
$regValue = '1'
 
# Specify download folder location
$DownloadPath = Join-Path $ScriptPath 'WSUSImport-Downloads'

# Specify WSUS server
$WSUS = Get-WsusServer

#=Functions======================================================================================================

# RegEdit function 
Function RegEdit {
    param(
    $regPath,
    $regName,
    $regValue
    )
    $regFull = Join-Path $regPath $regName
        Try {
                $CurrentKeyValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue).$regName
                If (Test-Path $regPath) {
                    If ($CurrentKeyValue -eq $regValue) {
                        Write-Host -ForegroundColor Green 'Registry key' $regFull 'value is set to the desired value of' $regValue'.'
                        $script:regTest = $True
                    } Else {
                       Write-Host -ForegroundColor Red 'Registry key'$regFull 'value is not' $regValue'.'
                        Write-Host -ForegroundColor Cyan 'Setting registry key' $regFull 'value to' $regValue'.'
                        New-ItemProperty -Path $regPath -Name $regName -Value $regValue -PropertyType DWORD -Force | Out-Null
                        $CurrentKeyValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue).$regName
                        If ($CurrentKeyValue -eq $regValue) {
                            Write-Host -ForegroundColor Green 'Registry key' $regFull 'value is set to the desired value of'$regValue'.'
                            $script:regTest = $True
                        } Else {
                            Write-Host -ForegroundColor Red 'Registry key' $regFull 'value could not be set to' $regValue '.'
                        }
                    }
                } Else {
                    Write-Host -ForegroundColor Red 'Registry key'$regFull 'path does not exist.'
                    Write-Host -ForegroundColor Cyan 'Creating registry key' $regFull'.'
                    New-Item -Path $regPath -Force | Out-Null
                    Write-Host -ForegroundColor Cyan 'Setting registry key' $regFull 'value to' $regValue'.'
                    New-ItemProperty -Path $regPath -Name $regName -Value $regValue -PropertyType DWORD -Force | Out-Null
                    $CurrentKeyValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue).$regName
                    If ($CurrentKeyValue -eq $regValue) {
                        Write-Host -ForegroundColor Green 'Registry key' $regFull 'value is set to the desired value of' $regValue'.'
                        $script:regTest = $True
                    } Else {
                        Write-Host -ForegroundColor Red 'Registry key'$regFull 'value could not be set to' $regValue '.'
                    }
                }
        } Catch {
            Write-Host -ForegroundColor Red 'Registry key' $regFull 'value could not be set to' $regValue '.'
        }
    Clear-Host
} # End RegEdit Function
 
# Get KB data from Microsoft catalog
Function Get-MSCatalogItems {
    param(
        $KBList
    )
    Write-Host "Getting information from Microsoft Update Catalog...`nKBIDs: $KBList"

    ForEach ($kb in $KBList) {
        $uc = Invoke-WebRequest -Uri https://www.catalog.update.microsoft.com/Search.aspx?q=$kb
        $Output += $uc.Links | where onClick -Like "*goToDetails*" | ForEach-Object {$_.innerText + ";" + $_.id -replace '_link',''} }
 
    # Delimit output and place in array
    $OutputArray = ,$Output | ConvertFrom-Csv -Delimiter ";" -Header "Description","ID"

    # Add line number to array
    $OutputArray | ForEach-Object { 
        $Line++ 
        Add-Member -InputObject $_ -NotePropertyName "Line" -NotePropertyValue "$Line" 
    }
 
    # Create array from output
    $script:OutputArray = For ($i = 1; $i -le $OutputArray.Count; $i++) {
        [PSCustomObject]@{
            Line        = $OutputArray | Where-Object {($_.Line -eq $i)} | Select-Object -ExpandProperty Line
            Select      = $False
            Description = $OutputArray | Where-Object {($_.Line -eq $i)} | Select-Object -ExpandProperty Description
            ID          = $OutputArray | Where-Object {($_.Line -eq $i)} | Select-Object -ExpandProperty ID
        }
    }
} # End Get-MSCatalogItems function
 
# Function Select-Lines
# InputArray must contain Select & Line properties
Function Select-Lines {
    param (
        $InputArray
    )
    Clear-Host
 
    # Reset selection
    $InputArray | ForEach-Object {
        $_.Select = $False
    }
 
    # Get Rows from input array
    $Rows = $InputArray | Format-Table -AutoSize | Out-String -Stream
    # Write rows
    For ($i = 0; $i -lt $Rows.Count; $i++) {
        Write-Host $Rows[$i]
    }
 
    # Make selection
    $Select = Read-Host -Prompt 'Please enter the line number(s) you would like to select (comma separated)'
    # Filter spaces, tabs, and delimit w/ comma
    $Select = $Select -replace '\s','' -split ','
 
    # Update selection
    $InputArray | ForEach-Object {
        If ($Select -contains $_.Line) {
            $_.Select = $True
            $SelectedItems += ,$_
        }
    }
 
    # Get Rows from input array
    $Rows = $InputArray | Format-Table -AutoSize | Out-String -Stream

    Clear-Host
 
    # Write headers + underline w/o color formatting
    Write-Host $Rows[1]
    Write-Host $Rows[2]

    # Write rows with color formatting
    For ($i = 3; $i -lt $Rows.Count; $i++) {
        If ($InputArray[$i - 3] -in $SelectedItems) {
            Write-Host $Rows[$i] -ForegroundColor Cyan
        } Else {
            Write-Host $Rows[$i]
        }
    }
    # Prompt for confirmation
    $Confirm = Read-Host -Prompt 'Please confirm your selection (Y/N)'
    # Convert to uppercase
    $Confirm = $Confirm.ToUpper()
 
    If ($Confirm -eq 'Y') {
        $script:SelectedItems = $SelectedItems
    }
    Clear-Host
} # End Select-Lines function

# Create downloads folder if not present
Function NewDir {
    param (
        $Path
    )
    # Derive directory name
    $Name = Split-Path -Path $Path -Leaf
    $Parent = Split-Path -Path $Path -Parent
 
    # Remove directory if present
    If (!(Test-Path $Path)) {
        # New directory
        New-Item -Path $Parent -Name $Name -ItemType 'Directory' -Force
    }
    Clear-Host
} # End NewDir function

# Download items from catalog to DownloadPath and display
Function DownloadItems {
    ForEach ($item in $script:SelectedItems) {
        # Get IDs
        $id = $item.ID

        # Create body to submit to Microsoft catalog
        $post = @{ size = 0; updateID = $id; uidInfo = $id } | ConvertTo-Json -Compress
        $body = @{ updateIDs = "[$post]" }

        # Get content from Microsoft catalog download page for body
        $Content = Invoke-WebRequest -Uri 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' -Method Post -Body $body |
            Select-Object -ExpandProperty Content 

        # Extract URLs from content using RegEx
        $URL = $Content | Select-String -Pattern "'http[s]?:\/\/catalog\..\.download\.windowsupdate\.com\/.\/msdownload\/.*kb.*\.*'" |
            Select-Object Matches | ForEach-Object { $_.Matches[0].Value }

        # Remove leading and trailing ' characters from URL
        $length = ($URL.Length) - 1
        $URL = $URL.Remove($length,1)
        $URL = $URL.Remove(0,1)

        # Derive filename from URL
        $FileName = $URL | Split-Path -Leaf

        # Derive full path
        $FilePath = Join-Path $DownloadPath $Filename

        # Add URL to object
        Add-Member -InputObject $item -NotePropertyName 'URL' -NotePropertyValue $URL -Force
        # Add Filename to object
        Add-Member -InputObject $item -NotePropertyName 'FileName' -NotePropertyValue $FileName -Force
        # Add Filepath to object
        Add-Member -InputObject $item -NotePropertyName 'FilePath' -NotePropertyValue $FilePath -Force

        Write-Host Downloading... `nFilename:($item.FileName) `nLocation: $DownloadPath`n

        # Ensure item is downloaded
        Do {
            # Check for downloaded files
            If (Test-Path $item.FilePath) {
                $script:DownloadedItems += ,$item
            } Else {
                Start-BitsTransfer -Source $URL -Destination $DownloadPath
            }
        } Until ($item -in $script:DownloadedItems)
    }
    Clear-Host
    Write-Host Successful Downloads:
    $script:DownloadedItems | ForEach-Object {
        Write-Host `nFilename:($_.FileName) `nLocation: $DownloadPath
    }
} # End DownloadItems function

# Import items to WSUS
Function ImportItems {
    param(
        $WSUS
    )

    Clear-Host
    # Prompt to import to WSUS
    $ImportPrompt = Read-Host -Prompt "Would you like to import the downloaded updates to WSUS `($WSUS`) (Y/N)"
    # Convert to uppercase
    $ImportPrompt = $ImportPrompt.ToUpper()

    Clear-Host
    # If yes...
    If ($ImportPrompt -eq "Y"){
        # If WSUS found...
        If ($WSUS -ne $null) {
            Write-Host "WSUS found: $WSUS"
            # Import downloaded items
            $script:DownloadedItems | ForEach-Object{
                Write-Host Importing... `nID:($_.ID) `nFilePath:($_.FilePath)
                # Import items to WSUS
                $WSUS.ImportUpdateFromCatalogSite($_.ID, $_.FilePath)
                Write-Host -ForegroundColor Green "Import Success"
            }
        } Else {
            Write-Host -ForegroundColor Red 'WSUS not found'
        }    
    }
} # End ImportItems function

#================================================================================================================

# Ensure SchUseStrongCrypto is set 
RegEdit -regPath $regPath -regName $regName -regValue $regValue

# Get items from MS catalog 
Get-MSCatalogItems -KBList $KBList

# Select Items 
Do {
    Select-Lines -InputArray $script:OutputArray
} Until ($script:SelectedItems -ne $null)

# Create downloads folder if not present
NewDir -Path $DownloadPath

# Download items to downloads folder
DownloadItems

# Import downloaded items to WSUS
ImportItems -WSUS $WSUS

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

About The Author

Author's Portrait

In my journey as a technologist and 11 years of experience as an IT professional, I have found my niche as Director of Infrastructure Services; developing my skillsets in management, scripting, cloud infrastructure, identity management, and networking.

I have experience as a Systems Administrator and Engineer for large enterprises including the DoD, government agencies, and a nuclear-generation site.

I've been blessed to collaborate with engineers at esteemed Fortune 50 corporations, and one of Africa's largest, to ensure successful implementation of my work.

GitHub Button

Credentials

M365 Endpoint Administrator Associate
M365 Fundamentals
Microsoft AZ-900
CompTIA CSIS
CompTIA CIOS
CompTIA Security+
CompTIA Network+
CompTIA A+
  • April 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
© 2022 Payton Flint | The views and opinions expressed on this website belong solely to the author/owner and do not represent the perspectives of any individuals, institutions, or organizations, whether affiliated personally or professionally, unless explicitly stated otherwise. The content and products on this website are provided as-is with no warranties or guaranties, are for informational/demonstrative purposes only, do not constitute professional advice, and are not to be used maliciously. The author/owner is not responsible for any consequences arising from actions taken based on information provided on this website, nor from the use/misuse of products from this site. All trademarks are the property of their respective owners.