How to apply all outstanding patches with PowerShell

Published on Friday, 27 April 2018

How to a Windows system with PowerShell

The following is a script which will apply all available Windows Updates that a system requires. Perfect for adding to a lab build script or to manage a small number of servers.

If you are going to patch a Windows 7 system however please look at the section at the bottom of the page as there are a few gotchas.

How to use

The script is simple enough to use, you just run it with:

powershell.exe -executionpolicy bypass -noninteractive -noprofile -file installupdates.ps1

This will then download and install all required updates and exit with one of the following exit codes.

Return codes:

Code Description
0 Updates successful
3010 Reboot required

If you get a 3010 you should run the script again on reboot. This way you can keep rebooting to allow updates to install till they are all applied.

Script: installupdates.ps1

#Add any patches you want to block from installing to this array.
#note: blocking defender updates to prevent script getting stuck in a loop. Defender updates appear available everytime you check.

$BlockedUpdates = @("*language pack*", "*Windows Defender*")



$script:Session = New-Object -ComObject Microsoft.Update.Session

<#
    .SYNOPSIS
    Uses windows update API to check what patches need to be installed.
    .PARAMETER Skip
    Array of patches to skip. Uses powershell -Like syntax.
#>
function Get-AvailableUpdates
{
    param([Parameter(Mandatory = $true)]
        [string[]]$skip)

    $Searcher = $script:Session.CreateUpdateSearcher()
    $UpdateServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
    $Query = "IsInstalled=0 and Type='Software' and IsHidden=0"
    $Patches = $Searcher.Search($Query).Updates
    $ReturnPatches = New-Object -ComObject Microsoft.Update.UpdateColl

    if ($patches -eq $null -or $patches.Count -eq 0 ) 
    { 
        return $null
    }

    foreach ($patch in $Patches)
    {
        $shouldSkip = $false
        foreach ($banned in $skip)
        {
            if ($patch.title.ToLower() -like $banned) { $shouldSkip = $true }
        }

        if (-not $shouldSkip) { $ReturnPatches.Add($patch) | out-null }
    }

    Write-Output $ReturnPatches
}

<#
    .SYNOPSIS
    Downloads patches to the windows install cache ready to install.
    
    .PARAMETER Patches
    Patch collection of patches to download. Use Get-AvailableUpdates to get the list.
#>
function Get-PatchCache
{
    param($Patches)

    $patchcount = $patches.Count
    $patchindex = 0

    foreach ($patch in $patches)
    {
        $patchindex++
        if ($patch.IsDownloaded)
        {
            Write-Host "Patch [$patchindex/$patchcount] $($patch.Title) is already downloaded!"
        }
        else
        {
            Write-Host "Patch [$patchindex/$patchcount] $($patch.Title) is being downloaded."
            $currentupdate = New-Object -ComObject Microsoft.Update.UpdateColl
            $currentupdate.Add($patch) | Out-Null
            $downloader = $script:Session.CreateUpdateDownloader() 
            $downloader.Updates = $currentupdate
            $downloader.Download() | Out-Null
        }
    }
}

<#
    .SYNOPSIS
    Accepts the EULA for all patches passed in.
    .PARAMETER Patches
    Patch collection of patches to approve eula on. Use Get-AvailableUpdates to get this list.
#>
function Approve-PatchEULA
{
    param($Patches)

    foreach ($patch in $Patches)
    {
        if (-not $patch.EulaAccepted)
        {
            $patch.AcceptEula() | Out-Null
        }
    }
}

<#
    .SYNOPSIS
    Installs all patches passed in.
    .PARAMETER Patches
    Patch collection of patches to install. Use Get-AvailableUpdates to get this list.

#>
function Install-Updates()
{
    param($Patches)

    $installer = $script:Session.CreateUpdateInstaller()

    $patchcount = $Patches.Count
    $patchindex = 0

    Write-Host "Waiting for patch installer to be ready..."
    if ($installer.IsBusy)
    {
        foreach ($i in 1..20) { if ($installer.IsBusy) { Start-Sleep -Seconds 5 } else { break }}

        if ($installer.IsBusy)
        {
            Write-Host "Patch installer still not ready...rebooting..."
            Exit 3010
        }
    }

    if ($installer.RebootRequiredBeforeInstallation) 
    { 
        Write-Host "Pending reboot detected...rebooting..."
        Exit 3010
    }

    foreach ($patch in $Patches)
    {
        $patchindex++

        $currentupdate = New-Object -ComObject Microsoft.Update.UpdateColl
        $currentupdate.Add($patch) | Out-Null

        Write-Host "Installing [$patchindex/$patchcount] $($patch.Title)"
        try 
        {                
            $installer.AllowSourcePrompts = $false
            $installer.IsForced = $true
            $installer.Updates = $currentupdate
            $installer.install() | Out-Null
            Write-Host "Installed Ok"
        }
        catch
        {
            Write-Host "Failed to install $($patch.Title) - Will try again next reboot if still applicable."
        }
    }
}
Write-Host "Searching for updates..."

$Patches = Get-AvailableUpdates -skip $BlockedUpdates

if ($Patches -eq $null -or $Patches.Count -eq 0)
{
    Write-Host "No more updates...exiting..."
    exit 0
}
Write-Host "Downloading patches..."
Get-PatchCache -Patches $Patches
Approve-PatchEULA -Patches $Patches
Write-Host "Installing Patches..."
Install-Updates -Patches $Patches

Write-Host "Current round of patches installed...Requires reboot"
exit 3010

Windows 7 Considerations

When patching Windows 7 there is an issue where it can take up to 12 hours to complete or become corrupt from a clean install. This is a known issue and Microsoft has released an update to fix this.

To apply it you need to first install SP1 and then apply KB3020369.

To simplfy the install I have put 2 scripts that will automatically install the Service pack and the hotfix below. You will need to do a reboot inbetween each script for it to complete properly.

Service Pack Installation Script

The following script will download the service pack and install it.

URLs for windows version:

Operating System URL
Windows 7 x64 https://download.microsoft.com/download/0/A/F/0AFB5316-3062-494A-AB78-7FB0D4461357/windows6.1-KB976932-X64.exe
Windows 7 x86 http://download.microsoft.com/download/0/A/F/0AFB5316-3062-494A-AB78-7FB0D4461357/windows6.1-KB976932-X86.exe

Script: InstallServicePack.ps1

$url = "url to service pack here"

Write-Host "Downloading Service Pack..."
(New-Object System.Net.WebClient).DownloadFile($url, "c:\sp.exe")

Write-Host "Extracting Package"
&c:\sp.exe /x:c:\servicepack | Out-Null

Write-Host "Installing Package..."
&c:\servicepack\SPInstall.exe /nodialog /norestart /quiet | Out-Null
Write-Host "Package installation completed..."

Windows Update Fix

URLs for Windows Update Fix:

Operating System Url
Windows 7 x64 https://download.microsoft.com/download/5/D/0/5D0821EB-A92D-4CA2-9020-EC41D56B074F/Windows6.1-KB3020369-x64.msu
Windows 7 x86 https://download.microsoft.com/download/C/0/8/C0823F43-BFE9-4147-9B0A-35769CBBE6B0/Windows6.1-KB3020369-x86.msu

Script: InstallWindowsUpdateFix.ps1

$url = "url to service pack here"

Write-Host "Downloading Windows Update Fix..."
(New-Object System.Net.WebClient).DownloadFile($url, "c:\wufix.msu")

Write-Host "Package downloaded...Now installing..."
&wusa.exe c:\wufix.msu /quiet /norestart /log:c:\wufix.log | out-null
Write-Host "Package installation completed...restarting..."