Learn What IT Pros Need to Know About Windows 11 - August 24th at 1 PM ET! Learn What IT Pros Need to Know About Windows 11 - August 24th at 1 PM ET!
PowerShell

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.

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.

$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.

Sponsored Content

Read the Best Personal and Business Tech without Ads

Staying updated on what is happening in the technology sector is important to your career and your personal life but ads can make reading news, distracting. With Thurrott Premium, you can enjoy the best coverage in tech without the annoying ads.

$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.

$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.

$regcim.cimclassmethods
Using the $regcim object to find definitions of all the class methods in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the $regcim object to find definitions of all the class methods in Windows PowerShell. (Image Credit: Jeff Hicks)

Let’s narrow the focus to EnumKey().

$regcim.CimClassMethods["EnumKey"].parameters
Looking at parameters for EnumKey() in Windows PowerShell. (Image Credit: Jeff Hicks)
Looking at parameters for EnumKey() in Windows PowerShell. (Image Credit: Jeff Hicks)

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.

$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.

$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.

Invoke-CimMethod @paramHash | select -expand snames
Writing an array of strings called sNames in Windows PowerShell. (Image Credit: Jeff Hicks)
Writing an array of strings called sNames in Windows PowerShell. (Image Credit: Jeff Hicks)

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.

$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.

Updating Invoke-Command parameters that have changed. (Image Credit: Jeff Hicks)
Updating Invoke-Command parameters that have changed. (Image Credit: Jeff Hicks)

On x64 systems I might also want to check this path:

$rpath = "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"

Re-running my code provides this result:

The Out-GridView dialog box. (Image Credit: Jeff Hicks)
The Out-GridView dialog box. (Image Credit: Jeff Hicks)

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.

$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.

$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:

Specifying a WMI path. (Image Credit: Jeff Hicks)
Specifying a WMI path. (Image Credit: Jeff Hicks)

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.

#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:

PS C:\> $d4 = .\Get-InstalledApplicationFromRegistry.ps1 -IncludeUsers -Computername chi-win81 -Credential globomantics\jeff
PS C:\> $d4 | out-gridview
The results of the CMI-based script. (Image Credit: Jeff Hicks)
The results of the CMI-based script. (Image Credit: Jeff Hicks)

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.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

Register for Advanced Microsoft 365 Day!

GET-IT: Advanced Microsoft 365 1-Day Virtual Conference - Live August 24th!

Join us on Tuesday, August 24th and hear from Microsoft MVPs and industry experts about how to take advantage of Microsoft 365 at a technical level and dive deep into the features and functionality that will make your environment more secure and compliant.

RSVP Now

Sponsored By