How to List All Installed Software Using PowerShell

PowerShell window listing installed applications with Name, Version and Publisher columns, generated by running Get-ItemProperty or Get-WmiObject queries against registry and WMI..

How to List All Installed Software Using PowerShell
SPONSORED

Sponsored by Dargslan.com โ€” Build Smarter, Not Harder

At Dargslan Publishing, we help modern entrepreneurs design scalable online businesses that run on automation, not exhaustion.
Whether youโ€™re building a digital product store, launching a content-driven brand, or creating your first AI-powered business, our resources guide you from idea to global impact.

๐Ÿ’ผ Inside our toolkit:

  • 350+ eBooks and guides on automation, AI tools, and marketing systems
  • Step-by-step Ghost publishing templates
  • Complete Notion + Make workflow blueprints for solopreneurs
  • Multi-language business strategy playbooks

Start building your automated business today โ†’ dargslan.com

How to List All Installed Software Using PowerShell

Managing software installations across Windows systems remains one of the most critical responsibilities for IT professionals and system administrators. Whether you're conducting security audits, troubleshooting application conflicts, or preparing for system migrations, having a comprehensive inventory of installed software provides the foundation for informed decision-making. Without proper visibility into what's installed on your machines, you're essentially flying blind through your infrastructure management tasks.

PowerShell offers robust capabilities for querying software installations through multiple registry paths and Windows Management Instrumentation (WMI) interfaces. This command-line automation framework provides several approaches to retrieve software information, each with distinct advantages depending on your specific requirements. Understanding these methods empowers you to choose the right tool for your particular scenario, whether you're working with local machines or managing remote systems across your network.

Throughout this guide, you'll discover multiple PowerShell techniques for listing installed software, from basic commands to advanced scripting solutions. We'll explore registry-based queries, WMI methods, and modern package management approaches, complete with practical examples and export options. You'll learn how to filter results, format output for different purposes, and troubleshoot common challenges that arise when inventorying software installations.

Understanding Windows Software Installation Registry

Windows maintains software installation records in specific registry locations that serve as the primary source for installed application data. The registry stores information about both 32-bit and 64-bit applications in separate hives, which becomes particularly important when working with 64-bit Windows systems. These registry keys contain valuable metadata including application names, versions, publishers, installation dates, and uninstall strings that PowerShell can query systematically.

The primary registry paths for installed software include HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall for system-wide installations and HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall for user-specific installations. On 64-bit systems, you'll also need to check HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall to capture 32-bit applications running under the Windows-on-Windows subsystem.

"The registry approach provides the most comprehensive view of installed software because it captures applications installed through various methods, not just those managed by modern package managers."

Basic Registry Query Command

The fundamental PowerShell command for querying installed software from the registry utilizes the Get-ItemProperty cmdlet. This approach reads the registry keys directly and returns property objects that you can manipulate, filter, and export according to your needs.

Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Format-Table -AutoSize

This command retrieves all subkeys under the Uninstall registry path, selects relevant properties, and formats them in a readable table. The asterisk wildcard captures all installed applications registered in that location, while Select-Object filters the output to show only the most pertinent information.

Comprehensive Multi-Path Registry Query

To capture software from all possible registry locations, you need to query multiple paths simultaneously. This comprehensive approach ensures you don't miss applications installed in different registry hives or those installed for specific users versus system-wide installations.

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, EstimatedSize | 
    Sort-Object DisplayName | 
    Format-Table -AutoSize

This script defines an array of registry paths and queries them all in a single operation. The Where-Object filter removes entries without display names, which typically represent system components or updates rather than standalone applications. The Sort-Object cmdlet organizes the results alphabetically for easier review.

WMI and CIM Instance Methods

Windows Management Instrumentation provides an alternative approach to querying installed software through the Win32_Product class. While this method offers certain advantages for remote management and filtering capabilities, it comes with important performance considerations that you should understand before implementation.

The Win32_Product class triggers a consistency check of all installed Windows Installer packages whenever queried, which can cause significant performance impact and may even trigger repairs of installations. Despite these drawbacks, this method remains valuable for scenarios requiring detailed Windows Installer-specific information or when working with remote systems.

"When querying Win32_Product, be prepared for potentially long execution times as Windows validates each MSI installation, which can take several minutes on systems with many installed applications."

Basic WMI Query

Get-WmiObject -Class Win32_Product | 
    Select-Object Name, Version, Vendor, InstallDate | 
    Format-Table -AutoSize

This straightforward command queries the Win32_Product WMI class and returns basic information about installed software. While simple to execute, this approach only captures applications installed via Windows Installer (MSI) and will not show software installed through other methods like executable installers or portable applications.

Modern CIM Instance Approach

PowerShell 3.0 and later versions introduced CIM cmdlets as the preferred method for querying WMI classes. These cmdlets offer improved performance, better error handling, and enhanced remote management capabilities compared to the legacy WMI cmdlets.

Get-CimInstance -ClassName Win32_Product | 
    Select-Object Name, Version, Vendor, InstallDate, InstallLocation | 
    Sort-Object Name | 
    Export-Csv -Path "C:\Temp\InstalledSoftware.csv" -NoTypeInformation

This command demonstrates the modern approach using Get-CimInstance, which provides the same functionality as Get-WmiObject but with better performance characteristics. The example also shows how to export results directly to CSV format for documentation or further analysis in spreadsheet applications.

Method Advantages Disadvantages Best Use Case
Registry Query Fast execution, comprehensive coverage, no side effects Requires multiple paths for complete results, may include obsolete entries General inventory, quick checks, local system audits
Win32_Product Remote capability, detailed MSI information, built-in filtering Slow performance, triggers consistency checks, MSI-only coverage Remote management, MSI-specific queries, detailed installer information
CIM Instance Modern syntax, improved performance over WMI, better error handling Still triggers consistency checks, MSI-only coverage Remote management with PowerShell 3.0+, scripted automation
Package Management Modern approach, integrates with package providers, consistent interface Limited to supported package sources, may miss legacy applications Modern Windows 10/11 systems, package manager-installed software

Package Management Module Approach

Windows 10 and later versions include the PackageManagement module (formerly OneGet), which provides a unified interface for managing software packages from various sources. This modern approach works with multiple package providers including Chocolatey, NuGet, and the Microsoft Store, offering a consistent method for querying installed software regardless of installation method.

The PackageManagement cmdlets represent Microsoft's vision for unified package management across Windows systems. While this approach doesn't capture every possible software installation method, it provides excellent coverage for applications installed through supported package managers and offers powerful filtering and management capabilities.

Using Get-Package Cmdlet

Get-Package | 
    Select-Object Name, Version, Source, ProviderName | 
    Sort-Object Name | 
    Format-Table -AutoSize

The Get-Package cmdlet queries all registered package providers and returns information about installed packages. This command provides a clean, modern interface for software inventory that integrates well with contemporary package management workflows.

Filtering by Package Provider

Get-Package -ProviderName Programs | 
    Select-Object Name, Version, Source, InstallDate | 
    Where-Object { $_.Name -like "*Microsoft*" } | 
    Format-Table -AutoSize

You can filter results by specific package providers to focus on particular installation sources. This example demonstrates filtering for the Programs provider and then applying an additional filter to show only Microsoft products, showcasing the flexible filtering capabilities available with this approach.

"The PackageManagement approach shines in environments where modern package managers are widely adopted, providing a consistent interface across different software sources."

Advanced Filtering and Output Formatting

Raw software inventory data often contains hundreds or thousands of entries, including system components, updates, and applications you're not interested in for your specific task. Effective filtering transforms this overwhelming data flood into actionable information that serves your actual needs, whether that's identifying specific software versions, finding applications from particular publishers, or locating installations within certain date ranges.

Filtering by Application Name

Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | 
    Where-Object { $_.DisplayName -like "*Adobe*" } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Format-Table -AutoSize

This command filters the software list to show only applications with "Adobe" in their display name. The -like operator with wildcard characters provides flexible pattern matching that accommodates variations in naming conventions.

Filtering by Publisher

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.Publisher -eq "Microsoft Corporation" } | 
    Select-Object DisplayName, DisplayVersion, InstallDate | 
    Sort-Object InstallDate -Descending | 
    Format-Table -AutoSize

Filtering by publisher helps identify all software from a specific vendor, which proves particularly useful during licensing audits or when assessing vendor-specific security vulnerabilities. This example sorts results by installation date in descending order to show the most recently installed applications first.

Filtering by Installation Date

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { 
        $_.InstallDate -and 
        [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null) -gt (Get-Date).AddMonths(-3)
    } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Sort-Object InstallDate -Descending | 
    Format-Table -AutoSize

This advanced filter identifies software installed within the last three months by parsing the InstallDate registry value and comparing it to the current date. The InstallDate format in the registry uses yyyyMMdd format, requiring conversion to a proper DateTime object for comparison operations.

"Effective filtering transforms software inventory from a data dump into actionable intelligence that directly supports your specific management or security objectives."

Custom Object Creation for Enhanced Output

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    ForEach-Object {
        [PSCustomObject]@{
            Name = $_.DisplayName
            Version = $_.DisplayVersion
            Publisher = $_.Publisher
            InstallDate = if ($_.InstallDate) { 
                [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null).ToString("yyyy-MM-dd") 
            } else { "Unknown" }
            SizeMB = if ($_.EstimatedSize) { 
                [math]::Round($_.EstimatedSize / 1024, 2) 
            } else { 0 }
            UninstallString = $_.UninstallString
        }
    } | Sort-Object Name

$Software | Format-Table -AutoSize

Creating custom objects gives you complete control over output formatting and property names. This example converts the estimated size from kilobytes to megabytes, formats dates consistently, and creates meaningful property names that enhance readability in reports and exports.

Exporting Results to Various Formats

Collecting software inventory data becomes truly valuable when you can share, analyze, and archive it in formats suitable for different audiences and purposes. PowerShell provides extensive export capabilities that transform your inventory data into formats ranging from simple text files to structured data formats compatible with databases and analysis tools.

๐Ÿ“„ Exporting to CSV Format

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Export-Csv -Path "C:\Temp\InstalledSoftware.csv" -NoTypeInformation -Encoding UTF8

CSV format provides universal compatibility with spreadsheet applications, databases, and analysis tools. The -NoTypeInformation parameter removes the type information header that PowerShell includes by default, while -Encoding UTF8 ensures proper character handling for international application names.

๐Ÿ“Š Exporting to HTML Report

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Sort-Object DisplayName

$HTML = $Software | ConvertTo-Html -Title "Installed Software Report" -PreContent "Installed Software InventoryGenerated: $(Get-Date)"

$HTML | Out-File -FilePath "C:\Temp\InstalledSoftware.html" -Encoding UTF8

HTML reports provide an immediately viewable format that requires no special software to open. This approach creates a basic HTML document with a title and timestamp, making it suitable for sharing with stakeholders who need quick visibility without technical tools.

๐Ÿ“‹ Exporting to JSON Format

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    ConvertTo-Json | 
    Out-File -FilePath "C:\Temp\InstalledSoftware.json" -Encoding UTF8

JSON format excels for data interchange between systems and provides excellent compatibility with modern web applications and APIs. This structured format preserves data types and hierarchies, making it ideal for integration with configuration management databases or automated analysis pipelines.

๐Ÿ“ Exporting to XML Format

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | 
    Export-Clixml -Path "C:\Temp\InstalledSoftware.xml"

XML format provides a structured, hierarchical data representation that preserves PowerShell object types and properties. The Export-Clixml cmdlet creates XML files that can be reimported into PowerShell with complete fidelity, making this format ideal for archiving inventory snapshots that you might need to analyze later.

"Choosing the right export format depends on your audience and purposeโ€”CSV for spreadsheet analysis, HTML for stakeholder reports, JSON for system integration, and XML for PowerShell-to-PowerShell data exchange."

๐Ÿ’พ Exporting to SQL Database

$ConnectionString = "Server=localhost;Database=Inventory;Integrated Security=True;"
$Connection = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
$Connection.Open()

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName }

foreach ($App in $Software) {
    $Query = @"
INSERT INTO InstalledSoftware (ComputerName, DisplayName, DisplayVersion, Publisher, InstallDate, ScanDate)
VALUES (@ComputerName, @DisplayName, @DisplayVersion, @Publisher, @InstallDate, @ScanDate)
"@
    
    $Command = $Connection.CreateCommand()
    $Command.CommandText = $Query
    $Command.Parameters.AddWithValue("@ComputerName", $env:COMPUTERNAME) | Out-Null
    $Command.Parameters.AddWithValue("@DisplayName", $App.DisplayName) | Out-Null
    $Command.Parameters.AddWithValue("@DisplayVersion", $App.DisplayVersion) | Out-Null
    $Command.Parameters.AddWithValue("@Publisher", $App.Publisher) | Out-Null
    $Command.Parameters.AddWithValue("@InstallDate", $App.InstallDate) | Out-Null
    $Command.Parameters.AddWithValue("@ScanDate", (Get-Date)) | Out-Null
    $Command.ExecuteNonQuery() | Out-Null
}

$Connection.Close()

Direct database integration enables centralized inventory management across multiple systems. This approach inserts software inventory data into a SQL Server database, allowing for complex queries, historical tracking, and integration with enterprise asset management systems.

Export Format Best For Advantages Considerations
CSV Spreadsheet analysis, data import Universal compatibility, easy to manipulate Limited formatting, flat structure
HTML Reports for stakeholders Immediately viewable, no special software needed Static content, limited interactivity
JSON API integration, web applications Structured format, preserves hierarchies Requires parsing for human readability
XML PowerShell data exchange, archiving Preserves object types, reimportable Verbose format, larger file sizes
SQL Database Centralized inventory, historical tracking Complex queries, enterprise integration Requires database infrastructure, more complex setup

Remote Software Inventory Collection

Managing software inventory across multiple remote systems represents one of PowerShell's most powerful capabilities for enterprise environments. Remote inventory collection enables centralized management, consistent reporting, and efficient auditing across your entire infrastructure without requiring physical access to each machine.

PowerShell remoting utilizes the WS-Management protocol to execute commands on remote systems securely. Before attempting remote inventory collection, you must ensure that PowerShell remoting is enabled on target systems and that appropriate firewall rules allow the necessary traffic. Network administrators should also verify that execution policies and authentication mechanisms support your remote management requirements.

Enabling PowerShell Remoting

Enable-PSRemoting -Force

This command configures the local system to receive remote PowerShell commands. The -Force parameter bypasses confirmation prompts, making it suitable for scripted deployments. Running this command requires administrative privileges and configures the WinRM service, creates firewall exceptions, and sets up session configurations.

Querying Single Remote System

Invoke-Command -ComputerName SERVER01 -ScriptBlock {
    $UninstallKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    
    Get-ItemProperty $UninstallKeys | 
        Where-Object { $_.DisplayName } | 
        Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
} | Export-Csv -Path "C:\Temp\SERVER01_Software.csv" -NoTypeInformation

The Invoke-Command cmdlet executes the software inventory script on the remote system named SERVER01. The script block contains the same registry queries used for local inventory, but executes entirely on the remote system, returning only the results across the network.

Querying Multiple Remote Systems

$Computers = @("SERVER01", "SERVER02", "WORKSTATION01", "WORKSTATION02")

$Results = Invoke-Command -ComputerName $Computers -ScriptBlock {
    $UninstallKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    
    Get-ItemProperty $UninstallKeys | 
        Where-Object { $_.DisplayName } | 
        Select-Object @{N="ComputerName";E={$env:COMPUTERNAME}}, DisplayName, DisplayVersion, Publisher, InstallDate
} -ErrorAction SilentlyContinue

$Results | Export-Csv -Path "C:\Temp\MultiSystem_Software.csv" -NoTypeInformation

This script queries multiple systems simultaneously by passing an array of computer names to Invoke-Command. The script block adds the computer name to each result, enabling you to identify which system each software entry came from. The -ErrorAction SilentlyContinue parameter prevents the script from stopping if one or more systems are unreachable.

"Remote inventory collection scales efficiently to hundreds or thousands of systems by leveraging PowerShell's parallel execution capabilities and efficient data serialization."

Using PowerShell Sessions for Improved Performance

$Computers = @("SERVER01", "SERVER02", "WORKSTATION01", "WORKSTATION02")
$Sessions = New-PSSession -ComputerName $Computers

$Results = Invoke-Command -Session $Sessions -ScriptBlock {
    $UninstallKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    
    Get-ItemProperty $UninstallKeys | 
        Where-Object { $_.DisplayName } | 
        Select-Object @{N="ComputerName";E={$env:COMPUTERNAME}}, DisplayName, DisplayVersion, Publisher, InstallDate
}

$Results | Export-Csv -Path "C:\Temp\MultiSystem_Software.csv" -NoTypeInformation

Remove-PSSession -Session $Sessions

Creating persistent sessions with New-PSSession improves performance when executing multiple commands against the same systems. This approach establishes the remote connection once and reuses it for multiple operations, reducing authentication overhead and network latency. Always remember to remove sessions when finished to free resources on both local and remote systems.

๐Ÿ“ก Querying Systems from Active Directory

Import-Module ActiveDirectory

$Computers = Get-ADComputer -Filter {OperatingSystem -like "*Windows*"} -Properties OperatingSystem | 
    Select-Object -ExpandProperty Name

$Results = Invoke-Command -ComputerName $Computers -ScriptBlock {
    $UninstallKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    
    Get-ItemProperty $UninstallKeys | 
        Where-Object { $_.DisplayName } | 
        Select-Object @{N="ComputerName";E={$env:COMPUTERNAME}}, DisplayName, DisplayVersion, Publisher, InstallDate
} -ErrorAction SilentlyContinue -ThrottleLimit 32

$Results | Export-Csv -Path "C:\Temp\Domain_Software_Inventory.csv" -NoTypeInformation

Integrating with Active Directory enables automated discovery of target systems based on organizational criteria. This example queries all Windows computers from Active Directory and performs software inventory across the entire domain. The -ThrottleLimit parameter controls how many remote operations execute simultaneously, balancing speed against network and system load.

Creating Comprehensive Software Inventory Reports

Raw inventory data requires transformation into meaningful reports that highlight important patterns, identify security concerns, and support decision-making processes. Comprehensive reporting combines data collection with analysis, formatting, and presentation techniques that communicate findings effectively to both technical and non-technical audiences.

Generating Summary Statistics

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName }

$TotalApplications = $Software.Count
$UniquePublishers = ($Software | Select-Object Publisher -Unique).Count
$TotalSizeMB = ($Software | Where-Object { $_.EstimatedSize } | 
    Measure-Object -Property EstimatedSize -Sum).Sum / 1024

Write-Host "Software Inventory Summary" -ForegroundColor Cyan
Write-Host "=========================" -ForegroundColor Cyan
Write-Host "Total Applications: $TotalApplications"
Write-Host "Unique Publishers: $UniquePublishers"
Write-Host "Total Estimated Size: $([math]::Round($TotalSizeMB, 2)) MB"

Summary statistics provide quick insights into the overall software landscape on a system. This script calculates total application count, the number of unique publishers, and total estimated disk space consumption, presenting the information in a formatted console output.

Identifying Outdated Software

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName -and $_.InstallDate }

$ThresholdDate = (Get-Date).AddYears(-2)

$OutdatedSoftware = $Software | Where-Object {
    try {
        $InstallDate = [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null)
        $InstallDate -lt $ThresholdDate
    } catch {
        $false
    }
} | Select-Object DisplayName, DisplayVersion, Publisher, 
    @{N="InstallDate";E={[datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null).ToString("yyyy-MM-dd")}} | 
    Sort-Object InstallDate

Write-Host "`nOutdated Software (Installed > 2 years ago):" -ForegroundColor Yellow
$OutdatedSoftware | Format-Table -AutoSize

Identifying outdated software helps prioritize update activities and security remediation efforts. This script flags applications installed more than two years ago, which may represent security risks or compatibility concerns with modern systems and applications.

"Regular software inventory analysis reveals patterns in application deployment, identifies license compliance issues, and highlights security vulnerabilities before they become critical problems."

Publisher Distribution Analysis

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName -and $_.Publisher }

$PublisherStats = $Software | 
    Group-Object Publisher | 
    Select-Object @{N="Publisher";E={$_.Name}}, @{N="ApplicationCount";E={$_.Count}} | 
    Sort-Object ApplicationCount -Descending | 
    Select-Object -First 10

Write-Host "`nTop 10 Publishers by Application Count:" -ForegroundColor Green
$PublisherStats | Format-Table -AutoSize

Publisher distribution analysis reveals which vendors dominate your software landscape. This information supports licensing negotiations, vendor risk assessments, and strategic decisions about software standardization across your organization.

Generating Detailed HTML Report with Styling

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    Select-Object @{N="Application";E={$_.DisplayName}}, 
                  @{N="Version";E={$_.DisplayVersion}}, 
                  @{N="Publisher";E={$_.Publisher}}, 
                  @{N="InstallDate";E={
                      if ($_.InstallDate) {
                          [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null).ToString("yyyy-MM-dd")
                      } else { "Unknown" }
                  }} | 
    Sort-Object Application

$HTMLStyle = @"

    body { font-family: Arial, sans-serif; margin: 20px; }
    h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
    table { border-collapse: collapse; width: 100%; margin-top: 20px; }
    th { background-color: #3498db; color: white; padding: 12px; text-align: left; }
    tr:nth-child(even) { background-color: #f2f2f2; }
    td { padding: 10px; border-bottom: 1px solid #ddd; }
    tr:hover { background-color: #e8f4f8; }
    .summary { background-color: #ecf0f1; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
    .timestamp { color: #7f8c8d; font-style: italic; }

"@

$TotalApps = $Software.Count
$ReportDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

$HTMLBody = @"
Software Inventory Report

    Computer Name: $env:COMPUTERNAME
    Report Generated: $ReportDate
    Total Applications: $TotalApps

"@

$HTML = $Software | ConvertTo-Html -Head $HTMLStyle -PreContent $HTMLBody
$HTML | Out-File -FilePath "C:\Temp\Software_Inventory_Report.html" -Encoding UTF8

Write-Host "HTML report generated: C:\Temp\Software_Inventory_Report.html" -ForegroundColor Green

Professional HTML reports with custom styling enhance readability and presentation quality. This comprehensive example includes CSS styling, summary information, timestamps, and formatted tables that create a polished report suitable for executive presentation or audit documentation.

Troubleshooting Common Issues

Software inventory collection encounters various challenges in real-world environments, from permission issues to incomplete data and performance problems. Understanding common issues and their solutions enables you to build robust inventory scripts that handle edge cases gracefully and produce reliable results consistently.

Handling Missing or Null Values

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$Software = Get-ItemProperty $UninstallKeys | 
    Where-Object { $_.DisplayName } | 
    ForEach-Object {
        [PSCustomObject]@{
            Name = if ($_.DisplayName) { $_.DisplayName } else { "Unknown" }
            Version = if ($_.DisplayVersion) { $_.DisplayVersion } else { "Not Specified" }
            Publisher = if ($_.Publisher) { $_.Publisher } else { "Unknown Publisher" }
            InstallDate = if ($_.InstallDate) { 
                try {
                    [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null).ToString("yyyy-MM-dd")
                } catch {
                    "Invalid Date"
                }
            } else { "Unknown" }
        }
    }

$Software | Format-Table -AutoSize

Registry entries often contain null or missing values for various properties. This script implements defensive programming practices by checking for null values and providing meaningful defaults, ensuring that missing data doesn't break your inventory process or produce confusing output.

Dealing with Access Denied Errors

function Get-InstalledSoftware {
    param(
        [string]$ComputerName = $env:COMPUTERNAME
    )
    
    try {
        $UninstallKeys = @(
            "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
            "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
        )
        
        $Software = Get-ItemProperty $UninstallKeys -ErrorAction Stop | 
            Where-Object { $_.DisplayName }
        
        return $Software
    }
    catch [System.UnauthorizedAccessException] {
        Write-Warning "Access denied when querying registry on $ComputerName. Run as Administrator."
        return $null
    }
    catch {
        Write-Warning "Error querying software on $ComputerName: $($_.Exception.Message)"
        return $null
    }
}

$Result = Get-InstalledSoftware
if ($Result) {
    $Result | Select-Object DisplayName, DisplayVersion, Publisher | Format-Table -AutoSize
}

Permission issues frequently arise when querying registry keys or accessing remote systems. This function wrapper implements proper error handling that catches specific exception types and provides meaningful feedback, allowing scripts to continue processing other systems even when one fails.

"Robust error handling transforms fragile scripts into production-ready tools that gracefully handle the unexpected conditions inevitable in real-world environments."

Optimizing Performance for Large Inventories

$UninstallKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall",
    "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
)

$Software = foreach ($Key in $UninstallKeys) {
    if (Test-Path $Key) {
        Get-ChildItem $Key | ForEach-Object {
            $App = Get-ItemProperty $_.PSPath
            if ($App.DisplayName) {
                [PSCustomObject]@{
                    Name = $App.DisplayName
                    Version = $App.DisplayVersion
                    Publisher = $App.Publisher
                    InstallDate = $App.InstallDate
                }
            }
        }
    }
}

$Software | Sort-Object Name | Format-Table -AutoSize

Performance optimization becomes critical when processing hundreds or thousands of registry entries. This approach uses Get-ChildItem to enumerate registry keys first, then selectively retrieves properties only for keys with display names, significantly reducing processing time compared to querying all properties for all keys upfront.

Handling Remote System Connectivity Issues

function Test-SystemAvailability {
    param([string]$ComputerName)
    
    $PingResult = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet
    if (-not $PingResult) {
        return $false
    }
    
    $PSRemotingTest = Test-WSMan -ComputerName $ComputerName -ErrorAction SilentlyContinue
    return ($null -ne $PSRemotingTest)
}

$Computers = @("SERVER01", "SERVER02", "WORKSTATION01", "WORKSTATION02")
$AvailableSystems = @()
$UnavailableSystems = @()

foreach ($Computer in $Computers) {
    Write-Host "Checking $Computer..." -NoNewline
    if (Test-SystemAvailability -ComputerName $Computer) {
        $AvailableSystems += $Computer
        Write-Host " Available" -ForegroundColor Green
    } else {
        $UnavailableSystems += $Computer
        Write-Host " Unavailable" -ForegroundColor Red
    }
}

if ($AvailableSystems.Count -gt 0) {
    $Results = Invoke-Command -ComputerName $AvailableSystems -ScriptBlock {
        $UninstallKeys = @(
            "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
            "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
        )
        
        Get-ItemProperty $UninstallKeys | 
            Where-Object { $_.DisplayName } | 
            Select-Object @{N="ComputerName";E={$env:COMPUTERNAME}}, DisplayName, DisplayVersion, Publisher
    } -ErrorAction SilentlyContinue
    
    $Results | Export-Csv -Path "C:\Temp\Available_Systems_Software.csv" -NoTypeInformation
}

if ($UnavailableSystems.Count -gt 0) {
    Write-Host "`nUnavailable systems:" -ForegroundColor Yellow
    $UnavailableSystems | ForEach-Object { Write-Host "  - $_" }
}

Remote inventory operations require pre-validation of system availability to avoid timeouts and wasted processing time. This comprehensive approach tests both network connectivity and PowerShell remoting availability before attempting inventory collection, then provides clear reporting of which systems were successfully inventoried and which were unavailable.

Automating Software Inventory Collection

Manual inventory collection serves immediate needs but becomes unsustainable as infrastructure scales or when regular monitoring is required. Automation transforms software inventory from a periodic manual task into a continuous monitoring capability that provides up-to-date information without ongoing manual intervention.

Creating a Scheduled Task with PowerShell

$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-SoftwareInventory.ps1"

$Trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM

$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable

Register-ScheduledTask -TaskName "Software Inventory Collection" -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings -Description "Collects installed software inventory daily"

Write-Host "Scheduled task created successfully" -ForegroundColor Green

Windows Task Scheduler provides native automation capabilities that PowerShell can configure programmatically. This script creates a scheduled task that runs daily at 2:00 AM under the SYSTEM account, ensuring it has necessary permissions to query all software installations regardless of which users are logged in.

Building a Complete Automated Inventory Script

# Get-SoftwareInventory.ps1
param(
    [string]$OutputPath = "C:\InventoryReports",
    [string[]]$Computers = @($env:COMPUTERNAME)
)

# Create output directory if it doesn't exist
if (-not (Test-Path $OutputPath)) {
    New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
}

$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$OutputFile = Join-Path $OutputPath "SoftwareInventory_$Timestamp.csv"

Write-Host "Starting software inventory collection..." -ForegroundColor Cyan
Write-Host "Target systems: $($Computers.Count)" -ForegroundColor Cyan

$Results = Invoke-Command -ComputerName $Computers -ScriptBlock {
    $UninstallKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    
    Get-ItemProperty $UninstallKeys | 
        Where-Object { $_.DisplayName } | 
        Select-Object @{N="ComputerName";E={$env:COMPUTERNAME}}, 
                      @{N="ScanDate";E={Get-Date -Format "yyyy-MM-dd HH:mm:ss"}},
                      @{N="Application";E={$_.DisplayName}},
                      @{N="Version";E={$_.DisplayVersion}},
                      @{N="Publisher";E={$_.Publisher}},
                      @{N="InstallDate";E={
                          if ($_.InstallDate) {
                              try {
                                  [datetime]::ParseExact($_.InstallDate, "yyyyMMdd", $null).ToString("yyyy-MM-dd")
                              } catch { "Unknown" }
                          } else { "Unknown" }
                      }}
} -ErrorAction SilentlyContinue

if ($Results) {
    $Results | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8
    Write-Host "Inventory completed successfully" -ForegroundColor Green
    Write-Host "Results saved to: $OutputFile" -ForegroundColor Green
    Write-Host "Total applications found: $($Results.Count)" -ForegroundColor Green
} else {
    Write-Warning "No results collected. Check system connectivity and permissions."
}

# Archive old reports (keep last 30 days)
Get-ChildItem -Path $OutputPath -Filter "SoftwareInventory_*.csv" | 
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } | 
    Remove-Item -Force

Write-Host "Inventory collection complete" -ForegroundColor Cyan

This production-ready script implements complete automation including parameterization, output directory management, timestamped files, progress reporting, error handling, and automatic cleanup of old reports. The script can inventory local or remote systems and maintains a rolling 30-day archive of historical data.

๐Ÿ“ง Email Notification Integration

function Send-InventoryReport {
    param(
        [string]$ReportPath,
        [string]$SmtpServer = "smtp.company.com",
        [string]$From = "inventory@company.com",
        [string[]]$To = @("admin@company.com"),
        [string]$Subject = "Software Inventory Report - $(Get-Date -Format 'yyyy-MM-dd')"
    )
    
    $Body = @"


Software Inventory Report
The automated software inventory collection has completed successfully.
Report Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Computer: $env:COMPUTERNAME
Please find the detailed inventory report attached.


"@
    
    Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To -Subject $Subject -Body $Body -BodyAsHtml -Attachments $ReportPath
}

# Use in your inventory script
$OutputFile = "C:\InventoryReports\SoftwareInventory_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
# ... inventory collection code ...
Send-InventoryReport -ReportPath $OutputFile

Email notification integration ensures stakeholders receive inventory reports automatically without needing to access file shares or remote systems. This function creates HTML-formatted emails with attached CSV reports, providing immediate visibility into inventory completion and results.

"Automation transforms software inventory from a snapshot activity into continuous monitoring that provides historical trends, change tracking, and proactive alerting capabilities."

Frequently Asked Questions

Why does Get-WmiObject Win32_Product take so long to execute?

The Win32_Product WMI class triggers a consistency check of all Windows Installer packages whenever queried, which validates and potentially repairs each MSI installation. This process can take several minutes on systems with many installed applications. For faster results, use registry-based queries instead, which read installation information directly without triggering validation processes. The registry approach typically completes in seconds rather than minutes while providing more comprehensive results that include non-MSI installations.

How can I list software installed for all users, not just the current user?

Query both HKLM (HKEY_LOCAL_MACHINE) and HKCU (HKEY_CURRENT_USER) registry hives to capture system-wide and user-specific installations. System-wide installations appear in HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall, while user-specific installations appear in HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall. For a complete inventory including all users, you'll need to load each user's registry hive using reg load commands or query the registry remotely under each user's profile. The HKLM locations capture most enterprise software since organizations typically install applications system-wide rather than per-user.

What's the difference between 32-bit and 64-bit software registry locations?

On 64-bit Windows systems, the registry maintains separate locations for 32-bit and 64-bit applications. Native 64-bit applications register in HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall, while 32-bit applications running under WOW64 (Windows-on-Windows 64-bit) register in HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall. The Wow6432Node path exists only on 64-bit systems and contains installation information for 32-bit applications. Comprehensive software inventory requires querying both locations to capture all installed applications regardless of their architecture.

Can I export software inventory to Excel format directly from PowerShell?

PowerShell doesn't include native Excel export functionality, but you can export to CSV format which Excel opens seamlessly. Use Export-Csv cmdlet to create comma-separated value files that Excel recognizes and formats automatically. For more advanced Excel integration with formatting, charts, and multiple worksheets, install the ImportExcel PowerShell module from the PowerShell Gallery using Install-Module ImportExcel. This community module provides extensive Excel manipulation capabilities without requiring Microsoft Excel installation on the system running the script.

How do I filter out Windows updates and system components from the software list?

Filter results by checking for empty DisplayName properties and excluding entries with specific characteristics common to updates and components. Most Windows updates and system components either lack a DisplayName property or include "Update for" or "Hotfix for" in their names. Use Where-Object with conditions like Where-Object { $_.DisplayName -and $_.DisplayName -notlike "*Update for*" -and $_.DisplayName -notlike "*Hotfix*" -and $_.UninstallString } to exclude these entries. The UninstallString check helps filter out components that aren't truly uninstallable applications. Additionally, you can exclude entries from specific publishers like "Microsoft Corporation" for system components, though this may also remove legitimate Microsoft applications you want to track.

Why do some applications not appear in the software inventory?

Several reasons explain missing applications in inventory results. Portable applications that don't install through traditional installers won't create registry entries. Some applications install only for specific users and appear only in HKCU registry hives that your query might not access. Windows Store applications use a different registration mechanism and require separate queries using Get-AppxPackage cmdlet. Additionally, some legacy applications or those using custom installers may not follow standard Windows installation conventions. For comprehensive inventory, combine registry queries with Get-AppxPackage for Store apps and potentially scan common installation directories for portable applications.

How can I compare software inventory between two different dates?

Export inventory results with timestamps to separate CSV files for each collection date, then use PowerShell's Compare-Object cmdlet to identify differences. Import both CSV files using Import-Csv, then compare them with Compare-Object -ReferenceObject $OldInventory -DifferenceObject $NewInventory -Property DisplayName, DisplayVersion. This comparison highlights additions, removals, and version changes between the two snapshots. For more sophisticated tracking, consider storing inventory data in a database with timestamps, enabling SQL queries that identify changes over time, track installation trends, and generate historical reports showing software lifecycle across your infrastructure.

What permissions are required to run software inventory queries?

Local software inventory requires local administrator privileges to access HKLM registry hives completely. Standard users can query HKCU for their own installations but lack access to system-wide software information. Remote inventory requires administrative credentials on target systems plus PowerShell remoting enabled via Enable-PSRemoting. Network administrators should ensure appropriate firewall rules allow WinRM traffic (TCP ports 5985 for HTTP and 5986 for HTTPS). For domain environments, Domain Admin credentials or delegated permissions through Group Policy enable inventory across multiple systems. Consider using service accounts with appropriate permissions for automated scheduled inventory tasks rather than personal administrative accounts.