The poor person’s way to finding the last patch date on multiple systems

Background

If you don’t have SCCM reporting in your Windows environment (extremely long story) and you need to figure out the last time a server had its OS patched for audit purposes (and your own peace of mind), here is a quick-and-dirty script to find the date the last patch was installed on the server (and, by extension, it was last patched).

Naturally, this isn’t going to help you much if you’re in the habit of installing hotfixes arbitrarily. The assumption is that you regularly install the month’s patches across your fleet, and so the last patch date will reflect when this group of patches was applied.

This script is written for Powershell 4 due to the way I create a custom PS-Object to hold the output. If you’re happy creating custom PS objects with multiple fields yourself, then this could be reworked for PS 3 (which includes Get-Hotfix) or even PS 2.0 if you’re happy to bake your own WMI for the hotfix query. I have Windows 2012 R2 available to me, so I took the easy route.

Syntax

I’m going to describe the major components of the script.

Firstly, gathering the list of computers. We only want servers – if the OperatingSystem attribute of the computer account contains the word “server”, that’s what it is. There are instances where you have a computer account that belongs to a cluster or non-Windows system. I recommend putting something specific to those in the Description field for non-server accounts that you can use as an exclusion filter.

Here we’re using Get-ADComputer to gather up all computer objects in the domain, and filter by the Operating System and whether the computer account is enabled. If you want to target a specific OU, you can use use SearchScope to limit it. Here, we’re gathering OS information as well to help us target our patches, if needed.

$serverlist = get-adcomputer -Filter { Enabled -eq $true -and OperatingSystem -like "*Server*" } -Properties OperatingSystem | select Name,OperatingSystem

Once we have that, we wrap nearly everything else into a ForEach loop. For each server in the list, we’re going to contact it to get the hotfix information, and then store the data. Also, as our loop kicks through each server, it does a quick Test-Connection to see if it responds to a ping. If not, onto the next!

foreach ($server in $serverlist) {
    $connex = Test-Connection $server.Name -Count 1 -TTL 100 -Quiet
    if ($connex) {
        [do lots of stuff here....
    }
    else {
        #server can't be contacted
        $patch =  "offline"
    }
    [and a little bit here...]
}

The guts of the process is using Get-Hotfix to gather up the list of patches on each server. Sure, we could use WMI, but if you have Get-Hotfix available, why go through the angst? This will do a remote call to Windows 2003 servers as well, no problem. It may even do 2000 servers, but who knows? Enjoy the Windows 2003 functionality for a few more months..

You’ll note this looks a bit convoluted. Basically, it’s Get-Hotfix -computername [servername]. We’re not doing PS-Remoting or anything like that (although if you’re trying to gather up info from non-domain servers, for example, you probably have to). However, at the same time, we to sort by the InstalledOn date to readily find the last patch date.

UNFORTUNATELY, if you’re not using a system with a US date format, you can’t simply sort the hotfixes by date and expect it to work (it’s a text date, not actually a date-time). So here we have a complicated custom object, which selects the data into a datetime format using localtime, which we can then sort by InstalledOn, and use [-1] to select the last hotfix listed in the array (which will be the most recent hotfix). From there, we can select the actual date information of the last-installed hotfix.

I’m breaking up the lines a bit below to show the elements in the Get-Hotfix

(Get-HotFix -computername $using:server.Name | 
Select @{l="InstalledOn";e={[DateTime]$_.psbase.properties["installedon"].value}} | 
Sort InstalledOn)[-1] | 
Select -ExpandProperty InstalledOn

Now, the next tricky bit is the fact that if your remote server decides it’s not going respond to a WMI query, it takes 5 minutes (or longer, I gave up) to time out. Obviously with hundreds of servers, your script will take a few days to execute if you leave it to fail to the timeout period.

So what we’ve done is jam that Get-Hotfix command above into a variable called $code, and then we’re going to run that as a background job with a ten second timeout. Once the job returns with the output or exceeds the timeout, the data (the date of the last patch time) is returned if it exists into the $patch variable, and the background job is then destroyed. ($timeoutseconds is set earlier in the script). Yay to the crew at StackOverflow for solving this little quandary.

$code = {
            (Get-HotFix -computername $using:server.Name | Select-Object @{l="InstalledOn";e={[DateTime]$_.psbase.properties["installedon"].value}}| Sort InstalledOn)[-1] | Select -ExpandProperty InstalledOn
         }
         $j = Start-Job -ScriptBlock $code
         if (Wait-Job $j -Timeout $timeoutSeconds) {
            $patch = Receive-Job $j
         }
         Remove-Job -force $j
    }

The last major part to the script is creating a custom PS object to store all the data as it passes by in the loop. Fortunately, I found this really nice constructor for Powershell 4 and up. I am still having problems wrapping my head around this area (for some reason, I had no problem with Perl hashes, which are the same thing, but there were lots of nifty modules that helped shortcut the thing). So I stole this unashamedly to create the custom PS object with three fields to store the server name, last patch date, and operating system version.

What we’re also doing is printing out the data from each server in the console, and then adding the results for each server to the $Results array that’s gathering up everything from $hash as it passes through the ForEach loop. $hash naturally gets new data with each loop.

$hash=[ordered]@{
        Computername=$server.Name
        PatchDate=$patch
        OperatingSystem=$Server.OperatingSystem
    }
    [pscustomobject]$hash
    $results += [pscustomobject]$hash

At the very end, and outside the ForEach loop, we can output our results any way we see fit. Here, I’m outputting just the computer names to one file, and then I’m exporting the full results to a CSV file. With both, I’m sorting by computer name, but you can obviously sort by OS version or patch date.

#output just the computer names, sorted alphabetically
$results | sort computername | select computername | Out-file E:\Server_names.txt

#output the computer names, patch date and operating system, sorted by computer name
$results | sort computername | Export-Csv E:\Server_patches.csv 

Full script is shown below.

Result

This shows the on-screen output, but the file output all works as expected, of course. Note that the patch datetime always shows as 12 am. As it’s a datetime object, you can format it how you like.

patches

Script

$timeoutSeconds = 10
$results = @()

$serverlist = get-adcomputer -Filter { Enabled -eq $true -and OperatingSystem -like "*Server*" } -Properties OperatingSystem | select Name,OperatingSystem

foreach ($server in $serverlist) {
    #re-initialise $hash
    $hash = @{}
    $connex = Test-Connection $server.Name -Count 1 -TTL 100 -Quiet
    if ($connex) {
        $code = {
            (Get-HotFix -computername $using:server.Name | Select-Object @{l="InstalledOn";e={[DateTime]$_.psbase.properties["installedon"].value}}| Sort InstalledOn)[-1] | Select -ExpandProperty InstalledOn
         }
         $j = Start-Job -ScriptBlock $code
         if (Wait-Job $j -Timeout $timeoutSeconds) {
            $patch = Receive-Job $j
         }
         Remove-Job -force $j
    }
    else {
        #server can't be contacted
        $patch =  "offline"
    }
    $hash=[ordered]@{
        Computername=$server.Name
        PatchDate=$patch
        OperatingSystem=$Server.OperatingSystem
    }
    [pscustomobject]$hash
    $results += [pscustomobject]$hash
}

#output just the computer names, sorted alphabetically
$results | sort computername | select computername | Out-file E:\Server_names.txt

#output the computer names, patch date and operating system, sorted by computer name
$results | sort computername | Export-Csv E:\Server_patches.csv 
Advertisements
This entry was posted in WindowsServer and tagged , , . Bookmark the permalink.

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 )

Google+ photo

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

Connecting to %s