PowerShell Problem Solver: An Advanced HotFix Reporting Tool

powershell-hero-16-9-ratio
Over the course of several articles, we’ve been developing a PowerShell tool to provide hot fix information. The command is built around the Get-HotFix cmdlet but takes it a step further. If you are just jumping in, I encourage you go back to the beginning so you’ll understand how we got here.

Pipeline Input

From the previous article, the function can take multiple computer names, but they can’t be piped into the command like other cmdlets.  The solution is to create an advanced PowerShell function. To do that, the first change is that the body of the function needs three special scriptblocks: Begin, Process and End. The only one that is truly required is Process, but I always use all of them. In the Begin scriptblock, you put any commands you want to run before processing any pipelined values. In the End scriptblock, you put code to run after everything has been processed from the pipeline. The code in the Process scriptblock runs once for each computer.
In my function, I’ll move the code that creates the parameter hashtable into the Begin block.

Begin {
    Write-Verbose "[BEGIN  ] Starting: $($MyInvocation.Mycommand)"
    #create a hashtable of parameters to splat to Get-Hotfix
    $params = @{
        ErrorAction = 'Stop'
        Computername = $Null
    }
    if ($Credential.UserName) {
        #add the credential
        Write-Verbose "[BEGIN  ] Using alternate credential: $($Credential.username)"
        $params.Add("Credential",$Credential)
    }
    if ($Description) {
        #add the description parameter
        Write-Verbose "[BEGIN  ] Querying for: $description"
        $params.add("Description",$Description)
    }
} #begin

I’ll talk about the Write-Verbose commands later.  The End block isn’t really necessary in this command.

End {
    Write-Verbose "[END    ] Ending: $($MyInvocation.Mycommand)"
} #end

The Process scriptblock has the majority of the code from the basic version of the function.

Process {
    foreach ($Computer in $Computername) {
        Write-Verbose "[PROCESS] Processing: $($Computer.ToUpper())"
        #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
            Write-Verbose "[PROCESS] Found: $($data.count) items"
            #filter on Username if it was specified
            if ($Username) {
                Write-Verbose "[PROCESS] Filtering for user: $Username"
               #filter with v4 Where method for performance
               #allow the use of wildcards
              $data = $data.Where({$_.InstalledBy -match $Username})
              Write-Verbose "[PROCESS] Total items now: $($data.count)"
            }
            #filter on Before
            if ($before) {
                Write-Verbose "[PROCESS] Filtering for hotfixes installed before: $Before"
                $data = $data.Where({$_.InstalledOn -le $Before})
                Write-Verbose "[PROCESS] Total items now: $($data.count)"
            }
            #filter on After
            if ($after) {
                Write-Verbose "[PROCESS] Filtering for hotfixes installed after: $After"
                $data = $data.Where({$_.InstalledOn -ge $After})
                Write-Verbose "[PROCESS] Total items now: $($data.count)"
            }
            #write the results
            #changing PSComputername to Computername for potential pipeline
            #expressions
            $data | Select-Object -Property @{Name="Computername";Expression={$_.PSComputername}},
            HotFixID,Description,InstalledBy,InstalledOn,
            @{Name="Online";Expression={$_.Caption}}
        } #Try
        Catch {
            Write-Verbose "[PROCESS] Exception caught"
            Write-Warning "$($computer.toUpper()) Failed. $($_.exception.Message)"
        } #Catch
    } #foreach computer
} #process

I can use the same parameter names for the pipelined input, which in this case is the computer name. But this means I need to tell PowerShell that the Computername parameter can take a value from the pipeline.

[Parameter(Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string[]]$Computername = $env:COMPUTERNAME,

You can specify whether you want to accept any value (ValueFromPipeline) or accept any object that has a property name the same as the parameter name (ValueFromPipelinebyPropertyName). This would be useful if you were importing from a CSV file that had a Computername property. You could then pipe the imported objects to the command and PowerShell would take the property name and hook it up to the parameter. In PowerShell v3 you would have explicitly set the value settings to $True.

[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]

With these settings  I can now run commands like this:

Testing pipelined input
Testing pipelined input (Image Credit: Jeff Hicks)

$C is a collection of computer names that I can now pipe to the command. I could also have gotten the same result by running the command like this:

get-myhotfix $c -After 1/1/2016 -Description 'Security Update'

By the way, astute readers will notice that I changed PSComputername to Computername. I did this to make the command more compatible with other commands that might take Computername as a parameter via the pipeline.

Decorating

The last step I usually take is to add elements that anticipate what silly things a user might do,  or something to make the command easier to use. For example, perhaps the Help Desk is used to using -Name or -CN instead of -Computername. I can add an alias to the parameter.

[Alias('CN','PSComputername','Name')]

Now I can use any of these alternate parameter names, even on imported objects, as long as the property name matches the parameter or one of its aliases.

Getting data from imported objects
Getting data from imported objects (Image Credit: Jeff Hicks)

I also typically add this parameter validation element to parameters that I want to ensure have a value:

[ValidateNotNullorEmpty()]

I could have made the Computername parameter Mandatory, but then I wouldn’t have been able to set a default value. Or I suppose I could have more closely mimicked the behavior of Get-HotFix. But I’ll stick with what I have for now.
The last element, which you saw earlier, was my use of Write-Verbose. I add these statements throughout the command to identify what is happening and often the state of key variables. Because I’m using [cmdletbinding()] in the function, I automatically get the -Verbose parameter. If someone runs the command with -Verbose, they will see all of the messages. Otherwise, they are ignored. This is very handy when debugging or troubleshooting. If someone is having a problem with the command, I can have them start a transcript, run the command with -Verbose, stop the transcript and send it to me for review. You can include as much Verbose output as you feel is necessary. Often the verbose messages can double as internal documentation!

Verbose output
Verbose output (Image Credit: Jeff Hicks)

Help

And last, but by no means least, I create some comment-based help. At a minimum, I recommend defining the Synopsis, Description and at least one example.

Displaying help for the function
Displaying help for the function (Image Credit: Jeff Hicks)

The command now looks and behaves like any PowerShell  command. Here is my complete, advanced function.

#requires -version 4.0
#AdvancedFunction-HotFixReport.ps1
Function Get-MyHotFix {
<#
.SYNOPSIS
Get company hotfix information
.DESCRIPTION
This command is an alternative to Get-Hotfix that supports additional filtering capabilities.
.PARAMETER Computername
Specifies a remote computer. The default is the local computer.
Type the NetBIOS name, an Internet Protocol (IP) address, or a fully qualified domain name of a remote computer.
The parameter does not rely on Windows PowerShell remoting. You can use the ComputerName parameter of  even if your computer is not configured to run remote commands.
This parameter has aliases of: CN,PSComputername, and Name.
.PARAMETER Description
Gets only hotfixes with the specified descriptions. The default is all hotfixes on the computer.
.PARAMETER Username
Use this parameter to filter on the InstalledBy value. You can use wildcards or regular expressions.
.PARAMETER Before
Find all hot fixes installed before this date.
.PARAMETER After
Find all hot fixes installed after this date.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default is the current user.
Type a user name, such as "User01" or "Domain01\User01", or enter a PSCredential object, such as one generated by the Get-Credential cmdlet. If you type a user name, you will be prompted for a password.
.EXAMPLE
PS C:\> Get-MyHotFix | measure-Object
Count    : 324
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
.EXAMPLE
PS C:\> get-myhotfix -Computername chi-hvr2 -Description HotFix
Computername : CHI-HVR2
HotFixID     : KB2959626
Description  : Hotfix
InstalledBy  : NT AUTHORITY\SYSTEM
InstalledOn  : 1/27/2015 12:00:00 AM
Online       : http://support.microsoft.com/?kbid=2959626
Computername : CHI-HVR2
HotFixID     : KB2996799
Description  : Hotfix
InstalledBy  : GLOBOMANTICS\Administrator
InstalledOn  : 11/16/2014 12:00:00 AM
Online       : http://support.microsoft.com/?kbid=2996799
.EXAMPLE
PS C:\> get-content c:\work\computers.txt | get-myhotfix -username globomantics -Before 1/1/2015 | Group InstalledBy
Count Name                      Group
----- ----                      -----
   28 GLOBOMANTICS\jeff         {@{Computername=CHI-HVR2; HotFixID=KB2883200; Description=Update; InstalledB...
   93 GLOBOMANTICS\Administr... {@{Computername=CHI-HVR2; HotFixID=KB2894852; Description=Security Update; I...
.EXAMPLE
PS C:\> get-myhotfix -Computername chi-dc04 -Credential globomantics\administrator | Group Description -NoElement
Count Name
----- ----
   89 Update
   96 Security Update
    4 Hotfix
.NOTES
NAME        :  Get-MyHotFix
VERSION     :  1.0
LAST UPDATED:  6/2/2016
AUTHOR      :  Jeff Hicks
Learn more about PowerShell:
Essential PowerShell Learning Resources
**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .LINK Get-HotFix .INPUTS [string[]] .OUTPUTS [pscustomobject] #> [cmdletbinding()] Param( [Parameter(Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('CN','PSComputername','Name')] [ValidateNotNullorEmpty()] [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 ) Begin { Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" #create a hashtable of parameters to splat to Get-Hotfix $params = @{ ErrorAction = 'Stop' Computername = $Null } if ($Credential.UserName) { #add the credential Write-Verbose "[BEGIN ] Using alternate credential: $($Credential.username)" $params.Add("Credential",$Credential) } if ($Description) { #add the description parameter Write-Verbose "[BEGIN ] Querying for: $description" $params.add("Description",$Description) } } #begin Process { foreach ($Computer in $Computername) { Write-Verbose "[PROCESS] Processing: $($Computer.ToUpper())" #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 Write-Verbose "[PROCESS] Found: $($data.count) items" #filter on Username if it was specified if ($Username) { Write-Verbose "[PROCESS] Filtering for user: $Username" #filter with v4 Where method for performance #allow the use of wildcards $data = $data.Where({$_.InstalledBy -match $Username}) Write-Verbose "[PROCESS] Total items now: $($data.count)" } #filter on Before if ($before) { Write-Verbose "[PROCESS] Filtering for hotfixes installed before: $Before" $data = $data.Where({$_.InstalledOn -le $Before}) Write-Verbose "[PROCESS] Total items now: $($data.count)" } #filter on After if ($after) { Write-Verbose "[PROCESS] Filtering for hotfixes installed after: $After" $data = $data.Where({$_.InstalledOn -ge $After}) Write-Verbose "[PROCESS] Total items now: $($data.count)" } #write the results #changing PSComputername to Computername for potential pipeline #expressions $data | Select-Object -Property @{Name="Computername";Expression={$_.PSComputername}}, HotFixID,Description,InstalledBy,InstalledOn, @{Name="Online";Expression={$_.Caption}} } #Try Catch { Write-Verbose "[PROCESS] Exception caught" Write-Warning "$($computer.toUpper()) Failed. $($_.exception.Message)" } #Catch } #foreach computer } #process End { Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" } #end } #end Get-MyHotFix function

I should probably use something like Git to maintain this as, more than likely, someone will come to me with a feature request. I’m not too concerned about bugs because I tested and developed slowly. I didn’t sit down and attempt to write the finished product all at once. This is something I strongly recommend to people just getting started with PowerShell scripting and toolmaking.  Start slow and small and increment features and complexity as you need and learn.

Next Steps

At this point, I’m quite happy with the finished result. If I was in a true corporate environment, I’d digitally sign the script and check it into source control. I might add this to a module to make it even easier to use and distribute. But the bottom line is that I can now use this command to get the hotfix information I need and process the results however I desire.
I hope you found this series educational and maybe even a little bit fun. Your comments are welcome and appreciated.