microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/security/Test-DependencyPinning.Tests.ps1

329lines · modecode

1#Requires -Modules Pester
2
3BeforeAll {
4 . $PSScriptRoot/../../security/Test-DependencyPinning.ps1
5
6 $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1'
7 Import-Module $mockPath -Force
8
9 # Fixture paths
10 $script:FixturesPath = Join-Path $PSScriptRoot '../Fixtures/Workflows'
11 $script:SecurityFixturesPath = Join-Path $PSScriptRoot '../Fixtures/Security'
12}
13
14Describe 'Test-SHAPinning' -Tag 'Unit' {
15 Context 'Valid SHA references for github-actions' {
16 It 'Returns true for valid 40-char lowercase SHA' {
17 Test-SHAPinning -Version 'a5ac7e51b41094c92402da3b24376905380afc29' -Type 'github-actions' | Should -BeTrue
18 }
19
20 It 'Returns true for valid 40-char mixed case SHA' {
21 Test-SHAPinning -Version 'A5AC7E51B41094c92402da3b24376905380afc29' -Type 'github-actions' | Should -BeTrue
22 }
23 }
24
25 Context 'Invalid SHA references for github-actions' {
26 It 'Returns false for tag reference' {
27 Test-SHAPinning -Version 'v4' -Type 'github-actions' | Should -BeFalse
28 }
29
30 It 'Returns false for branch reference' {
31 Test-SHAPinning -Version 'main' -Type 'github-actions' | Should -BeFalse
32 }
33
34 It 'Returns false for 39-char reference' {
35 Test-SHAPinning -Version 'a5ac7e51b41094c92402da3b24376905380afc2' -Type 'github-actions' | Should -BeFalse
36 }
37
38 It 'Returns false for 41-char reference' {
39 Test-SHAPinning -Version 'a5ac7e51b41094c92402da3b24376905380afc291' -Type 'github-actions' | Should -BeFalse
40 }
41
42 It 'Returns false for non-hex characters' {
43 Test-SHAPinning -Version 'g5ac7e51b41094c92402da3b24376905380afc29' -Type 'github-actions' | Should -BeFalse
44 }
45 }
46
47 Context 'Unknown type' {
48 It 'Returns false for unknown dependency type' {
49 Test-SHAPinning -Version 'a5ac7e51b41094c92402da3b24376905380afc29' -Type 'unknown-type' | Should -BeFalse
50 }
51 }
52}
53
54Describe 'Test-ShellDownloadSecurity' -Tag 'Unit' {
55 Context 'Insecure downloads' {
56 It 'Detects curl without checksum verification' {
57 $testFile = Join-Path $script:SecurityFixturesPath 'insecure-download.sh'
58 $result = Test-ShellDownloadSecurity -FilePath $testFile
59 $result | Should -Not -BeNullOrEmpty
60 $result[0].Severity | Should -Be 'warning'
61 }
62 }
63
64 Context 'File not found' {
65 It 'Returns empty array for non-existent file' {
66 $result = Test-ShellDownloadSecurity -FilePath 'TestDrive:/nonexistent/file.sh'
67 $result | Should -BeNullOrEmpty
68 }
69 }
70}
71
72Describe 'Get-DependencyViolation' -Tag 'Unit' {
73 Context 'Pinned workflows' {
74 It 'Returns no violations for fully pinned workflow' {
75 $pinnedPath = Join-Path $script:FixturesPath 'pinned-workflow.yml'
76 $fileInfo = @{
77 Path = $pinnedPath
78 Type = 'github-actions'
79 RelativePath = 'pinned-workflow.yml'
80 }
81 $result = Get-DependencyViolation -FileInfo $fileInfo
82 $result | Should -BeNullOrEmpty
83 }
84 }
85
86 Context 'Unpinned workflows' {
87 It 'Detects unpinned action references' {
88 $unpinnedPath = Join-Path $script:FixturesPath 'unpinned-workflow.yml'
89 $fileInfo = @{
90 Path = $unpinnedPath
91 Type = 'github-actions'
92 RelativePath = 'unpinned-workflow.yml'
93 }
94 $result = Get-DependencyViolation -FileInfo $fileInfo
95 $result | Should -Not -BeNullOrEmpty
96 $result.Count | Should -BeGreaterThan 0
97 }
98
99 It 'Returns correct violation type for unpinned actions' {
100 $unpinnedPath = Join-Path $script:FixturesPath 'unpinned-workflow.yml'
101 $fileInfo = @{
102 Path = $unpinnedPath
103 Type = 'github-actions'
104 RelativePath = 'unpinned-workflow.yml'
105 }
106 $result = Get-DependencyViolation -FileInfo $fileInfo
107 $result[0].Type | Should -Be 'github-actions'
108 }
109 }
110
111 Context 'Mixed workflows' {
112 It 'Detects only unpinned actions in mixed workflow' {
113 $mixedPath = Join-Path $script:FixturesPath 'mixed-pinning-workflow.yml'
114 $fileInfo = @{
115 Path = $mixedPath
116 Type = 'github-actions'
117 RelativePath = 'mixed-pinning-workflow.yml'
118 }
119 $result = Get-DependencyViolation -FileInfo $fileInfo
120 $result | Should -Not -BeNullOrEmpty
121 # Should only detect the unpinned setup-node action
122 $result.Name | Should -Contain 'actions/setup-node'
123 }
124 }
125
126 Context 'Non-existent file' {
127 It 'Returns empty array for non-existent file' {
128 $fileInfo = @{
129 Path = 'TestDrive:/nonexistent/file.yml'
130 Type = 'github-actions'
131 RelativePath = 'file.yml'
132 }
133 $result = Get-DependencyViolation -FileInfo $fileInfo
134 $result | Should -BeNullOrEmpty
135 }
136 }
137}
138
139Describe 'Export-ComplianceReport' -Tag 'Unit' {
140 BeforeEach {
141 $script:TestOutputPath = Join-Path $TestDrive 'report'
142 New-Item -ItemType Directory -Path $script:TestOutputPath -Force | Out-Null
143
144 # Create a proper ComplianceReport class instance
145 $script:MockReport = [ComplianceReport]::new()
146 $script:MockReport.ScanPath = $script:FixturesPath
147 $script:MockReport.ComplianceScore = 50
148 $script:MockReport.TotalFiles = 3
149 $script:MockReport.ScannedFiles = 3
150 $script:MockReport.TotalDependencies = 4
151 $script:MockReport.PinnedDependencies = 2
152 $script:MockReport.UnpinnedDependencies = 2
153 $script:MockReport.Violations = @(
154 [PSCustomObject]@{
155 File = 'unpinned-workflow.yml'
156 Line = 10
157 Type = 'github-actions'
158 Name = 'actions/checkout'
159 Version = 'v4'
160 Severity = 'High'
161 Description = 'Unpinned dependency'
162 Remediation = 'Pin to SHA'
163 }
164 )
165 $script:MockReport.Summary = @{
166 'github-actions' = @{
167 Total = 4
168 High = 2
169 Medium = 0
170 Low = 0
171 }
172 }
173 }
174
175 Context 'JSON format' {
176 It 'Generates valid JSON report' {
177 $outputFile = Join-Path $script:TestOutputPath 'report.json'
178
179 Export-ComplianceReport -Report $script:MockReport -Format 'json' -OutputPath $outputFile
180
181 Test-Path $outputFile | Should -BeTrue
182 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
183 $content | Should -Not -BeNullOrEmpty
184 }
185 }
186
187 Context 'SARIF format' {
188 It 'Generates valid SARIF report' {
189 $outputFile = Join-Path $script:TestOutputPath 'report.sarif'
190
191 Export-ComplianceReport -Report $script:MockReport -Format 'sarif' -OutputPath $outputFile
192
193 Test-Path $outputFile | Should -BeTrue
194 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
195 $content.'$schema' | Should -Match 'sarif'
196 }
197 }
198
199 Context 'Table format' {
200 It 'Generates table output without error' {
201 $outputFile = Join-Path $script:TestOutputPath 'report.txt'
202
203 { Export-ComplianceReport -Report $script:MockReport -Format 'table' -OutputPath $outputFile } | Should -Not -Throw
204 Test-Path $outputFile | Should -BeTrue
205 }
206 }
207
208 Context 'CSV format' {
209 It 'Generates CSV report' {
210 $outputFile = Join-Path $script:TestOutputPath 'report.csv'
211
212 Export-ComplianceReport -Report $script:MockReport -Format 'csv' -OutputPath $outputFile
213
214 Test-Path $outputFile | Should -BeTrue
215 }
216 }
217
218 Context 'Markdown format' {
219 It 'Generates Markdown report' {
220 $outputFile = Join-Path $script:TestOutputPath 'report.md'
221
222 Export-ComplianceReport -Report $script:MockReport -Format 'markdown' -OutputPath $outputFile
223
224 Test-Path $outputFile | Should -BeTrue
225 $content = Get-Content $outputFile -Raw
226 $content | Should -Match '# Dependency Pinning Compliance Report'
227 }
228 }
229}
230
231Describe 'ExcludePaths Filtering Logic' -Tag 'Unit' {
232 Context 'Pattern matching with -notlike operator' {
233 It 'Excludes paths containing pattern using -notlike wildcard' {
234 # Test the exclusion logic used in Get-FilesToScan:
235 # $files = $files | Where-Object { $_.FullName -notlike "*$exclude*" }
236 $testPaths = @(
237 @{ FullName = 'C:\repo\.github\workflows\test.yml' }
238 @{ FullName = 'C:\repo\vendor\.github\workflows\vendor.yml' }
239 )
240
241 $exclude = 'vendor'
242 $filtered = $testPaths | Where-Object { $_.FullName -notlike "*$exclude*" }
243
244 $filtered.Count | Should -Be 1
245 $filtered[0].FullName | Should -Not -Match 'vendor'
246 }
247
248 It 'Excludes multiple patterns correctly' {
249 $testPaths = @(
250 @{ FullName = 'C:\repo\.github\workflows\test.yml' }
251 @{ FullName = 'C:\repo\vendor\.github\workflows\vendor.yml' }
252 @{ FullName = 'C:\repo\node_modules\pkg\workflow.yml' }
253 )
254
255 $excludePatterns = @('vendor', 'node_modules')
256 $filtered = $testPaths
257 foreach ($exclude in $excludePatterns) {
258 $filtered = @($filtered | Where-Object { $_.FullName -notlike "*$exclude*" })
259 }
260
261 $filtered.Count | Should -Be 1
262 $filtered[0].FullName | Should -Be 'C:\repo\.github\workflows\test.yml'
263 }
264 }
265
266 Context 'Processes all files when ExcludePatterns is empty' {
267 It 'Returns all paths when no exclusion patterns provided' {
268 $testPaths = @(
269 @{ FullName = 'C:\repo\.github\workflows\test.yml' }
270 @{ FullName = 'C:\repo\vendor\.github\workflows\vendor.yml' }
271 )
272
273 $excludePatterns = @()
274 $filtered = $testPaths
275 if ($excludePatterns) {
276 foreach ($exclude in $excludePatterns) {
277 $filtered = $filtered | Where-Object { $_.FullName -notlike "*$exclude*" }
278 }
279 }
280
281 $filtered.Count | Should -Be 2
282 }
283 }
284
285 Context 'Comma-separated pattern parsing in main script' {
286 It 'Parses comma-separated exclude paths correctly' {
287 # Test the pattern used in main execution: $ExcludePaths.Split(',')
288 $excludePathsParam = 'vendor,node_modules,dist'
289 $patterns = $excludePathsParam.Split(',') | ForEach-Object { $_.Trim() }
290
291 $patterns.Count | Should -Be 3
292 $patterns | Should -Contain 'vendor'
293 $patterns | Should -Contain 'node_modules'
294 $patterns | Should -Contain 'dist'
295 }
296
297 It 'Handles single pattern without comma' {
298 $excludePathsParam = 'vendor'
299 $patterns = $excludePathsParam.Split(',') | ForEach-Object { $_.Trim() }
300
301 $patterns.Count | Should -Be 1
302 $patterns | Should -Contain 'vendor'
303 }
304
305 It 'Handles empty exclude paths' {
306 $excludePathsParam = ''
307 $patterns = if ($excludePathsParam) { $excludePathsParam.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
308
309 $patterns.Count | Should -Be 0
310 }
311 }
312
313 Context 'Pattern matching behavior' {
314 It 'Uses -notlike with wildcard for exclusion' {
315 $filePath = 'C:\repo\vendor\.github\workflows\test.yml'
316 $pattern = 'vendor'
317
318 # This matches how Get-FilesToScan uses: $_.FullName -notlike "*$exclude*"
319 $filePath -notlike "*$pattern*" | Should -BeFalse
320 }
321
322 It 'Passes through non-matching paths' {
323 $filePath = 'C:\repo\.github\workflows\main.yml'
324 $pattern = 'vendor'
325
326 $filePath -notlike "*$pattern*" | Should -BeTrue
327 }
328 }
329}
330