Skip to content

Commit 6abfb43

Browse files
Reproduce coerced provenance and inconsistent release archive proof
Malformed outer and field arrays are accepted as ownership, and a mismatched ZIP payload reaches Gallery in the isolated real-controller fixture. Preserve raw shape and bind both byte digests before mutation. (cherry picked from commit 2afb929)
1 parent ff6bf3f commit 6abfb43

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

‎.github/actions/Publish-PSModule/tests/Publish-PSModule.Preflight.Tests.ps1‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,82 @@ Describe 'Publish-PSModule source-bound preflight' {
290290
$failure | Should -Not -BeNullOrEmpty
291291
}
292292

293+
It 'rejects a <Shape> publication claim before all writes' -ForEach @(
294+
@{ Shape = 'top-level array' }
295+
@{ Shape = 'array-valued field' }
296+
) {
297+
$claim = $script:state.ExpectedClaim | ConvertTo-Json -Compress | ConvertFrom-Json
298+
if ($Shape -ceq 'array-valued field') {
299+
$claim.PullRequest = @(0)
300+
}
301+
$json = $claim | ConvertTo-Json -Compress
302+
if ($Shape -ceq 'top-level array') { $json = "[$json]" }
303+
Set-PSModuleFixtureClaim -State $script:state -Claim $script:state.ExpectedClaim
304+
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))
305+
$script:state.TagObject.message = "<!-- PSModule.Publication:v1:$encoded -->"
306+
307+
{ & $script:publishScriptPath 1>$null 6>$null } | Should -Throw '*publication*'
308+
309+
$script:state.GalleryWrites | Should -Be 0
310+
$script:state.GitHubWrites | Should -Be 0
311+
}
312+
313+
It 'rejects a <Shape> release footer before all writes' -ForEach @(
314+
@{ Shape = 'top-level array' }
315+
@{ Shape = 'array-valued field' }
316+
) {
317+
$script:state.ExpectedClaim.PullRequest = 42
318+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest = '{"Number":42}'
319+
Set-PSModuleFixtureClaim -State $script:state -Claim $script:state.ExpectedClaim
320+
$owner = [pscustomobject]@{
321+
Repository = 'PSModule/TestModule'; PullRequest = 42; CommitSha = $env:GITHUB_SHA
322+
}
323+
if ($Shape -ceq 'array-valued field') { $owner.PullRequest = @(42) }
324+
$json = $owner | ConvertTo-Json -Compress
325+
if ($Shape -ceq 'top-level array') { $json = "[$json]" }
326+
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))
327+
$script:state.Release = [pscustomobject]@{
328+
tag_name = 'v1.2.4'; html_url = 'https://github.com/PSModule/TestModule/releases/tag/v1.2.4'
329+
prerelease = $false; draft = $false; body = "<!-- PSModule.PrereleaseOwner:v1:$encoded -->"; assets = @()
330+
}
331+
332+
{ & $script:publishScriptPath 1>$null 6>$null } | Should -Throw '*footer*'
333+
334+
$script:state.GalleryWrites | Should -Be 0
335+
$script:state.GitHubWrites | Should -Be 0
336+
}
337+
338+
It 'rejects a ZIP payload inconsistent with the tested file-tree claim before all writes' {
339+
Mock -CommandName Get-FileHash -ParameterFilter { $LiteralPath -like '*.zip' } -MockWith {
340+
$archive = [IO.Compression.ZipFile]::Open($LiteralPath, [IO.Compression.ZipArchiveMode]::Update)
341+
try {
342+
$archive.GetEntry('TestModule/TestModule.psm1').Delete()
343+
$writer = [IO.StreamWriter]::new($archive.CreateEntry('TestModule/TestModule.psm1').Open())
344+
try { $writer.Write('# Different ZIP payload bytes.') } finally { $writer.Dispose() }
345+
} finally {
346+
$archive.Dispose()
347+
}
348+
$stream = [IO.File]::OpenRead($LiteralPath)
349+
$sha256 = [Security.Cryptography.SHA256]::Create()
350+
try {
351+
[pscustomobject]@{
352+
Hash = [BitConverter]::ToString($sha256.ComputeHash($stream)).Replace('-', '')
353+
}
354+
} finally {
355+
$sha256.Dispose()
356+
$stream.Dispose()
357+
}
358+
}
359+
360+
$failure = $null
361+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
362+
363+
Should -Invoke -CommandName Get-FileHash -Times 1 -Exactly -ParameterFilter { $LiteralPath -like '*.zip' }
364+
$script:state.GalleryWrites | Should -Be 0
365+
$script:state.GitHubWrites | Should -Be 0
366+
$failure.Exception.Message | Should -Match 'archive'
367+
}
368+
293369
It 'rejects changed Gallery package bytes despite matching numeric version and source claim' {
294370
Set-PSModuleFixtureClaim -State $script:state -Claim $script:state.ExpectedClaim
295371
$script:state.Package = [pscustomobject]@{ Name = 'TestModule'; Version = '1.2.4' }
@@ -450,6 +526,7 @@ Describe 'Publish-PSModule source-bound preflight' {
450526
@{ Json = '{}' }
451527
@{ Json = '{"Number":0}' }
452528
@{ Json = '{"Number":"unknown"}' }
529+
@{ Json = '{"Number":[42]}' }
453530
@{ Json = '42' }
454531
@{ Json = '[]' }
455532
@{ Json = '[null]' }

‎.github/actions/Release-PSModule/tests/Release-PSModule.Ownership.Tests.ps1‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,44 @@ Describe 'Source-bound publication proof' {
6767
{ Get-PSModulePublicationClaim -Message $Message } | Should -Throw '*publication*'
6868
}
6969

70+
It 'rejects an array-valued publication <Field> instead of coercing its proof' -ForEach @(
71+
@{ Field = 'Repository' }
72+
@{ Field = 'CommitSha' }
73+
@{ Field = 'ModuleName' }
74+
@{ Field = 'GalleryVersion' }
75+
@{ Field = 'VersionPrefix' }
76+
@{ Field = 'ReleaseTag' }
77+
@{ Field = 'ReleaseType' }
78+
@{ Field = 'ArtifactSha256' }
79+
@{ Field = 'AssetName' }
80+
@{ Field = 'AssetSha256' }
81+
) {
82+
$script:claim[$Field] = @($script:claim[$Field])
83+
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(
84+
($script:claim | ConvertTo-Json -Compress)
85+
))
86+
87+
{ Get-PSModulePublicationClaim -Message "<!-- PSModule.Publication:v1:$encoded -->" } |
88+
Should -Throw '*publication*'
89+
}
90+
91+
It 'rejects an array-valued ownership <Field> instead of projecting a scalar' -ForEach @(
92+
@{ Field = 'Repository' }
93+
@{ Field = 'PullRequest' }
94+
@{ Field = 'CommitSha' }
95+
) {
96+
$owner = @{
97+
Repository = 'PSModule/TestModule'; PullRequest = 42; CommitSha = $script:claim.CommitSha
98+
}
99+
$owner[$Field] = @($owner[$Field])
100+
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(
101+
($owner | ConvertTo-Json -Compress)
102+
))
103+
104+
{ Get-PSModulePrereleaseOwnership -Body "<!-- PSModule.PrereleaseOwner:v1:$encoded -->" } |
105+
Should -Throw '*footer*'
106+
}
107+
70108
It 'rejects a foreign PR footer even when its immutable source matches' {
71109
$owner = @{
72110
Repository = 'PSModule/TestModule'

0 commit comments

Comments
 (0)