Skip to content
This repository was archived by the owner on Mar 11, 2026. It is now read-only.

Commit 5d90d4c

Browse files
feat(scripts): add dependency pinning compliance scanning (#169)
- add scanner, security modules, and tool-checksums database - add Pester test infrastructure with fixtures and mock module - add dependency-pinning-scan and pester-tests reusable workflows - wire new workflows into pr-validation and main orchestrators 🔒 - Generated by Copilot
1 parent baef32d commit 5d90d4c

28 files changed

Lines changed: 4176 additions & 0 deletions
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
name: Dependency Pinning Scan
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
threshold:
7+
description: 'Compliance threshold percentage (0-100)'
8+
required: false
9+
type: number
10+
default: 95
11+
dependency-types:
12+
description: 'Comma-separated list of dependency types to check'
13+
required: false
14+
type: string
15+
default: 'github-actions'
16+
soft-fail:
17+
description: 'Whether to continue on compliance violations'
18+
required: false
19+
type: boolean
20+
default: false
21+
upload-sarif:
22+
description: 'Whether to upload SARIF results to Security tab'
23+
required: false
24+
type: boolean
25+
default: false
26+
upload-artifact:
27+
description: 'Whether to upload results as artifact'
28+
required: false
29+
type: boolean
30+
default: true
31+
outputs:
32+
compliance-score:
33+
description: 'Compliance score percentage'
34+
value: ${{ jobs.scan.outputs.compliance-score }}
35+
unpinned-count:
36+
description: 'Number of unpinned dependencies found'
37+
value: ${{ jobs.scan.outputs.unpinned-count }}
38+
is-compliant:
39+
description: 'Whether repository meets compliance threshold'
40+
value: ${{ jobs.scan.outputs.is-compliant }}
41+
42+
permissions:
43+
contents: read
44+
45+
jobs:
46+
scan:
47+
name: Validate SHA Pinning Compliance
48+
runs-on: ubuntu-latest
49+
permissions:
50+
contents: read
51+
security-events: write
52+
outputs:
53+
compliance-score: ${{ steps.pinning.outputs.compliance-score }}
54+
unpinned-count: ${{ steps.pinning.outputs.unpinned-count }}
55+
is-compliant: ${{ steps.pinning.outputs.is-compliant }}
56+
steps:
57+
- name: Checkout code
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
59+
with:
60+
persist-credentials: false
61+
62+
- name: Run SHA Pinning Validation
63+
id: pinning
64+
shell: pwsh
65+
run: |
66+
Write-Host "Validating dependency SHA pinning compliance..."
67+
68+
# Ensure logs directory exists
69+
New-Item -ItemType Directory -Force -Path logs | Out-Null
70+
71+
# Build parameter list for JSON output (always generate)
72+
$params = @{
73+
Path = '.'
74+
Recursive = $true
75+
Format = 'json'
76+
OutputPath = 'logs/dependency-pinning-results.json'
77+
}
78+
79+
# Enable failure on threshold violations unless soft-fail is requested
80+
if ('${{ inputs.soft-fail }}' -ne 'true') {
81+
$params['FailOnUnpinned'] = $true
82+
}
83+
84+
# Pass dependency types filter to script
85+
if ('${{ inputs.dependency-types }}') {
86+
$params['IncludeTypes'] = '${{ inputs.dependency-types }}'
87+
}
88+
89+
# Pass compliance threshold to script (script handles enforcement)
90+
if ('${{ inputs.threshold }}') {
91+
$params['Threshold'] = [int]'${{ inputs.threshold }}'
92+
}
93+
94+
# Run validation script (JSON format)
95+
& scripts/security/Test-DependencyPinning.ps1 @params
96+
$jsonExitCode = $LASTEXITCODE
97+
98+
# Generate SARIF format if requested
99+
if ('${{ inputs.upload-sarif }}' -eq 'true') {
100+
Write-Host "Generating SARIF format for Security tab..."
101+
$params['Format'] = 'sarif'
102+
$params['OutputPath'] = 'logs/dependency-pinning-results.sarif'
103+
104+
& scripts/security/Test-DependencyPinning.ps1 @params
105+
}
106+
107+
# Extract metrics from JSON report
108+
if (Test-Path logs/dependency-pinning-results.json) {
109+
$report = Get-Content logs/dependency-pinning-results.json | ConvertFrom-Json
110+
$complianceScore = $report.ComplianceScore
111+
$unpinnedCount = $report.UnpinnedDependencies
112+
113+
# Extract threshold from report metadata (script calculated compliance)
114+
$threshold = $report.Metadata.ComplianceThreshold
115+
$isCompliant = $complianceScore -ge $threshold
116+
117+
"compliance-score=$complianceScore" >> $env:GITHUB_OUTPUT
118+
"unpinned-count=$unpinnedCount" >> $env:GITHUB_OUTPUT
119+
"is-compliant=$($isCompliant.ToString().ToLower())" >> $env:GITHUB_OUTPUT
120+
121+
Write-Host "Compliance Score: $complianceScore%"
122+
Write-Host "Unpinned Dependencies: $unpinnedCount"
123+
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+
}
150+
}
151+
else {
152+
Write-Error "Failed to generate dependency pinning report"
153+
exit 1
154+
}
155+
156+
- name: Upload SARIF to Security tab
157+
if: inputs.upload-sarif && always()
158+
uses: github/codeql-action/upload-sarif@ce729e4d353d580e6cacd6a8cf2921b72e5e310a # v3.27.0
159+
with:
160+
sarif_file: logs/dependency-pinning-results.sarif
161+
category: dependency-pinning
162+
continue-on-error: true
163+
164+
- name: Upload validation report
165+
if: inputs.upload-artifact && always()
166+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3
167+
with:
168+
name: dependency-pinning-results
169+
path: logs/dependency-pinning-results.json
170+
retention-days: 90
171+
172+
- name: Add job summary
173+
if: always()
174+
shell: pwsh
175+
run: |
176+
$complianceScore = '${{ steps.pinning.outputs.compliance-score }}'
177+
$unpinnedCount = '${{ steps.pinning.outputs.unpinned-count }}'
178+
$isCompliant = '${{ steps.pinning.outputs.is-compliant }}'
179+
180+
@"
181+
## Dependency Pinning Scan Results
182+
183+
| Metric | Value |
184+
|--------|-------|
185+
| Compliance Score | $complianceScore% |
186+
| Unpinned Dependencies | $unpinnedCount |
187+
| Status | $(if ($isCompliant -eq 'true') { '✅ Compliant' } else { '⚠️ Non-Compliant' }) |
188+
189+
$(if ($unpinnedCount -ne '0') {
190+
@"
191+
192+
### ⚠️ Action Required
193+
194+
**$unpinnedCount dependencies are not SHA-pinned.**
195+
196+
Review the warnings in the workflow log and pin dependencies to specific SHA commits.
197+
198+
"@
199+
} else {
200+
@"
201+
202+
### ✅ All Dependencies Pinned
203+
204+
All dependencies are properly SHA-pinned.
205+
206+
"@
207+
})
208+
"@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding UTF8

.github/workflows/main.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77

88
permissions:
99
contents: read
10+
security-events: write
1011

1112
jobs:
1213
# Spell checking using cspell
@@ -53,6 +54,24 @@ jobs:
5354
permissions:
5455
contents: read
5556

57+
# SHA pinning compliance for GitHub Actions and dependencies
58+
dependency-pinning:
59+
name: Dependency Pinning
60+
uses: ./.github/workflows/dependency-pinning-scan.yml
61+
permissions:
62+
contents: read
63+
security-events: write
64+
65+
# PowerShell Pester test execution
66+
pester-tests:
67+
name: Pester Tests
68+
uses: ./.github/workflows/pester-tests.yml
69+
with:
70+
changed-files-only: false
71+
code-coverage: true
72+
permissions:
73+
contents: read
74+
5675
# Automated release PR management via release-please
5776
release-please:
5877
if: "${{ !startsWith(github.event.head_commit.message, 'chore(main): release') }}"
@@ -63,6 +82,8 @@ jobs:
6382
- psscriptanalyzer
6483
- link-lang-check
6584
- markdown-link-check
85+
- dependency-pinning
86+
- pester-tests
6687
name: Release Please
6788
runs-on: ubuntu-latest
6889
concurrency:

0 commit comments

Comments
 (0)