I spent a few years consulting in the field and one question would always come up. How do I tell whats installed on my servers? There’s tons of advice on how to do this, some good and some bad, lets go thru it.

The Good Stuff: A function to get installed programs from the registry. Get-InstalledProgram

The Bad - Win32_Product

Get-WmiObject -Class Win32_Product

Please don’t do this. I did this… What’s sad is if you look for how to get installed software this is still a common recommendation. While this does accomplish the goal, it comes with limitations. The first being that it’s incredibly slow.

The reason this command is so slow is that when you query the class, the provider actually performs a reconfiguration on every package installed on the system. If you check the event log after running the command (OMG you didn’t run it did you?!?!) you’ll see a ton of events from the MsiInstaller source.

Get-EventLog -LogName Application -After ( Get-Date ) .AddMinutes ( -5 ) -Source MsiInstaller | Where -Object { $PSItem .EventID -eq '1035' } | Measure-Object

If I perform just one query against Win32_Product on my machine, it generates over 550 events! All of them looking something like the example below.

WMIC

Please bear with me as we take a trip down memory lane. Did you know PowerShell wasn’t Snover’s first time at the command line? Before Posh was even a thing, there was WMIC.exe . This tool is interesting to say the least. With a syntax that is oddly familiar and built specifically to interact with WMI, WMIC.exe might even be PowerShells big brother. Take a look how to get the cpu information from the local machine.

WMIC.exe CPU GET NAME

Which produced the following output

Caption Name NumberOfCores Intel64 Family 6 Model 60 Stepping 3 Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz 4

Since it’s just interacting with WMI, you can use WMIC.exe to get installed software.

WMIC.exe Product Get Name,Version

This same query could even be run against remote machines using the node switch.

WMIC.exe /node:Server1 Product Get Name,Version

The reason I wanted to include WMIC is because there are still a lot of references to it online. It’s not obvious at first, but the above WMIC.exe command is performing a query against Win32_Product . Due to this, it suffers from all of the same limitations and should be avoided where ever possible.

The Good - Registry

A much more efficient way to enumerate software is to query the registry (in fact, this is how add remove programs does it). When software is installed it should leave an entry in the registry. There’s just one catch, the location of this entry could be in a couple of different places. There’s 2 sections for software installed at the machine level, one for 32-bit applications and one for 64-bit.

HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\

There’s also 2 other locations for software installed at the user level, again one for 32 bit applications and one for 64 bit.

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

Using The Registry Provider

Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ *

There’s a lot of great information in these registry keys, including the uninstall string. This is the command that get executed when you click uninstall from add remove programs. To make working with these keys easier, I created a function Get-InstallProgram. It basically wraps the loop functionality required to check all 4 locations and adds parameters for DisplayName and Publisher . Here’s a shot of it in action:

Using Dot Net

You can also use the raw dot net methods to interact with the registry. The below example uses the registry hive on the local machine to find the installed programs.

$computerName = $env :COMPUTERNAME $registryHive = [ Microsoft.Win32.RegistryHive]::LocalMachine $registry = [ Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey ( $registryHive , $computerName ) $registryPath = "Software\Microsoft\Windows\CurrentVersion\Uninstall" $keyNames = $registry .OpenSubKey ( $registryPath ) .GetSubKeyNames () ForEach ( $key in $keyNames ) { $registry .OpenSubKey ( $registryPath ) .OpenSubKey ( $key ) .GetValue ( 'DisplayName' ) }

What’s neat about using this method is, you can query computers using the remote registry service. This could be useful in some scenario where PowerShell remoting is not available but remote registry is. This function was created to make interacting with the remote registry a little easier.

The New - PackageManagement

In version 5 of PowerShell the team introduced the PackageManagement module. This module introduced a ton of great functionality for managing software. Get-Package is now built in, and can be used to retrieve locally installed software. Not only will it find installed programs, it’ll also list any chocolatey packages you have installed. If you’re on version 5 or later its quick, easy and built in.

Wrapping Up

I hope this post was helpful. With these new tools at your disposal, the next time the boss tells you to inventory the servers it should be a breeze.