Find the Compliance State for Software Updates on Remote Computers with PowerShell

Since the recent WannaCrypt ransomware attacks, many organisations have wanted to get the patch status of their systems to see if they are protected. Several reports and SQL queries for ConfigMgr were quickly posted online to help enterprises identify at-risk machines. But what for those who don’t use ConfigMgr, or what if you want to get the real-time status for a patch on a particular system instead of relying on deployment results or hardware inventory data?

For this, I wrote a couple of PowerShell scripts that will connect to a remote system, or group of systems, and check the status of any number of patches based on information from WMI. The scripts will use the Win32_QuickFixEngineering class on all targeted systems, as well as the SCCM WMI classes for those that are using ConfigMgr.

There are two scripts, one is single-threaded which is fine if you want to just check the local system, or a couple of remote systems.  If you want to check many remote systems at the same time, then use the multi-threaded version which will give you significantly quicker results.

Both scripts work the same way, but bear in mind the multi-threaded version will use more system resources, especially CPU, so set the throttle-limit (number of simultaneous threads) to something sensible for your CPU. The default limit is 32. The multi-threaded version uses my [BackgroundJob] custom class which will only work in PowerShell 5 +, and you will need to run the code for that class in your PowerShell session first.

To use the scripts, simply pass the computer name/s and Article ID/s (KB number) for the patch.

To run against the local machine:


Get-PatchStatus -ArticleID 4019264

Against a remote machine:


Get-PatchStatus -ComputerName PC001 -ArticleID 4019264

Against several remote machines:


Get-PatchStatus -ComputerName PC001,PC002,PC003,SRV001,SRV002 -ArticleID 4019264

Using several Article IDs


Get-PatchStatus -ComputerName PC001,PC002,PC003 -ArticleID @(4019264,4019265,4016871)

Set the throttle limit on the multi-threaded version, and using verbose output:


Get-PatchStatus -ComputerName $Computers -ArticleID $ArticleIDs -ThrottleLimit 64 -Verbose

Result

result

You might want to output to GridView if you have a lot of results for better filtering.

Note that if no results are returned in the (SCCM) or (QFE) columns then the patch is not installed, or if there was an error connecting to the remote system, this will be returned in the ‘Comments’ column.

The Scripts

Single-threaded version


# Get-PatchStatus.ps1
# Gets the status of software update/s on remote system/s
#
# The is the SINGLE-THREADED version
#
# Author: Trevor Jones
# May-2017
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[array]$ArticleID
)
Begin
{
# Create a datatable to hold the results
$Table = New-Object System.Data.DataTable
$Table.Columns.AddRange(@("ComputerName","Article ID","SCCM Installed Status","SCCM Pending Status","WMI InstalledOn Date","Comments"))
}
Process
{
# Process each computer
Foreach ($Computer in $ComputerName)
{
Try
{
# Make a cim session
Write-Verbose "[$Computer] Creating a Cim session"
$CimSession = New-CimSession ComputerName $Computer OperationTimeoutSec 5 ErrorAction Stop
}
Catch
{
$Comments = $_.Exception.Message
}
# Report any error
If ($Comments)
{
[void]$Table.Rows.Add($Computer,$null,$null,$null,$null,$Comments)
Remove-Variable Name Comments Force
}
Else
{
# Process each KB
Foreach ($KB in $ArticleID)
{
# Check SCCM WMI
Write-Verbose "[$Computer] Checking CCM WMI…"
Try
{
$CCMUpdate = Get-CimInstance CimSession $CimSession Query "SELECT * FROM CCM_UpdateStatus where Article=$KB" Namespace 'root\ccm\SoftwareUpdates\UpdatesStore' ErrorAction Stop | Select First 1 | Select ExpandProperty Status
}
Catch {}
# Check Win32 WMI
Write-Verbose "[$Computer] Checking Win32 WMI…"
Try
{
$WMIUpdate = Get-CimInstance CimSession $CimSession Query "SELECT * FROM Win32_QuickFixEngineering where HotFixID='KB$KB'" Namespace 'root\cimv2' ErrorAction Stop | Select ExpandProperty InstalledOn
}
Catch {}
# If relevant, check if the update is in a pending state
If ($CCMUpdate -ne "Installed" -or !$WMIUpdate)
{
Write-Verbose "[$Computer] Checking CCM Pending Status…"
Try
{
$CCMUpdatePending = Get-CimInstance CimSession $CimSession Query "SELECT * FROM CCM_SoftwareUpdate where ArticleID=$KB" Namespace 'ROOT\ccm\ClientSDK' ErrorAction Stop
}
Catch {}
If ($CCMUpdatePending)
{
# Convert the EvaluationState value
Switch ($CCMUpdatePending.EvaluationState)
{
0 {$PendingState = "None"}
1 {$PendingState = "Available"}
2 {$PendingState = "Submitted"}
3 {$PendingState = "Detecting"}
4 {$PendingState = "PreDownload"}
5 {$PendingState = "Downloading"}
6 {$PendingState = "WaitInstall"}
7 {$PendingState = "Installing"}
8 {$PendingState = "PendingSoftReboot"}
9 {$PendingState = "PendingHardReboot"}
10 {$PendingState = "WaitReboot"}
11 {$PendingState = "Verifying"}
12 {$PendingState = "InstallComplete"}
13 {$PendingState = "Error"}
14 {$PendingState = "WaitServiceWindow"}
15 {$PendingState = "WaitUserLogon"}
16 {$PendingState = "WaitUserLogoff"}
17 {$PendingState = "WaitJobUserLogon"}
18 {$PendingState = "WaitUserReconnect"}
19 {$PendingState = "PendingUserLogoff"}
20 {$PendingState = "PendingUpdate"}
21 {$PendingState = "WaitingRetry"}
22 {$PendingState = "WaitPresModeOff"}
23 {$PendingState = "WaitForOrchestration"}
}
}
}
# Add the results to the table
If ($PendingState)
{
[void]$Table.Rows.Add($Computer,$KB,$CCMUpdate,$PendingState,$WMIUpdate)
Remove-Variable Name PendingState Force
}
Else
{
[void]$Table.Rows.Add($Computer,$KB,$CCMUpdate,$null,$WMIUpdate)
If ($CCMUpdate)
{
Remove-Variable Name CCMUpdate Force
}
If ($WMIUpdate)
{
Remove-Variable Name WMIUpdate Force
}
}
}
}
# Close the cim session
If ($CimSession)
{
Remove-CimSession CimSession $CimSession
}
}
}
End
{
# Return the results
Return $Table
}

Multi-threaded version


# Get-PatchStatus.ps1
# Gets the status of software update/s on remote system/s
#
# The is the MULTI-THREADED version
#
# IMPORTANT: Requires my [BackgroundJob] custom class, available here: https://smsagent.wordpress.com/posh-5-custom-classes/background-job/
#
# Author: Trevor Jones
# May-2017
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[array]$ArticleID,
[Parameter(Mandatory=$false
)]
[int]$ThrottleLimit = 32
)
Begin
{
# Create some arrays
$Jobs = @()
$Results = @()
# Start a timer
$Timer = New-Object System.Diagnostics.Stopwatch
$Timer.Start()
}
Process
{
# Process each computer
Foreach ($Computer in $ComputerName)
{
$Code = {
Param($Computer,$ArticleID)
# Create a datatable to hold the results
$TempTable = New-Object System.Data.DataTable
$TempTable.Columns.AddRange(@("ComputerName","Article ID","SCCM Installed Status","SCCM Pending Status","WMI InstalledOn Date","Comments"))
Try
{
# Make a cim session
$CimSession = New-CimSession ComputerName $Computer OperationTimeoutSec 5 ErrorAction Stop
}
Catch
{
$Comments = $_.Exception.Message
}
# Report any error
If ($Comments)
{
[void]$TempTable.Rows.Add($Computer,$null,$null,$null,$null,$Comments)
Remove-Variable Name Comments Force
}
Else
{
# Process each KB
Foreach ($KB in $ArticleID)
{
# Check SCCM WMI
Try
{
$CCMUpdate = Get-CimInstance CimSession $CimSession Query "SELECT * FROM CCM_UpdateStatus where Article=$KB" Namespace 'root\ccm\SoftwareUpdates\UpdatesStore' ErrorAction Stop | Select First 1 | Select ExpandProperty Status
}
Catch {}
# Check Win32 WMI
Try
{
$WMIUpdate = Get-CimInstance CimSession $CimSession Query "SELECT * FROM Win32_QuickFixEngineering where HotFixID='KB$KB'" Namespace 'root\cimv2' ErrorAction Stop | Select ExpandProperty InstalledOn
}
Catch {}
# If relevant, check if the update is in a pending state
If ($CCMUpdate -ne "Installed" -or !$WMIUpdate)
{
Try
{
$CCMUpdatePending = Get-CimInstance CimSession $CimSession Query "SELECT * FROM CCM_SoftwareUpdate where ArticleID=$KB" Namespace 'ROOT\ccm\ClientSDK' ErrorAction Stop
}
Catch {}
If ($CCMUpdatePending)
{
# Convert the EvaluationState value
Switch ($CCMUpdatePending.EvaluationState)
{
0 {$PendingState = "None"}
1 {$PendingState = "Available"}
2 {$PendingState = "Submitted"}
3 {$PendingState = "Detecting"}
4 {$PendingState = "PreDownload"}
5 {$PendingState = "Downloading"}
6 {$PendingState = "WaitInstall"}
7 {$PendingState = "Installing"}
8 {$PendingState = "PendingSoftReboot"}
9 {$PendingState = "PendingHardReboot"}
10 {$PendingState = "WaitReboot"}
11 {$PendingState = "Verifying"}
12 {$PendingState = "InstallComplete"}
13 {$PendingState = "Error"}
14 {$PendingState = "WaitServiceWindow"}
15 {$PendingState = "WaitUserLogon"}
16 {$PendingState = "WaitUserLogoff"}
17 {$PendingState = "WaitJobUserLogon"}
18 {$PendingState = "WaitUserReconnect"}
19 {$PendingState = "PendingUserLogoff"}
20 {$PendingState = "PendingUpdate"}
21 {$PendingState = "WaitingRetry"}
22 {$PendingState = "WaitPresModeOff"}
23 {$PendingState = "WaitForOrchestration"}
}
}
}
# Add the results to the table
If ($PendingState)
{
[void]$TempTable.Rows.Add($Computer,$KB,$CCMUpdate,$PendingState,$WMIUpdate,$Comments)
Remove-Variable Name PendingState Force
}
Else
{
[void]$TempTable.Rows.Add($Computer,$KB,$CCMUpdate,$null,$WMIUpdate,$Comments)
If ($CCMUpdate)
{
Remove-Variable Name CCMUpdate Force
}
If ($WMIUpdate)
{
Remove-Variable Name WMIUpdate Force
}
}
}
}
# Close the cim session
If ($CimSession)
{
Remove-CimSession CimSession $CimSession
}
# Return the results
Return $TempTable
}
$Job = [BackgroundJob]::new($Code,@($Computer,$ArticleID))
$Jobs += $Job
if (($Jobs.GetStatus() | where {$_.State -eq "Running"}).Count -ge $ThrottleLimit)
{
Do {Start-Sleep Seconds 1}
Until (($Jobs.GetStatus() | where {$_.State -eq "Running"}).Count -lt $ThrottleLimit)
}
Write-Verbose "[$Computer] Starting job"
$Job.Start()
}
}
End
{
# Wait until all jobs have completed
Do {}
Until ($Jobs.GetStatus().State -notcontains "Running")
# Gather results and cleanup runspaces
$Jobs | foreach {
[void]$_.Receive()
$Results += $_.Result
$_.Remove()
}
$Timer.Stop()
Write-Verbose "Completed in $($Timer.Elapsed.Minutes) minutes and $($Timer.Elapsed.Seconds) seconds."
# Return the results
Return $Results
}

One thought on “Find the Compliance State for Software Updates on Remote Computers with PowerShell

  1. Thanks for this article. It shows an interesting solution. Especially because the status is queried, because many patches are only active after a reboot.
    Unfortunately, since the introduction of cumulative patches, the problem is that the KB numbers change monthly. And for different systems it is also again different KB numbers.
    Here, as with many other solutions and scripts, you would have to specify several patches (in the script or an extra file). That would still be my wish for improvement perhaps even a bit to beautify or simplify. For Wanna Cry, there are a lot of possible KBs, as it would be good to call it slightly better than with -ArticleID @ (4019264,4019265,4016871, xxxxxxx, xxxxxx, xxxxxxx).
    I’m looking forward to test the script tomorrow and remember for the future.

    Thanks again. 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.