microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/prerelease-version-override

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/security/Sign-PlannerArtifacts.Tests.ps1

308lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5BeforeAll {
6 # Stub cosign when not installed so Pester can mock it
7 if (-not (Get-Command cosign -ErrorAction SilentlyContinue)) { function global:cosign { } }
8
9 $script:ScriptPath = Join-Path $PSScriptRoot '../../security/Sign-PlannerArtifacts.ps1'
10
11 # Extract helper functions via AST. The script has a mandatory ProjectSlug
12 # parameter with script-scope execution, preventing dot-source.
13 $ast = [System.Management.Automation.Language.Parser]::ParseFile(
14 $script:ScriptPath, [ref]$null, [ref]$null)
15 $ast.FindAll(
16 { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true
17 ) | ForEach-Object { . ([scriptblock]::Create($_.Extent.Text)) }
18
19 Mock Write-Host {}
20}
21
22Describe 'Get-ArtifactHash' -Tag 'Unit' {
23 Context 'SHA-256 computation' {
24 It 'Returns a lowercase hex SHA-256 hash for a file' {
25 $testFile = Join-Path $TestDrive 'hash-test.txt'
26 Set-Content -Path $testFile -Value 'deterministic content' -NoNewline -Encoding utf8NoBOM
27
28 $hash = Get-ArtifactHash -FilePath $testFile
29
30 $hash | Should -Match '^[0-9a-f]{64}$'
31 }
32
33 It 'Returns consistent hashes for identical content' {
34 $file1 = Join-Path $TestDrive 'dup1.txt'
35 $file2 = Join-Path $TestDrive 'dup2.txt'
36 Set-Content -Path $file1 -Value 'same' -NoNewline -Encoding utf8NoBOM
37 Set-Content -Path $file2 -Value 'same' -NoNewline -Encoding utf8NoBOM
38
39 $hash1 = Get-ArtifactHash -FilePath $file1
40 $hash2 = Get-ArtifactHash -FilePath $file2
41
42 $hash1 | Should -Be $hash2
43 }
44
45 It 'Returns different hashes for different content' {
46 $file1 = Join-Path $TestDrive 'diff1.txt'
47 $file2 = Join-Path $TestDrive 'diff2.txt'
48 Set-Content -Path $file1 -Value 'alpha' -NoNewline -Encoding utf8NoBOM
49 Set-Content -Path $file2 -Value 'bravo' -NoNewline -Encoding utf8NoBOM
50
51 $hash1 = Get-ArtifactHash -FilePath $file1
52 $hash2 = Get-ArtifactHash -FilePath $file2
53
54 $hash1 | Should -Not -Be $hash2
55 }
56 }
57}
58
59Describe 'Manifest Generation' -Tag 'Unit' {
60 BeforeEach {
61 $script:projectSlug = 'test-project'
62 $script:artifactDir = Join-Path $TestDrive ".copilot-tracking/rai-plans/$($script:projectSlug)"
63 if (Test-Path $script:artifactDir) { Remove-Item $script:artifactDir -Recurse -Force }
64 New-Item -ItemType Directory -Path $script:artifactDir -Force | Out-Null
65 }
66
67 Context 'when artifact directory does not exist' {
68 It 'Exits with code 1 for missing directory' {
69 $missingSlug = 'nonexistent-project'
70 $originalPWD = $PWD
71 try {
72 Set-Location $TestDrive
73 & $script:ScriptPath -ProjectSlug $missingSlug
74 }
75 catch { $_ | Out-Null }
76 finally {
77 Set-Location $originalPWD
78 }
79 $LASTEXITCODE | Should -Be 1
80 }
81 }
82
83 Context 'when artifact directory is empty' {
84 It 'Exits with code 0 and reports no artifacts' {
85 $originalPWD = $PWD
86 try {
87 Set-Location $TestDrive
88 & $script:ScriptPath -ProjectSlug $script:projectSlug
89 }
90 catch { $_ | Out-Null }
91 finally {
92 Set-Location $originalPWD
93 }
94
95 Should -Invoke Write-Host -ParameterFilter { $Object -like '*No artifacts found*' }
96 }
97 }
98
99 Context 'when artifacts exist' {
100 BeforeEach {
101 Set-Content -Path (Join-Path $script:artifactDir 'state.json') -Value '{"status":"complete"}' -Encoding utf8NoBOM
102 Set-Content -Path (Join-Path $script:artifactDir 'findings.md') -Value '# Findings' -Encoding utf8NoBOM
103 $script:outputPath = Join-Path $script:artifactDir 'artifact-manifest.json'
104 }
105
106 It 'Generates a valid JSON manifest' {
107 $originalPWD = $PWD
108 try {
109 Set-Location $TestDrive
110 & $script:ScriptPath -ProjectSlug $script:projectSlug
111 }
112 finally {
113 Set-Location $originalPWD
114 }
115
116 Test-Path $script:outputPath | Should -BeTrue
117 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
118 $manifest.version | Should -Be '1.0'
119 $manifest.projectSlug | Should -Be $script:projectSlug
120 $manifest.algorithm | Should -Be 'SHA256'
121 }
122
123 It 'Includes correct file count' {
124 $originalPWD = $PWD
125 try {
126 Set-Location $TestDrive
127 & $script:ScriptPath -ProjectSlug $script:projectSlug
128 }
129 finally {
130 Set-Location $originalPWD
131 }
132
133 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
134 $manifest.fileCount | Should -Be 2
135 $manifest.artifacts.Count | Should -Be 2
136 }
137
138 It 'Computes SHA-256 hashes for each artifact' {
139 $originalPWD = $PWD
140 try {
141 Set-Location $TestDrive
142 & $script:ScriptPath -ProjectSlug $script:projectSlug
143 }
144 finally {
145 Set-Location $originalPWD
146 }
147
148 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
149 foreach ($artifact in $manifest.artifacts) {
150 $artifact.sha256 | Should -Match '^[0-9a-f]{64}$'
151 $artifact.path | Should -Not -BeNullOrEmpty
152 $artifact.sizeBytes | Should -BeGreaterThan 0
153 }
154 }
155
156 It 'Writes manifest to custom OutputPath when specified' {
157 $customPath = Join-Path $TestDrive 'custom-manifest.json'
158
159 $originalPWD = $PWD
160 try {
161 Set-Location $TestDrive
162 & $script:ScriptPath -ProjectSlug $script:projectSlug -OutputPath $customPath
163 }
164 finally {
165 Set-Location $originalPWD
166 }
167
168 Test-Path $customPath | Should -BeTrue
169 $manifest = Get-Content $customPath -Raw | ConvertFrom-Json
170 $manifest.fileCount | Should -Be 2
171 }
172
173 It 'Includes generatedAt in ISO 8601 format' {
174 $originalPWD = $PWD
175 try {
176 Set-Location $TestDrive
177 & $script:ScriptPath -ProjectSlug $script:projectSlug
178 }
179 finally {
180 Set-Location $originalPWD
181 }
182
183 $raw = Get-Content $script:outputPath -Raw
184 $raw | Should -Match '"generatedAt"\s*:\s*"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
185 }
186
187 It 'Orders artifacts alphabetically by path' {
188 $originalPWD = $PWD
189 try {
190 Set-Location $TestDrive
191 & $script:ScriptPath -ProjectSlug $script:projectSlug
192 }
193 finally {
194 Set-Location $originalPWD
195 }
196
197 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
198 $paths = $manifest.artifacts | ForEach-Object { $_.path }
199 $sorted = $paths | Sort-Object
200 $paths | Should -Be $sorted
201 }
202
203 It 'Contains all required top-level manifest fields' {
204 $originalPWD = $PWD
205 try {
206 Set-Location $TestDrive
207 & $script:ScriptPath -ProjectSlug $script:projectSlug
208 }
209 finally {
210 Set-Location $originalPWD
211 }
212
213 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
214 $fields = ($manifest | Get-Member -MemberType NoteProperty).Name | Sort-Object
215 $expected = @('algorithm', 'artifacts', 'fileCount', 'generatedAt', 'projectSlug', 'version') | Sort-Object
216 $fields | Should -Be $expected
217 }
218 }
219
220 Context 'exclude patterns' {
221 It 'Excludes artifact-manifest.json from the inventory' {
222 Set-Content -Path (Join-Path $script:artifactDir 'state.json') -Value '{}' -Encoding utf8NoBOM
223 Set-Content -Path (Join-Path $script:artifactDir 'artifact-manifest.json') -Value '{}' -Encoding utf8NoBOM
224
225 $outputPath = Join-Path $script:artifactDir 'artifact-manifest.json'
226 $originalPWD = $PWD
227 try {
228 Set-Location $TestDrive
229 & $script:ScriptPath -ProjectSlug $script:projectSlug
230 }
231 finally {
232 Set-Location $originalPWD
233 }
234
235 $manifest = Get-Content $outputPath -Raw | ConvertFrom-Json
236 $manifest.fileCount | Should -Be 1
237 $manifest.artifacts[0].path | Should -Be 'state.json'
238 }
239
240 It 'Excludes .sig and .bundle files from the inventory' {
241 Set-Content -Path (Join-Path $script:artifactDir 'data.md') -Value '# Data' -Encoding utf8NoBOM
242 Set-Content -Path (Join-Path $script:artifactDir 'manifest.json.sig') -Value 'sig' -Encoding utf8NoBOM
243 Set-Content -Path (Join-Path $script:artifactDir 'manifest.json.bundle') -Value 'bundle' -Encoding utf8NoBOM
244
245 $originalPWD = $PWD
246 try {
247 Set-Location $TestDrive
248 & $script:ScriptPath -ProjectSlug $script:projectSlug
249 }
250 finally {
251 Set-Location $originalPWD
252 }
253
254 $manifest = Get-Content $script:outputPath -Raw | ConvertFrom-Json
255 $manifest.fileCount | Should -Be 1
256 $manifest.artifacts[0].path | Should -Be 'data.md'
257 }
258 }
259}
260
261Describe 'Cosign Signing' -Tag 'Unit' {
262 BeforeEach {
263 $script:projectSlug = 'cosign-test'
264 $script:artifactDir = Join-Path $TestDrive ".copilot-tracking/rai-plans/$($script:projectSlug)"
265 if (Test-Path $script:artifactDir) { Remove-Item $script:artifactDir -Recurse -Force }
266 New-Item -ItemType Directory -Path $script:artifactDir -Force | Out-Null
267 Set-Content -Path (Join-Path $script:artifactDir 'data.md') -Value '# Test' -Encoding utf8NoBOM
268 $script:outputPath = Join-Path $script:artifactDir 'artifact-manifest.json'
269 }
270
271 Context 'when cosign is not installed' {
272 It 'Warns and skips signing gracefully' {
273 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'cosign' }
274 Mock cosign {}
275
276 $originalPWD = $PWD
277 try {
278 Set-Location $TestDrive
279 & $script:ScriptPath -ProjectSlug $script:projectSlug -IncludeCosign
280 }
281 catch { $_ | Out-Null }
282 finally {
283 Set-Location $originalPWD
284 }
285
286 Should -Invoke Write-Host -ParameterFilter { $Object -like '*cosign not found*' }
287 Should -Not -Invoke cosign
288 }
289 }
290
291 Context 'when cosign is available' {
292 It 'Invokes cosign sign-blob with correct arguments' {
293 Mock Get-Command { [pscustomobject]@{ Name = 'cosign' } } -ParameterFilter { $Name -eq 'cosign' }
294 Mock cosign {}
295
296 $originalPWD = $PWD
297 try {
298 Set-Location $TestDrive
299 & $script:ScriptPath -ProjectSlug $script:projectSlug -IncludeCosign
300 }
301 finally {
302 Set-Location $originalPWD
303 }
304
305 Should -Invoke cosign -Times 1 -Exactly
306 }
307 }
308}
309