PowerShell script that will download monthly updates into folders from Microsoft

Every month when the latest Microsoft OS updates are released I have to go to the Microsoft Update Catalog website ( https://www.catalog.update.microsoft.com/Home.aspx), and do the following:

  1. Type in the month and the OS version of the updates that I want to download (e.g. 2020-09 2012 R2).

  2. Copy the names of each of the updates and create folders with these names on my local computer

  3. Download the updates into the folder names matching the updates

I have to repeat this process to manually download updates for Server 2008R2, Server 2012R2, Server 2016, and Server 2019.

This has to be done since these updates don’t always appear within the SCCM Software Center console and because it always takes the SCCM team around a month to approve the updates after they have been released from Microsoft. I always need to install the latest updates whenever I patch servers.

 

Once these updates have been downloaded I put these updates into a folder on a NAS so that these updates can be copied over to servers that need them.

So I’m hoping that a PowerShell script can be written where I can provide the month and OS version (e.g. 2020-09 2012 R2) that will then:

  1. Create the folder names corresponding to the updates in the target folder

  2. Download the updates into the folder names matching the updates

Well hope no more because it’s absolutely possible! If you hit a snag come back and show what’s not working and plenty of us will be happy to help. Happy scripting!

Can you provide me with the PowerShell script that will do this?

ee124209

You can refer below scripts which will help you to achieve your task.

  1. Script is to download the updates based on the KB article
    https://gallery.technet.microsoft.com/42-POWERSHELL-TO-UPDATES-7071ab32

  2. How convert html into PowerShell object
    https://sysjam.wordpress.com/2016/01/08/consuming-html-tables-with-powershell/

Thanks,
Nitesh

Hi sorry for hijacking the thread, but I´ve been trying to get this to work now for a while now but there still is something I´m not getting. I´ve managed to get it to download the KB´s from the txt file but I can´t get it to filter it will always download everything under the KB# so both x86 and x64.

I´ve been trying webscraping to narrow down how til filter it out and got it somewhat to work when I did it in a small script but when I tried to implement it to the script it wouldn´t work only says “No such article exist”

Can someone please point to me what I´m doing wrong…

In line 7 there is the issue if I comment out the things after # the script runs but downloads all the KB´s if uncommented it says there is no KB´s

foreach ($KB in $KB_LIST)
{
$COMPLETE_LINKS=$LINKS=$ONEURL=$KB_ROOT_FOLDER= $null
$ONEURL="https://www.catalog.update.microsoft.com/Search.aspx?q=$KB"
Write-Host ""
write-host "Connecting to URL $ONEURL." -ForegroundColor Gray
$COMPLETE_LINKS=(Invoke-WebRequest -Uri $ONEURL -UseBasicParsing).Links | Where-Object id -like '*_link' | Select-Object @{Name = 'GUID';Expression = { $_.Id -replace '_link', '' }} #, @{Name = '*'; Expression = {if( $_.innerText -match 'x64') {$Matches[0].ToLower()}}}, innertext | Where-Object {$_.innertext -match $OS_FILTER}

if($null -ne $COMPLETE_LINKS)
{
#Filtering based on architecure
if($PLATFORM_FILTER -eq 'x86')
{
$LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x86' -or $_.platform -eq $null}
}
elseif($PLATFORM_FILTER -eq 'x64')
{
$LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x64'}
}
elseif($PLATFORM_FILTER -eq 'ARM64')
{
$LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'ARM64'}
}
write-host "List of patches found:" -ForegroundColor Green
write-host $($LINKS.innerText) -ForegroundColor Gray
Write-Host ""

 

Proper indentation as well as a decent IDE will help you see when you’re missing not only one, but two closing curly braces. You are also not showing how you populate your KB list.

foreach ($KB in $KB_LIST)
{
    $COMPLETE_LINKS=$LINKS=$ONEURL=$KB_ROOT_FOLDER= $null
    $ONEURL="https://www.catalog.update.microsoft.com/Search.aspx?q=$KB"
    Write-Host ""
    write-host "Connecting to URL $ONEURL." -ForegroundColor Gray
    $COMPLETE_LINKS=(Invoke-WebRequest -Uri $ONEURL -UseBasicParsing).Links | Where-Object id -like '*_link' | Select-Object @{Name = 'GUID';Expression = { $_.Id -replace '_link', '' }} #, @{Name = '*'; Expression = {if( $_.innerText -match 'x64') {$Matches[0].ToLower()}}}, innertext | Where-Object {$_.innertext -match $OS_FILTER}
    
    if($null -ne $COMPLETE_LINKS)
    {
        #Filtering based on architecure
        if($PLATFORM_FILTER -eq 'x86')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x86' -or $_.platform -eq $null}
        }
            elseif($PLATFORM_FILTER -eq 'x64')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x64'}
        }
        elseif($PLATFORM_FILTER -eq 'ARM64')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'ARM64'}
        }
    }
}
write-host "List of patches found:" -ForegroundColor Green
write-host $($LINKS.innerText) -ForegroundColor Gray
Write-Host ""

Hi Doug and thanks for the reply. Yeah sorry I didn´t post the whole script since it´s quite large but I guess you need all find out the problem. It´s basically the same script as Evila posted the link to but just with some tweeks I´ve made to get it to start do something.

The KB are stored in a .txt file for now just on C:\Temp while testing I´ve just been using this KB for testing KB4576630

Here is all the code

function Get-UpdateURL($GUID)
{
    foreach ($oneGUID in $GUID) 
    {
        $POST_BODY = @{ updateIDs = "[{$( $POST_BODY_TEMPLATE -f $oneGUID )}]" }
        if (( Invoke-WebRequest -Uri $UPDATE_CATALOG_DOWNLOAD_LINK -UseBasicParsing -Method Post -Body $POST_BODY).Content -match $UPDATE_URL_PATTERN)
        {
            $Matches[0]
        }
        else 
        {
            $null    
        }
    }
}

#Variable declaration
$UPDATE_CATALOG_DOWNLOAD_LINK='https://www.catalog.update.microsoft.com/DownloadDialog.aspx'
$UPDATE_URL_PATTERN =  "https?://download\.windowsupdate\.com\/[^ \'\""]+"
$POST_BODY_TEMPLATE = '"updateID": "{0}"'
$OS_FILTER=' Windows Server 2012 R2 '
$PLATFORM_FILTER='x64' #X86,ARM64
#$OSBUILD_FILTER='1809'
$ParentPath="$env:USERPROFILE"
$KB_LIST=Get-Content 'c:\Temp\KBlist.txt'
$PATCH_REPORT="C:\Temp\Patch_Download_Report.html"
if(test-path $PATCH_REPORT)
{
    Remove-Item $PATCH_REPORT -Force -ErrorAction SilentlyContinue
}
$DOWNLOAD_RESULT=@()

#--CSS formatting
$HEADER_FORMAT=@'
<style type="text/css">
h1, h5,h2, th { text-align: left; font-family: Segoe UI;font-size: 13px;}
h4 { text-align: left; font-family: Segoe UI;font-size: 12px;}
table { margin: left; font-family: Segoe UI; box-shadow: 10px 10px 5px #888; border: thin ridge grey; }
th { background: #0046c3; color: #fff; max-width: 400px; padding: 5px 10px; font-size: 12px;}
td { font-size: 11px; padding: 5px 20px; color: #000; }
tr { background: #b8d1f3; }
tr:nth-child(even) { background: #dae5f4; }
tr:nth-child(odd) { background: #b8d1f3; }
</style>
'@

Clear-Host
foreach ($KB in $KB_LIST)
{
    $COMPLETE_LINKS=$LINKS=$ONEURL=$KB_ROOT_FOLDER= $null
    $ONEURL="https://www.catalog.update.microsoft.com/Search.aspx?q=$KB"
    Write-Host ""
    write-host "Connecting to URL $ONEURL." -ForegroundColor Gray
    $COMPLETE_LINKS=(Invoke-WebRequest -Uri $ONEURL -UseBasicParsing).Links | Where-Object id -like '*_link' | Select-Object @{Name = 'GUID';Expression = { $_.Id -replace '_link', '' }}#, @{Name = 'platform';Expression = {if( $_.innerText -match '\b(x86|x64|arm64)\b') {$Matches[1].ToLower()}}}, innertext | Where-Object {$_.innertext -match $OS_FILTER}

    if($null -ne $COMPLETE_LINKS)
    {        
        #Filtering based on architecure
        if($PLATFORM_FILTER -eq 'x86')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x86' -or $_.platform -eq $null}
        }
        elseif($PLATFORM_FILTER -eq 'x64')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'x64'}
        }
        elseif($PLATFORM_FILTER -eq 'ARM64')
        {
            $LINKS = $COMPLETE_LINKS | Where-Object { $_.platform -eq 'ARM64'}
        }
        write-host "List of patches found:" -ForegroundColor Green
        write-host  $($LINKS.innerText) -ForegroundColor Gray
        Write-Host ""

        #Creating folder for each specific KB article
        $KB_ROOT_FOLDER="$ParentPath\$KB"
        if(Test-Path $KB_ROOT_FOLDER)
        {
            Remove-Item $KB_ROOT_FOLDER -Force -ErrorAction SilentlyContinue
        }
        New-Item -ItemType Directory -Path $KB_ROOT_FOLDER -ErrorAction SilentlyContinue | Out-Null

        if($null -ne $LINKS)
        {
            foreach ($ONELINK in $Links)
                {
                    if($null -ne $ONELINK)
                    {
                        Write-Host "Processing link $($ONELINK.innerText)"
                        $DOWNLOAD_LINK = Get-UpdateURL $ONELINK.GUID
                        if($null -ne $DOWNLOAD_LINK)
                        {
                            $DOWNLOAD_FOLDER_NAME=$ONELINK.innerText.TrimEnd(" ")
                            $DOWNLOAD_FILE_NAME=($DOWNLOAD_LINK -split "/")[-1]
                            Write-Host "Downloading update $DOWNLOAD_FILE_NAME...."
                            New-Item -ItemType Directory -Path "$KB_ROOT_FOLDER\$DOWNLOAD_FOLDER_NAME" | Out-Null
                            $DOWNLOAD_PATH="$KB_ROOT_FOLDER\$DOWNLOAD_FOLDER_NAME\$DOWNLOAD_FILE_NAME"
                            try 
                            {
                                Invoke-WebRequest $DOWNLOAD_LINK -OutFile $DOWNLOAD_PATH  
                                Write-Host "Download completed for update $DOWNLOAD_FILE_NAME" -ForegroundColor Green
                                $KB_STATUS=[PSCustomObject]@{
                                    'KB Article' = $KB
                                    'Downloaded'    = 'YES'
                                    'DownloadPath' = $DOWNLOAD_PATH
                                    'URL' = $DOWNLOAD_LINK
                                    'Patch Name'=$DOWNLOAD_FILE_NAME
                                }
                                $DOWNLOAD_RESULT+=$KB_STATUS
                                $KB_STATUS=$null
                            }
                            catch 
                            {
                                Write-Host "Exception occured during download." -Foreground Red
                                $KB_STATUS=[PSCustomObject]@{
                                    'KB Article' = $KB
                                    'Downloaded'    = 'Failed'
                                    'DownloadPath' = $DOWNLOAD_PATH
                                    'URL' = $DOWNLOAD_LINK
                                    'Patch Name'=$DOWNLOAD_FILE_NAME
                                }
                                $DOWNLOAD_RESULT+=$KB_STATUS
                                $KB_STATUS=$null
                            }
                        }
                        else 
                        {
                            Write-Host "Source URL for patch $ONELINK does not exist" -ForegroundColor Red
                            $KB_STATUS=[PSCustomObject]@{
                                'KB Article' = $KB
                                'Downloaded'    = "Source URL for patch $ONELINK does not exist"
                                'DownloadPath' = "Does not Exist"
                                'URL' = "Does not Exist"
                                'Patch Name'="Does not Exist"
                            }
                            $DOWNLOAD_RESULT+=$KB_STATUS
                            $KB_STATUS=$null
                        }
                    }
                    $ONELINK=$DOWNLOAD_FOLDER_NAME=$DOWNLOAD_FILE_NAME=$DOWNLOAD_LINK=$DOWNLOAD_PATH=$null
                }
        }
        else 
        {
            write-host "No patch exists for the selected Filter type." -ForegroundColor Red
            $KB_STATUS=[PSCustomObject]@{
                'KB Article' = $KB
                'Downloaded'    = "No patch exists for the selected Filter type."
                'DownloadPath' = ""
                'URL' = ""
                'Patch Name'=""
            }
            $DOWNLOAD_RESULT+=$KB_STATUS
            $KB_STATUS=$null
        }
    }
    else 
    {
        write-host "No such kb article exists" -ForegroundColor Red
        $KB_STATUS=[PSCustomObject]@{
            'KB Article' = $KB
            'Downloaded'    = "No such KB article exist."
            'DownloadPath' = ""
            'URL' = ""
            'Patch Name'=""
        }
        $DOWNLOAD_RESULT+=$KB_STATUS
        $KB_STATUS=$null
    }
}

Anyone got clue why it´s not working ?