Skip to main content

Reviewed against documentation Sep 10, 2026. Removed Win32_Product repair side effects, fixed remote execution and computer attribution, retained versions, and added explicit incomplete-result handling.

PowerShellIntermediate

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.

PowerShell
# 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, RegistryView

An 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.

PowerShell
$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" -NoTypeInformation

A 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.

PowerShell
Get-AppxPackage -AllUsers |
    Select-Object Name, PackageFullName, Version, Publisher
Get-AppxProvisionedPackage -Online |
    Select-Object DisplayName, PackageName, Version

Export 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.

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, Band 6 Endpoint Specialist and author of AdminSignal

Jack Hadcroft

LinkedIn

Band 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