"Distrust and caution are the parents of security." -Benjamin Franklin

If you've ever deployed Windows Updates to clients on your network, you have probably been asked by your manager(s) what KB's were deployed, and when if an issue comes up on a workstation or server. Unfortunately, sometimes the built-in WSUS reporting tool can leave you frustrated and doesn't have great functionality for generating them outside of the WSUS management GUI. You have an alternative; a PowerShell Windows Update report!

How to Tell When a PowerShell Report is Needed

I was recently asked by a group of managers that were working on validating a security vulnerability scan for some assistance. This vulnerability scan was claiming that a set of systems were missing particular Microsoft KB's, KB's that were recently approved, deadlined, and showing as installed in the WSUS management console.

I sent some screenshots of the console status along with my sysadmin reply. I didn't give it much thought at the time because I was busy with other projects and this was a routine request.

A day or so went by, and another vulnerability scan was run, producing the same results. Management was not convinced that the updates were installed. Having issues with WSUS from time to time, I started to distrust the built in reports and the management console.

To be cautious, and a little more diligent, I decided to bypass the WSUS management console and go straight to the workstations and servers that were showing up in the security vulnerability scan.

Brainstorming a Windows Update Report

Luckily, the security vulnerability scan only found about 4 workstations and 12 servers with these supposedly missing KB's. So I created a simple list in a text file using the fully qualified domain name (FQDN) of each host. �I also knew for a fact, that the missing KB's would have been installed in the past 30 days as I just completed a maintenance cycle.

With this knowledge in hand, I jotted down some pseudo code to help me begin. Here's what I outlined:

  • Store my text file that contains the list of hosts.
  • For each of the hosts in that file, run a command.
  • The command must gather installed KB's installed in the last 30 days.
  • The output only needs to contain the hostname, KB/HotFix ID, and the install date.
  • The output needs to be readable, and just needs to be a simple file.
  • No fancy coding needed, just comparing visually to what WSUS reporting was displaying.

Based on my notes, I had a good idea of what I was looking for and what cmdlets I might need. The primary focus was on the Get-HotFix cmdlet. This cmdlet queries all the hotfixes (more commonly referred to as security updates) that have been applied to a Windows host. You can read more about this cmdlet and how to use it here.

Get-HotFix does not support implicit remoting so I needed to come up with method to run this cmdlet on the systems I needed to report on. Invoke-Command does and you can pass multiple values to the ComputerName parameter.

I already have saved a list of hosts I am targeting, so I'll save myself some typing and store those hosts as a variable. To do so, I'll have to assign a variable name and make the value the list of hosts. Get-Content will read the content of the text file line by line creating an array of sorts. Let's call this array $Hosts. Now I have a command, some data to feed to the next set of commands, but I need to make the resulting data readable and concise.

I want to take a moment here to emphasize "Filter First, Format Last." . Remembering this will help you when working with these types of scripts. Now, running the Get-Hotfix cmdlet by itself will typically result in a long list of updates that have been applied to a host.

PS51> Get-HotFix

Source        Description      HotFixID      InstalledBy          InstalledOn
------        -----------      --------      -----------          -----------
MACWINVM      Update           KB2693643     MACWINVM\Administ... 3/14/2019 12:00:00 AM
MACWINVM      Update           KB4100347     NT AUTHORITY\SYSTEM  2/17/2019 12:00:00 AM
MACWINVM      Update           KB4230204     NT AUTHORITY\SYSTEM  7/6/2018 12:00:00 AM
MACWINVM      Security Update  KB4287903     NT AUTHORITY\SYSTEM  7/8/2018 12:00:00 AM
MACWINVM      Security Update  KB4338832     NT AUTHORITY\SYSTEM  7/21/2018 12:00:00 AM
MACWINVM      Update           KB4338853     NT AUTHORITY\SYSTEM  7/6/2018 12:00:00 AM
MACWINVM      Update           KB4343669     NT AUTHORITY\SYSTEM  7/19/2018 12:00:00 AM
MACWINVM      Security Update  KB4343902     NT AUTHORITY\SYSTEM  8/15/2018 12:00:00 AM
MACWINVM      Update           KB4346084     NT AUTHORITY\SYSTEM  5/11/2019 12:00:00 AM
MACWINVM      Update           KB4456655     NT AUTHORITY\SYSTEM  9/12/2018 12:00:00 AM
--snip--

Filtering helps gather just the information you need.

Without filtered data, formatting is useless at this point. Think of filtering as your data type requirements, and formatting as how you want that data displayed. For my purposes, I already had the requirements thought out. I needed to get updates installed in the past 30 days.

To filter, I will need to use the Where-Object cmdlet and then pass along some member properties and comparison operators with a dash of math. To do this, I will take every object returned ($_) from Get-HotFix and pass those to Where-Object �to find all updates installed on a date that is greater than (-gt) today's date (or whenever I run the script) minus (-30) days ago. That will get the initial data I'm looking for.

Get-HotFix | Where-Object { $_.InstalledOn -gt ((Get-Date).AddDays(-30)) }

But I want to filter the returned objects and their properties a little more. This is where Select-Object will help, allowing me to further trim the amount of data to be displayed to just a couple of crucial properties.

Get-HotFix | Where-Object { $_.InstalledOn -gt ((Get-Date).AddDays(-30)) } |
Select-Object -Property PSComputerName, Description, HotFixID, InstalledOn

Now that I have the data properly filtered, now I can move on to formatting the results into a usable format. To do so I'll pipe ( | ) the results from my previous filtering to Format-Table -Autosize and output as a file type of my choosing. I'll need to use Append and -ErrorAction SilentlyContinue parameters to ensure that each result is written to the next line in the output file and if an error occurs, it won't cause the rest of the hosts to not be contacted.

Format-Table -AutoSize |
Out-File -Encoding utf8 -FilePath '.\Recent_OS_Updates.txt' -Append -ErrorAction SilentlyContinue

I chose to go with a text file because I didn't require anything fancy. You can change the output to meet your needs. My output looked something similar to this:

Example Output text file

Here's the final script came up with and used:

$Hosts = Get-Content -Path '.\hosts.txt'
Invoke-Command -ComputerName $Hosts -ScriptBlock {
    Get-HotFix | Where-Object {
        $_.InstalledOn -gt ((Get-Date).AddDays(-30))
    } | Select-Object -Property PSComputerName, Description, HotFixID, InstalledOn
} | Format-Table -AutoSize |
Out-File -Encoding utf8 -FilePath '.\Recent_OS_Updates.txt' -Append -ErrorAction SilentlyContinue

For me, this was simple, concise, and offered proof that the KB's were indeed installed. The report was well received by the management team and in a format easily read.