In this second part we will look at querying for Microsoft Hotfixes against a given array of hosts. This Workflow will differ a bit as you will see from the one I showed in my previous post do to changes and improvements I was requested by a friend doing consulting at a client, he quickly modified the workflow in the previous post to use a list of IP Addresses and modified it to check for Internet Explorer versions so as to aid him in a risk assessment for a customer. On this one I took it a bit further and came with the following requirements:

Take an array of either computer names or IP addresses instead of an object.

Provide the ability of using alternate credentials for connecting to WMI on the remote hosts.

Test each given hosts to see if port 135 TCP is open.

Return an object with the patch information and a state of Installed or not Installed so as to be able to parse easier.

PowerShell provides 2 main ways to get patch information from a system:

Get-Hotfix Commandlet Query to the Win32_QuickFixEngineering WMI class

In my testing Get-Hotfix when used inside of a workflow in PowerShell v3 tended to default to WinRM not allowing me to use DCOM as the method of communication. This became very annoying since not all customer environments will have WinRM running but are more than likely to allow DCOM RPC connection to their Windows Hosts. So I opted with the second option of using the Get-WMIObject to query the Class Win32_QuickFixEngineering . The command as the base for the workflow is rather simple:

Get-WmiObject -Class Win32_QuickFixEngineering -Filter "HotFixID='$($hid)'"

Where $hid holds the HotFix ID of the hotfix we want to test. With the Get-WMIObject cmdlet we can also give it the option of alternate credentials to use as well as a Computer Name to target.

The logic for the workflow is a simple one, we go thru each computer in the computers array in parallel and for each we check for the hotfix id

Workflow Confirm-Hotfix { [cmdletbinding()] param( [parameter(Mandatory=$true)] [psobject[]]$Computers, [parameter(Mandatory=$true)] [string[]]$KB ...... foreach -parallel ($computer in $computers) { InlineScript { foreach ($hid in $using:KB){ ...... } } } }

When using workflows there are several restrictions that make working with them very different than working say with a function specially when it comes to how to set parameters, use of variables and that positional parameters can not be use. Also the cmdlets that can be use are limited. This is why you will see me using the $using variable to pass variables to the script block that is the InlineScript. The use of InlineScript relaxes many of the rules that are imposed inside the workflow.

My Next task is to test if TCP Port 135 is open and set a timeout so as to be able to test quickly if the host is up or not before attempting to connect with WMI, the main advantage of this are:

Checks if the host is alive since most environments have firewalls on the PCs and servers that block ICMP Echo.

Checks that the proper port for WMI is open and if it is blocked and reset is send by a firewall.

Workflow Confirm-Hotfix { [cmdletbinding()] param( [parameter(Mandatory=$true)] [psobject[]]$Computers, [parameter(Mandatory=$true)] [string[]]$KB ...... foreach -parallel ($computer in $computers) { InlineScript { $TCPclient = new-Object system.Net.Sockets.TcpClient $Connection = $TCPclient.BeginConnect($using:computer,135,$null,$null) $TimeOut = $Connection.AsyncWaitHandle.WaitOne(3000,$false) if(!$TimeOut) { $TCPclient.Close() Write-Verbose "Could not connect to $($using:computer) port 135." } else { Try { $TCPclient.EndConnect($Connection) | out-Null $TCPclient.Close() foreach ($hid in $using:KB){ ...... } } Catch { write-verbose "Connction to $($using:computer) on port 135 was refused." } } } }

The rest of the Workflow is just creating the object and passing the credentials. The final workflow looks like:

Workflow Confirm-HotFix { [cmdletbinding()] param( [parameter(Mandatory=$true)] [psobject[]]$Computers, [parameter(Mandatory=$true)] [string[]]$KB, [System.Management.Automation.PSCredential] $Credentials ) foreach -parallel ($computer in $computers) { Write-Verbose -Message "Running against $($computer)" InlineScript { # Move credentials in to the inline script for easier manipulation $creds = $using:Credentials # If none are provided create an empty PSCredential Object to force use of current user token. if (!$creds){ $creds = ([PSCredential]::Empty) } $TCPclient = new-Object system.Net.Sockets.TcpClient $Connection = $TCPclient.BeginConnect($using:computer,135,$null,$null) $TimeOut = $Connection.AsyncWaitHandle.WaitOne(3000,$false) if(!$TimeOut) { $TCPclient.Close() Write-Verbose "Could not connect to $($using:computer) port 135." } else { Try { $TCPclient.EndConnect($Connection) | out-Null $TCPclient.Close() # Check each computer for the info. foreach ($hid in $using:KB){ Write-Verbose -Message "Checking for $($hid) on $($using:computer)" $KBs = Get-WmiObject -class Win32_QuickFixEngineering -Filter "HotFixID='$($hid)'" -ComputerName $using:computer -Credential $creds if ($KBs){ # Process each version found Write-Verbose -Message "Hotfix $($hid) found on $($using:computer)" $objprops =[ordered] @{'Computer'=$Using:computer; 'HotFix'=$hid; 'InstalledDate' = $KBs.InstalledOn; 'InstalledBy' = $KBs.InstalledBy; 'Description' = $KBs.Description; 'Caption' = $KBs.Caption; 'Installed'=$true} [PSCustomObject]$objprops } else { #If not found return an object with Installed False Write-Verbose -Message "Hotfix $($hid) not found in $($using:computer)" $objprops =[ordered] @{'Computer'=$Using:computer; 'HotFix'=$hid; 'InstalledDate' = ""; 'InstalledBy' = ""; 'Description' = ""; 'Caption' = ""; 'Installed'=$false} [PSCustomObject]$objprops } } } Catch { write-verbose "Connction to $($using:computer) on port 135 was refused." } } } } }

Now we can use alternate credentials and have the workflow test for connectivity:

PS > Confirm-HotFix -Computers 192.168.10.20,192.168.10.1 -KB KB976902 -Credentials (Get-Credential acmelabs\administrator) -Verbose VERBOSE: [localhost]:Running against 192.168.10.1 VERBOSE: [localhost]:Running against 192.168.10.20 VERBOSE: [localhost]:Checking for KB976902 on 192.168.10.20 VERBOSE: [localhost]:Could not connect to 192.168.10.1 port 135. VERBOSE: [localhost]:Hotfix KB976902 found on 192.168.10.20 Computer : 192.168.10.20 HotFix : KB976902 InstalledDate : 1/22/2013 12:00:00 AM InstalledBy : NT AUTHORITY\SYSTEM Description : Update Caption : http://support.microsoft.com/?kbid=976902 Installed : True PSComputerName : localhost PSSourceJobInstanceId : 3704d139-8328-4bd2-adcc-06bc994bf8b5

And since we are returning Objects and not text we can manipulate the results:

PS C:\> $hosts = Get-ADComputer -Filter * | select -ExpandProperty name PS C:\> Confirm-HotFix -Computers $hosts -KB KB976902 | Format-Table -Property computer,hotfix,installed -AutoSize Computer HotFix Installed -------- ------ --------- WIN801 KB976902 False WIN2K01 KB976902 False WINXP01 KB976902 False WIN2K302 KB976902 False DC02 KB976902 True WIN2K301 KB976902 False WINXP02 KB976902 False DC01 KB976902 False WIN702 KB976902 True WIN701 KB976902 True

This workflow is great for testing that the patch management solution deployed the patches and they applied, good for a quick risk assessment on Patch Tuesdays and confirming what a Vulnerability Scanner reports. I added the workflow to my PowerShell Security Module I keep meaning of one day finishing and documenting https://github.com/darkoperator/PowerShellSecMod/blob/master/PSSec/PSSec.psm1#L63

As always I hope you find this useful and informative, any feedback you may have are more than welcome .