Remotely find missing updates with an offline scan file

Part of our Microsoft Baseline Security Analyzer offering is retrieving a list of missing security updates. Today I would like to share the script to remotely do this in your infrastructure with you.The first step before this script can be executed is downloading the actual cab file that can be used to scan for updates from here: https://support.microsoft.com/en-us/help/926464/a-new-version-of-the-windows-update-offline-scan-file--wsusscn2-cab--i. The script which we will examine closer will attempt an automatic download of the cab file.

The second step is downloading the actual script itself, which is hosted on GitHub: https://github.com/nyanhp/GetMissingUpdates

Let's go over this simple script step by step.

 param
(
    [Parameter(Mandatory = $true)]
    [System.String[]]
    $ComputerName,

    [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
    [System.String]
    $Path,

    # As of Sep 2017 https://go.microsoft.com/fwlink/?linkid=74689
    [Parameter(Mandatory = $true, ParameterSetName = 'Url')]
    [System.String]
    $DownloadUri,

    [Parameter()]
    [System.String]
    $UpdateSearchFilter = 'IsHidden = 0',

    [Parameter()]
    [pscredential]
    $Credential
)

The parameters should be quite clear. A list of computer names to connect to, either the source path or the download link to the wsusscn2.cab, the update search filter as well as a credential in case Kerberos is not available. If the parameter set 'Url' is chosen, the script will attempt to download the cab file to [System.IO.Path]::GetTempPath().

In general, the script will copy the wsusscn2.cab to all targets first. How long this step takes depends entirely on you: If the administrative share can be accessed, we will use Copy-Item. If it is not available, but the source system has at least WMF5.0 installed and the target system is running at least WMF3 we will make use of the new ToSession parameter of Copy-Item. If all else fails, we will use Lee Holmes' methods Send-File and Write-File to stream the file via WinRM to our targets. These methods range from fastest to slowest in this order.

 try
{
    $osRoot = Invoke-Command -Session $session -ScriptBlock { $env:SystemDrive } -ErrorAction Stop
}
catch
{
    Write-Host ('Error retrieving OS root path from {0}. Assuming issue with the connection. Error was {1}' -f $computer, $_.Exception.Message)
    Write-Error -Message ('Error retrieving OS root path from {0}. Assuming issue with the connection. Error was {1}' -f $computer, $_.Exception.Message)
}

try
{
    $osPSVersion = Invoke-Command -Session $session -ScriptBlock { $PSVersionTable.PSVersion.Major } -ErrorAction Stop
}
 catch
{
    Write-Host ('Error retrieving OS Powershell version from {0}. Assuming issue with the connection. Error was {1}' -f $computer, $_.Exception.Message)
    Write-Error -Message ('Error retrieving OS Powershell version from {0}. Assuming issue with the connection. Error was {1}' -f $computer, $_.Exception.Message)
}

$adminShare = '\\{0}\{1}$' -f $computer, ($osRoot -replace '[:\\]')
$useSmb = Test-Path $adminShare

$destination = (Join-Path -Path $osRoot -ChildPath wsusscn2.cab)

On the target, a script block is executed. In this block, we instanciate the COM classes Microsoft.Update.Session and Microsoft.Update.ServiceManager and make use of a couple of methods to initiate the offline scan.
First of all, an UpdateManager object is needed that knows the offline scan cab file:

 $UpdateService = $UpdateServiceManager.AddScanPackageService("Offline Sync Service", $Destination)

Afterwards an UpdateSearcher will be created with the offline scan repository. Notice here the ServiceID which is set to the GUID of the updater service. The server selection value of three classifies this as an unmanaged server ( https://msdn.microsoft.com/en-us/library/windows/desktop/aa387280(v=vs.85).aspx ).

 $UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$UpdateSearcher.ServerSelection = 3
$UpdateSearcher.ServiceID = $UpdateService.ServiceID

Lastly, the updates are located by using a search filter:

 $SearchResult = $UpdateSearcher.Search($UpdateSearchFilter)

All results are retrieved and will be returned from the script. All in all, you can simply use the script like this:

 .\GetMissingUpdates.ps1 -ComputerName serverA,serverB,serverC -Path D:\wsusscn2.cab -Credential (Get-Credential) -Verbose

Thanks for taking your time and reading all the way to the bottom and enjoy!