Install Windows Server Servicing Stack Updates (SSU) using PowerShell

Install Servicing Stack Updates (SSU) for Windows Server 2012R2, 2016 and Windows Server 2019 using PowerShell, without downtime
Published on Wednesday, 12 June 2019

You can install Servicing Stack Updates (SSU) for Windows Server 2012R2, 2016 and Windows Server 2019 using PowerShell, without downtime. Because they must be installed prior to your normal Windows Server security updates, you can install them anytime you want to during the day. Here's a small PowerShell example to do so.

Servicing the Windows servicing stack

Keeping the servicing stack updated is crucial for the security of your Windows-based infrastructure. Unfortunately, it's also becoming more of a challenge. This post helps you installing Windows servicing stack updates prior to Windows Updates, using PowerShell, and streamlines your workflow a bit.

But first, what exactly are Servicing Stack Updates, or SSU's? Microsoft writes:

Servicing stack updates provide fixes to the servicing stack, the component that installs Windows updates. Additionally, it contains the "component-based servicing stack" (CBS), which is a key underlying component for several elements of Windows deployment, such as DISM, SFC, changing Windows features or roles, and repairing components. The CBS is a small component that typically does not have updates released every month.

Servicing stack updates - Microsoft Docs

I mentioned Windows Component-Based Servicing and CBS log files in my blog post with 5 ways to clean up files and free up disk space in Windows Server.

In my monthly Windows Updates routine, mostly using WSUS, I use the following PowerShell snippets to install Servicing Stack Updates manually in a loop.

Simple PowerShell introduction for Windows Server administration, automation and scripting

Check WSUS client servers for downloaded SSU update files

First I approve all updates available and wait a moment for the updates to be downloaded to the WSUS client servers. In that moment of waiting, I write down the KB numbers for Server 2016 and Server 2019 Servicing Stack Updates (this month: KB4503537 for Server 2016 and KB4504369 for Server 2019.

Using PowerShell, loop through your servers' file system to check if SSU updates files have been downloaded. Server 2019 as an example, adjust to your needs.

foreach( $server in (Get-ADComputer -Filter {(enabled -eq $True) -and (OperatingSystem -like "Windows Server 2019*")}).DNSHostName ) {
  invoke-command -computername $server -scriptblock {
    Try {
      $update = ((Get-ChildItem -Recurse C:\Windows\SoftwareDistribution\Download | Where-Object { $_.PSPath -like "*KB4504369*" }) | Select-Object Name,DirectoryName)
      if(Test-Path($update.DirectoryName)) {
        write-output "KB4566425 found on server $using:server"
      }
    }
    Catch {
      write-output "KB4566425 not found on server $using:server"
    }
  }
}

The Try{} Catch{} block prevents ugly PowerShell error messages like "Cannot bind argument to parameter 'Path' because it is null." when no update file has been found yet. The file wasn't downloaded yet.

Ever wondered why Windows Server Update Services (WSUS) offers Flash updates for Windows Server? Here is how to uninstall and remove Adobe Flash Player in Windows Server. Here are some more WMI filters to manage Windows Server versions.

Continue when all servers have downloaded the SSU update files.

Install Servicing Stack Updates in Windows Server using PowerShell

Next, my PowerShell script takes a KB number as input, and uses Get-ChildItem cmdlet to search for the update .cab file. If that file is found, it is installed using DISM.exe to install the SSU .cab file.

[CmdletBinding()]
  Param(
    [Parameter(Mandatory = $true, Position = 0, HelpMessage="Windows Server OS version (2019, 2016, 2012)")]
    [string]$serveros,
    [Parameter(Mandatory = $true, Position = 0, HelpMessage="Servicing Stack Update KB number")]
    [ValidateScript({
      if (!($_ -match "KB")) {
        Throw "[!!] the letters KB are missing"
      }
      else {
        $true
      }
    })]
    [string]$ssukb
  )

function install_SSU($servername, $kb) {
  invoke-command -computername $servername -scriptblock {
    $update = ((Get-ChildItem -Recurse C:\Windows\SoftwareDistribution\Download | Where-Object { $_.PSPath -like "*${using:kb}*" }) | Select-Object Name,DirectoryName)
    if(Test-Path("$($update.DirectoryName)")) {
      $process = Start-Process -NoNewWindow "c:\windows\system32\DISM.exe" -argument "/Online /Add-Package /PackagePath:$($update.DirectoryName)\$($update.Name)" -PassThru -Wait
      if($process.ExitCode -ne 0) {
        $a = "Installation process returned error code $($process.ExitCode) on ${Using:servername}"
        return $a
      }
      else {
        $a = "true, update {$Using:kb} installed on ${Using:servername}"
        return $a
      }
    } else {
      $a = "$($update.DirectoryName)\$($update.Name) not found on ${Using:servername}"
      return $a
    }
  }
}

function restart_WindowsUpdate($servername, $os) {
  invoke-command -computername $servername -scriptblock {
    &net stop wuauserv
    &net start wuauserv
    if($os -eq "2019" -or $os -eq "2016") {
      Start-Process -NoNewWindow "c:\windows\system32\UsoClient.exe" -argument "startscan" -Wait  
      Start-Process -NoNewWindow "c:\windows\system32\UsoClient.exe" -argument "RefreshSettings" -Wait
    } else {
      Start-Process -NoNewWindow "c:\windows\system32\wuauclt.exe" -argument "/resetauthorization /DetectNow" -Wait
      Start-Process -NoNewWindow "c:\windows\system32\wuauclt.exe" -argument "/ReportNow" -Wait
    }
  }
}

if(($serveros -eq "2016") -or ($serveros -eq "2019")) {
  $serverversion = "Windows Server " + $serveros + "*"
  foreach($server in (Get-ADComputer -Filter "OperatingSystem -like '${serverversion}' -and enabled -eq '$true'").DNSHostName) {
    $b = install_SSU -servername "${server}" -kb "${ssukb}"
    $b
    restart_WindowsUpdate -server "${server}" -os "${serveros}"
  }
}

if($serveros -eq "2012") {
  foreach($server in (Get-ADComputer -Filter {(enabled -eq $True) -and (OperatingSystem -like "Windows Server 2012*")}).DNSHostName) {
    $b = install_SSU -servername "${server}" -kb "${ssukb}"
    $b
    restart_WindowsUpdate -server "${server}" -os "${serveros}"
  }
}

It may not be perfect but works for my situation. You can execute it for your Windows Server operating system versions like:

.\install-SSU.ps1 -serveros 2019 -ssukb KB4539571
.\install-SSU.ps1 -serveros 2016 -ssukb KB4540723
.\install-SSU.ps1 -serveros 2012 -ssukb KB4540725

The regular Windows Updates are installed using WSUS and PowerShell module PSWindowsUpdate. You can install this with NuGet:

Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Confirm:$false -Force
Install-Module -Name PSWindowsUpdate -Confirm:$false -Force

I have a Dutch article about PSWindowsUpdate at ITFAQ.nl, and here on Saotn.org: Install Windows Updates using PowerShell.