AppList.psm1

<# The following function converts indirect strings. For example, it converts:
ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_OutlookDesktop_DisplayName
to Mail and Calendar
 
C# code to expose SHLoadIndirectString(), derived from:
  Title: Expand-IndirectString.ps1
  Author: Jason Fossen, Enclave Consulting LLC (www.sans.org/sec505)
  Date: 20 September 2016
  URL: https://github.com/SamuelArnold/StarKill3r/blob/master/Star%20Killer/Star%20Killer/bin/Debug/Scripts/SANS-SEC505-master/scripts/Day1-PowerShell/Expand-IndirectString.ps1
  License: "Public domain, no rights reserved, no warranties or guarantees."
#>



function Expand-IndirectString {

Param([String] $IndirectString = "")


# Source code in C# to P/Invoke SHLoadIndirectString from shlwapi.dll:
$CSharpSHLoadIndirectString = @'
using System;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.Win32;
 
namespace SHLWAPIDLL
{
    public class IndirectStrings
    {
        [DllImport("shlwapi.dll", CharSet=CharSet.Unicode)]
        private static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, string ppvReserved);
 
        public static string GetIndirectString(string indirectString)
        {
            try
            {
                int returnValue;
                StringBuilder lptStr = new StringBuilder(1024);
                returnValue = SHLoadIndirectString(indirectString, lptStr, 1024, null);
 
                if (returnValue == 0)
                {
                    return lptStr.ToString();
                }
                else
                {
                    return null;
                    //return "SHLoadIndirectString Failure: " + returnValue;
                }
            }
            catch //(Exception ex)
            {
                return null;
                //return "Exception Message: " + ex.Message;
            }
        }
    }
}
'@




# Create the type [SHLWAPIDLL.IndirectStrings]:
# Check if type is already created to avoid TYPE_ALREADY_EXISTS exception
if ("SHLWAPIDLL.IndirectStrings" -as [type]) {}
    else {Add-Type -TypeDefinition $CSharpSHLoadIndirectString -Language CSharp}

# Call method to expand the indirect string:
[SHLWAPIDLL.IndirectStrings]::GetIndirectString($IndirectString)

}

function Get-InstalledApps {
<#
    .Synopsis
    Installed apps for PowerShell
 
    .Description
    This script outputs the list of programs in Installed apps on Windows 10 and 11 (formerly Apps & features)
#>



#Desktop/Win32 apps

function Get-AppsFromReg {
    param($Regkey = $(throw = "Missing required parameter Regkey"))
    Get-ItemProperty $Regkey |
    Where-Object {($_.SystemComponent -ne 1) -and ($null -ne $_.DisplayName -or $null -ne $_.DisplayName_Localized) -and ($null -eq $_.ReleaseType)} |
    Select-Object @{label="Name";expression={if ($_.DisplayName_Localized) {$(Expand-IndirectString $_.DisplayName_Localized)} else {$_.DisplayName} `
        }},Publisher,@{label="Type";expression={"Desktop"}}
}

$regkey64   = "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
$regkey32   = "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
$regkeyuser = "REGISTRY::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"

$apps64bit = Get-AppsFromReg -regkey $regkey64
$apps32bit = Get-AppsFromReg -regkey $regkey32
$appsuser  = Get-AppsFromReg -regkey $regkeyuser

$desktopapps = @()
$desktopapps += $apps64bit
$desktopapps += $apps32bit
$desktopapps += $appsuser


#Modern/Metro/UWP apps

#load list of provisioned apps that don't appear in Installed apps

#Windows 10
$blacklist10 = @("Microsoft.StorePurchaseApp","Microsoft.VP9VideoExtensions", "Microsoft.Wallet"
"Microsoft.XboxGameOverlay","Microsoft.XboxIdentityProvider","Microsoft.XboxSpeechToTextOverlay")

#Windows 11
$blacklist11 = @("Microsoft.DesktopAppInstaller","Microsoft.HEIFImageExtension","Microsoft.StorePurchaseApp",
"Microsoft.VP9VideoExtensions","Microsoft.WebpImageExtension","Microsoft.XboxGameOverlay",
"Microsoft.XboxIdentityProvider","Microsoft.XboxSpeechToTextOverlay","MicrosoftWindows.Client.WebExperience",
"Microsoft.HEVCVideoExtension","Microsoft.RawImageExtension","Microsoft.XboxGamingOverlay","Microsoft.GetHelp",
"Microsoft.WindowsStore","Microsoft.YourPhone","Microsoft.GetStarted","Microsoft.SecHealthUI")

#Check OS build
$osbuild = (Get-ItemProperty "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion").CurrentBuildNumber
$osubr = (Get-ItemProperty "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion").UBR

#Remove Dev Home (Preview) app if greater than or equal to OS Build 22631.2792
If ($osbuild -ge 22631 -and $osubr -ge 2792) {
    $blacklist11 += "Microsoft.Windows.DevHome"
}

#Remove Cross Device Experience Host app if greater than or equal to OS Build 22631.3235
If ($osbuild -ge 22631 -and $osubr -ge 3235) {
    $blacklist11 += "MicrosoftWindows.CrossDevice"
}

#Remove System Components apps in Windows 10
If ($osbuild -ge 19045 -and $osubr -ge 3803) {
    $blacklist10 += @("Microsoft.XboxGamingOverlay","Microsoft.GetHelp","Microsoft.WindowsStore","Microsoft.YourPhone","Microsoft.Getstarted")
}

#Remove additional System Components apps in Windows 10 19045.4123
If ($osbuild -ge 19045 -and $osubr -ge 4123) {
    $blacklist10 += @("Microsoft.DesktopAppInstaller","Microsoft.Windows.DevHome","Microsoft.MixedReality.Portal")
}

#22000 is Windows 11, if less than 22000 then Windows 10
if ($osbuild -lt 22000) {$blacklist = $blacklist10} else {$blacklist = $blacklist11}

#Handle Windows 10 with Pwsh (PowerShell Core)
if (($osbuild -lt 22000) -and ($PSVersionTable.PSEdition -eq "Core")) {Import-Module -Name Appx -UseWindowsPowerShell -WarningAction Ignore}

#Get all packages except those with System signature and no install loction
$allpackages = Get-AppxPackage -PackageTypeFilter Main | Where-Object {($null -ne $_.InstallLocation) -and ($_.SignatureKind -ne "System")} | Select-Object -ExpandProperty Name

#Diff blacklisted apps
$whitelist = Compare-Object $allpackages $blacklist | Select-Object -ExpandProperty InputObject

$packages = $whitelist | ForEach-Object {Get-AppxPackage $_}

#Declare array
$modernapps = @()

#Loop through each package
foreach ($pkg in $packages) {

    $manifest = $pkg | Get-AppxPackageManifest
    
    # Filter out Sparse Packages
    if ($manifest.Package.Properties.AllowExternalContent -ne 'true') {
        $apps = $manifest.package.Applications.Application

        # Show the package display name if more than one app in package
        # Otherwise, if there's only one app in the package, show the app display name
        if ($apps.Count -gt 1) {
            $DisplayName = $manifest.Package.Properties.DisplayName
        } else {
            $DisplayName = $manifest.Package.Applications.Application.VisualElements.DisplayName
        }

        #If the DisplayName contains ms-resource: it's an indirect string that we need to transpose to
        #@{PackageFullName?ms-resource:Resources/AppDisplayName} syntax
        if ($DisplayName -match "ms-resource:") {
            #If there's not Resources/ after ms-resource: and it's not ms-resources://
            #then add it so it's ms-resource:Resources/
            if (($DisplayName -notmatch "Resources/") -and ($DisplayName -notmatch "ms-resource://")) {
                $DisplayName = $DisplayName.Insert(12,"Resources/")
            }
            #Convert to name as it displays in Installed apps
            $DisplayName = Expand-IndirectString "@{$($pkg.PackageFullName)?$DisplayName}"
        }

        # Grab Publisher from manifest
        $PublisherDisplayName = $manifest.Package.Properties.PublisherDisplayName

        #If the PublisherDisplayName contains ms-resource: it's an indirect string that we need to transpose to
        #@{PackageFullName?ms-resource:Resources/PublisherDisplayName} syntax
        if ($PublisherDisplayName -match "ms-resource:") {
            #If there's not Resources/ after ms-resource: and it's not ms-resources://
            #then add it so it's ms-resource:Resources/
            if (($PublisherDisplayName -notmatch "Resources/") -and ($PublisherDisplayName -notmatch "ms-resource://")) {
                $PublisherDisplayName = $PublisherDisplayName.Insert(12,"Resources/")
            }
            #Convert to publisher name as it displays in Installed apps
            $PublisherDisplayName = Expand-IndirectString "@{$($pkg.PackageFullName)?$PublisherDisplayName}"
        }

        #wrap in custom object for output purposes
        $objApp = [PSCustomObject]@{
            Name = $DisplayName
            Publisher = $PublisherDisplayName
            Type = "Modern"
        }

        #append to array
        $modernapps += $objApp
    }
}

# Combine desktop and modern apps
$allapps = @()
$allapps += $desktopapps
# Specify columns
$allapps += $modernapps | Select-Object Name,Publisher,Type

$allapps | Sort-Object Name

}

function Get-ProgramsAndFeatures {
    <#
    .Synopsis
    Programs and Features for PowerShell
 
    .Description
    This script outputs the list of programs in Programs and Features on Windows 10 and 11
#>



    function Get-AppsFromReg {
        param($Regkey = $(throw = "Missing required parameter Regkey"))
        Get-ItemProperty $Regkey |
        Where-Object {(($null -ne $_.DisplayName) -or ($null -ne $_.DisplayName_Localized) -and ($_.SystemComponent -ne 1) `
            -and ($null -ne $_.UninstallString) -and ($null -eq $_.ReleaseType)) `
            -or (($null -ne $_.DisplayName) -or ($null -ne $_.DisplayName_Localized) -and ($_.SystemComponent -ne 1) `
            -and ($_.NoRemove -eq 1) -and ($null -eq $_.UninstallString) `
            -and ($null -eq $_.ReleaseType) -and ($_.WindowsInstaller -eq 1))} |
        Select-Object @{label="DisplayName";expression={if ($_.DisplayName_Localized) {$(Expand-IndirectString $_.DisplayName_Localized)} else {$_.DisplayName} `
            }},Publisher,InstallDate,EstimatedSize,DisplayVersion
    }

    $regkey64   = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $regkey32   = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $regkeyuser = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"

    $apps64bit = Get-AppsFromReg -regkey $regkey64
    $apps32bit = Get-AppsFromReg -regkey $regkey32
    $appsuser  = Get-AppsFromReg -regkey $regkeyuser

    $allapps = @()
    $allapps += $apps64bit
    $allapps += $apps32bit
    $allapps += $appsuser

    $allapps | Sort-Object DisplayName
}
Export-ModuleMember -Function Get-InstalledApps, Get-ProgramsAndFeatures