How to apply all outstanding patches 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:
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:
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..."