Using PowerShell to get a List of Installed Software from a Remote Computer Fast as Lightning
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,$Name)
$remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$Name)
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