Scripts/Get-InstalledPrograms.ps1

<#
    .SYNOPSIS
    Retrieves installed programs
 
    .DESCRIPTION
    Enumerates all installed programs for the system and current user.
 
    The results should be nearly identical to those displayed via the "Programs and Features" view of the Windows Control Panel.
 
    For the "Apps & features" pane of the Settings app the results will be a subset of those displayed (see the Notes section).
 
    .EXAMPLE
    Get-InstalledPrograms
 
    Retrieves all programs installed system-wide or for the current user.
 
    .NOTES
    Only native Windows applications which register an uninstaller are displayed.
 
    Microsoft Store apps are not currently enumerated, which the "Apps & features" pane of the Settings app does display.
 
    The available information displayed for each program is expected to vary, as each program is itself responsible for recording it.
 
    If the installation date is not explicitly recorded by an installed program, we attempt to derive it based on the last write time of the registry key.
 
    There is no documented API for enumerating installed native Windows applications, so an approach based off reverse engineering Microsoft's implementation is used.
 
    There are three registry keys which are inspected to populate the list of installed programs:
    - System-wide in native bitness
      HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall
    - System-wide under the 32-bit emulation layer (64-bit Windows only)
      HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
    - Current-user (any bitness)
      HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall
 
    .LINK
    https://github.com/ralish/PSWinGlue
#>


#Requires -Version 3.0

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
[CmdletBinding()]
[OutputType([PSCustomObject[]])]
Param()

$PowerShellCore = New-Object -TypeName Version -ArgumentList 6, 0
if ($PSVersionTable.PSVersion -ge $PowerShellCore -and $PSVersionTable.Platform -ne 'Win32NT') {
    throw '{0} is only compatible with Windows.' -f $MyInvocation.MyCommand.Name
}

if (!('PSWinGlue.GetInstalledPrograms' -as [Type])) {
    $RegQueryInfoKey = @'
[DllImport("advapi32.dll", EntryPoint = "RegQueryInfoKeyW")]
public static extern int RegQueryInfoKey(Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey,
                                            IntPtr lpClass,
                                            IntPtr lpcchClass,
                                            IntPtr lpReserved,
                                            IntPtr lpcSubKeys,
                                            IntPtr lpcbMaxSubKeyLen,
                                            IntPtr lpcbMaxClassLen,
                                            IntPtr lpcValues,
                                            IntPtr lpcbMaxValueNameLen,
                                            IntPtr lpcbMaxValueLen,
                                            IntPtr lpcbSecurityDescriptor,
                                            out UInt64 lpftLastWriteTime);
'@


    $AddTypeParams = @{}

    if ($PSVersionTable['PSEdition'] -eq 'Core') {
        $AddTypeParams['ReferencedAssemblies'] = 'Microsoft.Win32.Registry'
    }

    Add-Type -Namespace 'PSWinGlue' -Name 'GetInstalledPrograms' -MemberDefinition $RegQueryInfoKey @AddTypeParams
}

$TypeName = 'PSWinGlue.InstalledProgram'
Update-TypeData -TypeName $TypeName -DefaultDisplayPropertySet @('Name', 'Publisher', 'Version', 'Scope') -Force

# System-wide in native bitness
$ComputerNativeRegPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
# System-wide under the 32-bit emulation layer (64-bit Windows only)
$ComputerWow64RegPath = 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
# Current-user (any bitness)
$UserRegPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'

# Retrieve all installed programs from available keys
$UninstallKeys = Get-ChildItem -Path $ComputerNativeRegPath
if (Test-Path -Path $ComputerWow64RegPath -PathType Container) {
    $UninstallKeys += Get-ChildItem -Path $ComputerWow64RegPath
}
if (Test-Path -Path $UserRegPath -PathType Container) {
    $UninstallKeys += Get-ChildItem -Path $UserRegPath
}

# Filter out all the uninteresting installation results
$Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
foreach ($UninstallKey in $UninstallKeys) {
    $Program = Get-ItemProperty -Path $UninstallKey.PSPath

    # Skip any program which doesn't define a display name
    if (!$Program.PSObject.Properties['DisplayName']) {
        continue
    }

    # Skip any program without an uninstall command which is not marked non-removable
    if (!($Program.PSObject.Properties['UninstallString'] -or ($Program.PSObject.Properties['NoRemove'] -and $Program.NoRemove -eq 1))) {
        continue
    }

    # Skip any program which defines a parent program
    if ($Program.PSObject.Properties['ParentKeyName'] -or $Program.PSObject.Properties['ParentDisplayName']) {
        continue
    }

    # Skip any program marked as a system component
    if ($Program.PSObject.Properties['SystemComponent'] -and $Program.SystemComponent -eq 1) {
        continue
    }

    # Skip any program which defines a release type
    if ($Program.PSObject.Properties['ReleaseType']) {
        continue
    }

    $Result = [PSCustomObject]@{
        PSTypeName    = $TypeName
        PSPath        = $Program.PSPath
        Name          = $Program.DisplayName
        Publisher     = $null
        InstallDate   = $null
        EstimatedSize = $null
        Version       = $null
        Location      = $null
        Uninstall     = $null
        Scope         = $null
    }

    if ($Program.PSObject.Properties['Publisher']) {
        $Result.Publisher = $Program.Publisher
    }

    # Try and convert any InstallDate value to a DateTime
    if ($Program.PSObject.Properties['InstallDate']) {
        $RegInstallDate = $Program.InstallDate
        if ($RegInstallDate -match '^[0-9]{8}') {
            try {
                $Result.InstallDate = New-Object -TypeName 'DateTime' -ArgumentList $RegInstallDate.Substring(0, 4), $RegInstallDate.Substring(4, 2), $RegInstallDate.Substring(6, 2)
            } catch { }
        }

        if (!$Result.InstallDate) {
            Write-Warning -Message ('[{0}] Registry key has invalid value for InstallDate: {1}' -f $Program.DisplayName, $RegInstallDate)
        }
    }

    # Fall back to the last write time of the registry key
    if (!$Result.InstallDate) {
        [UInt64]$RegLastWriteTime = 0
        $Status = [PSWinGlue.GetInstalledPrograms]::RegQueryInfoKey($UninstallKey.Handle, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, [ref]$RegLastWriteTime)

        if ($Status -eq 0) {
            $Result.InstallDate = [DateTime]::FromFileTime($RegLastWriteTime)
        } else {
            Write-Warning -Message ('[{0}] Retrieving registry key last write time failed with status: {1}' -f $Program.DisplayName, $Status)
        }
    }

    if ($Program.PSObject.Properties['EstimatedSize']) {
        $Result.EstimatedSize = $Program.EstimatedSize
    }

    if ($Program.PSObject.Properties['DisplayVersion']) {
        $Result.Version = $Program.DisplayVersion
    }

    if ($Program.PSObject.Properties['InstallLocation']) {
        $Result.Location = $Program.InstallLocation
    }

    if ($Program.PSObject.Properties['UninstallString']) {
        $Result.Uninstall = $Program.UninstallString
    }

    if ($Program.PSPath.startswith('Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE')) {
        $Result.Scope = 'System'
    } else {
        $Result.Scope = 'User'
    }

    $Results.Add($Result)
}

return ($Results.ToArray() | Sort-Object -Property Name)