microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3a3a0fdf923d96a9e8a9ac734c73f24433b525e8

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-ActionVersionConsistency.ps1

486lines · modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#Requires -Version 7.0
5
6<#
7.SYNOPSIS
8 Validates GitHub Actions version comment consistency across workflows.
9
10.DESCRIPTION
11 Scans workflow files for SHA-pinned actions and validates:
12 - Same SHA has consistent version comments across all workflows
13 - SHA-pinned actions include version comments for traceability
14
15 Version comments follow the Renovate convention: action@sha # vX.Y.Z
16
17.PARAMETER Path
18 Path to scan for workflow files. Defaults to .github/workflows.
19
20.PARAMETER Format
21 Output format: Table, Json, Sarif. Defaults to Table.
22
23.PARAMETER OutputPath
24 Path to write output file when using Json or Sarif format.
25
26.PARAMETER FailOnMismatch
27 Exit with error code 1 if version mismatches are found.
28
29.PARAMETER FailOnMissingComment
30 Exit with error code 1 if missing version comments are found.
31
32.EXAMPLE
33 ./Test-ActionVersionConsistency.ps1
34 Scan workflows and display results in table format.
35
36.EXAMPLE
37 ./Test-ActionVersionConsistency.ps1 -Format Sarif -OutputPath results.sarif
38 Export results in SARIF format for CI integration.
39
40.EXAMPLE
41 ./Test-ActionVersionConsistency.ps1 -FailOnMismatch -FailOnMissingComment
42 Fail the script if any consistency issues are found.
43
44.NOTES
45 Requires:
46 - PowerShell 7.0 or later for cross-platform compatibility
47
48.LINK
49 https://docs.renovatebot.com/modules/manager/github-actions/
50#>
51
52using module ./Modules/SecurityClasses.psm1
53
54[CmdletBinding()]
55param(
56 [Parameter(Mandatory = $false)]
57 [string]$Path = '.github/workflows',
58
59 [Parameter(Mandatory = $false)]
60 [ValidateSet('Table', 'Json', 'Sarif')]
61 [string]$Format = 'Table',
62
63 [Parameter(Mandatory = $false)]
64 [string]$OutputPath,
65
66 [Parameter(Mandatory = $false)]
67 [switch]$FailOnMismatch,
68
69 [Parameter(Mandatory = $false)]
70 [switch]$FailOnMissingComment
71)
72
73$ErrorActionPreference = 'Stop'
74
75# Import CIHelpers for workflow command escaping
76Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
77
78function Write-ConsistencyLog {
79 param(
80 [Parameter(Mandatory = $true)]
81 [string]$Message,
82
83 [Parameter(Mandatory = $false)]
84 [ValidateSet('Info', 'Warning', 'Error', 'Success')]
85 [string]$Level = 'Info'
86 )
87
88 $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
89 $color = switch ($Level) {
90 'Info' { 'Cyan' }
91 'Warning' { 'Yellow' }
92 'Error' { 'Red' }
93 'Success' { 'Green' }
94 }
95
96 Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
97
98 # Surface warnings and errors as CI annotations so they appear in the Actions/ADO UI
99 if ($Level -eq 'Warning') {
100 Write-CIAnnotation -Message $Message -Level Warning
101 }
102 elseif ($Level -eq 'Error') {
103 Write-CIAnnotation -Message $Message -Level Error
104 }
105}
106
107function Get-ActionVersionViolations {
108 <#
109 .SYNOPSIS
110 Scans workflow files for version consistency violations.
111 #>
112 param(
113 [Parameter(Mandatory)]
114 [string]$WorkflowPath
115 )
116
117 # Enhanced regex to capture action, SHA, and optional version comment
118 $actionPattern = 'uses:\s*(?<action>[^@\s]+)@(?<ref>[a-fA-F0-9]{40})(?:\s*#\s*(?<version>.+))?'
119
120 $shaVersionMap = @{}
121 $violations = [System.Collections.ArrayList]::new()
122 $totalActions = 0
123
124 # Resolve to absolute path
125 $resolvedPath = Resolve-Path -Path $WorkflowPath -ErrorAction SilentlyContinue
126 if (-not $resolvedPath) {
127 Write-ConsistencyLog "Workflow path not found: $WorkflowPath" -Level Warning
128 return @{
129 Violations = @()
130 ShaVersionMap = @{}
131 TotalActions = 0
132 }
133 }
134
135 $workflowFiles = @(Get-ChildItem -Path $resolvedPath -Filter '*.yml' -Recurse -ErrorAction SilentlyContinue)
136 $workflowFiles += @(Get-ChildItem -Path $resolvedPath -Filter '*.yaml' -Recurse -ErrorAction SilentlyContinue)
137
138 foreach ($file in $workflowFiles) {
139 $lines = Get-Content -Path $file.FullName
140 $lineNumber = 0
141
142 foreach ($line in $lines) {
143 $lineNumber++
144
145 if ($line -match $actionPattern) {
146 $totalActions++
147 $action = $Matches['action']
148 $sha = $Matches['ref']
149 $version = if ($Matches['version']) { $Matches['version'].Trim() } else { $null }
150 $relativePath = [System.IO.Path]::GetRelativePath((Get-Location).Path, $file.FullName)
151
152 # Initialize SHA entry if not present
153 if (-not $shaVersionMap.ContainsKey($sha)) {
154 $shaVersionMap[$sha] = @{
155 Action = $action
156 Versions = [System.Collections.ArrayList]::new()
157 Sources = [System.Collections.ArrayList]::new()
158 }
159 }
160
161 # Track version and source
162 if ($version -and $version -notin $shaVersionMap[$sha].Versions) {
163 [void]$shaVersionMap[$sha].Versions.Add($version)
164 }
165 [void]$shaVersionMap[$sha].Sources.Add(@{
166 File = $relativePath
167 FullPath = $file.FullName
168 Line = $lineNumber
169 Version = $version
170 LineContent = $line.Trim()
171 })
172
173 # Detect missing version comment
174 if (-not $version) {
175 $violation = [DependencyViolation]::new()
176 $violation.File = $relativePath
177 $violation.Line = $lineNumber
178 $violation.Type = 'github-actions'
179 $violation.Name = $action
180 $violation.Version = $sha.Substring(0, 7)
181 $violation.Severity = 'Medium'
182 $violation.ViolationType = 'MissingVersionComment'
183 $violation.Description = 'SHA-pinned action missing version comment'
184 $violation.Remediation = "Add version comment: $action@$sha # vX.Y.Z"
185 $violation.Metadata = @{
186 FullSha = $sha
187 LineContent = $line.Trim()
188 }
189 [void]$violations.Add($violation)
190 }
191 }
192 }
193 }
194
195 # Detect version mismatches (same SHA, different version comments)
196 foreach ($sha in $shaVersionMap.Keys) {
197 $entry = $shaVersionMap[$sha]
198
199 if ($entry.Versions.Count -gt 1) {
200 # Report one violation per SHA with all affected locations in Metadata
201 $primarySource = $entry.Sources[0]
202 $allLocations = $entry.Sources | ForEach-Object { "$($_.File):$($_.Line)" }
203
204 $violation = [DependencyViolation]::new()
205 $violation.File = $primarySource.File
206 $violation.Line = $primarySource.Line
207 $violation.Type = 'github-actions'
208 $violation.Name = $entry.Action
209 $violation.Version = $sha.Substring(0, 7)
210 $violation.Severity = 'High'
211 $violation.ViolationType = 'VersionMismatch'
212 $violation.Description = "Same SHA has conflicting version comments across $($entry.Sources.Count) files: $($entry.Versions -join ' vs ')"
213 $violation.Remediation = 'Standardize version comment across all workflows'
214 $violation.Metadata = @{
215 FullSha = $sha
216 ConflictingVersions = $entry.Versions -join ', '
217 AffectedLocations = $allLocations
218 LineContent = $primarySource.LineContent
219 }
220 [void]$violations.Add($violation)
221 }
222 }
223
224 return @{
225 Violations = $violations
226 ShaVersionMap = $shaVersionMap
227 TotalActions = $totalActions
228 }
229}
230
231function Export-ConsistencyReport {
232 <#
233 .SYNOPSIS
234 Exports consistency report in the specified format.
235 #>
236 param(
237 [Parameter(Mandatory)]
238 [AllowEmptyCollection()]
239 [object[]]$Violations,
240
241 [Parameter(Mandatory)]
242 [string]$Format,
243
244 [Parameter()]
245 [string]$OutputPath,
246
247 [Parameter()]
248 [int]$TotalActions
249 )
250
251 $reportData = @{
252 Timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
253 TotalActions = $TotalActions
254 MismatchCount = @($Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' }).Count
255 MissingComments = @($Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }).Count
256 Violations = $Violations
257 }
258
259 switch ($Format) {
260 'Table' {
261 if ($Violations.Count -eq 0) {
262 Write-ConsistencyLog 'No version consistency violations found.' -Level Success
263 }
264 else {
265 $Violations | Format-Table -Property @(
266 @{ Label = 'File'; Expression = { $_.File } }
267 @{ Label = 'Line'; Expression = { $_.Line } }
268 @{ Label = 'Type'; Expression = { $_.ViolationType } }
269 @{ Label = 'Action'; Expression = { $_.Name } }
270 @{ Label = 'Severity'; Expression = { $_.Severity } }
271 @{ Label = 'Description'; Expression = { $_.Description } }
272 ) -AutoSize -Wrap
273 }
274
275 if ($OutputPath) {
276 $Violations | Format-Table -Property File, Line, ViolationType, Name, Severity, Description -AutoSize |
277 Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
278 }
279 }
280
281 'Json' {
282 $json = $reportData | ConvertTo-Json -Depth 10
283
284 if ($OutputPath) {
285 $json | Out-File -FilePath $OutputPath -Encoding UTF8
286 Write-ConsistencyLog "Report exported to: $OutputPath" -Level Success
287 }
288 else {
289 Write-Output $json
290 }
291 }
292
293 'Sarif' {
294 $sarif = @{
295 version = '2.1.0'
296 '$schema' = 'https://json.schemastore.org/sarif-2.1.0.json'
297 runs = @(@{
298 tool = @{
299 driver = @{
300 name = 'action-version-consistency'
301 version = '1.0.0'
302 informationUri = 'https://github.com/microsoft/hve-core'
303 rules = @(
304 @{
305 id = 'version-mismatch'
306 name = 'VersionMismatch'
307 shortDescription = @{ text = 'Same SHA has conflicting version comments' }
308 defaultConfiguration = @{ level = 'error' }
309 }
310 @{
311 id = 'missing-version-comment'
312 name = 'MissingVersionComment'
313 shortDescription = @{ text = 'SHA-pinned action missing version comment' }
314 defaultConfiguration = @{ level = 'warning' }
315 }
316 )
317 }
318 }
319 results = @($Violations | ForEach-Object {
320 $ruleId = switch ($_.ViolationType) {
321 'VersionMismatch' { 'version-mismatch' }
322 'MissingVersionComment' { 'missing-version-comment' }
323 default { 'unknown' }
324 }
325 $level = switch ($_.Severity) {
326 'High' { 'error' }
327 'Medium' { 'warning' }
328 default { 'note' }
329 }
330 @{
331 ruleId = $ruleId
332 level = $level
333 message = @{ text = $_.Description }
334 locations = @(@{
335 physicalLocation = @{
336 artifactLocation = @{ uri = $_.File }
337 region = @{ startLine = $_.Line }
338 }
339 })
340 properties = @{
341 actionName = $_.Name
342 sha = $_.Version
343 remediation = $_.Remediation
344 }
345 }
346 })
347 })
348 }
349
350 $json = $sarif | ConvertTo-Json -Depth 15
351
352 if ($OutputPath) {
353 $json | Out-File -FilePath $OutputPath -Encoding UTF8
354 Write-ConsistencyLog "SARIF report exported to: $OutputPath" -Level Success
355 }
356 else {
357 Write-Output $json
358 }
359 }
360 }
361}
362
363function Invoke-ActionVersionConsistency {
364 <#
365 .SYNOPSIS
366 Orchestrates the version consistency analysis.
367 #>
368 [OutputType([int])]
369 [CmdletBinding()]
370 param(
371 [Parameter(Mandatory = $false)]
372 [string]$Path = '.github/workflows',
373
374 [Parameter(Mandatory = $false)]
375 [ValidateSet('Table', 'Json', 'Sarif')]
376 [string]$Format = 'Table',
377
378 [Parameter(Mandatory = $false)]
379 [string]$OutputPath,
380
381 [Parameter(Mandatory = $false)]
382 [switch]$FailOnMismatch,
383
384 [Parameter(Mandatory = $false)]
385 [switch]$FailOnMissingComment
386 )
387
388 Write-ConsistencyLog 'Starting GitHub Actions version consistency analysis...' -Level Info
389 Write-ConsistencyLog "Scanning path: $Path" -Level Info
390
391 # Scan for violations
392 $result = Get-ActionVersionViolations -WorkflowPath $Path
393
394 $violations = $result.Violations
395 $mismatchCount = @($violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' }).Count
396 $missingCount = @($violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }).Count
397
398 Write-ConsistencyLog "Scanned $($result.TotalActions) SHA-pinned actions" -Level Info
399 Write-ConsistencyLog "Found $mismatchCount version mismatches" -Level $(if ($mismatchCount -gt 0) { 'Warning' } else { 'Info' })
400 Write-ConsistencyLog "Found $missingCount missing version comments" -Level $(if ($missingCount -gt 0) { 'Warning' } else { 'Info' })
401
402 # Emit CI annotations per violation
403 foreach ($violation in $violations) {
404 $annotationLevel = switch ($violation.Severity) {
405 'High' { 'Error' }
406 'Medium' { 'Warning' }
407 default { 'Notice' }
408 }
409 Write-CIAnnotation `
410 -Message "$($violation.ViolationType): $($violation.Description)" `
411 -Level $annotationLevel `
412 -File $violation.File `
413 -Line $violation.Line
414 }
415
416 # Export report (pipe to Out-Host to prevent pipeline pollution of return value)
417 Export-ConsistencyReport -Violations $violations -Format $Format -OutputPath $OutputPath -TotalActions $result.TotalActions | Out-Host
418
419 # Emit CI step summary
420 if ($violations.Count -eq 0) {
421 Write-CIStepSummary -Content @"
422## Action Version Consistency
423
424:white_check_mark: **Status**: Passed
425
426All $($result.TotalActions) SHA-pinned actions have consistent version comments.
427"@
428 }
429 else {
430 $summaryLines = [System.Collections.ArrayList]::new()
431 [void]$summaryLines.Add(@"
432## Action Version Consistency
433
434:x: **Status**: Failed
435
436| Metric | Count |
437|--------|-------|
438| SHA-Pinned Actions | $($result.TotalActions) |
439| Version Mismatches | $mismatchCount |
440| Missing Comments | $missingCount |
441
442### Violations
443
444| File | Line | Type | Action | Severity | Description |
445|------|------|------|--------|----------|-------------|
446"@)
447 foreach ($v in $violations) {
448 [void]$summaryLines.Add("| ``$($v.File)`` | $($v.Line) | $($v.ViolationType) | ``$($v.Name)`` | $($v.Severity) | $($v.Description) |")
449 }
450
451 Write-CIStepSummary -Content ($summaryLines -join "`n")
452 }
453
454 # Determine exit code
455 $exitCode = 0
456
457 if ($FailOnMismatch -and $mismatchCount -gt 0) {
458 Write-ConsistencyLog "Failing due to $mismatchCount version mismatch(es) (-FailOnMismatch enabled)" -Level Error
459 $exitCode = 1
460 }
461
462 if ($FailOnMissingComment -and $missingCount -gt 0) {
463 Write-ConsistencyLog "Failing due to $missingCount missing version comment(s) (-FailOnMissingComment enabled)" -Level Error
464 $exitCode = 1
465 }
466
467 if ($exitCode -eq 0 -and $violations.Count -eq 0) {
468 Write-ConsistencyLog 'All SHA-pinned actions have consistent version comments!' -Level Success
469 }
470
471 return $exitCode
472}
473
474#region Main Execution
475if ($MyInvocation.InvocationName -ne '.') {
476 try {
477 $exitCode = Invoke-ActionVersionConsistency @PSBoundParameters
478 exit $exitCode
479 }
480 catch {
481 Write-Error -ErrorAction Continue "Test-ActionVersionConsistency failed: $($_.Exception.Message)"
482 Write-CIAnnotation -Message $_.Exception.Message -Level Error
483 exit 1
484 }
485}
486#endregion Main Execution
487