microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/621-ai-artifacts

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-ActionVersionConsistency.ps1

429lines · 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
99function Get-ActionVersionViolations {
100 <#
101 .SYNOPSIS
102 Scans workflow files for version consistency violations.
103 #>
104 param(
105 [Parameter(Mandatory)]
106 [string]$WorkflowPath
107 )
108
109 # Enhanced regex to capture action, SHA, and optional version comment
110 $actionPattern = 'uses:\s*(?<action>[^@\s]+)@(?<ref>[a-fA-F0-9]{40})(?:\s*#\s*(?<version>.+))?'
111
112 $shaVersionMap = @{}
113 $violations = [System.Collections.ArrayList]::new()
114 $totalActions = 0
115
116 # Resolve to absolute path
117 $resolvedPath = Resolve-Path -Path $WorkflowPath -ErrorAction SilentlyContinue
118 if (-not $resolvedPath) {
119 Write-ConsistencyLog "Workflow path not found: $WorkflowPath" -Level Warning
120 return @{
121 Violations = @()
122 ShaVersionMap = @{}
123 TotalActions = 0
124 }
125 }
126
127 $workflowFiles = @(Get-ChildItem -Path $resolvedPath -Filter '*.yml' -Recurse -ErrorAction SilentlyContinue)
128 $workflowFiles += @(Get-ChildItem -Path $resolvedPath -Filter '*.yaml' -Recurse -ErrorAction SilentlyContinue)
129
130 foreach ($file in $workflowFiles) {
131 $lines = Get-Content -Path $file.FullName
132 $lineNumber = 0
133
134 foreach ($line in $lines) {
135 $lineNumber++
136
137 if ($line -match $actionPattern) {
138 $totalActions++
139 $action = $Matches['action']
140 $sha = $Matches['ref']
141 $version = if ($Matches['version']) { $Matches['version'].Trim() } else { $null }
142 $relativePath = [System.IO.Path]::GetRelativePath((Get-Location).Path, $file.FullName)
143
144 # Initialize SHA entry if not present
145 if (-not $shaVersionMap.ContainsKey($sha)) {
146 $shaVersionMap[$sha] = @{
147 Action = $action
148 Versions = [System.Collections.ArrayList]::new()
149 Sources = [System.Collections.ArrayList]::new()
150 }
151 }
152
153 # Track version and source
154 if ($version -and $version -notin $shaVersionMap[$sha].Versions) {
155 [void]$shaVersionMap[$sha].Versions.Add($version)
156 }
157 [void]$shaVersionMap[$sha].Sources.Add(@{
158 File = $relativePath
159 FullPath = $file.FullName
160 Line = $lineNumber
161 Version = $version
162 LineContent = $line.Trim()
163 })
164
165 # Detect missing version comment
166 if (-not $version) {
167 $violation = [DependencyViolation]::new()
168 $violation.File = $relativePath
169 $violation.Line = $lineNumber
170 $violation.Type = 'github-actions'
171 $violation.Name = $action
172 $violation.Version = $sha.Substring(0, 7)
173 $violation.Severity = 'Medium'
174 $violation.ViolationType = 'MissingVersionComment'
175 $violation.Description = 'SHA-pinned action missing version comment'
176 $violation.Remediation = "Add version comment: $action@$sha # vX.Y.Z"
177 $violation.Metadata = @{
178 FullSha = $sha
179 LineContent = $line.Trim()
180 }
181 [void]$violations.Add($violation)
182 }
183 }
184 }
185 }
186
187 # Detect version mismatches (same SHA, different version comments)
188 foreach ($sha in $shaVersionMap.Keys) {
189 $entry = $shaVersionMap[$sha]
190
191 if ($entry.Versions.Count -gt 1) {
192 # Report one violation per SHA with all affected locations in Metadata
193 $primarySource = $entry.Sources[0]
194 $allLocations = $entry.Sources | ForEach-Object { "$($_.File):$($_.Line)" }
195
196 $violation = [DependencyViolation]::new()
197 $violation.File = $primarySource.File
198 $violation.Line = $primarySource.Line
199 $violation.Type = 'github-actions'
200 $violation.Name = $entry.Action
201 $violation.Version = $sha.Substring(0, 7)
202 $violation.Severity = 'High'
203 $violation.ViolationType = 'VersionMismatch'
204 $violation.Description = "Same SHA has conflicting version comments across $($entry.Sources.Count) files: $($entry.Versions -join ' vs ')"
205 $violation.Remediation = 'Standardize version comment across all workflows'
206 $violation.Metadata = @{
207 FullSha = $sha
208 ConflictingVersions = $entry.Versions -join ', '
209 AffectedLocations = $allLocations
210 LineContent = $primarySource.LineContent
211 }
212 [void]$violations.Add($violation)
213 }
214 }
215
216 return @{
217 Violations = $violations
218 ShaVersionMap = $shaVersionMap
219 TotalActions = $totalActions
220 }
221}
222
223function Export-ConsistencyReport {
224 <#
225 .SYNOPSIS
226 Exports consistency report in the specified format.
227 #>
228 param(
229 [Parameter(Mandatory)]
230 [AllowEmptyCollection()]
231 [object[]]$Violations,
232
233 [Parameter(Mandatory)]
234 [string]$Format,
235
236 [Parameter()]
237 [string]$OutputPath,
238
239 [Parameter()]
240 [int]$TotalActions
241 )
242
243 $reportData = @{
244 Timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
245 TotalActions = $TotalActions
246 MismatchCount = @($Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' }).Count
247 MissingComments = @($Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }).Count
248 Violations = $Violations
249 }
250
251 switch ($Format) {
252 'Table' {
253 if ($Violations.Count -eq 0) {
254 Write-ConsistencyLog 'No version consistency violations found.' -Level Success
255 }
256 else {
257 $Violations | Format-Table -Property @(
258 @{ Label = 'File'; Expression = { $_.File } }
259 @{ Label = 'Line'; Expression = { $_.Line } }
260 @{ Label = 'Type'; Expression = { $_.ViolationType } }
261 @{ Label = 'Action'; Expression = { $_.Name } }
262 @{ Label = 'Severity'; Expression = { $_.Severity } }
263 @{ Label = 'Description'; Expression = { $_.Description } }
264 ) -AutoSize -Wrap
265 }
266
267 if ($OutputPath) {
268 $Violations | Format-Table -Property File, Line, ViolationType, Name, Severity, Description -AutoSize |
269 Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
270 }
271 }
272
273 'Json' {
274 $json = $reportData | ConvertTo-Json -Depth 10
275
276 if ($OutputPath) {
277 $json | Out-File -FilePath $OutputPath -Encoding UTF8
278 Write-ConsistencyLog "Report exported to: $OutputPath" -Level Success
279 }
280 else {
281 Write-Output $json
282 }
283 }
284
285 'Sarif' {
286 $sarif = @{
287 version = '2.1.0'
288 '$schema' = 'https://json.schemastore.org/sarif-2.1.0.json'
289 runs = @(@{
290 tool = @{
291 driver = @{
292 name = 'action-version-consistency'
293 version = '1.0.0'
294 informationUri = 'https://github.com/microsoft/hve-core'
295 rules = @(
296 @{
297 id = 'version-mismatch'
298 name = 'VersionMismatch'
299 shortDescription = @{ text = 'Same SHA has conflicting version comments' }
300 defaultConfiguration = @{ level = 'error' }
301 }
302 @{
303 id = 'missing-version-comment'
304 name = 'MissingVersionComment'
305 shortDescription = @{ text = 'SHA-pinned action missing version comment' }
306 defaultConfiguration = @{ level = 'warning' }
307 }
308 )
309 }
310 }
311 results = @($Violations | ForEach-Object {
312 $ruleId = switch ($_.ViolationType) {
313 'VersionMismatch' { 'version-mismatch' }
314 'MissingVersionComment' { 'missing-version-comment' }
315 default { 'unknown' }
316 }
317 $level = switch ($_.Severity) {
318 'High' { 'error' }
319 'Medium' { 'warning' }
320 default { 'note' }
321 }
322 @{
323 ruleId = $ruleId
324 level = $level
325 message = @{ text = $_.Description }
326 locations = @(@{
327 physicalLocation = @{
328 artifactLocation = @{ uri = $_.File }
329 region = @{ startLine = $_.Line }
330 }
331 })
332 properties = @{
333 actionName = $_.Name
334 sha = $_.Version
335 remediation = $_.Remediation
336 }
337 }
338 })
339 })
340 }
341
342 $json = $sarif | ConvertTo-Json -Depth 15
343
344 if ($OutputPath) {
345 $json | Out-File -FilePath $OutputPath -Encoding UTF8
346 Write-ConsistencyLog "SARIF report exported to: $OutputPath" -Level Success
347 }
348 else {
349 Write-Output $json
350 }
351 }
352 }
353}
354
355function Invoke-ActionVersionConsistency {
356 <#
357 .SYNOPSIS
358 Orchestrates the version consistency analysis.
359 #>
360 [OutputType([int])]
361 [CmdletBinding()]
362 param(
363 [Parameter(Mandatory = $false)]
364 [string]$Path = '.github/workflows',
365
366 [Parameter(Mandatory = $false)]
367 [ValidateSet('Table', 'Json', 'Sarif')]
368 [string]$Format = 'Table',
369
370 [Parameter(Mandatory = $false)]
371 [string]$OutputPath,
372
373 [Parameter(Mandatory = $false)]
374 [switch]$FailOnMismatch,
375
376 [Parameter(Mandatory = $false)]
377 [switch]$FailOnMissingComment
378 )
379
380 Write-ConsistencyLog 'Starting GitHub Actions version consistency analysis...' -Level Info
381 Write-ConsistencyLog "Scanning path: $Path" -Level Info
382
383 # Scan for violations
384 $result = Get-ActionVersionViolations -WorkflowPath $Path
385
386 $violations = $result.Violations
387 $mismatchCount = @($violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' }).Count
388 $missingCount = @($violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }).Count
389
390 Write-ConsistencyLog "Scanned $($result.TotalActions) SHA-pinned actions" -Level Info
391 Write-ConsistencyLog "Found $mismatchCount version mismatches" -Level $(if ($mismatchCount -gt 0) { 'Warning' } else { 'Info' })
392 Write-ConsistencyLog "Found $missingCount missing version comments" -Level $(if ($missingCount -gt 0) { 'Warning' } else { 'Info' })
393
394 # Export report (pipe to Out-Host to prevent pipeline pollution of return value)
395 Export-ConsistencyReport -Violations $violations -Format $Format -OutputPath $OutputPath -TotalActions $result.TotalActions | Out-Host
396
397 # Determine exit code
398 $exitCode = 0
399
400 if ($FailOnMismatch -and $mismatchCount -gt 0) {
401 Write-ConsistencyLog "Failing due to $mismatchCount version mismatch(es) (-FailOnMismatch enabled)" -Level Error
402 $exitCode = 1
403 }
404
405 if ($FailOnMissingComment -and $missingCount -gt 0) {
406 Write-ConsistencyLog "Failing due to $missingCount missing version comment(s) (-FailOnMissingComment enabled)" -Level Error
407 $exitCode = 1
408 }
409
410 if ($exitCode -eq 0 -and $violations.Count -eq 0) {
411 Write-ConsistencyLog 'All SHA-pinned actions have consistent version comments!' -Level Success
412 }
413
414 return $exitCode
415}
416
417#region Main Execution
418if ($MyInvocation.InvocationName -ne '.') {
419 try {
420 $exitCode = Invoke-ActionVersionConsistency @PSBoundParameters
421 exit $exitCode
422 }
423 catch {
424 Write-Error -ErrorAction Continue "Test-ActionVersionConsistency failed: $($_.Exception.Message)"
425 Write-CIAnnotation -Message $_.Exception.Message -Level Error
426 exit 1
427 }
428}
429#endregion Main Execution