Skip to content

Commit 9d3b71d

Browse files
fix(scripts): add per-violation Write-Host and Write-CIAnnotation output to Test-DependencyPinning (#640)
# Pull Request ## Description - add grouped-by-file Write-Host output with per-violation detail lines - add Write-CIAnnotation Warning call per unpinned dependency with file and line - add global Pester mocks for Write-Host, Write-CIAnnotation, and Write-CIStepSummary - add 6 test assertions covering violation and clean-scan CI output paths ## Related Issue(s) Fixes #631 ## Type of Change Select all that apply: **Code & Documentation:** - [x] Bug fix (non-breaking change fixing an issue) - [ ] New feature (non-breaking change adding functionality) - [ ] Breaking change (fix or feature causing existing functionality to change) - [ ] Documentation update **Infrastructure & Configuration:** - [ ] GitHub Actions workflow - [ ] Linting configuration (markdown, PowerShell, etc.) - [ ] Security configuration - [ ] DevContainer configuration - [ ] Dependency update **Other:** - [x] Script/automation (`.ps1`, `.sh`, `.py`) - [ ] Other (please describe): ## Testing - npm run lint:ps — All 49 PowerShell files pass PSScriptAnalyzer checks - npm run test:ps — 62/62 dependency pinning tests pass, 0 failures, 0 regressions - Patch coverage: 100% (9/9 executable lines), exceeds 80% gate ## Checklist ### Required Checks - [x] Documentation is updated (if applicable) - [x] Files follow existing naming conventions - [x] Changes are backwards compatible (if applicable) - [x] Tests added for new functionality (if applicable) ### Required Automated Checks The following validation commands must pass before merging: - [x] Markdown linting: `npm run lint:md` - [x] Spell checking: `npm run spell-check` - [x] Frontmatter validation: `npm run lint:frontmatter` - [x] Link validation: `npm run lint:md-links` - [x] PowerShell analysis: `npm run lint:ps` ## Security Considerations <!-- ⚠️ WARNING: Do not commit sensitive information such as API keys, passwords, or personal data --> - [x] This PR does not contain any sensitive or NDA information - [x] Any new dependencies have been reviewed for security issues - [x] Security-related scripts follow the principle of least privilege ## Additional Notes - Annotation placement order The reference pattern in [Invoke-PSScriptAnalyzer.ps1](vscode-file://vscode-app/c:/Program%20Files/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) emits Write-CIAnnotation before Write-Host for each violation. This implementation reverses that order (Write-Host first, then Write-CIAnnotation). Both execute synchronously within the same loop iteration so there is no functional impact. A follow-up can swap the order for cross-script consistency. - Workflow annotation deduplication Dependency-pinning-scan.yml (lines 116–136) iterates violations from the JSON report and emits ::warning annotations. The script now also emits Write-CIAnnotation -Level Warning per violation, producing duplicate annotations with different message formats. This duplication is accepted per issue #631 scope. A follow-up issue can remove the workflow's inline ::warning loop or gate it on a script output flag. --------- Co-authored-by: Copilot <[email protected]>
1 parent ae3a35d commit 9d3b71d

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

scripts/security/Test-DependencyPinning.ps1

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,40 @@ function Invoke-DependencyPinningAnalysis {
860860
$allViolations += $violations
861861
}
862862

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
866+
$groupedByFile = $allViolations | Group-Object -Property File
867+
foreach ($fileGroup in $groupedByFile) {
868+
Write-Host "`n📄 $($fileGroup.Name)" -ForegroundColor Cyan
869+
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+
}
880+
}
881+
catch {
882+
$annotationFile = $dep.File
883+
}
884+
885+
Write-CIAnnotation `
886+
-Message "Unpinned $($dep.Type) dependency: $($dep.Name)@$($dep.CurrentRef)" `
887+
-Level Warning `
888+
-File $annotationFile `
889+
-Line $dep.Line
890+
}
891+
}
892+
}
893+
else {
894+
Write-Host "`n✅ All dependencies are properly SHA-pinned." -ForegroundColor Green
895+
}
896+
863897
Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
864898

865899
# Generate compliance report

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

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Requires -Modules Pester
1+
#Requires -Modules Pester
22
# Copyright (c) Microsoft Corporation.
33
# SPDX-License-Identifier: MIT
44

@@ -11,6 +11,11 @@ BeforeAll {
1111
# Fixture paths
1212
$script:FixturesPath = Join-Path $PSScriptRoot '../Fixtures/Workflows'
1313
$script:SecurityFixturesPath = Join-Path $PSScriptRoot '../Fixtures/Security'
14+
15+
# CI helper mocks — suppress console output and enable assertions
16+
Mock Write-Host {}
17+
Mock Write-CIAnnotation {}
18+
Mock Write-CIStepSummary {}
1419
}
1520

1621
Describe 'Test-SHAPinning' -Tag 'Unit' {
@@ -793,6 +798,20 @@ Describe 'Invoke-DependencyPinningAnalysis' -Tag 'Unit' {
793798
It 'Logs success message without throwing' {
794799
{ Invoke-DependencyPinningAnalysis -Path TestDrive: } | Should -Not -Throw
795800
}
801+
802+
It 'emits success Write-Host message when no violations' {
803+
Invoke-DependencyPinningAnalysis -Path TestDrive:
804+
Should -Invoke Write-Host -ParameterFilter {
805+
$Object -like '*✅*' -and $Object -like '*SHA-pinned*'
806+
}
807+
}
808+
809+
It 'does not emit Write-CIAnnotation warnings when no violations' {
810+
Invoke-DependencyPinningAnalysis -Path TestDrive:
811+
Should -Not -Invoke Write-CIAnnotation -ParameterFilter {
812+
$Level -eq 'Warning'
813+
}
814+
}
796815
}
797816

798817
Context 'Violations below threshold with -FailOnUnpinned' {
@@ -824,6 +843,58 @@ Describe 'Invoke-DependencyPinningAnalysis' -Tag 'Unit' {
824843
}
825844
}
826845

846+
Context 'CI output for violations in soft-fail mode' {
847+
BeforeAll {
848+
Mock Get-FilesToScan {
849+
return @(@{ Path = 'TestDrive:\f.yml'; Type = 'github-actions'; RelativePath = 'f.yml' })
850+
}
851+
Mock Get-DependencyViolation {
852+
$v = [DependencyViolation]::new('f.yml', 1, 'github-actions', 'a/b', 'High', 'Not pinned')
853+
$v.CurrentRef = 'v4'
854+
return @($v)
855+
}
856+
Mock Get-RemediationSuggestion { return 'pin it' }
857+
Mock Get-ComplianceReportData {
858+
return @{
859+
ComplianceScore = 50.0
860+
TotalDependencies = 2
861+
UnpinnedDependencies = 1
862+
Violations = @()
863+
}
864+
}
865+
Mock Export-ComplianceReport {}
866+
Mock Export-CICDArtifact {}
867+
}
868+
869+
It 'emits summary header with violation count' {
870+
Invoke-DependencyPinningAnalysis -Path TestDrive: -Threshold 80
871+
Should -Invoke Write-Host -ParameterFilter {
872+
$Object -like '*unpinned*'
873+
}
874+
}
875+
876+
It 'emits file header with file icon' {
877+
Invoke-DependencyPinningAnalysis -Path TestDrive: -Threshold 80
878+
Should -Invoke Write-Host -ParameterFilter {
879+
$Object -like '*📄*'
880+
}
881+
}
882+
883+
It 'emits per-violation detail line' {
884+
Invoke-DependencyPinningAnalysis -Path TestDrive: -Threshold 80
885+
Should -Invoke Write-Host -ParameterFilter {
886+
$Object -like '*⚠️*' -and $Object -like '*a/b*'
887+
}
888+
}
889+
890+
It 'emits Write-CIAnnotation with Warning level per violation' {
891+
Invoke-DependencyPinningAnalysis -Path TestDrive: -Threshold 80
892+
Should -Invoke Write-CIAnnotation -ParameterFilter {
893+
$Level -eq 'Warning' -and $File -eq 'f.yml' -and $Line -eq 1
894+
}
895+
}
896+
}
897+
827898
Context 'Score meets threshold' {
828899
BeforeAll {
829900
Mock Get-FilesToScan {

0 commit comments

Comments
 (0)