Skip to content

Commit bd7d512

Browse files
feat(scripts): add per-violation CI annotations and colorized console output (#637)
## Description Add per-violation `Write-CIAnnotation` calls and colorized `Write-Host` output to both `Test-DependencyPinning.ps1` and `Test-ActionVersionConsistency.ps1`. Previously, CI annotations fired only on fatal errors (catch block) and console output used plain `Write-Output`. Contributors now see inline PR annotations per violation and colored terminal output grouped by file, with severity-based icons and levels. Also adds `Write-CIStepSummary` to `Test-ActionVersionConsistency.ps1` with a markdown violations table, and removes the redundant `::warning` annotation loop from `dependency-pinning-scan.yml` since the script now handles annotations natively. Closes #632 ## Related Issue(s) - Closes #632 ## Type of Change ### Code & Documentation - [x] New feature (non-breaking change that adds functionality) - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement ### Infrastructure & Configuration - [ ] CI/CD pipeline changes - [x] GitHub Actions workflow changes - [ ] Build system changes - [x] Security configuration - [x] Script/automation changes ### AI Artifacts - [ ] Instructions file (.instructions.md) - [ ] Prompt template (.prompt.md) - [ ] Agent definition (.agent.md) - [ ] Skill package (SKILL.md) - [ ] Copilot configuration (copilot-instructions.md) ### Other - [ ] Refactoring (no functional changes) - [ ] Code style/formatting - [ ] Test coverage improvements - [ ] Other (please describe): ## Testing - **Test-DependencyPinning.Tests.ps1**: 68/68 tests passing, 80.67% code coverage - Added 7 tests for CI annotations per violation (severity mapping, file/line inclusion, message content, multi-violation) - Added 3 tests for Write-PinningLog CI annotation forwarding (Warning, Error, Info-excluded) - Added 2 tests for per-violation console output (colored output, success message) - **Test-ActionVersionConsistency.Tests.ps1**: 62/62 tests passing, 94.62% code coverage - Added 3 tests for Write-ConsistencyLog CI annotation forwarding - Added 5 tests for CI annotations per violation - Added 8 tests for CI step summary (pass/fail status, violation counts, table headers, mixed violations) ## Checklist ### Required Checks - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings or errors - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes ### AI Artifact Contributions - [ ] Frontmatter follows conventions in contributing guide - [ ] applyTo patterns are specific and accurate - [ ] No secrets, tokens, or sensitive data in artifact content - [ ] Tested with GitHub Copilot to verify behavior ### Required Automated Checks - [x] Linting passes (`npm run lint:all`) - [x] Tests pass (`npm run test:ps`) ## Security Considerations - [x] No sensitive information (API keys, passwords, tokens) included - [ ] Security-related changes have been reviewed for vulnerabilities - [ ] Dependencies have been checked for known vulnerabilities ## Additional Notes ### Changes by file | File | Summary | |------|---------| | `scripts/security/Test-DependencyPinning.ps1` | Converted `Write-PinningLog` from `Write-Output` to `Write-Host` with color mapping (Info→Cyan, Warning→Yellow, Error→Red, Success→Green). Added CI annotation forwarding for Warning/Error levels. Added per-violation loop emitting `Write-CIAnnotation` with severity mapping (High→Error, Medium→Warning, Low→Notice) and colorized `Write-Host` output grouped by file with severity icons. | | `scripts/security/Test-ActionVersionConsistency.ps1` | Added CI annotation forwarding to `Write-ConsistencyLog` for Warning/Error levels. Added per-violation `Write-CIAnnotation` loop with severity mapping. Added `Write-CIStepSummary` with pass/fail status and markdown violations table. | | `.github/workflows/dependency-pinning-scan.yml` | Removed redundant `::warning` annotation loop (26 lines including `ConvertTo-GHAEscaped` function) since `Test-DependencyPinning.ps1` now handles annotations natively via `Write-CIAnnotation`. | | `scripts/tests/security/Test-DependencyPinning.Tests.ps1` | Added 12 Pester tests across 3 new contexts for CI annotations per violation, Write-PinningLog CI annotation forwarding, and per-violation console output. | | `scripts/tests/security/Test-ActionVersionConsistency.Tests.ps1` | Added 16 Pester tests across 3 new contexts for Write-ConsistencyLog CI annotation forwarding, CI annotations per violation, and CI step summary content. | 🔧 - Generated by Copilot
1 parent 5fa6328 commit bd7d512

5 files changed

Lines changed: 549 additions & 51 deletions

File tree

.github/workflows/dependency-pinning-scan.yml

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,32 +121,6 @@ jobs:
121121
Write-Host "Compliance Score: $complianceScore%"
122122
Write-Host "Unpinned Dependencies: $unpinnedCount"
123123
Write-Host "Is Compliant (>=$threshold%): $isCompliant"
124-
125-
# Fire GitHub Actions warnings for each violation
126-
if ($unpinnedCount -gt 0) {
127-
# Escape function to prevent workflow command injection
128-
function ConvertTo-GHAEscaped($Value, [switch]$ForProperty) {
129-
if ([string]::IsNullOrEmpty($Value)) { return $Value }
130-
$escaped = $Value -replace '%', '%25'
131-
$escaped = $escaped -replace "`r", '%0D'
132-
$escaped = $escaped -replace "`n", '%0A'
133-
$escaped = $escaped -replace '::', '%3A%3A'
134-
if ($ForProperty) {
135-
$escaped = $escaped -replace ':', '%3A'
136-
$escaped = $escaped -replace ',', '%2C'
137-
}
138-
return $escaped
139-
}
140-
141-
foreach ($violation in $report.Violations) {
142-
$escapedFile = ConvertTo-GHAEscaped $violation.File -ForProperty
143-
$escapedName = ConvertTo-GHAEscaped $violation.Name
144-
$escapedVersion = ConvertTo-GHAEscaped $violation.Version
145-
$escapedType = ConvertTo-GHAEscaped $violation.Type
146-
$escapedSeverity = ConvertTo-GHAEscaped $violation.Severity
147-
Write-Output "::warning file=$escapedFile,line=$($violation.Line)::Unpinned $escapedType dependency: $escapedName@$escapedVersion (Severity: $escapedSeverity)"
148-
}
149-
}
150124
}
151125
else {
152126
Write-Error "Failed to generate dependency pinning report"

scripts/security/Test-ActionVersionConsistency.ps1

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ function Write-ConsistencyLog {
9494
}
9595

9696
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+
}
97105
}
98106

99107
function Get-ActionVersionViolations {
@@ -391,9 +399,58 @@ function Invoke-ActionVersionConsistency {
391399
Write-ConsistencyLog "Found $mismatchCount version mismatches" -Level $(if ($mismatchCount -gt 0) { 'Warning' } else { 'Info' })
392400
Write-ConsistencyLog "Found $missingCount missing version comments" -Level $(if ($missingCount -gt 0) { 'Warning' } else { 'Info' })
393401

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+
394416
# Export report (pipe to Out-Host to prevent pipeline pollution of return value)
395417
Export-ConsistencyReport -Violations $violations -Format $Format -OutputPath $OutputPath -TotalActions $result.TotalActions | Out-Host
396418

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+
426+
All $($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+
397454
# Determine exit code
398455
$exitCode = 0
399456

scripts/security/Test-DependencyPinning.ps1

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,21 @@ function Write-PinningLog {
319319
)
320320

321321
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
322-
Write-Output "[$timestamp] [$Level] $Message"
322+
$color = switch ($Level) {
323+
'Info' { 'Cyan' }
324+
'Warning' { 'Yellow' }
325+
'Error' { 'Red' }
326+
'Success' { 'Green' }
327+
}
328+
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
329+
330+
# Surface warnings and errors as CI annotations so they appear in the Actions/ADO UI
331+
if ($Level -eq 'Warning') {
332+
Write-CIAnnotation -Message $Message -Level Warning
333+
}
334+
elseif ($Level -eq 'Error') {
335+
Write-CIAnnotation -Message $Message -Level Error
336+
}
323337
}
324338

325339
function Get-FilesToScan {
@@ -860,32 +874,35 @@ function Invoke-DependencyPinningAnalysis {
860874
$allViolations += $violations
861875
}
862876

863-
# Per-violation CI output — grouped by file
864-
if (@($allViolations).Count -gt 0) {
865-
Write-Host "`n❌ Found $(@($allViolations).Count) unpinned dependencies:" -ForegroundColor Red
877+
Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
878+
879+
# Emit per-violation CI annotations and console output
880+
if ($allViolations.Count -gt 0) {
881+
Write-Host "`n❌ Found $($allViolations.Count) unpinned dependencies:" -ForegroundColor Red
866882
$groupedByFile = $allViolations | Group-Object -Property File
867883
foreach ($fileGroup in $groupedByFile) {
868884
Write-Host "`n📄 $($fileGroup.Name)" -ForegroundColor Cyan
869885
foreach ($dep in $fileGroup.Group) {
870-
$displayVersion = if ($dep.Version) { $dep.Version } elseif ($dep.CurrentRef) { $dep.CurrentRef } else { '<unknown>' }
871-
Write-Host " ⚠️ Line $($dep.Line): $($dep.Name)$displayVersion (type: $($dep.Type))" -ForegroundColor Yellow
872-
873-
# Normalize file path for CI annotations: prefer absolute path based on scan root
874-
$annotationFile = $dep.File
875-
try {
876-
if ($Path) {
877-
$resolved = Resolve-Path -LiteralPath (Join-Path -Path $Path -ChildPath $dep.File) -ErrorAction Stop
878-
$annotationFile = $resolved.Path
879-
}
886+
$annotationLevel = switch ($dep.Severity) {
887+
'High' { 'Error' }
888+
'Medium' { 'Warning' }
889+
default { 'Notice' }
880890
}
881-
catch {
882-
$annotationFile = $dep.File
891+
$icon = switch ($dep.Severity) {
892+
'High' { '' }
893+
'Medium' { '⚠️' }
894+
default { 'ℹ️' }
883895
}
884-
896+
$color = switch ($dep.Severity) {
897+
'High' { 'Red' }
898+
'Medium' { 'Yellow' }
899+
default { 'Cyan' }
900+
}
901+
Write-Host " $icon [$($dep.Severity)] $($dep.Name)@$($dep.Version): $($dep.Description) (Line $($dep.Line))" -ForegroundColor $color
885902
Write-CIAnnotation `
886-
-Message "Unpinned $($dep.Type) dependency: $($dep.Name)@$($dep.CurrentRef)" `
887-
-Level Warning `
888-
-File $annotationFile `
903+
-Message "[$($dep.ViolationType)] $($dep.Name): $($dep.Description)" `
904+
-Level $annotationLevel `
905+
-File $dep.File `
889906
-Line $dep.Line
890907
}
891908
}
@@ -894,8 +911,6 @@ function Invoke-DependencyPinningAnalysis {
894911
Write-Host "`n✅ All dependencies are properly SHA-pinned." -ForegroundColor Green
895912
}
896913

897-
Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
898-
899914
# Generate compliance report
900915
$report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
901916

0 commit comments

Comments
 (0)