PowerShell Problem Solver: Find Installed Software using WMI and StdRegProv

In the first few articles of this series, I guided you through several different techniques for identifying installed applications. Remember, I’m only talking about applications that have been installed via an installation package. Stand alone or portable applications are much more difficult to identify. In the previous article, I demonstrated how to query the registry. I ended by showing you how to query a registry path under HKEY_CURRENT_USER.

The challenge is that when you search that registry hive, you are doing it for your current credentials. So if you try the following command, then the search will be for whatever credentials you connect with. This is probably not what you really want to accomplish.

Invoke-Command -scriptblock {
dir HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall -pv p |
get-ItemProperty |Select @{Name="Path";Expression={$p.name}},Displayname,DisplayVersion,
Publisher,InstallDate,InstallLocation,Comments,UninstallString
} -ComputerName DESK01	

Today I have a few options using WMI. As a benefit, you can easily query remote computers, although you will need to make sure the RemoteRegistry service is running. Allow me to introduce you to something called StdRegProv.

This is WMI provider that coordinates access to the registry. It is a bit cumbersome and very much a developer-oriented topic, but we can still use it. First, I’m going to create a variable to represent the HKEY_USERS hive.

$HKEY_USERS=2147483651

Each registry hive is a separate numeric value, but let’s stick to this for now. Next is a variable for the computer to be queried.

$computername = $env:computername

I’m going to test with the local host. The tricky part with the registry provider is that it isn’t something with instances. Instead you need to use the WMI class directly, which you can do by using the [WMIClass] type accelerator.

[WMIClass]$Reg = "\\$computername\root\default:StdRegProv"

To make my code more flexible, I’m going to define a new hive variable.

$HIVE = $HKEY_USERS

Next, I need to enumerate the hive, which should show me the SIDS for all of the currently logged on user accounts.

$reg.EnumKey($HIVE,"")

Enumerating the hive. A return result of zero indicates a success. (Image Credit: Jeff Hicks)
Enumerating the hive. A return result of zero indicates a success. (Image Credit: Jeff Hicks)

The ReturnValue of 0 indicates success. The sNames property is a collection of the child names. Here is a quick way to get that list of names.

$reg.EnumKey($HIVE,"").snames

Obtaining a list of names in the registry. (Image Credit: Jeff Hicks)
Obtaining a list of names in the registry. (Image Credit: Jeff Hicks)

Each entry can be used to construct the corresponding path under HKEY_USERS. From there we can enumerate each of these hives.

$reg.EnumKey($HIVE,"").snames | foreach {
$regpath = "$_\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$reg.EnumKey($HIVE,$regpath) }

Enumerating each hive. (Image Credit: Jeff Hicks)
Enumerating each hive. (Image Credit: Jeff Hicks)

All I care about are those entries that have values for sNames. I could do something as simple as this:
Querying for values for the sNames property. (Image Credit: Jeff Hicks)
Querying for values for the sNames property. (Image Credit: Jeff Hicks)

But this doesn’t provide any context. There is also one other caveat with querying HKEY_USERS. Information in these hives is only updated after the user logs out and back in again. If the current user has installed an application and it writes to the corresponding path under HKEY_CURRENT_USER, then I don’t believe you will detect it until the user logs out and the user hive under HKEY_USERS is updated.
Why don’t I show you my complete solution? I’ll also highlight a few key points.

$reg.EnumKey($HIVE,"").snames | foreach {
$sid = $_
write-host "Processing $sid" -ForegroundColor cyan
$regpath = "$sid\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$r = $reg.EnumKey($HIVE,$regpath)
if ($r.snames ) {
foreach ($app in $r.snames) {
#resolve SID
[WMI]$Resolve = "\\$computername\root\cimv2:Win32_SID.SID='$SID'"
if ($resolve.accountname) {
$Username = "$($resolve.ReferencedDomainName)\$($resolve.AccountName)"
}
else {
$Username = $SID
}
$hash = [ordered]@{Username = $Username;Name = $app}
#add a list of known properties
"Displayname","DisplayVersion","Publisher",
"InstallDate","InstallLocation","Comments","UninstallString" | foreach {
$value = $reg.GetStringValue($hive,"$regpath\$app",$_).sValue
$hash.Add($_,$value)
}
#write a custom object to the pipeline
[pscustomobject]$hash
} #foreach app
} #if snames
} #foreach sname in HKEY_USERS

Whenever I write PowerShell, I’m always thinking about objects in the pipeline, and this is no different. I want to create an object with application information. If the initial enumeration finds a hive with data in the sNames property, then that will require additional processing. The potentially tricky part is that there are different methods to call depending on the type of registry data, such as String or DWord. Fortunately, everything we are after is a string. And I already know the names. All I need to do is get the string value for each.
As you can see, I use the EnumValues() method that requires the Hive value and the registry entry path.

   "Displayname","DisplayVersion","Publisher",
   "InstallDate","InstallLocation","Comments","UninstallString" | foreach {
    $value = $reg.GetStringValue($hive,"$regpath\$app",$_).sValue
     $hash.Add($_,$value)
   }	

The GetStringValue() method is retrieving the value from the appropriate hive for a specific registry key and name. The sValue property is the actual piece of data that I want. In my code, I’m adding it to a hash table that I created earlier. I did that so when I’m finished I can turn that hashtable into an object.

[pscustomobject]$hash

Note that the [pscustomobject] accelerator requires PowerShell 3.0 and later. The other piece of the puzzle is that I want to identify the user. Right now all I have is the SID string. Fortunately, I can use WMI and the Win32_SID class to resolve it.

[WMI]$Resolve = "\\$computername\root\cimv2:Win32_SID.SID='$SID'"

I’ve added some error handling so that if the account doesn’t get resolved to a user name, then I’ll have to use the original SID. The end result is something like this:

Error handling for when the account doesn't get resolved to a user name. (Image Credit: Jeff Hicks)
Error handling for when the account doesn’t get resolved to a user name. (Image Credit: Jeff Hicks)

Because these are objects I could export the data, group it, or whatever I need to do. There is one final potential roadblock especially when querying remote computers. The [WMIClass] accelerator I used to connect to StdRegProv has no provision for alternate credentials.

The solution is to use the StdRegProv in a Powershell remoting session because that can use alternate credentials. Here’s a variation on my code sample:

$sb = {
$computername = $env:computername
[WMIClass]$Reg = "\\$computername\root\default:StdRegProv"
$HIVE = 2147483651
#enumerate subkeys
$reg.EnumKey($HIVE,"").snames | foreach {
$sid = $_
write-host "Processing $sid" -ForegroundColor cyan
$regpath = "$sid\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$r = $reg.EnumKey($HIVE,$regpath)
if ($r.snames ) {
foreach ($app in $r.snames) {
#$data = $reg.EnumValues($Hive,"$regpath\$app")
#resolve SID
[WMI]$Resolve = "\\$computername\root\cimv2:Win32_SID.SID='$SID'"
if ($resolve.accountname) {
$Username = "$($resolve.ReferencedDomainName)\$($resolve.AccountName)"
}
else {
$Username = $SID
}
$hash = [ordered]@{Username = $Username;Name = $app}
#add a list of known properties
"Displayname","DisplayVersion","Publisher",
"InstallDate","InstallLocation","Comments","UninstallString" | foreach {
$value = $reg.GetStringValue($hive,"$regpath\$app",$_).sValue
$hash.Add($_,$value)
}
#write a custom object to the pipeline
[pscustomobject]$hash
} #foreach app
} #if snames
} #foreach sname in HKEY_USERS
} #close scriptblock
$d = invoke-command $sb -computername $computername -Credential globomantics\administrator
$d | out-gridview -Title "$computername User Apps" 

Results of code sample. (Image Credit: Jeff Hicks)
Results of code sample. (Image Credit: Jeff Hicks)

It wouldn’t take much effort to take my code sample and turn it into a re-usable PowerShell function.
As you’ve seen, this can be a complicated task. The steps I’ve show you require WMI, which may not be an appropriate solution in your environment. In the next part of this series, I’ll show you how to use the CIM cmdlets to achieve the same results.