PowerShell Software Inventory: List Installed Apps on Windows

Your license audit requires knowing how many PCs have Adobe Acrobat installed and which version — a question that requires checking the registry on every machine. PowerShell list installed software scripts query the Windows registry uninstall keys and CIM to enumerate all applications with name, version, and publisher. This post covers registry-based queries (the reliable method), CIM-based queries with their tradeoffs, remote software inventory, cross-machine comparison, and multi-PC CSV export.
Query via Registry (Uninstall Keys)
The Windows registry stores installed software under two paths: one for 64-bit applications and one for 32-bit (WOW6432Node). Querying both gives a complete picture:
function Get-InstalledSoftware {
param([string]$ComputerName = $env:COMPUTERNAME)
$regPaths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$software = foreach ($path in $regPaths) {
try {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object DisplayName |
Select-Object DisplayName, DisplayVersion, Publisher,
InstallDate,
@{N='Architecture'; E={ if ($path -like '*WOW6432*') { '32-bit' } else { '64-bit' } }}
}
catch { }
}
$software | Sort-Object DisplayName
}
$apps = Get-InstalledSoftware
Write-Host "Total installed applications: $($apps.Count)"
$apps | Select-Object -First 10 | Format-Table -AutoSize
DisplayName DisplayVersion Publisher Architecture
----------- -------------- --------- ------------
7-Zip 22.01 (x64) 22.01.00.0 Igor Pavlov 64-bit
Google Chrome 124.0.6367.61 Google LLC 64-bit
Microsoft 365 Apps 16.0.17628.20144 Microsoft 64-bit
Query via CIM Win32_Product
Get-CimInstance -ClassName Win32_Product returns MSI-installed software. Use it with caution: Microsoft documents that querying Win32_Product triggers a Windows Installer consistency check on every installed package, which can cause reconfiguration dialogs or slow performance.
# Use sparingly — triggers Windows Installer repair checks
Get-CimInstance -ClassName Win32_Product |
Select-Object Name, Version, Vendor, InstallDate |
Sort-Object Name | Format-Table -AutoSize
For most inventory purposes, the registry approach is faster, safer, and returns more applications (including non-MSI installers).
Filter by Vendor or Name
Filter the software list to find specific products — useful for license audits and security vulnerability checks:
$apps = Get-InstalledSoftware
# Find all Adobe products
$apps | Where-Object Publisher -like "Adobe*" | Format-Table DisplayName, DisplayVersion, Publisher
# Find specific product by name
$apps | Where-Object DisplayName -like "*Acrobat*"
# Find software installed in the last 30 days
$cutoff = (Get-Date).AddDays(-30).ToString("yyyyMMdd")
$apps | Where-Object { $_.InstallDate -and $_.InstallDate -ge $cutoff } |
Select-Object DisplayName, DisplayVersion, InstallDate | Sort-Object InstallDate -Descending
Remote Software Inventory
Query installed software on remote computers using Invoke-Command with the registry function:
$targetPC = "LAPTOP-001"
$remoteSoftware = Invoke-Command -ComputerName $targetPC -ScriptBlock {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $paths) {
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object DisplayName |
Select-Object DisplayName, DisplayVersion, Publisher
}
} | Sort-Object DisplayName
Write-Host "$($remoteSoftware.Count) applications on $targetPC"
Compare Software Lists Across PCs
Find software present on one machine but not another — useful for troubleshooting “it works on my machine” issues:
$pc1Software = (Get-InstalledSoftware).DisplayName
$pc2Software = (Invoke-Command -ComputerName "LAPTOP-002" -ScriptBlock {
$paths = @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')
$paths | ForEach-Object { Get-ItemProperty $_ -ErrorAction SilentlyContinue | Where-Object DisplayName | Select-Object -ExpandProperty DisplayName }
}).DisplayName
$onlyOnPC1 = $pc1Software | Where-Object { $_ -notin $pc2Software }
$onlyOnPC2 = $pc2Software | Where-Object { $_ -notin $pc1Software }
Write-Host "Only on local PC: $($onlyOnPC1.Count)"
Write-Host "Only on LAPTOP-002: $($onlyOnPC2.Count)"
$onlyOnPC1 | Sort-Object
Export Multi-PC Inventory to CSV
Collect software inventory from multiple computers in parallel and export a combined report:
$computers = Get-Content "C:\Scripts\workstations.txt"
$reportPath = "C:\Reports\SoftwareInventory_$(Get-Date -Format 'yyyyMMdd').csv"
$inventory = Invoke-Command -ComputerName $computers -ThrottleLimit 20 -ScriptBlock {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $paths) {
Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object DisplayName |
Select-Object @{N='Computer';E={$env:COMPUTERNAME}},
DisplayName, DisplayVersion, Publisher
}
} -ErrorAction SilentlyContinue
$inventory | Export-Csv -Path $reportPath -NoTypeInformation
Write-Host "Inventory: $($inventory.Count) entries from $($computers.Count) PCs → $reportPath"
Common Errors and Fixes
-
Win32_Product triggers Windows Installer repair on every query. Querying
Win32_Productis a well-documented issue — it callsmsiexec.exe /fvon every package. This can cause unexpected reconfiguration dialogs and is slow. Prefer the registry-based approach for all inventory scripts. -
32-bit vs 64-bit registry paths return different results. 32-bit applications on 64-bit Windows are registered under
WOW6432Node. Querying onlyHKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstallmisses 32-bit applications. Always query both paths for a complete inventory.
Related Cmdlets / See Also
Wrapping Up
Registry-based software inventory is faster and safer than Win32_Product. Always query both the 64-bit and WOW6432Node paths, use Invoke-Command with -ThrottleLimit for parallel multi-PC collection, and export to CSV for license audits and compliance reporting.


