<#PSScriptInfo .VERSION 2026.10.30.0 .GUID 3a7b9c4d-2e8f-4a1b-9d6c-5e3f7a8b9c2d .AUTHOR Michael Escamilla .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI https://github.com/MichaelEscamilla/GetMSIInformation .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.0.0.0 - Initial release 2.0.0.0 - 10-4-2024 - Added file hash information, and context menu items for the installation and uninstallation of a Right-Click Option in Windows Explorer. And some other UI improvements. 2024.10.4.1 - Updated the version numbering, and a sepearator in the context menu. 2024-10.13.0 - Added an error message when the file is locked 2024-12.8.0 - Formatted Script for Publishing to PowerShell Gallery 2026-1.5.0 - Added Icon extraction and export functionality. Added context menu items to open the icon temp folder and right-click menu folder. 2026.1.5.1 - Fixed a bug when launching the script from the internet 2026.1.6.0 - Fixed a bug where the 'No Icon' label would not hide 2026.1.6.1 - Added the michaeltheadmin.com icon to the Form and Right-Click Menu 2026.1.7.0 - Simplified console output messages during script download from GitHub 2026.8.11.0 - Updated UI for less 'default' look. Added the Compressed Product Code (Compressed GUID) to the GUI. Right-Click Install will directly call pwsh.exe if available. 2026.8.12.0 - Added a check for updates feature. Manual check available under the 'About' menu. A non-blocking background check runs at startup and notifies via the status bar when a newer release is available. 2026.8.12.1 - Added Logic to Update the script depending on the way it was launched. 2026.8.12.2 - Added status bar feedback when installing or removing the right-click menu. The 'latest version' confirmation now shows in the status bar instead of a pop-up. 2026.8.13.0 - Added screenshot options under the File menu to copy the window to the clipboard or save it as a PNG. Fixed the drag-and-drop box staying highlighted gray after being clicked. Property fields now show the accent outline on hover instead of staying outlined after clicking. 2026.8.14.0 - The right-click menu now works on any file type to get hash information, not just MSI files. Fixed repeating errors when the status bar message tried to reset. The window now comes to the foreground faster on the first launch in a session. 2026.10.5.0 - Added a reload button for the current file and improved the compressed GUID labels. 2026.10.27.0 - The Upgrade Code label is now clickable and opens a window showing the MSI's full Upgrade table. 2026.10.28.0 - Added a Package Code row to the MSI properties, read from the Summary Information stream. 2026.10.30.0 - Widened the Upgrade table columns so the full Upgrade Code GUID is no longer truncated. .PRIVATEDATA #> <# .SYNOPSIS This script provides a graphical user interface (GUI) for viewing and copying properties of MSI files. .DESCRIPTION The script creates a WPF-based GUI that allows users to drag and drop MSI files to view their properties such as Product Name, Manufacturer, Product Version, Product Code, and Upgrade Code. It also provides functionality to copy these properties to the clipboard and to clear the displayed information. Additionally, the script includes options to install and uninstall a context menu item for MSI files to retrieve their properties. .PARAMETER FilePath Optional parameter to specify the path of the MSI file to automatically load the information for. .NOTES #> param ( [Parameter(Mandatory = $false)] [string]$FilePath ) ############################################# ################# Variables ################# ############################################# # Script Name $Script:ScriptName = "GetMSIInformation.ps1" # Script Version [System.Version]$Script:ScriptVersion = "2026.10.30.0" $Script:RightClickMenuName = "Get MSI Information" $Script:RightClickMenuFolderPath = "$env:LOCALAPPDATA\GetMSIInformation" # Icon Temp Folder Path $Script:IconTempFolderPath = "$env:TEMP\GetMSIInformation\Icons" # GitHub Repository (used for the update check) $Script:GitHubRepo = "MichaelEscamilla/GetMSIInformation" $Script:ReleasesApiUrl = "https://api.github.com/repos/$Script:GitHubRepo/releases/latest" $Script:ReleasesPageUrl = "https://github.com/$Script:GitHubRepo/releases" # Headers required by the GitHub REST API (a User-Agent is mandatory) $Script:UpdateCheckHeaders = @{ 'User-Agent' = 'GetMSIInformation-UpdateCheck' 'Accept' = 'application/vnd.github+json' } # How this script was launched; drives which update action the 'Update Available' click takes. $Script:UpdateChannel = $null # File path to retry after a file-lock error. $Script:RetryFilePath = $null # Get the Security Principal $Script:currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) # Get PowerShell Version $Script:ScriptPSVersion = $PSVersionTable.PSVersion # Get Pwsh Path $Script:PowerShellPath = (Get-Command pwsh.exe -ErrorAction SilentlyContinue) # michaeltheadmin.com Icon $Script:WindowIconBase64 = "AAABAAEAIBwAAAEAIACYDgAAFgAAACgAAAAgAAAAOAAAAAEAIAAAAAAAcA4AAMQOAADEDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaSFoNRkZGEUtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEFRGRhFaSEgNAAAAAAAAAABUVFQLUkxPrFNOUOlRTE7tUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7VROUOhSTU+qTExMCU5KTH5lYWP/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bmxt/2RfYf9OSkx6TkhLtWtoaf94eHj/dnZ2/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3Z2dv94eHj/aWZn/05IS69OSUuxamdo/3h4eP9mZGX/Uk1P/05JS/9OSUv/VVFT/1VRUv9OSUv/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/TklL/1VRU/9VUVP/TklL/05JS/9STlD/ZmRl/3h4eP9oZWb/T0lMqk9JTLBqZ2j/eHl4/1dUVf90cHH/rqys/62rrP9rZ2j/b2tt/66srP+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+tq6z/a2do/29rbP+urKz/rays/3Bsbv9YVVb/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/U1BR/5GPkP/09PT/8/Pz/399ff+Hg4X/9PT0/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo//Pz8/9/fX3/hoOE//T09P/z9PP/i4iJ/1VRUv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9VUlP/hIGC/9PS0v/S0dH/dXJz/3t3ef/T0tL/ycjI/8rJyv/Kycr/ycjI/8nIyP/Kycr/ysnK/8nIyP/JyMj/ysnK/8vKyf/Lycj/1NLR/3dzc/98eHj/1dPS/9TT0f9/fHz/V1NU/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/19cXv9TTU//V1JU/1dSVP9TTlD/VE9R/1dSVP9XUlT/V1NV/1dTVf9XUlT/V1JU/1dTVf9XU1X/V1JU/1dSVP9XU1X/WFNU/1hSVP9ZU1T/VU9Q/1VPUP9ZU1T/WVNU/1JNTv9gXV7/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/V1NV/3x4ef/Hx8j/v76//8HAwf/BwMD/v76//8jHx/9ybnD/dnN0/8jIyP/Ix8f/cm9w/3Vxc//Ix8j/yMfI/4B2cP85VYL/LXrf/y933P8xed7/MXne/y933P8tet7/Q1p//2NaU/94eXn/aGVm/09JTKpPSUywamdo/3h5eP9UUFL/jImK//L09P/n6Oj/5+jo/+fo6P/n6Oj/8/Pz/4B9f/+Gg4T/9PT0//L08/+Afn//hICC//L09P/y9PT/k4h//zRblv8jjf//J4n//yeJ//8nif//J4n//yON//9AYJL/Y1lQ/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/1hUVv95dHP/v7u3/7izr/+3tLP/tLO0/7GwsP+5t7j/cGxu/3Rwcf+5uLj/ubi4/3Bsbv9ybnD/ubi4/7m4uP98c27/PVV9/y5zzf8xcMr/NHTO/zR0zv8xcMr/LnPN/0RXef9jWlX/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFxd/01LUv9ITVz/SE5d/0pJUP9TTU7/XFdZ/1tXWf9VUVL/VlFT/1tXWf9bV1n/VVFT/1ZRU/9cV1n/XFdZ/1VQU/9ZU1L/ZFtY/2RbV/9ZUEz/WlFN/2RbWP9kW1j/VlBQ/19cXv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9jWFH/QF+Q/ymE+f8phPn/OFiJ/4yCe//e3t7/3d3d/3p2eP9/fH3/3t7e/93e3f96d3j/fXl7/93e3v/d3t7/fXp7/4B9fv/f3t7/3t3d/3p2d/9/fH3/397e/97e3f+DgIH/VlJT/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/2NYT/8+Ypr/JI3//ySN//81WpL/lYuC//Pz8//y8vL/gHx+/4aDhP/z8/P/8vPy/4B9fv+DgIL/8vPz//Lz8/+DgYL/h4SF//Pz8//y8vL/f3x9/4aDhP/z8/P/8vPy/4qIiP9VUVL/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFhV/0ZTbv87aaj/O2mo/0NRbP9waGX/m5iZ/5uYmf9mYWP/aGRm/5uYmf+bmJn/ZmJj/2djZf+bmJn/m5iZ/2djZf9pZWb/m5iZ/5uYmf9lYWL/amZo/6Cdnv+fnZ7/a2Zo/1lVV/94eXn/aGVm/09JTKpOSUuxamdo/3d4eP9nZmj/XFVT/2BUTP9gVEz/X1hV/1dUVv9QTE7/UExO/1lVV/9ZVVb/UExO/1BMTv9ZVVf/WVVW/1BMTv9QTE7/WVVW/1hVVv9QTE7/UExO/1lVV/9ZVVb/UE1O/1FNTv9WUlT/aWdo/3d4eP9oZWb/TkhLq09JTLNraGn/eHl5/3d3d/93eHj/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4eP93d3f/eHl4/2hmZ/9OSEuuUUpMbWRfYf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/amhp/2poaf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9samv/Yl1f/1BLS2hRUVECUUtNjFNNT89QS0zTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BLTdFQS03yUEtN8FBLTdFQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEtM01NNT89RTE6IfHx8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNMT7lTTVCmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU01Qs1JNT7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABTTU1RWFNV/1NOUIhUTE9pVFBSaExMTBYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRTE9mVlFS11NOUOJWUVLdUEtQNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////wAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//n////5////+B////wf///////////8=" ############################################# ################# Functions ################# ############################################# #region Functions function Test-FileLock { param ( [Parameter(Mandatory = $true)] [string]$Path ) try { $FileStream = [System.IO.File]::Open("$($Path)", 'Open', 'Write') $FileStream.Close() $FileStream.Dispose() return $false } catch { return $true } } function Show-FileLockedMessage { param ( [Parameter(Mandatory = $true)] [string]$Path ) $Script:RetryFilePath = $Path $lsbox_FilePath.Items.Clear() $lsbox_FilePath.Items.Add("ERROR: The file is locked:`n[$Path]") $lsbox_FilePath.Background = [System.Windows.Media.Brushes]::Red $lsbox_FilePath.Foreground = [System.Windows.Media.Brushes]::Yellow $lsbox_FilePath.FontWeight = 'Bold' $lsbox_FilePath.FontSize = 16 $btn_FilePath_Retry.Visibility = [System.Windows.Visibility]::Visible $btn_FilePath_Retry.IsEnabled = $true } function Get-MsiProperties { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path ) Write-Host "Getting MSI Properties for: [$Path]" # Check if the MSI file path exists if (-not (Test-Path $Path)) { throw "The file $Path does not exist." } # Create a new Windows Installer COM object $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer # Open the MSI database in read-only mode $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) # Open a view on the Property table $MSIPropertyView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT * FROM Property")) # Execute the view query $MSIPropertyView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null # Fetch the first record from the result set $MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null) # Initialize an empty System Object to store properties [System.Object]$Properties = @{} # Loop through all records in the result set while ($null -ne $MSIRecord) { # Get the property name from the first column $property = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(1)) # Get the property value from the second column $Value = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(2)) # Add the property name and value to the hashtable $Properties[$Property] = $Value # Fetch the next record from the result set $MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null) } # Close the Property view $MSIPropertyView.GetType().InvokeMember("Close", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null # The Package Code lives in the Summary Information stream (Revision Number, PID 9), not the Property table try { $SummaryInfo = $WindowsInstaller.GetType().InvokeMember("SummaryInformation", "GetProperty", $null, $WindowsInstaller, @($Path.FullName, 0)) $Properties["PackageCode"] = $SummaryInfo.GetType().InvokeMember("Property", "GetProperty", $null, $SummaryInfo, @(9)) } catch { Write-Verbose "Error retrieving Package Code: $_" } # Return the System Object of properties $Properties } function Get-MsiUpgradeTable { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path ) Write-Host "Getting MSI Upgrade Table for: [$Path]" # Check if the MSI file path exists if (-not (Test-Path $Path)) { throw "The file $Path does not exist." } # Create a new Windows Installer COM object $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer # Open the MSI database in read-only mode $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) # Start building the list of Upgrade entries [Collections.Generic.List[PSCustomObject]]$UpgradeEntries = @() # Confirm the Upgrade table exists before querying it (not every MSI has one) $TableView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name FROM _Tables WHERE Name='Upgrade'")) $TableView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $TableView, $null) | Out-Null $TableExists = $TableView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $TableView, $null) $TableView.GetType().InvokeMember("Close", "InvokeMethod", $null, $TableView, $null) | Out-Null if ($TableExists) { # Open a view on the Upgrade table $UpgradeView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT UpgradeCode,VersionMin,VersionMax,Language,Attributes,Remove,ActionProperty FROM Upgrade")) # Execute the view query $UpgradeView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $UpgradeView, $null) | Out-Null # Fetch the first record from the result set $UpgradeRecord = $UpgradeView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $UpgradeView, $null) # Loop through all records in the result set while ($null -ne $UpgradeRecord) { $UpgradeEntries.Add([PSCustomObject]@{ UpgradeCode = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(1)) VersionMin = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(2)) VersionMax = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(3)) Language = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(4)) Attributes = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(5)) Remove = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(6)) ActionProperty = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(7)) }) # Fetch the next record from the result set $UpgradeRecord = $UpgradeView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $UpgradeView, $null) } # Close the Upgrade view $UpgradeView.GetType().InvokeMember("Close", "InvokeMethod", $null, $UpgradeView, $null) | Out-Null } # Return the list of Upgrade entries $UpgradeEntries } # Stolen from: https://github.com/codaamok # https://gist.github.com/codaamok/7ed30d01280ce28bb451621966707c1b function Convert-GuidToCompressedGuid { param( [Parameter(Mandatory = $true)] [string]$Guid ) function Get-ReversedString ([array]$a) { [String]::Join('', $a[-1.. - ($a.Count)]) } function Get-ReversedBytes ([String]$a) { [String]::Join('', ($a -split '(..)' -ne '' -replace '(\w)(\w)', '$2$1')) } # Strip braces and dashes from the GUID $Guid = $Guid -replace '\{|\}|\-' $data1 = Get-ReversedString $Guid[0..7] $data2 = Get-ReversedString $Guid[8..11] $data3 = Get-ReversedString $Guid[12..15] $data4 = Get-ReversedBytes ($Guid[16..19] -join '') $data5 = Get-ReversedBytes ($Guid[20..31] -join '') return '{0}{1}{2}{3}{4}' -f $data1, $data2, $data3, $data4, $data5 } function Enable-AllButtons { param ( [Parameter(Mandatory = $false)] [string]$Exclude ) # Get all button variables $Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Button in $Buttons) { # Check that the button name does not contain the exclude variable if ($Button.Name -notlike "*$Exclude*") { # Enable Button $Button.IsEnabled = $true } } } function Disable-AllButtons { # Get all button variables $Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Button in $Buttons) { # Disable Button $Button.IsEnabled = $false } } function Clear-Textboxes { # Get all textbox variables $Textboxes = Get-Variable -Name "txt_*" -ValueOnly -ErrorAction SilentlyContinue foreach ($Textbox in $Textboxes) { # Clear textbox $Textbox.Clear() } # Reset the Upgrade Code label back to its non-clickable look Set-UpgradeCodeLabelClickable -Enabled $false } function Set-UpgradeCodeLabelClickable { param ( [Parameter(Mandatory = $true)] [bool]$Enabled ) if ($Enabled) { $lbl_UpgradeCode.Foreground = $formMSIProperties.FindResource("Accent") $lbl_UpgradeCode.Cursor = [System.Windows.Input.Cursors]::Hand $lbl_UpgradeCode.ToolTip = "Click to view the MSI Upgrade table" } else { $lbl_UpgradeCode.Foreground = $formMSIProperties.FindResource("TextMuted") $lbl_UpgradeCode.Cursor = [System.Windows.Input.Cursors]::Arrow $lbl_UpgradeCode.ToolTip = $null } } # Stolen from: https://github.com/PatchMyPCTeam/CustomerTroubleshooting/blob/Release/PowerShell/Get-LocalContentHashes.ps1 function Get-EncodedHash { [CmdletBinding()] Param( [Parameter(Position = 0)] [System.Object]$HashValue ) $hashBytes = $hashValue.Hash -split '(?<=\G..)(?=.)' | ForEach-Object { [byte]::Parse($_, 'HexNumber') } Return [Convert]::ToBase64String($hashBytes) } function Get-FileHashInformation { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path ) Write-Host "Getting File Hash Information for: [$Path]" # Initialize the hash object $Hashes = @{} # Get File Hash - MD5 $FileHashMD5 = Get-FileHash -Path $Path -Algorithm MD5 $Hashes["MD5"] = $FileHashMD5 # Get File Hash - SHA1 $FileHashSHA1 = Get-FileHash -Path $Path -Algorithm SHA1 $Hashes["SHA1"] = $FileHashSHA1 # Get File Hash - SHA256 $FileHashSHA256 = Get-FileHash -Path $Path -Algorithm SHA256 $Hashes["SHA256"] = $FileHashSHA256 # Get File Hash - SHA1 - Encoded $FileHashEncoded = Get-EncodedHash -HashValue $FileHashSHA1 $Hashes["Digest"] = $FileHashEncoded # Get File Hash - SHA256 - Encoded $FileHashSHA256Encoded = Get-EncodedHash -HashValue $FileHashSHA256 $Hashes["D256"] = $FileHashSHA256Encoded # Return the hash object $Hashes } function Get-MsiIcon { param ( [Parameter(Mandatory = $true)] [IO.FileInfo[]]$Path, [Parameter(Mandatory = $false)] [string]$ExportFolder = "$($Script:IconTempFolderPath)" ) Write-Host "Getting MSI Icon for: [$Path]" # Ensure the export folder exists if (-not (Test-Path -Path $ExportFolder)) { New-Item -ItemType Directory -Path $ExportFolder -Force | Out-Null } # Create a new Windows Installer COM object $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer # Open the MSI database in read-only mode $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) # Get all Icons in the Icon table try { # Open a view on the Icon table to get the icon binary data $IconView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name FROM _Tables WHERE Name='Icon'")) # Execute the view query $IconView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconView, $null) | Out-Null # Fetch Record $IconTable = $IconView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconView, $null) # Close the Icon view $IconView.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconView, $null) | Out-Null if ($IconTable) { # Open a view on the Icon table $IconData = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name,Data FROM Icon")) # Execute the view query $IconData.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconData, $null) | Out-Null # Start Building PSCustomObject [Collections.Generic.List[PSCustomObject]]$IconInformation = @() Do { # Fetch the next record $IconRecord = $IconData.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconData, $null) if ($IconRecord) { # Get the 'Name' Field $IconDataName = $IconData.GetType().InvokeMember("StringData", 'Public, Instance, GetProperty', $null, $IconRecord, 1) # Get the DataSize of the Binary Data $IconDataSize = $IconData.GetType().InvokeMember("DataSize", "GetProperty", $null, $IconRecord, 2) Write-Verbose "Found Icon: [$IconDataName] with Size: [$IconDataSize] bytes" # Read the Binary Data $IconBinaryData = $IconData.GetType().InvokeMember("ReadStream", "InvokeMethod", $null, $IconRecord, @(2, $IconDataSize, 2)) # Get Binary data as ANSI string - use Windows-1252 encoding $ByteArray = [System.Text.Encoding]::GetEncoding(1252).GetBytes($IconBinaryData) # Construct the path to save the ICO file $IconPathTemp = Join-Path -Path $ExportFolder -ChildPath "$($IconDataName)" # Check if the file already exists and delete it if necessary if (Test-Path -Path $IconPathTemp) { Remove-Item -Path $IconPathTemp -Force } # Write the byte array to an ICO file [System.IO.File]::WriteAllBytes($IconPathTemp, $ByteArray) # Create PSCustomObject for Icon Information $IconInfoObject = [PSCustomObject]@{ Name = $IconDataName Size = $IconDataSize Path = $IconPathTemp } # Add to Icon Information List $IconInformation.Add($IconInfoObject) } } While ($IconRecord) # Close the IconData view $IconData.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconData, $null) | Out-Null } } catch { Write-Verbose "Error retrieving Icons: $_" } Return $IconInformation } function Set-TextboxInformation { param ( [Parameter(Mandatory = $false)] [System.Object]$MSIPropertiesInfo, [Parameter(Mandatory = $true)] [hashtable]$FileHashInfo ) # Set the MSI file properties textboxes (only when MSI properties are provided) if ($MSIPropertiesInfo) { $txt_ProductName.Text = $MSIPropertiesInfo.ProductName $txt_Manufacture.Text = $MSIPropertiesInfo.Manufacturer $txt_ProductVersion.Text = $MSIPropertiesInfo.ProductVersion $txt_ProductCode.Text = $MSIPropertiesInfo.ProductCode $txt_UpgradeCode.Text = $MSIPropertiesInfo.UpgradeCode $txt_PackageCode.Text = $MSIPropertiesInfo.PackageCode # Set the compressed GUIDs from the Product and Upgrade Codes if ($MSIPropertiesInfo.ProductCode) { $txt_CompressedGUID.Text = Convert-GuidToCompressedGuid -Guid $MSIPropertiesInfo.ProductCode } if ($MSIPropertiesInfo.UpgradeCode) { $txt_CompressedUpgradeGUID.Text = Convert-GuidToCompressedGuid -Guid $MSIPropertiesInfo.UpgradeCode } } # Toggle the clickable look of the Upgrade Code label based on whether a code is present Set-UpgradeCodeLabelClickable -Enabled ([bool]$MSIPropertiesInfo.UpgradeCode) # Set the File Hash Information textboxes $txt_MD5.Text = $FileHashInfo.MD5.Hash $txt_SHA1.Text = $FileHashInfo.SHA1.Hash $txt_SHA256.Text = $FileHashInfo.SHA256.Hash $txt_Digest.Text = $FileHashInfo.Digest $txt_D256.Text = $FileHashInfo.D256 } function Show-UpgradeCodeDetails { # Nothing to show without a loaded MSI if (-not $Script:LoadedMSIPath) { return } # Read the Upgrade table from the currently loaded MSI try { $UpgradeEntries = Get-MsiUpgradeTable -Path $Script:LoadedMSIPath } catch { Write-Host "Error reading Upgrade table: $_" $UpgradeEntries = @() } # Themed popup window (self-contained palette so it matches the main window) [xml]$XAMLUpgrade = @"