PowerShell Problem Solver: Extending the Hot Fix Reporting Tool

powershell hero
We’ve come along way since we started this project on building a PowerShell hot fix reporting tool. We’ve wrapped up our functionality around the Get-Hotfix cmdlet. But there’s more to be done. Let’s say our manager has gotten wind of the project and decides to make a few “suggestions”. In addition to finding hotfixes installed by a certain user, he also wants to be able to easily see updates installed before and/or after a given set of dates. Yes, you could write a PowerShell expression with Where-Object but you’d have to write it anew every time. That’s why we’re building a re-usable PowerShell tool.

New Parameters

To begin, I’ll add some new parameters to the script.

Param(
[string[]]$Computername = $env:COMPUTERNAME,
[ValidateSet("Security Update","HotFix","Update")]
[string]$Description,
[string]$Username,
[datetime]$Before,
[datetime]$After,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)

Notice that I have specified that any value for -Before or -After will be treated as a datetime object. This allows the user to enter a value like “1/1/2016” and have PowerShell automatically convert it to a datetime value. I also chose parameter names that are used in other cmdlets like Get-Eventlog. There’s no reason to invent something new.  The new parameters are optional and have no default values. It also also possible to use all of the parameters. I might want to find all Security Updates installed by the Administrator account between January 1, 2015 and June 1, 2015. So how do we use these new parameters?

Filtering with Where()

Since I know that anyone running my script is likely to be running PowerShell 4.0 or later, I’m going to use the Where() method. This feature was introduced for Desired State Configuration but we can use it in our scripts. The primary reason is performance. Using the Where() method is much faster than using the Where-Object cmdlet. The syntax is very similar between the two approaches so if you needed to modify the script for v3 you could. Or you could take an extra step to check the $PSVersionTable and filter based on the PowerShell version.
The syntax for Where-Object looks like this:

$s = Get-Service
$s | where {$_.status -eq 'running'}

The Where() method uses the same filter.

$s.where({$_.status -eq 'running'})

The one thing to remember is that the Where() method should be invoked on a collection of objects. In the script I’m going to save the Get-HotFix results to a variable.

#get all matching results and save to a variable
$data = Get-Hotfix @params

Everything else is done to these results.  Because it is possible to filter on username, before and after, I need to filter the data cumulatively. I decided to filter first on the user name.

#filter on Username if it was specified
 if ($Username) {
    #filter with v4 Where method for performance
    #allow the use of wildcards
    $data = $data.Where({$_.InstalledBy -match $Username})
 }

Note that I’m using the -Match operator. This will allow me to use wildcards or regular expressions for the Username. I do similar filtering for Before and After.

#filter on Before
if ($before) {
    $data = $data.Where({$_.InstalledOn -le $Before})
}
#filter on After
if ($after) {
    $data = $data.Where({$_.InstalledOn -ge $After})
}

With each pass $data is further filtered. Once filtered, the script writes the remaining results to the pipeline.

$data | Select-Object -Property PSComputername,HotFixID,Description,InstalledBy,InstalledOn,
@{Name="Online";Expression={$_.Caption}}

Want to see it in action?

C:\scripts\AddFeatures2-HotfixReport.ps1 -Computername chi-hvr2,chi-dc04,chi-dc01 -Before "12/31/2015" -after "1/1/2015" -Username Jeff -Description 'Security Update' | Out-GridView -title "Jeff 2015"

The target was any Security Updates installed on 3 computers by any account with Jeff in the name in 2015.

Filtered hot fix results
Filtered hot fix results (Image Credit: Jeff Hicks)

I don’t know about you but I find this pretty handy. Well, up to a point.

Converting to a Function

One big downside is that this is PowerShell script which means I need to specify the full path to the script everytime I want to run it. This would be easier to use as a function.  That way, I can run the function like I would any other PowerShell command. Fortunately, turning this into a function is very easy.  All I need to do is wrap the body of the script inside a Function declaration.

#requires -version 4.0
#BasicFunction-HotFixReport.ps1
Function Get-MyHotFix {
[cmdletbinding()]
Param(
[string[]]$Computername = $env:COMPUTERNAME,
[ValidateSet("Security Update","HotFix","Update")]
[string]$Description,
[string]$Username,
[datetime]$Before,
[datetime]$After,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)
#create a hashtable of parameters to splat to Get-Hotfix
$params = @{
    ErrorAction = 'Stop'
    Computername = $Null
}
if ($Credential.UserName) {
    #add the credential
    $params.Add("Credential",$Credential)
}
if ($Description) {
    #add the description parameter
    $params.add("Description",$Description)
}
foreach ($Computer in $Computername) {
    #add the computer name to the parameter hashtable
    $params.Computername = $Computer
    Try {
        #get all matching results and save to a variable
        $data = Get-Hotfix @params
        #filter on Username if it was specified
        if ($Username) {
           #filter with v4 Where method for performance
           #allow the use of wildcards
          $data = $data.Where({$_.InstalledBy -match $Username})
        }
        #filter on Before
        if ($before) {
            $data = $data.Where({$_.InstalledOn -le $Before})
        }
        #filter on After
        if ($after) {
            $data = $data.Where({$_.InstalledOn -ge $After})
        }
        #write the results
        $data | Select-Object -Property PSComputername,HotFixID,Description,InstalledBy,InstalledOn,
        @{Name="Online";Expression={$_.Caption}}
    } #Try
    Catch {
        Write-Warning "$($computer.toUpper()) Failed. $($_.exception.Message)"
    } #Catch
} #foreach computer
} #end Get-MyHotFix function

I’ve also added the cmdletbinding attribute so that PowerShell will treat the command like a cmdlet. This will give me the common parameters like -Verbose and -Outvariable. I gave the function a meaningful name using one of the standard verbs from Get-Verb. You can be a bit more creative with the Noun but I recommend trying to avoid potential naming collisions. I think Get-MyEventLog is meaningful enough and unlikely to cause a problem. Another idea is to use some part of your organization’s name. My test domain is Globomantics.local so I could have called it Get-GlobomanticsEventLog or Get-GLEventLog. As long as I’m consistent this make it easy to find my commands. And of course you can always create aliases.
In order to use the function, I need to dot source the script file in my PowerShell session.

. C:\scripts\BasicFunction-HotfixReport.ps1

But now I can see help for the command.

Loading the function
Loading the function (Image Credit: Jeff Hicks)

And here it is in action:
Get-MyHotFix in action
Get-MyHotFix in action (Image Credit: Jeff Hicks)

Pretty nice. I can dot source the script in my PowerShell profile script and have it ready to go. Or I can give it to the help desk, instruct them on how to load and use it and take a breather.
Well, that is at least until someone starts using the function. The help desk may be used to getting a list of computer names from a text file and piping it to a command. Or some of the help desk interns are still struggling to learn PowerShell and trying to run the command in “creative” ways.  Since I wrote the function I know how to use it. But most likely you are building tools for other people.

When you create PowerShell tools it is very important that you consider who will use it and how. In the next article in this series we’ll address these issues.