Last active
November 6, 2025 11:00
-
-
Save martincostello/132011ee7ec00688379eefee578f8c0f to your computer and use it in GitHub Desktop.
Verify NuGet Package DLL Attestations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Downloads a specific NuGet package (.nupkg), extracts it, and runs | |
| `gh attestation verify --owner <owner> <file>` for each .dll found under the extracted contents. | |
| Writes an error to the console if `gh` returns a non-zero exit code for any file, | |
| and exits with non-zero code if any verification fails. | |
| .PARAMETER Package | |
| The NuGet package ID (e.g. Polly.Core). | |
| .PARAMETER Version | |
| The package version (e.g. 8.6.4). | |
| .PARAMETER Owner | |
| The GitHub owner to pass to `gh attestation verify --owner` (e.g. App-vNext). | |
| .PARAMETER OutputDir | |
| Where to save the downloaded .nupkg and the extracted files. Defaults to a temporary directory under the current folder. | |
| .PARAMETER Keep | |
| Switch. If present, keeps the downloaded .nupkg and extracted files. By default the script cleans up the temporary files on success. | |
| .EXAMPLE | |
| .\download-and-verify-nuget.ps1 -Package "Polly.Core" -Version "8.6.4" -Owner App-vNext | |
| .EXAMPLE | |
| .\download-and-verify-nuget.ps1 -Package "My.Package" -Version "2.0.0" -Owner "my-github-login" -Keep | |
| .NOTES | |
| - Requires `gh` (GitHub CLI) available on PATH. | |
| - Uses the NuGet v3 flat container API to download the nupkg: | |
| https://api.nuget.org/v3-flatcontainer/{id-lower}/{version}/{id-lower}.{version}.nupkg | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory = $true, Position = 0)] | |
| [string] $Package, | |
| [Parameter(Mandatory = $true, Position = 1)] | |
| [string] $Version, | |
| [Parameter(Mandatory = $true, Position = 2)] | |
| [string] $Owner, | |
| [Parameter(Mandatory = $false)] | |
| [string] $OutputDir = (Join-Path -Path (Get-Location) -ChildPath ("nuget_verify_{0}_{1}" -f $Package, $Version)), | |
| [switch] $Keep | |
| ) | |
| function Assert-GhAvailable { | |
| $gh = Get-Command gh -ErrorAction SilentlyContinue | |
| if (-not $gh) { | |
| Write-Error "The 'gh' (GitHub CLI) executable was not found on PATH. Install or add it to PATH and try again." | |
| exit 2 | |
| } | |
| } | |
| function Download-Nupkg { | |
| param( | |
| [string] $packageId, | |
| [string] $version, | |
| [string] $outPath | |
| ) | |
| $idLower = $packageId.ToLowerInvariant() | |
| $fileName = "$($idLower).$($version).nupkg" | |
| $url = "https://api.nuget.org/v3-flatcontainer/$idLower/$version/$fileName" | |
| Write-Verbose "Downloading $url" | |
| try { | |
| Invoke-WebRequest -Uri $url -OutFile $outPath -UseBasicParsing -ErrorAction Stop | |
| } catch { | |
| Write-Error "Failed to download package from $url. $_" | |
| return $false | |
| } | |
| return $true | |
| } | |
| function Extract-Nupkg { | |
| param( | |
| [string] $nupkgPath, | |
| [string] $destDir | |
| ) | |
| try { | |
| if (-not (Test-Path -Path $destDir)) { | |
| New-Item -ItemType Directory -Path $destDir -Force | Out-Null | |
| } | |
| if (Get-Command Expand-Archive -ErrorAction SilentlyContinue) { | |
| Expand-Archive -LiteralPath $nupkgPath -DestinationPath $destDir -Force | |
| } else { | |
| Add-Type -AssemblyName System.IO.Compression.FileSystem | |
| [System.IO.Compression.ZipFile]::ExtractToDirectory($nupkgPath, $destDir) | |
| } | |
| } catch { | |
| Write-Error "Failed to extract $nupkgPath to $destDir. $_" | |
| return $false | |
| } | |
| return $true | |
| } | |
| function Verify-Dlls { | |
| param( | |
| [string] $rootDir, | |
| [string] $owner | |
| ) | |
| $dlls = Get-ChildItem -Path $rootDir -Filter *.dll -Recurse -File -ErrorAction SilentlyContinue | |
| if (-not $dlls -or $dlls.Count -eq 0) { | |
| Write-Warning "No .dll files found under $rootDir" | |
| return @() | |
| } | |
| $failures = @() | |
| foreach ($dll in $dlls) { | |
| $dllPath = $dll.FullName | |
| Write-Host "Verifying attestation for $dllPath ..." | |
| gh attestation verify --owner $owner $dllPath | |
| $exit = $LASTEXITCODE | |
| if ($exit -ne 0) { | |
| $msg = "gh attestation verify failed for '$dllPath' with exit code $exit" | |
| Write-Error $msg | |
| $failures += [PSCustomObject]@{ | |
| File = $dllPath | |
| ExitCode = $exit | |
| } | |
| } else { | |
| Write-Host "Verified: $dllPath" | |
| } | |
| } | |
| Write-Output $failures | |
| return $failures | |
| } | |
| try { | |
| Assert-GhAvailable | |
| $outDirFull = [System.IO.Path]::GetFullPath($OutputDir) | |
| $deleteOutDir = $false | |
| if (-not (Test-Path -Path $outDirFull)) { | |
| New-Item -ItemType Directory -Path $outDirFull -Force | Out-Null | |
| $deleteOutDir = $true | |
| } | |
| $idLower = $Package.ToLowerInvariant() | |
| $nupkgFileName = "$($idLower).$($Version).nupkg" | |
| $nupkgPath = Join-Path -Path $outDirFull -ChildPath $nupkgFileName | |
| $extractDir = Join-Path -Path $outDirFull -ChildPath "extracted" | |
| Write-Output "Downloading package $Package v$Version to $nupkgPath" | |
| $downloaded = Download-Nupkg -packageId $Package -version $Version -outPath $nupkgPath | |
| if (-not $downloaded) { | |
| Write-Error "Download failed. Exiting." | |
| exit 3 | |
| } | |
| Write-Output "Extracting $nupkgPath to $extractDir" | |
| $extracted = Extract-Nupkg -nupkgPath $nupkgPath -destDir $extractDir | |
| if (-not $extracted) { | |
| Write-Error "Extraction failed. Exiting." | |
| exit 4 | |
| } | |
| Write-Output "Searching for DLL files to verify for owner $Owner ..." | |
| $failures = Verify-Dlls -rootDir $extractDir -owner $Owner | |
| Write-Host $failures | |
| if ($failures.Count -gt 0) { | |
| Write-Host "" | |
| Write-Host "Summary: $($failures.Count) file(s) failed verification:" | |
| foreach ($f in $failures) { | |
| Write-Host " - $($f.File) (exit $($f.ExitCode))" | |
| } | |
| if (-not $Keep) { | |
| Write-Output "Cleaning up downloaded/extracted files (use -Keep to preserve files)..." | |
| try { | |
| Remove-Item -LiteralPath $nupkgPath -Force -ErrorAction SilentlyContinue | |
| Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue | |
| if ($deleteOutDir) { | |
| Remove-Item -LiteralPath $outDirFull -Force -ErrorAction SilentlyContinue | |
| } | |
| } catch {} | |
| } | |
| exit 1 | |
| } else { | |
| Write-Output "All DLL files verified successfully." | |
| if (-not $Keep) { | |
| Write-Output "Cleaning up downloaded/extracted files..." | |
| try { | |
| Remove-Item -LiteralPath $nupkgPath -Force -ErrorAction SilentlyContinue | |
| Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue | |
| if ($deleteOutDir) { | |
| Remove-Item -LiteralPath $outDirFull -Force -ErrorAction SilentlyContinue | |
| } | |
| } catch {} | |
| } | |
| exit 0 | |
| } | |
| } catch { | |
| Write-Error "Unexpected error: $_" | |
| exit 5 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment