It is a fact of life for any help desk or support professional that managing patches, updates and hot fixes is an important and often critical task. The challenge, especially during a critical event like a rampant malware exploit, is to identify where a particular hot fix is installed and more importantly, where it is not. How do you check not only one computer but 100 computers?

When an update or hotfix, and there is a distinction, is installed on a Microsoft Windows platform it is recorded by Windows Management Instrumentation (WMI). The specific class is Win32_QuickFixEngineering. In the past, you might have relied on VBScript to create complex scripts to determine what hot fixes were installed on a given computer. With the arrival of Windows PowerShell, we were given a better alternative with the Get-WMIObject cmdlet. PowerShell 2.0 introduced a new tool that leverages WMI, making this even easier: Get-HotFix.

Get-HotFix

The Get-HotFix cmdlet makes finding installed hot fixes and patches a snap. On any computer running PowerShell 2.0, all you need to do is run the cmdlet to retrieve a collection of hot-fix objects. Note, I'm collectively referring to these entries as hot fixes, but they can include updates, security updates, service packs and more.

PS C:\> get-hotfix
Source''''''' Description''''' HotFixID''''' InstalledBy''''' InstalledOn
------''''''' -----------''''' --------''''' -----------''''' -----------
QUARK'''''''' Update'''''''''' KB958830''''' QUARK\Jeff'''''' 7/6/2010 12:'
QUARK'''''''' Security Update' KB2079403'''' QUARK\Jeff'''''' 8/17/2010 12'
QUARK'''''''' Update'''''''''' KB2158563'''' NT AUTHORITY\... 10/7/2010 12'
'

While this looks like a text list, it is actually a collection of Win32_QuickFixEngineering objects. This means you can use standard PowerShell cmdlets to sort or filter these objects. For example, to find all updates installed on or after 10/1/2010, I can use a PowerShell expression like this.

PS C:\> $hot=get-hotfix | where {$_.InstalledOn -gt "10/1/2010"}
PS C:\> $hot.count
15
PS C:\> $hot[0] | format-list
Description'''''''' : Update
FixComments'''''''' :
HotFixID''''''''''' : KB2158563
InstallDate'''''''' :
InstalledBy'''''''' : NT AUTHORITY\SYSTEM
InstalledOn'''''''' : 10/7/2010 12:00:00 AM
Name''''''''''''''' :
ServicePackInEffect :
Status''''''''''''' :

I saved all hot fixes to a variable, $hot. As you can see from the count property, there are 15 items. The first one, in the collection, KB2158563, was installed on 10/7/2010. What I want you to take away is the object structure and how I gathered the information.

By default, the cmdlet returns all hot fix information although you can also get a single hotfix by its ID number.

PS C:\> get-hotfix -id KB983590 | format-list
Description'''''''' : Security Update
FixComments'''''''' :
HotFixID''''''''''' : KB983590
InstallDate'''''''' :
InstalledBy'''''''' : QUARK\Jeff
InstalledOn'''''''' : 8/17/2010 12:00:00 AM
Name''''''''''''''' :
ServicePackInEffect :
Status''''''''''''' :

The cmdlet also makes it easy to retrieve hot fix information by the description. Think of this more as a category. I can filter hot fix information by using the 'Description parameter.

PS C:\> get-hotfix -Description hotfix | select HotfixID,InstalledOn
HotfixID'''''''''''''''''''''''''''''''''''''''' InstalledOn
--------'''''''''''''''''''''''''''''''''''''''' -----------
KB975467'''''''''''''''''''''''''''''''''''''''' 6/12/2010 12:00:00 AM
KB976422'''''''''''''''''''''''''''''''''''''''' 6/12/2010 12:00:00 AM
KB979538'''''''''''''''''''''''''''''''''''''''' 10/7/2010 12:00:00 AM

In the above example I selected only the HotFixID and InstalledOn properties.

There is one major caveat with all of this, which also leads into the next section of this article. The Win32_QuickFixEngineering class has changed slightly over time and requires at least Windows XP. However, every OS has some particular quirks. For example on Windows Vista, you may not get any value for InstalledOn. On Windows XP and Windows 7 the HotFixID includes the 'KB' prefix, but on Vista it does not. On computers that have been operational for many years, typically Windows XP and Windows 2003 you might even find hotfix objects with very little information or incomplete, when compared with Windows 7. XP and 2003, for example, don't provide the full name for InstalledBy, only the name. Thus you see Jeff instead of MyCompany\Jeff. Depending on your environment these factors may play a role in how you use this cmdlet.

Remote Management

As I have alluded to, you can retrieve hot fix information from one or more remote computers as well by using the 'Computername parameter.

PS C:\> get-hotfix -Description Hotfix -ComputerName Serenity |
>> Select HotfixID
>>
HotfixID
--------
KB975467
KB975778
KB976422
KB979538

I connected to the computer called Serenity and retrieved all hot fixes with a description (or category) of Hotfix. In this case, I just wanted to see the HotFixID.

It is important to note that this type of remote connectivity is NOT using the remoting features available in PowerShell 2.0. The remote computer does not need to be running PowerShell 2.0. In fact, it doesn't need to be running PowerShell at all!

Get-Hotfix uses WMI to establish remote connections, which means you need RPC connectivity to any remote computer. Generally, if you configure a system's firewall to enable remote management, you should be able to use Get-HotFix remotely.

WMI also allows you to specify alternate credentials when connecting to any remote computer.

PS C:\> get-hotfix -ComputerName ResearchDC `
>> -Credential mycompany\administrator |
>> Group-Object -Property Description | Select Name,Count
>>
Name''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Count
----''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -----
Update'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 39
Security Update''''''''''''''''''''''''''''''''''''''''''''''''''''''' 44
Hotfix''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 3

Because RESEARCHDC is in a different domain, I specify an alternate credential. PowerShell will display a dialog box as you see in Figure 1.


Figure 1 Credential Request

This creates a PSCredential object which is passed to WMI for the remote connection. The rest of the expression takes all of the hot fix objects and pipes them to Group-Object so I can see who many updates are installed by category. If you find yourself repeatedly using this credential you can make life easier by saving the credential to a variable.

PS C:\> $admin=get-credential mycompany\administrator

Be aware that this doesn't authenticate you as that user, it merely created a credential object. Only when you pass it to a command like Get-HotFix will you find out if the credential is valid, so be careful when entering the name and password. Now it is much simpler to query remote computers. If you are querying multiple computers and using alternate credentials, be aware that the same credential will apply to all computers and you cannot use alternate credentials for the local computer.

Hot Fix Reporting

One common scenario I can imagine is that you want to find where a particular patch or update is installed. The following steps can all be done in an interactive PowerShell script. No scripting is required. We'll start by defining variables for the hot fix and alternate credentials.

PS C:\> $id="KB982665"
PS C:\> $admin=get-credential "mycompany\administrator"

Instead of typing all the computers I want to query, I've put them in a text file, one computer name per line. I'll use Get-Content to retrieve them. PowerShell will pipe each one to Get-HotFix.

PS C:\> get-hotfix -ComputerName (get-content computers.txt) -id $id `
>>-Credential $admin | Sort InstalledOn -erroraction "SilentlyContinue" |
>> Select CSName,InstalledOn
CSName'''''''''''''''''''''''''''''''''''''''''''''' InstalledOn
------''''''''''''''''''''''''''''''''''''''''''''''' -----------
VISTA01
WIN7DESK01''''''''''''''''''''''''''''''''''''''''''' 8/13/2010 12:00:00 AM
XP01''''''''''''''''''''''''''''''''''''''''''''''''' 9/17/2010 12:00:00 AM
'

Get-Hotfix will get the hotfix specified by $id for each computer in computers.txt. I've sorted the output on the InstalledOn property. Because my Vista systems don't display that information I include the common parameter 'ErrorAction to suppress the error message I would otherwise get about a missing property. The last part of the pipeline displays the computername and install date. Did you notice that the computername is CSName and not Source like you see in regular cmdlet output?

That's because there is a default special formatting for Get-HotFix that creates more admin-friendly results. The actual underlying computer name property for the Win32_QuickFixEngineering class is CSName. You can see for yourself by piping Get-Hotfix to Get-Member. What this means is that you can create your own custom properties. Here's the previous command but now I've created a custom property called ComputerName.

PS C:\> get-hotfix -ComputerName (get-content computers.txt) -id $id `
>>-Credential $admin | Sort InstalledOn -erroraction "SilentlyContinue" |
>> Select @{Name="ComputerName";Expression={$_.CSName}},InstalledOn
ComputerName''''''''''''''''''''''''''''''''''''''''' InstalledOn
------''''''''''''''''''''''''''''''''''''''''''''''' -----------
VISTA01
WIN7DESK01''''''''''''''''''''''''''''''''''''''''''' 8/13/2010 12:00:00 AM
XP01''''''''''''''''''''''''''''''''''''''''''''''''' 9/17/2010 12:00:00 AM
'

In fact, I can take this a step further and create entirely new properties.

PS C:\> get-hotfix -ComputerName (get-content computers.txt) -id $id `
>>-Credential $admin | Sort InstalledOn -erroraction "SilentlyContinue" |
>> Select @{Name="ComputerName";Expression={$_.CSName}},InstalledOn,
>> @{Name="AuditedOn";Expression={(Get-Date)}}
>>
ComputerName''''''''''''''''' InstalledOn'''''''''''' AuditedOn
------------''''''''''''''''' -----------'''''''''''' ---------
VISTA01'''''''''''''''''''''''''''''''''''''''''''''' 10/18/2010 4:51:02 PM
WIN7DESK01''''''''''''''''''' 10 12:00:00 AM''''''''' 10/18/2010 4:51:02 PM
XP01''''''''''''''''''''''''' 9/17/2010 12:00:00 AM'' 10/18/2010 4:51:02 PM
'

Here I've added a new property called AuditedOn which is simply the current date and time. From here I could export the results to a file or convert to a CSV or XML file.

In fact, let's say what you really need is a list of computers that don't have the specified fix. To accomplish this we'll need to modify the expression somewhat so that we can get a reference to each computer name.

PS C:\> get-content computers.txt | ForEach {
>> if (-Not (Get-HotFix -id $id -ComputerName $_ -Credential $Admin)) {
>>'' Add-Content $_ -Path .\MissingFix.txt
>>' }
>> }

In this example each computer name from the text list is piped to ForEach-Object, which has an alias of ForEach. In the construct $_ refers to the current computername. The 'Not operator 'reverses' the output of the Get-Hotfix command so that in other words if the hot fix is NOT found, then pipe the computer name to a text file called MissingFix.txt using the Add-Content cmdlet.

All of the commands I've shown you can be saved to a text file by piping to Out-File. Other options would be to export to CSV or XML files. One final option that I like is creating an HTML report. The basic syntax is something like this:

PS C:\> get-hotfix |
>> convertto-html -Property CSName,Description,HotFixID,
>> InstalledBy,InstalledOn | out-file c:\work\report.html

You'll want to specify the properties to convert, otherwise you get all the properties including WMI system properties which only clutter your report. Figure 2 shows the sample output.


Figure 2 Basic HotFix HTML Report

Looks pretty plain which is by design. If you want formatting you can specify a CSS file. We can also enhance this by creating some custom properties so that we have ComputerName instead of CSName.

PS C:\> $css="c:\work\sample.css"
PS C:\> get-hotfix | select @{Name="Computer";Expression={$_.CSName}},
>> @{Name="Category";Expression={$_.Description}},HotfixID,InstalledBy,
>> InstalledOn | ConvertTo-html -Title "Hot Fix Report" `
>> -PostContent "report date $(get-date)" CssUri $css |
>> out-file c:\work\report.html

Now we get a nicer looking report as in Figure 3.


Figure 3 Enhanced HotFix HTML Report

But I'm going to take this one step further. Each hot fix is almost always supported by a support article on Microsoft.com. The URL for each update is generally the same and incorporates the hotfix ID we see from Get-HotFix like KB2345886. I don't know about you but I don't always know what these updates are fixing. But having a live link makes this much easier.

To accomplish this requires a little sophisticated PowerShell manipulation that I don't expect you to fully be able to accomplish on your own, especially if you are still new to PowerShell. So I'll simply share my script, Get-HotFixReport.ps1.

#Requires -version 2.0
#define a text list of computers to check
$source=Get-Content ".\Computers.txt"
#define the output file
$file="c:\work\hotfixreport.html"
#define a report title
$title="Hot Fix Report"
#define the the path to the CSS file
$css="c:\work\sample.css"
Get-HotFix 'computer $source | select @{Name="Computer";Expression={$_.CSName}},
Description,HotfixID,InstalledBy,InstalledOn,@{Name="URL";Expression={
"http://support.microsoft.com/kb/$($_.HotfixID.substring(2))"}} |
ConvertTo-html -Title $title -PostContent "reported $(get-date)" `
-CssUri $css |
ForEach {
#use a regex to find the url
[regex]$urlRegEx="(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?"
[regex]$tdRegEx="

"
#only get urls in the table
if ($_ -match $tdRegEx)
{
#replace the link text with an html link
$_ -match $urlRegEx | Out-Null
$link="$($matches[0])"
$_.Replace($($matches[0]),$link)
}
else
{
#pass the line on
write $_
}
} | out-file -FilePath $file

Much of this we've already gone over. The script goes through a list of computers, gets hot fix information and creates an HTML report. Before the report is saved to a file, the script uses regular expressions to find the KB number and replaces it with HTML code that includes an anchor to what should be the online support article. I can't guarantee that every single link will be correct, but I expect it will work for almost everything. Running this for the local computer produces a report like Figure 4.


Figure 4 Linked HotFix HTML Report

I've saved my reports locally, but I expect you'll want to save them to an intranet site. Or take advantage of the Send-MailMessage cmdlet and email them.

The bottom line is that is now very easy to identify what hotfixes, patches and updates are installed on computers in your network using Windows PowerShell and Get-HotFix. You can query 10 or 100 computers just as easily as you can query 1. You can create reports ranging from simple text files to complex HTML reports. Naturally the more PowerShell you know the farther you can take this topic, but you can accomplish a great deal right from the command prompt with no scripting.