Using PowerShell to get a List of Installed Software from a Remote Computer Fast as Lightning

7 minute read

Hey! I’m pulling out a time-tested PowerShell function from my days on the service desk today. Sure it is an old script, but there ain’t a faster way to get a real-time list of installed software using PowerShell, guaranteed.

WMI…

Don’t use WMI. It is slow, clunky, and only moderately useful. I don’t want to go into details on that because there is a multitude of information on this topic already.

Software installs from the registry

Locations

Installed software is tracked in 2 hives in the registry, depending on how it was installed. If it was installed for all users, it’ll be listed in one of two locations:

# x86
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall"
# x64
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"

And if it was installed for the current user, it can be found:

"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall"

If you are a human being and you take a look at any of those directories, you’ll probably notice why there is the App Wizard for tracking installed software.

Getting Data isn’t super easy

So if we are simply getting data on our local computer, we can just:

Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall"

And we get great data in a moderate to poorly usable format:

{C4B618E5-7F53-4F09-9FD3-A1CEA AuthorizedCDFPrefix :
DEFF5CB}                       Comments            :
                               Contact             :
                               DisplayVersion      : 6.1.1.0
                               HelpLink            : https://github.com/PowerShell/PowerShell
                               HelpTelephone       :
                               InstallDate         : 20181213
                               InstallLocation     :
                               InstallSource       : C:\Users\Anthony\Downloads\
                               ModifyPath          : MsiExec.exe /X{C4B618E5-7F53-4F09-9FD3-A1CEADEFF5CB}
                               NoModify            : 1
                               Publisher           : Microsoft Corporation
                               Readme              :
                               Size                :
                               EstimatedSize       : 146306
                               UninstallString     : MsiExec.exe /X{C4B618E5-7F53-4F09-9FD3-A1CEADEFF5CB}
                               URLInfoAbout        :
                               URLUpdateInfo       :
                               VersionMajor        : 6
                               VersionMinor        : 1
                               WindowsInstaller    : 1
                               Version             : 100728833
                               Language            : 1033
                               DisplayName         : PowerShell 6-x64

Immediate usefulness aside, we now know that PowerShell 6.1.1 is currently installed on my system and that I should probably update that.

Now, if we wanted to parse that for just the ```DisplayName`` we could:

Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" | Select-Object DisplayName

Except that doesn’t work because:

(Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall")[0].GetType().Name

Is RegistryKey which, apparently, doesn’t play well with the pipeline because it functions similar to a hashtable which requires us to access key value pairs instead of properties. Thats fine, it just makes things a little more complicated and gives us even more reason to turn this into a function!

But it can be done

So, with that in mind, lets actually get some specific data from each key! Looking at the members for the object:

(Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall")[0] | Get-Member

We see a GetValue method. We can use said method like so:

$tmp = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$tmp[0].GetValue('DisplayName')

And in my case, that outputs:

Streamlabs OBS 0.11.9

And of course, we could write a foreach loop to look at all the values:

foreach($reg in $tmp){
    $reg.GetValue('DisplayName')
}

But that is only good for 1 registry location on 1 computer, so thats not going to do us much of any good, unless you only manage your own computer.

Getting remote data

Using the following method, getting remote data from the registry requires admin permissions and the RemoteRegistry service to work. It does NOT, however, require PowerShell remoting to be enabled.

We’re going to start by creating a .NET registry object:

$lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine

And then open a remote connection, specifying a computer name:

$remoteLMReg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,'AlphaWolf')

And if it is successful, we won’t get any ouput.

Then, to list out the list of software on that computer, we simply query the registry using $remoteLMReg:

$remoteLMReg.OpenSubkey("Software\Microsoft\Windows\CurrentVersion\Uninstall")

And if that computer has anything installed on it, we’ll get a nice list of registry keys exactly like before.

Make it a function!

So! To make this a function we need to account for a number of things I’ve mentioned in this post. First, the different registry locations. I’ll do this by declaring the following in the Begin{} block:

$lmKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall","SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
$lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine
$cuKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
$cuReg = [Microsoft.Win32.RegistryHive]::CurrentUser

Then, since we are doing this all remotely, we need to make sure the computer is online before we try to connect to it. There are many ways to do this, here’s what I’m using inside of the Process{} block:

if (!(Test-Connection -ComputerName $Name -count 1 -quiet)) {
    Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Name
    Break
}

Then we need to declare our output object and the 2 [Microsoft.Win32.RegistryKey] objects for connecting to the remote registries:

$masterKeys = @()
$remoteCURegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($cuReg,$Name)
$remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$Name)

Finally, we’ll have our loop where we grab all of the data. For each of the listed $lmKeys we are going to open it, get all of its subkeys, and grab data from them and store it in our output variable, $masterKeys.

foreach ($key in $lmKeys) {
    $regKey = $remoteLMRegKey.OpenSubkey($key)
    foreach ($subName in $regKey.GetSubkeyNames()) {
        foreach($sub in $regKey.OpenSubkey($subName)) {
            $masterKeys += (New-Object PSObject -Property @{
                # Data to grab
            })
        }
    }
}

The data that I’ve decided is the most useful is the following, and you’ll notice that I’m using the .GetValue() method we saw from before:

$masterKeys += (New-Object PSObject -Property @{
    "ComputerName" = $Name
    "Name" = $sub.GetValue("displayname")
    "SystemComponent" = $sub.GetValue("systemcomponent")
    "ParentKeyName" = $sub.GetValue("parentkeyname")
    "Version" = $sub.GetValue("DisplayVersion")
    "UninstallCommand" = $sub.GetValue("UninstallString")
    "InstallDate" = $sub.GetValue("InstallDate")
    "RegPath" = $sub.ToString()
})

So that turns into the following Get-InstalledSoftware function (Which you can now find in my Utilities Repo). You will notice that I added some aliases for the $Name parameter and set it to accept input from the pipeline.

Function Get-InstalledSoftware {
    Param(
        [Alias('Computer','ComputerName','HostName')]
        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$true,
            Position=1
        )]
        [string]$Name = $env:COMPUTERNAME
    )
    Begin{
        $lmKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall","SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        $lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine
        $cuKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
        $cuReg = [Microsoft.Win32.RegistryHive]::CurrentUser
    }
    Process{
        if (!(Test-Connection -ComputerName $Name -count 1 -quiet)) {
            Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Computer
            Break
        }
        $masterKeys = @()
        $remoteCURegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($cuReg,$computer)
        $remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$computer)
        foreach ($key in $lmKeys) {
            $regKey = $remoteLMRegKey.OpenSubkey($key)
            foreach ($subName in $regKey.GetSubkeyNames()) {
                foreach($sub in $regKey.OpenSubkey($subName)) {
                    $masterKeys += (New-Object PSObject -Property @{
                        "ComputerName" = $Name
                        "Name" = $sub.GetValue("displayname")
                        "SystemComponent" = $sub.GetValue("systemcomponent")
                        "ParentKeyName" = $sub.GetValue("parentkeyname")
                        "Version" = $sub.GetValue("DisplayVersion")
                        "UninstallCommand" = $sub.GetValue("UninstallString")
                        "InstallDate" = $sub.GetValue("InstallDate")
                        "RegPath" = $sub.ToString()
                    })
                }
            }
        }
        foreach ($key in $cuKeys) {
            $regKey = $remoteCURegKey.OpenSubkey($key)
            if ($regKey -ne $null) {
                foreach ($subName in $regKey.getsubkeynames()) {
                    foreach ($sub in $regKey.opensubkey($subName)) {
                        $masterKeys += (New-Object PSObject -Property @{
                            "ComputerName" = $Name
                            "Name" = $sub.GetValue("displayname")
                            "SystemComponent" = $sub.GetValue("systemcomponent")
                            "ParentKeyName" = $sub.GetValue("parentkeyname")
                            "Version" = $sub.GetValue("DisplayVersion")
                            "UninstallCommand" = $sub.GetValue("UninstallString")
                            "InstallDate" = $sub.GetValue("InstallDate")
                            "RegPath" = $sub.ToString()
                        })
                    }
                }
            }
        }
        $woFilter = {$null -ne $_.name -AND $_.SystemComponent -ne "1" -AND $null -eq $_.ParentKeyName}
        $props = 'Name','Version','ComputerName','Installdate','UninstallCommand','RegPath'
        $masterKeys = ($masterKeys | Where-Object $woFilter | Select-Object $props | Sort-Object Name)
        $masterKeys
    }
    End{}
}

Usage

You can get the local computer’s software installation:

Get-InstalledSoftware

Or you can get a remote computer’s installed software:

Get-InstalledSoftware AlphaWolf

Or, most usefully I would argue, you can get a list of computers from AD and get their installed software:

Get-ADComputer -SearchBase 'OU=IT,OU=Workstations,DC=theposhwolf,DC=com' -Filter * | Get-InstalledSoftware

In closing

If you found this useful, great! Leave me a comment, tweet at me on Twitter, email me, whatever. If you find an issue with Get-InstalledSoftware, feel free to open an issue on it in my Utilities Github repo.

Leave a Comment