- Create a certificate-signed RDP shortcut via Group Policy - Fri, Aug 9 2019
- Monitor web server uptime with a PowerShell script - Tue, Aug 6 2019
- How to build a PowerShell inventory script for Windows Servers - Fri, Aug 2 2019
Software management in Windows has always been a painful ordeal. It seems like every piece of software installs differently! But, usually, they have one thing in common – the product GUID.
First, you'll need to know where a product's GUID is located. Spoiler alert: it's in the registry.
The registry is a big place though. We're looking for two (or more) keys in the following paths:
HKEY_LOCAL_MACHINE:\Software\Microsoft\Windows\CurrentVersion\Uninstall HKEY_LOCAL_MACHINE:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
These are where each piece of software places its keys when installed on the system. If the user decided to install the software under a user context, you'd find the registry keys here:
HKEY_USERS:\<User GUID>\Software\Microsoft\Windows\CurrentVersion\Uninstall
In these registry paths, you'll find the keys that represent each piece of software installed on a Windows PC.
You'll notice that some key names start with a GUID while others don't. This is what I mean about no standards! The type of installer is what dictates the changes to the PC that the installer makes.
We can use the Get-ChildItem cmdlet to query the registry keys and extract the GUID from each of the paths mentioned above. Notice in the screenshot below that I can narrow down the output to only those keys with a GUID, using the PSChildName property.
This is great, but not much use because I can't see which software title relates to each GUID. It'd also be nice if we could enter a title and then be returned a single GUID.
So let's create a script for this.
First, we need to pull together the results from each of the registry paths. You can see below that I'm creating an array from each registry path. Since there could be multiple users on a PC, I'm forced to manually mount the HKEY_USERS PS drive and check each of those paths as well. This gives me a full picture of every piece of software installed on the system.
$UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS $UninstallKeys += Get-ChildItem HKU: -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" }
Once I’ve collected the keys from all of the paths, I need to loop through them and extract the registry value that represents the title from each of the keys. I do this by running Get-ChildItem on each of the keys, limiting it to only the keys named 'GUID'. I then use calculated properties to return both the GUID property and the name property, by using the GetValue() method on each key to extract the name of the software.
foreach ($UninstallKey in $UninstallKeys) { Get-ChildItem -Path $UninstallKey -ErrorAction SilentlyContinue | Where {$_.PSChildName -match '^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$'} | Select-Object @{n='GUID';e={$_.PSChildName}}, @{n='Name'; e={$_.GetValue('DisplayName')}} }
Once I have this code in place, I get output that looks like this:
At this point, I could be done if I wanted, but I'm usually not looking for all of the software titles' GUIDs. I only want one.
This is a great place to create a function, so I've done exactly that. The function is called Get-InstalledSoftware and pulls all of this logic together to allow us to pass a software title to a function and return the software’s GUID:
function Get-InstalledSoftware { <# .SYNOPSIS Retrieves a list of all software installed .EXAMPLE Get-InstalledSoftware This example retrieves all software installed on the local computer .PARAMETER Name The software title you'd like to limit the query to. #> [OutputType([System.Management.Automation.PSObject])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$Name ) $UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS $UninstallKeys += Get-ChildItem HKU: -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" } if (-not $UninstallKeys) { Write-Verbose -Message 'No software registry keys found' } else { foreach ($UninstallKey in $UninstallKeys) { if ($PSBoundParameters.ContainsKey('Name')) { $WhereBlock = { ($_.PSChildName -match '^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$') -and ($_.GetValue('DisplayName') -like "$Name*") } } else { $WhereBlock = { ($_.PSChildName -match '^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$') -and ($_.GetValue('DisplayName')) } } $gciParams = @{ Path = $UninstallKey ErrorAction = 'SilentlyContinue' } $selectProperties = @( @{n='GUID'; e={$_.PSChildName}}, @{n='Name'; e={$_.GetValue('DisplayName')}} ) Get-ChildItem @gciParams | Where $WhereBlock | Select-Object -Property $selectProperties } } }
Using this Get-InstalledSoftware function, I can now use a single command to query all registry paths for the GUID for a single software title.
Great work, very usefull!
OK, I’m assuming it’s something stupid I’m doing, or not doing, but I’m obviously missing something here. Saved the script, called with the name parameter and without. No output in either case. No errors, nothing. What am I missing?
Awesome, works great, saved me the time to build my own function, thanks again. 🙂
Good staff thanks!
PC inventory ? using GPO and PS script ? asynchronous for not delay logon ?
alternatives ?
As an alternative, I find the following PS one liner easier to use rather than parse the registry.
Get-WMIObject Win32_Product | Where {$_.Name -like "*Office*"} | FT Name,IdentifyingNumber
That works great, thank you, Adam!
I would add that once you set up the function by copying the script into PowerShell and hit enter, you won't see anything happen yet. Then by calling the function and putting a space and ? in after the -name part and hit enter you get all of the software installed by name. Then you can copy the full name and version, hit the up key at the prompt and paste the name in and you get the single GUID you want.
The next thing I would like to know is how to put all of this into a .bat file that will open PowerShell, create the GetSoftwareInstalled function, pull up the list of installed software and prompt me for a name. Does anybody know how to do that?
Using Get-WMIObject -class Win32_Product is a bad idea. If available, it will trigger the repair function of the installation, possibly resetting things. It's also incredibly slow compared to parsing the registry.
David F.
Nice work Adam. I was able to use the * wildcard and get a list of all product modules from the same vendor.
Adam,
Can this be revised to enter a vendor name instead to ensure I get all software from the same vendor and can it be run on a remote computer? I just realized not all software display names have the vendor name.within them.