Get-WingetList.ps1
<#PSScriptInfo .VERSION 0.0.3 .GUID fd8842b4-23d7-4180-a482-f5c0c23f504c .AUTHOR bfcns .COMPANYNAME .COPYRIGHT .TAGS microsoft.winGet.client winget wingetlist get-wingetlist microsoft.winget winget.client .LICENSEURI https://github.com/bfcns/wingetlist/blob/main/LICENSE .PROJECTURI https://github.com/bfcns/wingetlist .ICONURI .EXTERNALMODULEDEPENDENCIES Microsoft.WinGet.Client Winget .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .SYNOPSIS Retrieves and displays a list of installed packages from winget and Microsoft Store, with options to export reports and fetch VirusTotal links. .DESCRIPTION The Get-WingetList script enumerates installed packages using the WinGet PowerShell client and displays them grouped by source (winget, msstore, or none) and update availability. It can optionally export the package list to Markdown or HTML reports, and fetch VirusTotal links for winget packages by retrieving their installer SHA256 hashes. The script maintains a local cache of SHA256 hashes to minimize redundant lookups. .PARAMETER Name Winget List .PARAMETER IncludeVirusTotalLink -i Includes VirusTotal links for winget packages in the output and exported reports, using cached SHA256 hashes. .PARAMETER UpdateVirusTotalLink -u Updates the local cache of SHA256 hashes for winget packages by querying 'winget show' for each package. This enables accurate VirusTotal links. .PARAMETER ExportMarkdown -m Exports the package list to a Markdown file named 'WingetListReport.md' in the current directory. .PARAMETER ExportHtml -h Exports the package list to an HTML file named 'WingetListReport.html' in the current directory. .EXAMPLE .\Get-WingetList.ps1 Displays the grouped list of installed packages in the console. .EXAMPLE .\Get-WingetList.ps1 -IncludeVirusTotalLink Displays the package list with VirusTotal links for winget packages. .EXAMPLE .\Get-WingetList.ps1 -ExportMarkdown Exports the package list to a Markdown report. .EXAMPLE .\Get-WingetList.ps1 -UpdateVirusTotalLink Updates the SHA256 cache for winget packages and enables accurate VirusTotal links. .NOTES - Requires PowerShell 7.0 or later. - Requires 'winget.exe' and the 'Microsoft.WinGet.Client' PowerShell module. - The SHA256 cache is stored in 'wingetlist-sha-cache.json' in the script directory. - VirusTotal links are generated using the installer SHA256 hash for each winget package. .LINK https://github.com/microsoft/winget-cli https://www.powershellgallery.com/packages/Microsoft.WinGet.Client https://www.virustotal.com #> #Requires -PSEdition Core -Version 7.0 [CmdletBinding()] param( [string]$Name, [Alias('i')][switch]$IncludeVirusTotalLink, [Alias('u')][switch]$UpdateVirusTotalLink, [Alias('m')][switch]$ExportMarkdown, [Alias('h')][switch]$ExportHtml ) $wingetExePath = Get-Command winget.exe -ErrorAction SilentlyContinue if (-not $wingetExePath) { Write-Warning "winget.exe not found." exit } $winGetCmdPath = Get-Command Get-WinGetPackage -ErrorAction SilentlyContinue if (-not $winGetCmdPath) { Write-Warning "Get-WinGetPackage PowerShell command not found. Install https://www.powershellgallery.com/packages/Microsoft.WinGet.Client/1.10.340" exit } $packages = Get-WinGetPackage | Sort-Object Source, Name -ErrorAction SilentlyContinue $update = $false $shacacheFile = "$PSScriptRoot\wingetlist-sha-cache.json" $shaCache = @{} if (Test-Path $shaCacheFile) { $raw = Get-Content $shaCacheFile | ConvertFrom-Json foreach ($entry in $raw.PSObject.Properties) { $shaCache[$entry.Name] = $entry.Value } } function Get-VirusTotalLink($id) { if ($shaCache.ContainsKey($id) -and $shaCache[$id].SHA) { $sha = $shaCache[$id].SHA return "https://www.virustotal.com/gui/file/$sha" } return "================== Run with -UpdateVirusTotalLink to update missing ones in cache ==================" } function Get-PackagesList($source, $update) { $filter = { $_.Source -eq $source -and ($update ? $_.IsUpdateAvailable : -not $_.IsUpdateAvailable) } $props = @( @{ Name = 'Name'; Expression = { $_.Name } }, @{ Name = 'Id'; Expression = { $_.Id } }, @{ Name = 'Version'; Expression = { $_.InstalledVersion } } ) if ($update) { $props += @{ Name = 'New Version'; Expression = { $_.AvailableVersions[0] } } } if (($IncludeVirusTotalLink -or $UpdateVirusTotalLink) -and $source -eq "winget") { $props += @{ Name = 'VirusTotalLink'; Expression = { Get-VirusTotalLink $_.Id } } } $packages | Where-Object $filter | Select-Object $props } $wingetUpdateGroup = Get-PackagesList "winget" $true $wingetGroup = Get-PackagesList "winget" $false $msstoreUpdateGroup = Get-PackagesList "msstore" $true $msstoreGroup = Get-PackagesList "msstore" $false $emptyUpdateGroup = Get-PackagesList $null $true $emptyGroup = Get-PackagesList $null $false $groups = @( @{ Label = "Winget Updateable Installed Packages"; Data = $wingetUpdateGroup }, @{ Label = "Winget Installed Packages"; Data = $wingetGroup }, @{ Label = "Store Updateable Installed Packages"; Data = $msstoreUpdateGroup }, @{ Label = "Store Installed Packages"; Data = $msstoreGroup }, @{ Label = "No Source Updateable Installed Packages"; Data = $emptyUpdateGroup }, @{ Label = "No Source Installed Packages"; Data = $emptyGroup } ) foreach ($g in $groups) { if ($g.Data.Count) { Write-Host Write-Host $g.Label -ForegroundColor Cyan $g.Data | Format-Table | Out-Host } } if ($ExportMarkdown) { $md = @() $md += "## Winget Packages" $md += "| Name | Version | Id | VirusTotalLink |" $md += "|------|---------|----|----------------|" $wingetGroup | ForEach-Object { $md += "| $($_.Name) | $($_.Version) | $($_.Id) | $($_.VirusTotalLink) |" } $md += "" $md += "## MSStore Packages" $md += "| Name | Version | Id |" $md += "|------|---------|----|" $msstoreGroup | ForEach-Object { $md += "| $($_.Name) | $($_.Version) | $($_.Id) |" } $md += "" $md += "## No Source Packages" $md += "| Name | Version | Id |" $md += "|------|---------|----|" $emptyGroup | ForEach-Object { $md += "| $($_.Name) | $($_.Version) | $($_.Id) |" } $md -join "`n" | Out-File -Encoding UTF8 "WingetListReport.md" Write-Host "Markdown report saved as WingetListReport.md" } if ($ExportHtml) { $html = @() $html += "<html><head><title>WingetList Report</title><style>body{font-family:sans-serif;}table{border-collapse:collapse;width:100%;margin-bottom:20px;}th,td{border:1px solid #ccc;padding:6px;}th{background:#eee;}</style></head><body>" $html += "<h2>Winget Packages</h2>" $html += "<table><tr><th>Name</th><th>Version</th><th>Id</th><th>VirusTotalLink</th></tr>" foreach ($pkg in $wingetGroup) { $vt = $pkg.VirusTotalLink if ($vt -and $vt -like "https://www.virustotal.com*") { $vt = "<a href='$vt' target='_blank'>VirusTotal</a>" } $html += "<tr><td>$($pkg.Name)</td><td>$($pkg.Version)</td><td>$($pkg.Id)</td><td>$vt</td></tr>" } $html += "</table>" $html += "<h2>MSStore Packages</h2>" $html += "<table><tr><th>Name</th><th>Version</th><th>Id</th></tr>" foreach ($pkg in $msstoreGroup) { $html += "<tr><td>$($pkg.Name)</td><td>$($pkg.Version)</td><td>$($pkg.Id)</td></tr>" } $html += "</table>" $html += "<h2>No Source Packages</h2>" $html += "<table><tr><th>Name</th><th>Version</th><th>Id</th></tr>" foreach ($pkg in $emptyGroup) { $html += "<tr><td>$($pkg.Name)</td><td>$($pkg.Version)</td><td>$($pkg.Id)</td></tr>" } $html += "</table>" $html += "</body></html>" $html -join "`n" | Out-File -Encoding UTF8 "WingetListReport.html" Write-Host "HTML report saved as WingetListReport.html" } if ($UpdateVirusTotalLink) { $checkmark = [char]0x2705 # Load cache if it exists $cache = @{} if (Test-Path $shacacheFile) { $raw = Get-Content $shacacheFile | ConvertFrom-Json # Convert PSCustomObject to hashtable foreach ($entry in $raw.PSObject.Properties) { $cache[$entry.Name] = $entry.Value } } # Prepare a hashtable to store SHA results $shaTable = @{} # Only process winget packages $wingetPkgs = $packages | Where-Object { $_.Source -eq 'winget' } # Determine which packages need to be updated (not in cache or version changed) $toFetch = @() foreach ($pkg in $wingetPkgs) { $id = $pkg.Id $ver = $pkg.InstalledVersion if (-not $cache.ContainsKey($id) -or $cache[$id].Version -ne $ver) { $toFetch += $pkg } else { $shaTable[$id] = $cache[$id].SHA } } $tofetchcount = $toFetch.Count # Parallel fetch SHA256 for only needed packages if ($tofetchcount) { Write-Host "Processing $($tofetchcount) package(s)..." -ForegroundColor Cyan foreach ($pkg in $toFetch) { $id = $pkg.Id $ver = $pkg.InstalledVersion $sha = "" $info = winget show --id $id 2>$null $shaLine = ($info | Select-String 'Installer SHA256:').Line if ($shaLine) { $sha = $shaLine -replace '.*Installer SHA256:\s*', '' } $shaTable[$id] = $sha $cache[$id] = @{ Version = $ver; SHA = $sha } Write-Host "$checkmark $id " -ForegroundColor Green -NoNewline Write-Host "$ver" -ForegroundColor White } # Save updated cache $cache | ConvertTo-Json | Set-Content $shacacheFile } # Output the results as a table if ($shaTable.Count -and $tofetchcount) { Write-Host "$tofetchcount Packages updated in the cache." -ForegroundColor Green } elseif ($shaTable.Count -and -not $tofetchcount) { Write-Host "`nNo package updates necessary in the cache." -ForegroundColor White } else { Write-Host "No SHA256 hashes found." -ForegroundColor Red } } |