Collecting Installed Software with PowerShell on Unmanaged Endpoints
What this collection answers
Use this when you need a point-in-time list of machine-installed desktop software on Windows computers you administer. The result answers “which uninstall registrations can this account read on these computers?” It is not proof of application use, licence compliance, or complete software coverage. Portable and per-user applications can be absent.
The examples below collect registry data and export CSV. They are starter code, reviewed against documentation without a recorded execution test. There is no separately downloadable inventory product, database or agent behind this article.
Prerequisites and permissions
Use Windows PowerShell 5.1 on supported Windows endpoints, with permission to read the machine uninstall keys. For remote collection, use an already approved PowerShell remoting configuration and an account allowed to connect. Do not enable WinRM or weaken authentication merely to collect this report. Run a local collection through your management platform if remoting is unavailable. See Microsoft's remote-command guidance.
Choose a restricted output directory before starting. Software names and paths can disclose business and security tooling. Do not put exports into a public share or commit them to a repository.
Collect both registry views on the computer being queried
This self-contained script block runs locally or can be sent to a remote session. It reads the 64-bit and 32-bit views explicitly, so the result does not depend on the caller's PowerShell process bitness. Each row retains the registry view and subkey; different versions are not collapsed simply because their names match.
# Illustrative starter: machine-level uninstall registrations only.
$collectRegistry = {
$views = if ([Environment]::Is64BitOperatingSystem) {
@([Microsoft.Win32.RegistryView]::Registry64,
[Microsoft.Win32.RegistryView]::Registry32)
} else { @([Microsoft.Win32.RegistryView]::Registry32) }
foreach ($view in $views) {
$base = $null
$uninstall = $null
try {
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
[Microsoft.Win32.RegistryHive]::LocalMachine, $view)
$uninstall = $base.OpenSubKey(
'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
if ($null -eq $uninstall) { continue }
foreach ($subkeyName in $uninstall.GetSubKeyNames()) {
$key = $null
try {
$key = $uninstall.OpenSubKey($subkeyName)
if ($null -eq $key) { continue }
$name = $key.GetValue('DisplayName')
if ([string]::IsNullOrWhiteSpace([string]$name)) { continue }
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Name = $name
Version = $key.GetValue('DisplayVersion')
Publisher = $key.GetValue('Publisher')
InstallDate = $key.GetValue('InstallDate')
RegistryView = $view.ToString()
RegistrySubkey = $subkeyName
Source = 'HKLM uninstall registry'
}
} finally { if ($key) { $key.Dispose() } }
}
} finally {
if ($uninstall) { $uninstall.Dispose() }
if ($base) { $base.Dispose() }
}
}
}
# Local collection; this writes no endpoint configuration.
$localInventory = & $collectRegistry
$localInventory | Sort-Object Name, Version, RegistryView |
Format-Table ComputerName, Name, Version, RegistryViewAn absent version remains an empty cell rather than causing the application to disappear. InstallDate is vendor-supplied data, not a reliable installation timestamp. The registry API opens these subkeys for reading; see RegistryKey.OpenSubKey.
Why this guide excludes Win32_Product
Querying Win32_Product can initiate Windows Installer consistency checks and repairs. That makes it unsuitable for this read-only inventory workflow, including a one-off collection. The previous WMI example has been removed. Microsoft documents the side effects in KB 974524.
Remote collection and error handling
Keep the script block above in the same session, then replace the sample hostname with one approved pilot device. Invoke-Command transmits the block itself; a function defined only on your workstation would not automatically exist on the endpoint.
$computers = @('LAPTOP-042') # Replace with approved target hostnames.
$inventoryErrors = @()
$results = Invoke-Command -ComputerName $computers -ThrottleLimit 4 `
-ScriptBlock $collectRegistry -ErrorVariable inventoryErrors -ErrorAction Continue
# Create this restricted folder through your approved process first.
$outputDirectory = 'C:\Inventory'
if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) {
throw 'Create and secure the inventory output directory before exporting.'
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$results | Select-Object ComputerName, Name, Version, Publisher, InstallDate,
RegistryView, RegistrySubkey, Source |
Export-Csv -LiteralPath "$outputDirectory\software-$stamp.csv" -NoTypeInformation
$inventoryErrors | Select-Object PSComputerName, TargetObject,
@{Name='Error';Expression={$_.Exception.Message}} |
Export-Csv -LiteralPath "$outputDirectory\errors-$stamp.csv" -NoTypeInformationA connection or registry error can leave a partial result. Treat any computer with an error as incomplete and reconcile requested hostnames against returned ComputerName values. A computer with no returned rows is not proof of “no software.” Resolve the failed read and repeat only the affected target. Do not silently turn failures into compliant devices.
Store packages are a separate dataset
In an elevated local Windows PowerShell session, Get-AppxPackage -AllUsers lists installed packages across users. Provisioned packages are the packages offered to new profiles and are a different inventory. Neither collection removes applications. See Get-AppxPackage and Get-AppxProvisionedPackage.
Get-AppxPackage -AllUsers |
Select-Object Name, PackageFullName, Version, Publisher
Get-AppxProvisionedPackage -Online |
Select-Object DisplayName, PackageName, VersionExport these separately if required. Do not merge package identities into desktop application names and claim a deduplicated total.
Validate the pilot and handle failures
Compare a known 32-bit and 64-bit application against Settings > Apps on the same endpoint. Confirm the remote row identifies that endpoint, a missing version remains visible, and an unreachable test target is recorded as incomplete. Review exports as data in a trusted viewer; untrusted application names can resemble spreadsheet formulas.
If access is denied, resolve the approved account or remoting configuration rather than broadening fleet permissions. If per-user or portable software is needed, use a documented additional source such as Configuration Manager inventory; this script does not enumerate other users' hives. There is no endpoint configuration rollback for these reads. Remove wrongly shared exports, revoke their shared links and follow your organisation's data handling process.
Related resources
- Microsoft Graph PowerShell migration covers cloud directory queries rather than local software discovery.
- PowerShell hub provides the other published automation guidance.
Correction record
10 September 2026 — Reviewed against documentation. Removed Win32_Product repair side effects, fixed remote execution and computer attribution, retained versions, and added explicit incomplete-result handling.
Jack Hadcroft
LinkedInBand 6 Endpoint Specialist and author of AdminSignal
Technical claims are reviewed against current Microsoft documentation. Lab or tenant checks are named only when they were done.
Independent publication. About · Editorial policy