Reviewed and updated Mar 31, 2026.

PowerShellIntermediate

Building a PowerShell-Driven Software Inventory System for Unmanaged Endpoints

Jack Hadcroft18 min read

Why Build Your Own

Enterprise RMM tools and Intune both provide software inventory. But for legacy environments, air-gapped networks, or incident response scenarios where you need to know right now what is installed on a machine without an agent, a PowerShell-based pipeline is indispensable.

This guide builds a three-source inventory system that combines:

  1. Win32_Product WMI class (installed MSI packages)
  2. Registry uninstall keys (broader coverage, including non-MSI installs)
  3. A lightweight SQLite output for persistence and querying across multiple hosts

Use this only on systems you are authorised to administer. Software inventory can expose installed security tools, business applications, usernames embedded in paths, and licensing data, so treat exports as internal operational evidence rather than general-purpose files.

Prerequisites and Scope

Before you run inventory collection across more than your own workstation, confirm:

  • You have permission to query each endpoint.
  • WinRM or another approved remote execution method is enabled and monitored.
  • The account has read access to the target registry paths.
  • The output folder is restricted to IT administrators.
  • You know whether the fleet includes 32-bit apps, per-user apps, portable apps, or AppX/MSIX packages.

This guide focuses on installed desktop software visible through machine-level registry uninstall keys and MSI inventory. It does not fully enumerate browser extensions, user-profile portable applications, Store apps installed per user, or application usage frequency.

The registry uninstall keys provide the most complete picture of installed software and are faster to query than WMI:

PowerShell
function Get-InstalledSoftwareFromRegistry {
    param([string]$ComputerName = $env:COMPUTERNAME)
    
    $uninstallPaths = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )
    
    $software = foreach ($path in $uninstallPaths) {
        Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
            Where-Object { $_.DisplayName -and $_.DisplayVersion } |
            Select-Object @{N='Name';E={$_.DisplayName}},
                          @{N='Version';E={$_.DisplayVersion}},
                          @{N='Publisher';E={$_.Publisher}},
                          @{N='InstallDate';E={$_.InstallDate}},
                          @{N='Source';E={'Registry'}},
                          @{N='ComputerName';E={$ComputerName}}
    }
    return $software | Sort-Object Name -Unique
}

For remote collection, avoid assuming the local registry provider can see another computer. Use PowerShell remoting and run the registry query on the remote endpoint:

PowerShell
Invoke-Command -ComputerName "LAPTOP-042" -ScriptBlock {
    Get-InstalledSoftwareFromRegistry
}

If WinRM is not available, fall back to your approved endpoint management platform rather than enabling remoting across production without change control.

Source 2: WMI Win32_Product

Use this as a supplementary source. Note that querying Win32_Product triggers an MSI reconfiguration check, which can cause performance issues on some systems. Do not use it as a primary source for frequent polling.

PowerShell
function Get-InstalledSoftwareFromWmi {
    param([string]$ComputerName = $env:COMPUTERNAME)
    
    Get-CimInstance -ClassName Win32_Product -ComputerName $ComputerName -ErrorAction SilentlyContinue |
        Select-Object @{N='Name';E={$_.Name}},
                      @{N='Version';E={$_.Version}},
                      @{N='Publisher';E={$_.Vendor}},
                      @{N='InstallDate';E={$_.InstallDate}},
                      @{N='Source';E={'WMI'}},
                      @{N='ComputerName';E={$ComputerName}}
}

Use Win32_Product only for targeted troubleshooting or one-off validation. For scheduled inventory, registry-based collection plus Intune, Configuration Manager, or an approved RMM export is usually safer and faster.

Source 3: AppX and Store Apps

Modern Store and MSIX packages do not always appear in the classic uninstall keys. For Windows 10/11 endpoints, collect provisioned and installed packages separately:

PowerShell
function Get-AppxInventory {
    Get-AppxPackage -AllUsers |
        Select-Object Name, PackageFullName, Version, Publisher, InstallLocation
}

Do not mix AppX package names directly into the same deduplication key as desktop software. Keep a Source column so later reporting can distinguish MSI, registry, and AppX records.

Merging and Deduplicating

PowerShell
function Get-CompleteSoftwareInventory {
    param([string]$ComputerName = $env:COMPUTERNAME)
    
    $registry = Get-InstalledSoftwareFromRegistry -ComputerName $ComputerName
    $wmi = Get-InstalledSoftwareFromWmi -ComputerName $ComputerName
    
    $combined = @($registry) + @($wmi)
    
    # Deduplicate by Name + Version, preferring registry source
    $combined | Group-Object Name, Version | 
        ForEach-Object { $_.Group | Where-Object { $_.Source -eq 'Registry' } | 
            Select-Object -First 1 } |
        Where-Object { $_ -ne $null }
}

For better deduplication, normalise publisher names and trim whitespace before grouping. Do not deduplicate on name alone: products such as Microsoft Visual C++ Redistributable legitimately exist in multiple versions.

Running Across Multiple Hosts

For environments where WinRM is available:

PowerShell
$computers = Get-Content "C:\Inventory\computers.txt"
$results = foreach ($computer in $computers) {
    try {
        Get-CompleteSoftwareInventory -ComputerName $computer
    } catch {
        Write-Warning "Failed to inventory $computer`: $_"
    }
}

$results | Export-Csv "C:\Inventory\software-inventory-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation

For larger environments, add throttling and error capture so one offline device does not hide the rest of the inventory run:

PowerShell
$inventory = Invoke-Command -ComputerName $computers -ThrottleLimit 16 -ScriptBlock {
    Get-CompleteSoftwareInventory
} -ErrorVariable inventoryErrors -ErrorAction Continue

$inventoryErrors | Select-Object TargetObject, Exception |
    Export-Csv "C:\Inventory\software-inventory-errors-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation

Output and Querying

Export to HTML for a shareable report:

PowerShell
$results | 
    Sort-Object ComputerName, Name |
    ConvertTo-Html -Title "Software Inventory $(Get-Date -Format 'yyyy-MM-dd')" `
                   -PreContent "<h1>Software Inventory Report</h1>" |
    Out-File "C:\Inventory\report.html"

For environments where you want persistent, queryable results, consider exporting to CSV and importing into a scheduled task that builds a rolling inventory database.

Validation Steps

After the first run, validate the data before making decisions from it:

  1. Pick five known endpoints: a clean Windows build, a heavy developer workstation, a shared device, a recently rebuilt machine, and an older legacy device.
  2. Compare the report against Control Panel, Settings > Apps, Intune discovered apps, or Configuration Manager inventory.
  3. Confirm 32-bit applications appear from the WOW6432Node registry path.
  4. Confirm failed devices are recorded in the error export.
  5. Check that the output does not include secrets, command-line arguments, or user data outside the software inventory scope.

Common Errors

Access is denied

The account does not have permission to query the remote endpoint or WinRM is blocked. Confirm the device is reachable, the account is in an approved admin group, and remote management is allowed by policy.

The report misses per-user apps

Machine-level uninstall keys do not cover every per-user installer. Add user-hive collection only if you have a documented need and a privacy-approved handling process for user-profile data.

Win32_Product run is slow

This is expected on some systems because MSI consistency checks are triggered. Use it as a supplementary source, not the default scheduled source.

Rollback and Data Handling

The inventory functions shown here are read-only. There is no endpoint rollback action unless you separately deploy changes based on the report. If an export was written to the wrong location, remove it from that location, rotate any exposed shared links, and regenerate the report into an IT-restricted path.

Microsoft Intune

Recommended

Manage, secure, and report on all your endpoints from a single cloud-native console.

Try it
Jack Hadcroft, Endpoint specialist and author of AdminSignal

Jack Hadcroft

LinkedIn

Endpoint specialist and author of AdminSignal

Jack Hadcroft is an endpoint specialist working with Microsoft Intune, Windows clients, Microsoft Entra ID, Group Policy, and PowerShell in Microsoft 365 estates. He publishes independent, source-backed guidance that focuses on prerequisites, validation evidence, operational risk, and safe rollout decisions, with examples and limitations labelled clearly.

AdminSignal content is produced independently. Editorial policy