Public/Get-InstalledApplications.ps1

function Invoke-RegistryHiveCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('LOAD', 'UNLOAD')]
        [string]$Action,

        [Parameter(Mandatory)]
        [string]$Key,

        [string]$Hive
    )

    $argumentList = @($Action, $Key)
    if ($Hive) {
        $argumentList += $Hive
    }

    $output = & reg.exe @argumentList 2>&1
    foreach ($line in $output) {
        Write-Verbose "$line"
    }

    return $LASTEXITCODE
}

function Get-InstalledApplications {
    <#
    .SYNOPSIS
    Retrieve installed applications from the Windows registry.
    .DESCRIPTION
    Queries the Windows registry uninstall keys to find installed applications.
    Supports multiple scopes: global (HKLM), current user, all users, or combinations.
    Querying all users requires administrative privileges to mount offline user hives.
    Use -WhatIf to preview offline user hive mount and unmount operations without changing the registry.
    .PARAMETER Scope
    The scope of applications to query. Defaults to 'GlobalAndAllUsers'.
    Valid values: Global, GlobalAndCurrentUser, GlobalAndAllUsers, CurrentUser, AllUsers.
    .EXAMPLE
    Get-InstalledApplications
    Returns all installed applications (global + all users). Requires elevation.
    .EXAMPLE
    Get-InstalledApplications -Scope Global
    Returns only machine-wide installed applications.
    .EXAMPLE
    Get-InstalledApplications -Scope CurrentUser
    Returns applications installed for the current user only.
    .NOTES
    Windows only.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [ValidateSet('Global', 'GlobalAndCurrentUser', 'GlobalAndAllUsers', 'CurrentUser', 'AllUsers')]
        [string]$Scope = 'GlobalAndAllUsers'
    )

    Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name

    # Check if running with Administrative privileges if required
    if ($Scope -in @('GlobalAndAllUsers', 'AllUsers')) {
        if (-not (Test-IsElevated)) {
            throw 'Finding all user applications requires an elevated (Administrator) PowerShell session.'
        }
    }

    # Empty array to store applications
    $Apps = [System.Collections.Generic.List[PSObject]]::new()
    $32BitPath = "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $64BitPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"

    # Retrieve globally installed applications
    if ($Scope -in @('Global', 'GlobalAndAllUsers', 'GlobalAndCurrentUser')) {
        Write-Verbose "Processing global hive"
        Get-ItemProperty "HKLM:\$32BitPath" | ForEach-Object { $Apps.Add($_) }
        Get-ItemProperty "HKLM:\$64BitPath" | ForEach-Object { $Apps.Add($_) }
    }

    if ($Scope -in @('CurrentUser', 'GlobalAndCurrentUser')) {
        Write-Verbose "Processing current user hive"
        Get-ItemProperty "Registry::\HKEY_CURRENT_USER\$32BitPath" | ForEach-Object { $Apps.Add($_) }
        Get-ItemProperty "Registry::\HKEY_CURRENT_USER\$64BitPath" | ForEach-Object { $Apps.Add($_) }
    }

    if ($Scope -in @('AllUsers', 'GlobalAndAllUsers')) {
        Write-Verbose "Collecting hive data for all users"
        $AllProfiles = Get-CimInstance Win32_UserProfile | Select-Object LocalPath, SID, Loaded, Special | Where-Object {$_.SID -like "S-1-5-21-*"}
        $MountedProfiles = $AllProfiles | Where-Object {$_.Loaded -eq $true}
        $UnmountedProfiles = $AllProfiles | Where-Object {$_.Loaded -eq $false}

        Write-Verbose "Processing mounted hives"
        $MountedProfiles | ForEach-Object {
            Get-ItemProperty -Path "Registry::\HKEY_USERS\$($_.SID)\$32BitPath" | ForEach-Object { $Apps.Add($_) }
            Get-ItemProperty -Path "Registry::\HKEY_USERS\$($_.SID)\$64BitPath" | ForEach-Object { $Apps.Add($_) }
        }

        Write-Verbose "Processing unmounted hives"
        $UnmountedProfiles | ForEach-Object {

            $Hive = "$($_.LocalPath)\NTUSER.DAT"
            Write-Verbose " -> Mounting hive at $Hive"

            if (Test-Path $Hive) {

                if ($PSCmdlet.ShouldProcess("HKU\temp from $Hive", 'REG LOAD')) {
                    $loadExitCode = Invoke-RegistryHiveCommand -Action LOAD -Key 'HKU\temp' -Hive $Hive
                    if ($loadExitCode -ne 0) {
                        Write-Warning "Failed to load registry hive '$Hive' into HKU\temp. REG LOAD exited with code $loadExitCode; skipping this profile."
                    } else {
                        try {
                            Get-ItemProperty -Path "Registry::\HKEY_USERS\temp\$32BitPath" | ForEach-Object { $Apps.Add($_) }
                            Get-ItemProperty -Path "Registry::\HKEY_USERS\temp\$64BitPath" | ForEach-Object { $Apps.Add($_) }
                        } finally {
                            # Run manual GC to allow hive to be unmounted
                            [GC]::Collect()
                            [GC]::WaitForPendingFinalizers()

                            if ($PSCmdlet.ShouldProcess('HKU\temp', 'REG UNLOAD')) {
                                $unloadExitCode = Invoke-RegistryHiveCommand -Action UNLOAD -Key 'HKU\temp'
                                if ($unloadExitCode -ne 0) {
                                    Write-Warning "Failed to unload registry hive HKU\temp after reading '$Hive'. REG UNLOAD exited with code $unloadExitCode."
                                }
                            }
                        }
                    }
                }

            } else {
                Write-Warning "Unable to access registry hive at $Hive"
            }
        }
    }

    Write-Output $Apps
}