Skip to content

Commit fbb48a2

Browse files
Add integrity tree validation to release pipeline
1 parent 7054ea8 commit fbb48a2

5 files changed

Lines changed: 141 additions & 0 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ jobs:
229229
shell: pwsh
230230
run: .\scripts\generate-integrity-tree.ps1 -Path $PWD/unigetui_bin -MinOutput
231231

232+
- name: Validate integrity tree
233+
shell: pwsh
234+
run: .\scripts\verify-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
235+
232236
- name: Build installer
233237
shell: pwsh
234238
run: |

scripts/build.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe")
9898
Write-Host "`n=== Generating integrity tree ===" -ForegroundColor Cyan
9999
& (Join-Path $PSScriptRoot "generate-integrity-tree.ps1") -Path $BinDir -MinOutput
100100

101+
Write-Host "`n=== Validating integrity tree ===" -ForegroundColor Cyan
102+
& (Join-Path $PSScriptRoot "verify-integrity-tree.ps1") -Path $BinDir -FailOnUnexpectedFiles
103+
101104
# --- Package output ---
102105
if (Test-Path $OutputPath) { Remove-Item $OutputPath -Recurse -Force }
103106
New-Item $OutputPath -ItemType Directory | Out-Null

scripts/sign.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,28 @@ function Invoke-BatchSign {
106106
}
107107
}
108108

109+
function Update-IntegrityTree {
110+
param(
111+
[string] $RootPath
112+
)
113+
114+
if (-not $RootPath -or -not (Test-Path $RootPath -PathType Container)) {
115+
return
116+
}
117+
118+
$TreeScriptPath = Join-Path $PSScriptRoot "generate-integrity-tree.ps1"
119+
if (-not (Test-Path $TreeScriptPath -PathType Leaf)) {
120+
Write-Warning "Integrity tree generator not found at $TreeScriptPath"
121+
return
122+
}
123+
124+
Write-Host "Refreshing integrity tree in $RootPath..."
125+
& $TreeScriptPath -Path $RootPath -MinOutput
126+
if ($LASTEXITCODE -ne 0) {
127+
throw "generate-integrity-tree.ps1 failed with exit code $LASTEXITCODE"
128+
}
129+
}
130+
109131
# --- Sign binaries in BinDir ---
110132
if ($FileListPath -and (Test-Path $FileListPath)) {
111133
Write-Host "`n=== Signing binaries from list: $FileListPath ===" -ForegroundColor Cyan
@@ -118,6 +140,7 @@ if ($FileListPath -and (Test-Path $FileListPath)) {
118140
Write-Warning "No .exe or .dll files found in $BinDir"
119141
} else {
120142
Invoke-BatchSign -Files ($filesToSign | ForEach-Object { $_.FullName })
143+
Update-IntegrityTree -RootPath $BinDir
121144
Write-Host "Binary signing complete."
122145
}
123146
}

scripts/verify-integrity-tree.ps1

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Validates a generated IntegrityTree.json against the files in a directory.
5+
6+
.DESCRIPTION
7+
This mirrors UniGetUI runtime integrity verification and can optionally fail
8+
if the directory contains files that are not listed in IntegrityTree.json.
9+
10+
.PARAMETER Path
11+
The directory containing IntegrityTree.json and the files to validate.
12+
13+
.PARAMETER FailOnUnexpectedFiles
14+
Fail validation if files exist in the directory tree but are not present in
15+
IntegrityTree.json.
16+
#>
17+
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory, Position = 0)]
21+
[string] $Path,
22+
23+
[switch] $FailOnUnexpectedFiles
24+
)
25+
26+
$ErrorActionPreference = 'Stop'
27+
28+
if (-not (Test-Path $Path -PathType Container)) {
29+
throw "The directory '$Path' does not exist."
30+
}
31+
32+
$Path = (Resolve-Path $Path).Path
33+
$IntegrityTreePath = Join-Path $Path 'IntegrityTree.json'
34+
35+
if (-not (Test-Path $IntegrityTreePath -PathType Leaf)) {
36+
throw "IntegrityTree.json was not found in '$Path'."
37+
}
38+
39+
$rawData = Get-Content $IntegrityTreePath -Raw
40+
41+
try {
42+
$data = ConvertFrom-Json $rawData -AsHashtable
43+
}
44+
catch {
45+
throw "IntegrityTree.json is not valid JSON: $($_.Exception.Message)"
46+
}
47+
48+
if ($null -eq $data) {
49+
throw 'IntegrityTree.json did not deserialize into a JSON object.'
50+
}
51+
52+
$missingFiles = New-Object System.Collections.Generic.List[string]
53+
$mismatchedFiles = New-Object System.Collections.Generic.List[string]
54+
$unexpectedFiles = New-Object System.Collections.Generic.List[string]
55+
56+
$expectedFiles = @{}
57+
foreach ($entry in $data.GetEnumerator()) {
58+
$relativePath = [string] $entry.Key
59+
$expectedHash = [string] $entry.Value
60+
$expectedFiles[$relativePath] = $true
61+
62+
$fullPath = Join-Path $Path $relativePath
63+
if (-not (Test-Path $fullPath -PathType Leaf)) {
64+
$missingFiles.Add($relativePath)
65+
continue
66+
}
67+
68+
$currentHash = (Get-FileHash $fullPath -Algorithm SHA256).Hash.ToLowerInvariant()
69+
if ($currentHash -ne $expectedHash.ToLowerInvariant()) {
70+
$mismatchedFiles.Add("$relativePath|expected=$expectedHash|got=$currentHash")
71+
}
72+
}
73+
74+
if ($FailOnUnexpectedFiles) {
75+
Get-ChildItem $Path -Recurse -File | ForEach-Object {
76+
$relativePath = $_.FullName.Substring($Path.Length).TrimStart('\', '/') -replace '\\', '/'
77+
if ($relativePath -eq 'IntegrityTree.json') {
78+
return
79+
}
80+
81+
if (-not $expectedFiles.ContainsKey($relativePath)) {
82+
$unexpectedFiles.Add($relativePath)
83+
}
84+
}
85+
}
86+
87+
if ($missingFiles.Count -or $mismatchedFiles.Count -or $unexpectedFiles.Count) {
88+
if ($missingFiles.Count) {
89+
Write-Error "Missing files listed in IntegrityTree.json:`n - $($missingFiles -join "`n - ")"
90+
}
91+
92+
if ($mismatchedFiles.Count) {
93+
Write-Error "Files with mismatched SHA256 values:`n - $($mismatchedFiles -join "`n - ")"
94+
}
95+
96+
if ($unexpectedFiles.Count) {
97+
Write-Error "Unexpected files not present in IntegrityTree.json:`n - $($unexpectedFiles -join "`n - ")"
98+
}
99+
100+
throw 'Integrity tree validation failed.'
101+
}
102+
103+
$validatedFileCount = $data.Count
104+
Write-Host "Integrity tree validation succeeded for $validatedFileCount file(s) in $Path"

src/UniGetUI/UniGetUI.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@
4949
<Exec Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(OutputPath) -MinOutput" />
5050
</Target>
5151

52+
<Target Name="PostPublishGenerateIntegrityTree" AfterTargets="Publish">
53+
<Exec
54+
Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(PublishDir) -MinOutput"
55+
Condition="'$(PublishDir)' != '' and Exists('$(PublishDir)')"
56+
/>
57+
</Target>
58+
5259
<Target
5360
Name="EnsureBundledElevatorFromNuGet"
5461
BeforeTargets="PrepareForBuild"

0 commit comments

Comments
 (0)