microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.2.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-ActionVersionConsistency.ps1

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