PowerShell Problem Solver: Finding Installed Software Using CIM Cmdlets
Over the last several articles I’ve been guiding you on how to discover what applications might be installed. In the previous article in this series I demonstrated how to use WMI to query the registry on remote computers, where I specifically showed you how to use StdRegProv. One of the drawbacks is that this uses legacy technology and requires DCOM, which means it is not very firewall friendly.
- Related: PowerShell Problem Solver: Find Installed Software Using PowerShell
- Related: PowerShell Problem Solver: Searching the Registry to Find Installed Software
There is also no provision for alternate credentials like there is with the Get-WMIObject cmdlet. Although you can get around that using PowerShell remoting and Invoke-Command, then you might as well use the CIM cmdlets. With CIM, we get an easy way to provide alternate credentials and also eliminate DCOM and RPC from the picture.
The first step is to create a CIM session to the remote computer. If you need to use alternate credentials, this is where you would do so.
1 2 |
$cred = Get-Credential globomantics\administrator $cs = New-CimSession -ComputerName chi-win81 -Credential $cred |
Even though we will be using CIM, we are still accessing the same StdRegProv provider, so we still need to provide the appropriate hive values and paths.
1 2 |
$HKLM=2147483650 $rpath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" |
For now I’m going to search the Uninstall key under HKLM.
Next we’ll create an object for StdRedProv using Get-CimClass. Remember, StdRegProv is not something with instances. We will be using the class’s methods directly.
1 |
$regcim = Get-CimClass -Namespace root\default -class StdRegProv -CimSession $cs |
To query, we will use the EnumKey() method that requires a few parameters. If you have been following the article series, then these will look familiar. Let me quickly show you how to discover them. The $regcim object includes definitions of all the class methods.
1 |
$regcim.cimclassmethods |
Let’s narrow the focus to EnumKey().
1 |
$regcim.CimClassMethods["EnumKey"].parameters |
The “IN” parameters are what we need. We will be using Invoke-CimMethod, so all we need to do is define a hashtable of the necessary parameters and their values.
1 |
$enumArgs = @{hDefKey=$HKLM;sSubKeyName=$rpath} |
To make the Invoke-CimMethod expression easier to read, I’m going to create a hashtable of parameters to splat to the cmdlet.
1 2 3 4 5 6 |
$paramHash = @{ cimclass = $regcim CimSession = $cs Name = "EnumKey" Arguments = $enumArgs } |
Essentially I’m going to invoke EnumKey with my arguments on the StdRegProv object using the remote CIMSession. I know that the method is going to write an array of strings called sNames that I’ll expand, which should give me the names of each entry under Uninstall.
1 |
Invoke-CimMethod @paramHash | select -expand snames |
Once I know the name of the subkey, then I can get its values. In turn, I can grab the string value of the properties I am interested in. Here’s a version of code that will also filter out the Microsoft Office updates.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$data = Invoke-CimMethod @paramHash | select -expand snames | where {$_ -notmatch '(\.)?KB\d+'} -pv p | foreach { $keyPath = "$rpath\$_" write-host $keyPath -ForegroundColor Cyan #revise paramhash $paramHash.Name = "EnumValues" $paramHash.Arguments = @{hDefKey=$HKLM;sSubKeyName=$keyPath} Invoke-CimMethod @paramHash | foreach { #get value data $hash = [ordered]@{Path = $KeyPath} #add a list of known properties "Displayname","DisplayVersion","Publisher", "InstallDate","InstallLocation","Comments","UninstallString" | foreach { $paramHash.Name = "GetStringValue" $paramhash.Arguments = @{hDefKey = $HKLM ;sSubKeyName=$KeyPath;sValueName=$_} $value = Invoke-CimMethod @paramhash $hash.Add($_,$($value.sValue)) } #foreach property name #write a custom object to the pipeline [pscustomobject]$hash } #foreach subkey name } #foreach sname $data | out-gridview |
Essentially there is an iterative process here of enumerating the sNames, the values, and their actual data. This requires different methods and parameters, but by splatting my code, it stays relatively simple, as all I have to do is update the Invoke-Command parameters that have changed.
On x64 systems I might also want to check this path:
1 |
$rpath = "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" |
Re-running my code provides this result:
Finally, let’s check for user specific applications using CIM. The concepts are the same as what I used with WMI in the previous article, except I’m going to use CIM methods to enumerate keys and values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
$HKEY_USERS=2147483651 #use an empty string not $Null $enumArgs = @{hDefKey=$HKEY_USERS;sSubKeyName=""} $paramHash = @{ cimclass = $regcim CimSession = $cs Name = "EnumKey" Arguments = $enumArgs } #snames is the collection of user hives and filter out *Classes and .DEFAULT $snames = Invoke-CimMethod @paramhash | Select -ExpandProperty sNames | Where {$_ -notmatch "_Classes$"} $results = foreach ($item in $snames) { $rpath = "$item\Software\Microsoft\Windows\CurrentVersion\Uninstall" Write-Host "Checking $rpath" -ForegroundColor cyan $paramHash.Arguments = @{hDefKey=$HKEY_USERS;sSubKeyName=$rpath} #get subkeys with defined sname values $data = Invoke-CimMethod @paramhash if ($data.snames) { #only process if sname data discovered #$paramHash.Name = "EnumValues" $paramHash.Arguments = @{hDefKey=$HKEY_USERS;sSubKeyName=$rpath} $appnames = Invoke-CimMethod @paramhash if ($appnames.snames) { #resolve the SID using Get-WSManInstance write-host "Resolving $item" -ForegroundColor Cyan $Resolve = Get-WSManInstance -resourceURI "wmi/root/cimv2/Win32_SID?SID=$item" -computername $cs.ComputerName -Credential $cred if ($resolve.accountname) { $Username = "$($resolve.ReferencedDomainName)\$($resolve.AccountName)" } else { $Username = $item } #enumerate each sname which will be an application of some kind foreach ($app in $appnames.sNames) { $hash = [ordered]@{Username = $Username; Path = $rpath} write-host "Querying $rpath\$app" -ForegroundColor Cyan #add a list of known properties "Displayname","DisplayVersion","Publisher", "InstallDate","InstallLocation","Comments","UninstallString" | foreach { $paramHash.Name = "GetStringValue" $paramHash.Arguments = @{hDefKey= $HKEY_USERS;sSubKeyName = "$rpath\$app";sValueName = $_} $value = Invoke-CimMethod @paramhash $hash.Add($_,$value.svalue) } #write a custom object to the pipeline [pscustomobject]$hash } #foreach app } #if appnames #reset Clear-Variable data } #if snames found } #foreach item in snames $results | Out-GridView -title "HKEY_USERS" |
One step I had to do differently was to resolve the SID name.
1 |
$Resolve = Get-WSManInstance -resourceURI "wmi/root/cimv2/Win32_SID?SID=$item" -computername $cs.ComputerName -Credential $cred |
You can’t query the Win32_SID class with a filter, and I have yet to find any way to specify the WMI path to the specific SID instance. Instead I use Get-WSManInstance, which allows me to specify a WMI path. Here’s my final result:
Querying the registry with either WMI or CIM is certainly not a beginning PowerShell topic and in terms of identifying applications, it still may not be the most foolproof way to identify applications. But this solution may be enough. I will leave you with a script that uses CIM to pull everything together that I have shown you.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
#requires -version 3.0 #Get-InstalledApplicationFromRegistry.ps1 <# Get-InstalledApplicationfromRegistry CHI-WIN81 -credential globomantics\administrator -includeUsers #> [cmdletbinding()] Param( [Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [Alias("cn")] [ValidateNotNullorEmpty()] [string]$Computername = $env:computername, [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty, [switch]$IncludeUsers ) Begin { Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" $HKEY_USERS=2147483651 $HKLM=2147483650 $rpaths = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" $progParam = @{ Activity = $MyInvocation.MyCommand Status = "Starting" CurrentOperation = "" PercentComplete = 0 } } #begin Process { Write-Verbose "Creating CIMSession to $computername" $progParam.CurrentOperation = "Creating CIMSession to $computername" Write-Progress @progParam #initialize $data $data = @() Try { $ncsParams = @{ ComputerName = $computername ErrorAction = "Stop" } if ($Credential.UserName) { $ncsParams.Add("Credential",$Credential) } $progparam.currentOperation = "Creating CIM session and class objects" Write-Progress @progParam $cs = New-CimSession @ncsParams $regcim = Get-CimClass -Namespace root\default -class StdRegProv -CimSession $cs } Catch { Write-Warning "There was a problem creating a CIMSession to $computername" Throw } foreach ($rpath in $rpaths) { Write-Verbose "Querying $rpath" $progParam.Status = "Querying $rpath" $progParam.CurrentOperation = "EnumKey" Write-Progress @progParam $enumArgs = @{hDefKey=$HKLM;sSubKeyName=$rpath} $paramHash = @{ cimclass = $regcim CimSession = $cs Name = "EnumKey" Arguments = $enumArgs } $mySnames = Invoke-CimMethod @paramHash | select -expand snames | where {$_ -notmatch '(\.)?KB\d+'} $i=0 $data += foreach ($item in $MySnames) { $i++ $progParam.CurrentOperation = $_ $progParam.Status = "EnumValues $rpath" $pct = ($i/$mySNames.count)*100 $progParam.PercentComplete = $pct Write-Progress @progParam $keyPath = "$rpath\$item" #revise paramhash $paramHash.Name = "EnumValues" $paramHash.Arguments = @{hDefKey=$HKLM;sSubKeyName=$keyPath} Invoke-CimMethod @paramHash | foreach { #get value data $hash = [ordered]@{Path = $KeyPath} #add a list of known properties "Displayname","DisplayVersion","Publisher", "InstallDate","InstallLocation","Comments","UninstallString" | foreach { $paramHash.Name = "GetStringValue" $paramhash.Arguments = @{hDefKey = $HKLM ;sSubKeyName=$KeyPath;sValueName=$_} $value = Invoke-CimMethod @paramhash $hash.Add($_,$($value.sValue)) } #foreach property name #write a custom object to the pipeline [pscustomobject]$hash } #foreach subkey name } #foreach sname } #foreach rpath #get information from HKEY_USERS if ($IncludeUsers) { Write-Verbose "Getting data from HKEY_USERS" $progParam.Status = "Getting data from HKEY_USERS" $progParam.CurrentOperation = "" $progParam.PercentComplete = 0 Write-Progress @progParam $enumArgs = @{hDefKey=$HKEY_USERS;sSubKeyName=""} $paramHash = @{ cimclass = $regcim CimSession = $cs Name = "EnumKey" Arguments = $enumArgs } #snames is the collection of user hives and filter out *Classes and .DEFAULT $snames = Invoke-CimMethod @paramhash | Select -ExpandProperty sNames | Where {$_ -notmatch "_Classes$"} $i = 0 $data+= foreach ($item in $snames) { $i++ $pct = ($i/$snames.count)*100 $progParam.CurrentOperation = $item $progParam.PercentComplete = $pct Write-Progress @progParam $rpath = "$item\Software\Microsoft\Windows\CurrentVersion\Uninstall" Write-Verbose "Checking $rpath" $paramHash.Arguments = @{hDefKey=$HKEY_USERS;sSubKeyName=$rpath} #get subkeys with defined sname values $mydata = Invoke-CimMethod @paramhash if ($mydata.snames) { #only process if sname data discovered $paramHash.Arguments = @{hDefKey=$HKEY_USERS;sSubKeyName=$rpath} $appnames = Invoke-CimMethod @paramhash if ($appnames.snames) { #resolve the SID using Get-WSManInstance write-verbose "Resolving $item" $WSManParamHash = @{ resourceURI = "wmi/root/cimv2/Win32_SID?SID=$item" computername = $cs.ComputerName Credential = $credential } $Resolve = Get-WSManInstance @WSManParamHash if ($resolve.accountname) { $Username = "$($resolve.ReferencedDomainName)\$($resolve.AccountName)" } else { $Username = $item } #enumerate each sname which will be an application of some kind foreach ($app in $appnames.sNames) { $hash = [ordered]@{Username = $Username; Path = $rpath} Write-Verbose "Querying $rpath\$app" #add a list of known properties "Displayname","DisplayVersion","Publisher", "InstallDate","InstallLocation","Comments","UninstallString" | foreach { $paramHash.Name = "GetStringValue" $paramHash.Arguments = @{hDefKey= $HKEY_USERS;sSubKeyName = "$rpath\$app";sValueName = $_} $value = Invoke-CimMethod @paramhash $hash.Add($_,$value.svalue) } #foreach property name #write a custom object to the pipeline [pscustomobject]$hash } #foreach app } #if appnames #reset Clear-Variable mydata } #if snames found } #foreach item in snames } #if users #write results to pipeline $data #clean up Write-Verbose "Removing CIMSession" $progParam.CurrentOperation = "" $progParam.Status = "Removing CIMSession" Write-Progress @progParam Remove-CimSession $cs } #process End { $progParam.Status = "Finished" Write-Progress @Progparam -Completed Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } #end |
You can run it like this:
1 2 |
PS C:\> $d4 = .\Get-InstalledApplicationFromRegistry.ps1 -IncludeUsers -Computername chi-win81 -Credential globomantics\jeff PS C:\> $d4 | out-gridview |
Hopefully I’ve given you enough information to build the tools you need to discover what applications are installed using PowerShell. If you run into issues or have follow up questions, feel free to leave a comment or post in the PowerShell forum here on the site.