Finding the Real "Last Patched" Day (Interim Version), (Tue, May 3rd)

This post was originally published on this site

I've been using a PowerShell script since forever that enumerates the patch dates across an AD domain.  Yesterday I found a use case where it was broken.

I found a number of servers that received an update last month for Windows Update itself (KB5011570: Servicing stack update for Windows 10, version 1607 and Server 2016: March 8, 2022).  However, they did not install The April or in some cases even the March cumulative updates.  In the graphic below, you'll see that "Servicing stack update" KB5011570, first in the list, with February's Cumulative update (KB5010359) as SECOND.  If I'm measuring patch compliance for a client's domain, or looking for servers that have missed a patch cycle, it's that second patch I'm interested in.

However, my previous script happily enumerated the "last patch date" and found that April update of course, which should not have counted (given what I'm enumerating for)

So, how to fix this?  The easy answer is "look for the word "Cumulative" in the patch description.  It was at this point that I discovered that the get-hotfix and the gwmi methods of collecting patches run against the local machine, and the actual plain-language text description of the update is NOT kept locally, you need to make a web request using the KB number to get that!  These commands collect the KB number and call it a day.  The web request you woudl make from a browser (more on this later) to collect the "real" description looks like:$kbnum

This collects a full HTML page with a TON of information:

You can parse this until you reach sub-atomic particles, but in my script all I'm interested in is if the word "Cumulative" exists in the patch titles.  We find this in the "outertext" section of the HTML that's returned – which you get from:
$returntable = $WebResponse.ParsedHtml.body.getElementsByTagName("table") | Where {$_.className -match "resultsBorder"}
write-host $returntable.outertext   # only needed for debugging and illustration

Now add a quick check:
if ($returntable.outertext -like "*Cumulative*")

Before I make these calls, I sort the full patch list in descending order, so I can start from the most recent one, looking for the newest patch with that key word "Cumulative".  The final script is in my github at:

And yes, I will be creating a CIS Controls Version 8 set of scripts (sometime soon).

The final script, with these changes is:

$pcs = get-adcomputer -filter * -property Name,OperatingSystem,Operatingsystemversion,LastLogonDate,IPV4Address
$patchinfo = @()
$count = $pcs.count
foreach ($pc in $pcs) {
    # keep total progress count
    write-host "Host" $i "of" $count "is being checked"
    if (Test-Connection -ComputerName $pc.DNSHostName -count 2 -Quiet) {
        # echo the host being assessed (only live hosts hit this print)
        write-host $pc.dnshostname "is up, and is being assessed"
        $tempval = new-object psobject
        $hfs = get-hotfix -computername $pc.dnshostname | sort -descending InstalledOn
        # look only for the latest Cumulative update
        foreach ($hf in $hfs) {
                $kbnum = $hf.hotfixid
                $WebResponse = Invoke-WebRequest "$kbnum"
                $returntable = $WebResponse.ParsedHtml.body.getElementsByTagName("table") | Where {$_.className -match "resultsBorder"}
                # write-host $returntable.outertext    # no need to write this to the screen unless debugging
                if ($returntable.outertext -like "*Cumulative*")  {
                     $lasthf = $hf
        $tempval | add-member -membertype noteproperty -name Name -value $pc.dnshostname
        $tempval | add-member -membertype noteproperty -name PatchDate -value $lasthf.installedon
        $tempval | add-member -membertype noteproperty -name OperatingSystem -value $pc.OperatingSystem
        $tempval | add-member -membertype noteproperty -name OperatingSystemVersion -value $pc.OperatingSystemVersion
        $tempval | add-member -membertype noteproperty -name IpAddress -value $pc.IPV4Address
        $tempval | add-member -membertype noteproperty -name LastLogonDate -value $pc.LastLogonDate
        $patchinfo += $tempval
$patchinfo | export-csv -path ./patchdate.csv


This script is by no means done!  This is a quick-and-dirty "how can I get the info really quickly" script, since I needed it before planning a series of updates.  There's definitely an API for this, and Microsoft has also published a graphql approach (which is WAY too complicated for what I'm collecting)

I also found a nice module in PowershellGallery "PSWindowsUpdate", but it doesn't seem to work 100% yet – the get-wuhistory command hangs fairly consistently.  (  Once that's fixed though it'll be a good way to go!.  

The WindowsUpdateProvider module from Microsoft also looks great, but is native to W11 and S2019 – something version agnostic that doesn't need to be installed would be ideal.  Lots of us run scripts like this on customer servers, so you can't depend on a specific OS version, and installing additional tools that the client hasn't approved is also generally frowned upon  ..

What I'm really looking for is a a good, clear PowerShell call using a supported API for this, which doesn't require an third party module and isn't OS or PowerShell version-specific.  My google-fu isn't finding this today, so for now I have the approach above – but I certainly have not given up.  If anyone can point me at such a thing, I'll gladly update the script above and repost it when it's finished.  The method I've described above works, but will only work until some dev at Microsoft decides to change that Update Catalog results page.

Enjoy! (and a follow up to come soon!)

Rob VandenBrink

(c) SANS Internet Storm Center. Creative Commons Attribution-Noncommercial 3.0 United States License.

Leave a Reply

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