microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.0.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

923lines · modecode

1#!/usr/bin/env pwsh
2<#
3.SYNOPSIS
4 Verifies and reports on SHA pinning compliance for supply chain security.
5
6.DESCRIPTION
7 Cross-platform PowerShell script that analyzes GitHub Actions workflows, Docker images,
8 and other dependency declarations to verify compliance with SHA pinning security practices.
9 Identifies unpinned dependencies and provides remediation guidance.
10
11.PARAMETER Path
12 Root path to scan for dependency files. Defaults to current directory.
13
14.PARAMETER Recursive
15 Scan recursively through subdirectories. Default is true.
16
17.PARAMETER Format
18 Output format for compliance report. Options: json, sarif, csv, markdown, table.
19 Default is 'json' for programmatic processing.
20
21.PARAMETER OutputPath
22 Path where compliance results should be saved. Defaults to 'dependency-pinning-report.json'
23 in the current directory.
24
25.PARAMETER FailOnUnpinned
26 Exit with error code if pinning violations are found. Default is false for reporting mode.
27
28.PARAMETER ExcludePaths
29 Comma-separated list of paths to exclude from scanning (glob patterns supported).
30
31.PARAMETER IncludeTypes
32 Comma-separated list of dependency types to check. Options: github-actions, npm, pip.
33 Default is all types.
34
35.PARAMETER Threshold
36 Minimum compliance score percentage required for passing grade (0-100).
37 Script will exit with code 1 if compliance falls below threshold when -FailOnUnpinned is set.
38 Default is 95%.
39
40.PARAMETER Remediate
41 Generate remediation suggestions with specific SHA pins for unpinned dependencies.
42
43.EXAMPLE
44 ./Test-DependencyPinning.ps1
45 Scan current directory for dependency pinning compliance.
46
47.EXAMPLE
48 ./Test-DependencyPinning.ps1 -Path "/workspace" -Format "sarif" -FailOnUnpinned
49 Scan workspace directory, output SARIF format, fail on violations.
50
51.EXAMPLE
52 ./Test-DependencyPinning.ps1 -IncludeTypes "github-actions,pip" -Remediate
53 Check only GitHub Actions and pip dependencies with remediation suggestions.
54
55.EXAMPLE
56 ./Test-DependencyPinning.ps1 -Threshold 90 -FailOnUnpinned
57 Enforce 90% compliance threshold and fail build if not met.
58
59.EXAMPLE
60 ./Test-DependencyPinning.ps1 -Threshold 100 -IncludeTypes "github-actions"
61 Require 100% SHA pinning for GitHub Actions only.
62
63.EXAMPLE
64 ./Test-DependencyPinning.ps1 -Threshold 80
65 Report compliance against 80% threshold but continue on violations.
66
67.NOTES
68 Requires:
69 - PowerShell 7.0 or later for cross-platform compatibility
70 - Internet connectivity for SHA resolution (with -Remediate)
71 - GitHub API access for action SHA resolution (optional)
72
73 Compatible with:
74 - Windows PowerShell 5.1+ (limited cross-platform features)
75 - PowerShell 7.x on Windows, Linux, macOS
76 - GitHub Actions runners (ubuntu-latest, windows-latest, macos-latest)
77 - Azure DevOps agents (Microsoft-hosted and self-hosted)
78
79.LINK
80 https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions
81#>
82
83[CmdletBinding()]
84param(
85 [Parameter(Mandatory = $false)]
86 [string]$Path = ".",
87
88 [Parameter(Mandatory = $false)]
89 [switch]$Recursive,
90
91 [Parameter(Mandatory = $false)]
92 [ValidateSet('json', 'sarif', 'csv', 'markdown', 'table')]
93 [string]$Format = 'json',
94
95 [Parameter(Mandatory = $false)]
96 [string]$OutputPath = 'logs/dependency-pinning-results.json',
97
98 [Parameter(Mandatory = $false)]
99 [switch]$FailOnUnpinned,
100
101 [Parameter(Mandatory = $false)]
102 [string]$ExcludePaths = "",
103
104 [Parameter(Mandatory = $false)]
105 [string]$IncludeTypes = "github-actions,npm,pip,shell-downloads",
106
107 [Parameter(Mandatory = $false)]
108 [ValidateRange(0, 100)]
109 [int]$Threshold = 95,
110
111 [Parameter(Mandatory = $false)]
112 [switch]$Remediate
113)
114
115# Set error action preference for consistent error handling
116$ErrorActionPreference = 'Stop'
117
118# Define dependency patterns for different ecosystems
119$DependencyPatterns = @{
120 'github-actions' = @{
121 FilePatterns = @('**/.github/workflows/*.yml', '**/.github/workflows/*.yaml')
122 VersionPatterns = @(
123 @{
124 Pattern = 'uses:\s*([^@\s]+)@([^#\s]+)'
125 Groups = @{ Action = 1; Version = 2 }
126 Description = 'GitHub Actions uses statements'
127 }
128 )
129 SHAPattern = '^[a-fA-F0-9]{40}$'
130 RemediationUrl = 'https://api.github.com/repos/{0}/commits/{1}'
131 }
132
133 'npm' = @{
134 FilePatterns = @('**/package.json')
135 ValidationFunc = 'Get-NpmDependencyViolations'
136 SHAPattern = '^[a-fA-F0-9]{40}$'
137 RemediationUrl = 'https://registry.npmjs.org/{0}/{1}'
138 }
139
140 'pip' = @{
141 FilePatterns = @('**/requirements*.txt', '**/Pipfile', '**/pyproject.toml', '**/setup.py')
142 VersionPatterns = @(
143 @{
144 Pattern = '([a-zA-Z0-9\-_]+)==([^#\s]+)'
145 Groups = @{ Package = 1; Version = 2 }
146 Description = 'Python pip requirements'
147 }
148 )
149 SHAPattern = '^[a-fA-F0-9]{40}$'
150 RemediationUrl = 'https://pypi.org/pypi/{0}/{1}/json'
151 }
152
153 'shell-downloads' = @{
154 FilePatterns = @('**/.devcontainer/scripts/*.sh', '**/scripts/*.sh')
155 ValidationFunc = 'Test-ShellDownloadSecurity'
156 Description = 'Shell script downloads must include checksum verification'
157 }
158}
159
160class DependencyViolation {
161 [string]$File
162 [int]$Line
163 [string]$Type
164 [string]$Name
165 [string]$Version
166 [string]$CurrentRef
167 [string]$Severity
168 [string]$Description
169 [string]$Remediation
170 [hashtable]$Metadata
171
172 DependencyViolation() {
173 $this.Metadata = @{}
174 }
175}
176
177class ComplianceReport {
178 [string]$ScanPath
179 [datetime]$Timestamp
180 [int]$TotalFiles
181 [int]$ScannedFiles
182 [int]$TotalDependencies
183 [int]$PinnedDependencies
184 [int]$UnpinnedDependencies
185 [decimal]$ComplianceScore
186 [DependencyViolation[]]$Violations
187 [hashtable]$Summary
188 [hashtable]$Metadata
189
190 ComplianceReport() {
191 $this.Timestamp = Get-Date
192 $this.Violations = @()
193 $this.Summary = @{}
194 $this.Metadata = @{}
195 }
196}
197
198function Test-ShellDownloadSecurity {
199 <#
200 .SYNOPSIS
201 Scans shell scripts for curl/wget downloads lacking checksum verification.
202
203 .DESCRIPTION
204 Analyzes shell scripts to detect download commands (curl/wget) that do not
205 have corresponding checksum verification (sha256sum/shasum) within the
206 following lines.
207
208 .PARAMETER FileInfo
209 Hashtable with Path, Type, and RelativePath keys from Get-FilesToScan.
210 #>
211 [CmdletBinding()]
212 param(
213 [Parameter(Mandatory)]
214 [hashtable]$FileInfo
215 )
216
217 $FilePath = $FileInfo.Path
218
219 if (-not (Test-Path $FilePath)) {
220 return @()
221 }
222
223 $lines = Get-Content $FilePath
224 $violations = @()
225
226 # Pattern to match curl/wget download commands
227 $downloadPattern = '(curl|wget)\s+.*https?://[^\s]+'
228 $checksumPattern = 'sha256sum|shasum|Get-FileHash|openssl\s+dgst\s+-sha256|sha256sum\s+-c'
229
230 for ($i = 0; $i -lt $lines.Count; $i++) {
231 $line = $lines[$i]
232 if ($line -match $downloadPattern) {
233 # Check next 5 lines for checksum verification
234 $hasChecksum = $false
235 $searchEnd = [Math]::Min($i + 5, $lines.Count - 1)
236
237 for ($j = $i; $j -le $searchEnd; $j++) {
238 if ($lines[$j] -match $checksumPattern) {
239 $hasChecksum = $true
240 break
241 }
242 }
243
244 if (-not $hasChecksum) {
245 $violation = [DependencyViolation]::new()
246 $violation.File = $FileInfo.RelativePath
247 $violation.Line = $i + 1
248 $violation.Type = $FileInfo.Type
249 $violation.Name = $line.Trim()
250 $violation.Severity = 'warning'
251 $violation.Description = 'Download without checksum verification'
252 $violation.Metadata = @{ Pattern = $line.Trim() }
253 $violations += $violation
254 }
255 }
256 }
257
258 return $violations
259}
260
261function Get-NpmDependencyViolations {
262 <#
263 .SYNOPSIS
264 Analyzes package.json files for unpinned npm dependencies.
265 .DESCRIPTION
266 Parses package.json as JSON and checks only actual dependency sections
267 (dependencies, devDependencies, peerDependencies, optionalDependencies)
268 for SHA-pinned versions. Ignores metadata fields like name, version,
269 description, contributes, scripts, repository, etc.
270 .PARAMETER FileInfo
271 Hashtable with Path, Type, and RelativePath keys from Get-FilesToScan.
272 .OUTPUTS
273 Array of PSCustomObjects representing dependency violations.
274 #>
275 [CmdletBinding()]
276 param(
277 [Parameter(Mandatory)]
278 [hashtable]$FileInfo
279 )
280
281 $filePath = $FileInfo.Path
282 $relativePath = $FileInfo.RelativePath
283 $type = $FileInfo.Type
284 $violations = @()
285
286 if (-not (Test-Path -Path $filePath -PathType Leaf)) {
287 return $violations
288 }
289
290 try {
291 $content = Get-Content -Path $filePath -Raw -ErrorAction Stop
292 $packageJson = $content | ConvertFrom-Json -ErrorAction Stop
293 }
294 catch {
295 Write-Warning "Failed to parse $relativePath as JSON: $_"
296 return $violations
297 }
298
299 $dependencySections = @('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies')
300
301 foreach ($section in $dependencySections) {
302 $deps = $packageJson.$section
303 if ($null -eq $deps) {
304 continue
305 }
306
307 foreach ($prop in $deps.PSObject.Properties) {
308 $packageName = $prop.Name
309 $version = $prop.Value
310
311 if ([string]::IsNullOrWhiteSpace($version)) {
312 continue
313 }
314
315 $isPinned = Test-SHAPinning -Version $version -Type $type
316
317 if (-not $isPinned) {
318 $violation = [DependencyViolation]::new()
319 $violation.File = $relativePath
320 $violation.Line = 0
321 $violation.Type = $type
322 $violation.Name = $packageName
323 $violation.Version = $version
324 $violation.Severity = 'warning'
325 $violation.Description = "Unpinned npm dependency in $section"
326 $violation.Metadata = @{ Section = $section }
327 $violations += $violation
328 }
329 }
330 }
331
332 return $violations
333}
334
335function Write-PinningLog {
336 param(
337 [Parameter(Mandatory = $true)]
338 [string]$Message,
339
340 [Parameter(Mandatory = $false)]
341 [ValidateSet('Info', 'Warning', 'Error', 'Success')]
342 [string]$Level = 'Info'
343 )
344
345 $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
346 Write-Output "[$timestamp] [$Level] $Message"
347}
348
349function Get-FilesToScan {
350 <#
351 .SYNOPSIS
352 Discovers files to scan based on dependency type patterns.
353 #>
354 param(
355 [string]$ScanPath,
356 [string[]]$Types,
357 [string[]]$ExcludePatterns,
358 [switch]$Recursive
359 )
360
361 $allFiles = @()
362
363 foreach ($type in $Types) {
364 if ($DependencyPatterns.ContainsKey($type)) {
365 $patterns = $DependencyPatterns[$type].FilePatterns
366
367 foreach ($pattern in $patterns) {
368 # Convert glob pattern to PowerShell-compatible path
369 $searchPath = Join-Path $ScanPath $pattern
370
371 try {
372 if ($Recursive) {
373 $files = Get-ChildItem -Path $searchPath -Recurse -File -ErrorAction SilentlyContinue
374 }
375 else {
376 $files = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue
377 }
378
379 # Apply exclusion filters
380 if ($ExcludePatterns) {
381 foreach ($exclude in $ExcludePatterns) {
382 $files = $files | Where-Object { $_.FullName -notlike "*$exclude*" }
383 }
384 }
385
386 $allFiles += $files | ForEach-Object {
387 @{
388 Path = $_.FullName
389 Type = $type
390 RelativePath = [System.IO.Path]::GetRelativePath($ScanPath, $_.FullName)
391 }
392 }
393 }
394 catch {
395 Write-PinningLog "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
396 }
397 }
398 }
399 }
400
401 return $allFiles | Sort-Object Path -Unique
402}
403
404function Test-SHAPinning {
405 <#
406 .SYNOPSIS
407 Tests if a version reference is properly SHA-pinned.
408 #>
409 param(
410 [string]$Version,
411 [string]$Type
412 )
413
414 if ($DependencyPatterns.ContainsKey($Type) -and $DependencyPatterns[$Type].SHAPattern) {
415 $shaPattern = $DependencyPatterns[$Type].SHAPattern
416 return $Version -match $shaPattern
417 }
418
419 return $false
420}
421
422function Get-DependencyViolation {
423 <#
424 .SYNOPSIS
425 Scans a file for dependency pinning violations.
426 #>
427 param(
428 [hashtable]$FileInfo
429 )
430
431 $violations = @()
432 $filePath = $FileInfo.Path
433 $fileType = $FileInfo.Type
434
435 if (!(Test-Path $filePath)) {
436 return $violations
437 }
438
439 # Check if this type uses a validation function instead of regex patterns
440 if ($null -ne $DependencyPatterns[$fileType].ValidationFunc) {
441 $funcName = $DependencyPatterns[$fileType].ValidationFunc
442 $rawViolations = & $funcName -FileInfo $FileInfo
443
444 if ($null -eq $rawViolations) {
445 return @()
446 }
447
448 foreach ($v in $rawViolations) {
449 if ($null -eq $v) {
450 continue
451 }
452
453 if (-not ($v -is [DependencyViolation])) {
454 $actualType = $v.GetType().FullName
455 throw "Validation function '$funcName' must return [DependencyViolation] objects, got '$actualType'."
456 }
457
458 if (-not $v.File) {
459 $v.File = $FileInfo.RelativePath
460 }
461
462 if ($v.Line -lt 1) {
463 $v.Line = 0
464 }
465
466 if (-not $v.Type) {
467 $v.Type = $fileType
468 }
469 }
470
471 return $rawViolations
472 }
473
474 try {
475 $content = Get-Content -Path $filePath -Raw
476 $lines = Get-Content -Path $filePath
477
478 $patterns = $DependencyPatterns[$fileType].VersionPatterns
479
480 foreach ($patternInfo in $patterns) {
481 $pattern = $patternInfo.Pattern
482 $description = $patternInfo.Description
483
484 $regexMatches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
485
486 foreach ($match in $regexMatches) {
487 # Find line number
488 $lineNumber = 1
489 $position = $match.Index
490 for ($i = 0; $i -lt $position; $i++) {
491 if ($content[$i] -eq "`n") {
492 $lineNumber++
493 }
494 }
495
496 # Extract dependency information
497 $dependencyName = $match.Groups[1].Value
498 $version = $match.Groups[2].Value
499
500 # Check if properly pinned
501 if (!(Test-SHAPinning -Version $version -Type $fileType)) {
502 $violation = [DependencyViolation]::new()
503 $violation.File = $FileInfo.RelativePath
504 $violation.Line = $lineNumber
505 $violation.Type = $fileType
506 $violation.Name = $dependencyName
507 $violation.Version = $version
508 $violation.CurrentRef = $match.Value
509 $violation.Description = "Unpinned dependency: $description"
510 $violation.Severity = if ($fileType -eq 'github-actions') { 'High' } else { 'Medium' }
511 $violation.Metadata['PatternDescription'] = $description
512 $violation.Metadata['LineContent'] = $lines[$lineNumber - 1]
513
514 $violations += $violation
515 }
516 }
517 }
518 }
519 catch {
520 Write-PinningLog "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
521 }
522
523 return $violations
524}
525
526function Get-RemediationSuggestion {
527 <#
528 .SYNOPSIS
529 Generates remediation suggestions for unpinned dependencies.
530 #>
531 param(
532 [DependencyViolation]$Violation,
533
534 [switch]$Remediate
535 )
536
537 $type = $Violation.Type
538 $name = $Violation.Name
539 $version = $Violation.Version
540
541 if (!$Remediate) {
542 return "Enable -Remediate flag for specific SHA suggestions"
543 }
544
545 try {
546 switch ($type) {
547 'github-actions' {
548 # For GitHub Actions, resolve tag to commit SHA
549 $apiUrl = "https://api.github.com/repos/$name/commits/$version"
550 $headers = @{}
551
552 if ($env:GITHUB_TOKEN) {
553 $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN"
554 }
555
556 $response = Invoke-RestMethod -Uri $apiUrl -Headers $headers -TimeoutSec 30
557 $sha = $response.sha
558
559 if ($sha) {
560 return "Pin to SHA: uses: $name@$sha # $version"
561 }
562 }
563
564 default {
565 return "Research and pin to specific commit SHA or content hash for $type dependencies"
566 }
567 }
568 }
569 catch {
570 Write-PinningLog "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
571 }
572
573 return "Manually research and pin to immutable reference"
574}
575
576function Get-ComplianceReportData {
577 <#
578 .SYNOPSIS
579 Generates a comprehensive compliance report.
580 #>
581 param(
582 [DependencyViolation[]]$Violations,
583 [hashtable[]]$ScannedFiles,
584 [string]$ScanPath,
585 [switch]$Remediate
586 )
587
588 $report = [ComplianceReport]::new()
589 $report.ScanPath = $ScanPath
590 $report.ScannedFiles = $ScannedFiles.Count
591 $report.Violations = $Violations
592
593 # Calculate metrics
594 $totalDeps = ($Violations | Measure-Object).Count
595 $unpinnedDeps = ($Violations | Where-Object { $_.Severity -ne 'Info' } | Measure-Object).Count
596 $pinnedDeps = $totalDeps - $unpinnedDeps
597
598 $report.TotalDependencies = $totalDeps
599 $report.PinnedDependencies = $pinnedDeps
600 $report.UnpinnedDependencies = $unpinnedDeps
601
602 if ($totalDeps -gt 0) {
603 $report.ComplianceScore = [math]::Round(($pinnedDeps / $totalDeps) * 100, 2)
604 }
605 else {
606 $report.ComplianceScore = 100.0
607 }
608
609 # Generate summary by type
610 $report.Summary = @{}
611 foreach ($type in ($Violations | Group-Object Type)) {
612 $report.Summary[$type.Name] = @{
613 Total = $type.Count
614 High = ($type.Group | Where-Object { $_.Severity -eq 'High' } | Measure-Object).Count
615 Medium = ($type.Group | Where-Object { $_.Severity -eq 'Medium' } | Measure-Object).Count
616 Low = ($type.Group | Where-Object { $_.Severity -eq 'Low' } | Measure-Object).Count
617 }
618 }
619
620 # Add metadata
621 $report.Metadata = @{
622 PowerShellVersion = $PSVersionTable.PSVersion.ToString()
623 Platform = $PSVersionTable.Platform
624 ScanTimestamp = $report.Timestamp.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
625 IncludedTypes = $IncludeTypes
626 ExcludedPaths = $ExcludePaths
627 RemediationEnabled = $Remediate.IsPresent
628 ComplianceThreshold = $Threshold
629 }
630
631 return $report
632}
633
634function Export-ComplianceReport {
635 <#
636 .SYNOPSIS
637 Exports compliance report in specified format.
638 #>
639 param(
640 # Use duck typing to avoid class type collision during code coverage instrumentation
641 $Report,
642 [string]$Format,
643 [string]$OutputPath
644 )
645
646 # Validate required properties on duck-typed $Report parameter (ComplianceReport schema)
647 $requiredProperties = @('ComplianceScore', 'Violations', 'TotalDependencies', 'UnpinnedDependencies', 'Metadata')
648 foreach ($prop in $requiredProperties) {
649 if ($null -eq $Report.PSObject.Properties[$prop]) {
650 throw "Report object missing required property: $prop"
651 }
652 }
653
654 # Ensure parent directory exists
655 $parentDir = Split-Path -Path $OutputPath -Parent
656 if ($parentDir -and -not (Test-Path $parentDir)) {
657 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
658 }
659
660 switch ($Format.ToLower()) {
661 'json' {
662 $Report | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
663 }
664
665 'sarif' {
666 $sarif = @{
667 version = "2.1.0"
668 "`$schema" = "https://json.schemastore.org/sarif-2.1.0.json"
669 runs = @(@{
670 tool = @{
671 driver = @{
672 name = "dependency-pinning-analyzer"
673 version = "1.0.0"
674 informationUri = "https://github.com/microsoft/hve-core"
675 }
676 }
677 results = @($Report.Violations | ForEach-Object {
678 @{
679 ruleId = "dependency-not-pinned"
680 level = switch ($_.Severity) { 'High' { 'error' } 'Medium' { 'warning' } default { 'note' } }
681 message = @{ text = $_.Description }
682 locations = @(@{
683 physicalLocation = @{
684 artifactLocation = @{ uri = $_.File }
685 region = @{ startLine = $_.Line }
686 }
687 })
688 properties = @{
689 dependencyName = $_.Name
690 currentVersion = $_.Version
691 remediation = $_.Remediation
692 }
693 }
694 })
695 })
696 }
697 $sarif | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
698 }
699
700 'csv' {
701 $Report.Violations | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
702 }
703
704 'markdown' {
705 $markdown = @"
706# Dependency Pinning Compliance Report
707
708**Scan Date:** $($Report.Timestamp.ToString('yyyy-MM-dd HH:mm:ss'))
709**Scan Path:** $($Report.ScanPath)
710**Compliance Score:** $($Report.ComplianceScore)%
711
712## Summary
713
714| Metric | Count |
715|--------|--------|
716| Total Files Scanned | $($Report.ScannedFiles) |
717| Total Dependencies | $($Report.TotalDependencies) |
718| Pinned Dependencies | $($Report.PinnedDependencies) |
719| Unpinned Dependencies | $($Report.UnpinnedDependencies) |
720
721## Violations by Type
722
723"@
724 foreach ($type in $Report.Summary.Keys) {
725 $summary = $Report.Summary[$type]
726 $markdown += @"
727
728### $type
729- **Total:** $($summary.Total)
730- **High Severity:** $($summary.High)
731- **Medium Severity:** $($summary.Medium)
732- **Low Severity:** $($summary.Low)
733
734"@
735 }
736
737 if ($Report.Violations.Count -gt 0) {
738 $markdown += @"
739
740## Detailed Violations
741
742| File | Line | Type | Dependency | Current Version | Severity | Remediation |
743|------|------|------|------------|----------------|----------|-------------|
744"@
745 foreach ($violation in $Report.Violations) {
746 $markdown += "|$($violation.File)|$($violation.Line)|$($violation.Type)|$($violation.Name)|$($violation.Version)|$($violation.Severity)|$($violation.Remediation)|`n"
747 }
748 }
749
750 $markdown | Out-File -FilePath $OutputPath -Encoding UTF8
751 }
752
753 'table' {
754 # Display formatted table to console and save simple text format
755 if ($Report.Violations.Count -gt 0) {
756 $Report.Violations | Format-Table -Property File, Line, Type, Name, Version, Severity -AutoSize | Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
757 }
758 else {
759 "No dependency pinning violations found." | Out-File -FilePath $OutputPath -Encoding UTF8
760 }
761 }
762 }
763
764 Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
765}
766
767function Export-CICDArtifact {
768 <#
769 .SYNOPSIS
770 Exports compliance report as CI/CD artifacts for both GitHub Actions and Azure DevOps.
771 #>
772 param(
773 [ComplianceReport]$Report,
774 [string]$ReportPath
775 )
776
777 Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
778
779 # GitHub Actions artifact export
780 if ($env:GITHUB_ACTIONS -eq 'true') {
781 Write-PinningLog "Detected GitHub Actions environment - setting up artifacts" -Level Info
782
783 # Set outputs for GitHub Actions
784 if ($env:GITHUB_OUTPUT) {
785 "dependency-report=$ReportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
786 "compliance-score=$($Report.ComplianceScore)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
787 "unpinned-count=$($Report.UnpinnedDependencies)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
788 }
789
790 # Set up for actions/upload-artifact@v4
791 $artifactDir = Join-Path $PWD "dependency-pinning-artifacts"
792 New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
793 Copy-Item -Path $ReportPath -Destination $artifactDir -Force
794
795 # Create GitHub summary
796 $summaryPath = Join-Path $artifactDir "github-summary.md"
797 @"
798# 📌 Dependency Pinning Analysis
799
800**Compliance Score:** $($Report.ComplianceScore)%
801**Unpinned Dependencies:** $($Report.UnpinnedDependencies)
802**Total Dependencies Scanned:** $($Report.TotalDependencies)
803
804$(if ($Report.UnpinnedDependencies -gt 0) { "⚠️ **Action Required:** $($Report.UnpinnedDependencies) dependencies are not properly pinned to immutable references." } else { "✅ **All Clear:** All dependencies are properly pinned!" })
805"@ | Out-File -FilePath $summaryPath -Encoding UTF8
806
807 if ($env:GITHUB_STEP_SUMMARY) {
808 Copy-Item -Path $summaryPath -Destination $env:GITHUB_STEP_SUMMARY -Force
809 }
810 }
811
812 # Azure DevOps artifact export
813 if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') {
814 Write-PinningLog "Detected Azure DevOps environment - setting up artifacts" -Level Info
815
816 # Set Azure DevOps variables
817 Write-Output "##vso[task.setvariable variable=dependencyReport;isOutput=true]$ReportPath"
818 Write-Output "##vso[task.setvariable variable=complianceScore;isOutput=true]$($Report.ComplianceScore)"
819 Write-Output "##vso[task.setvariable variable=unpinnedCount;isOutput=true]$($Report.UnpinnedDependencies)"
820
821 # Publish as pipeline artifact
822 Write-Output "##vso[artifact.upload containerfolder=dependency-pinning;artifactname=dependency-pinning-report]$ReportPath"
823
824 # Add to build summary
825 Write-Output "##[section]Dependency Pinning Compliance Report"
826 Write-Output "Compliance Score: $($Report.ComplianceScore)%"
827 Write-Output "Unpinned Dependencies: $($Report.UnpinnedDependencies)"
828 Write-Output "Total Dependencies: $($Report.TotalDependencies)"
829 }
830
831 Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
832}
833
834#region Main Execution
835
836# Only execute when invoked directly (not dot-sourced)
837try {
838 if ($MyInvocation.InvocationName -ne '.') {
839 Write-PinningLog "Starting dependency pinning compliance analysis..." -Level Info
840 Write-PinningLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
841 Write-PinningLog "Platform: $($PSVersionTable.Platform)" -Level Info
842
843 # Parse include types and exclude paths
844 $typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
845 $excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
846
847 Write-PinningLog "Scanning path: $Path" -Level Info
848 Write-PinningLog "Include types: $($typesToCheck -join ', ')" -Level Info
849 if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
850
851 # Discover files to scan
852 $filesToScan = Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive
853 Write-PinningLog "Found $($filesToScan.Count) files to scan" -Level Info
854
855 # Scan for violations
856 $allViolations = @()
857 foreach ($fileInfo in $filesToScan) {
858 Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
859 $violations = Get-DependencyViolation -FileInfo $fileInfo
860
861 # Add remediation suggestions
862 foreach ($violation in $violations) {
863 $violation.Remediation = Get-RemediationSuggestion -Violation $violation -Remediate:$Remediate
864 }
865
866 $allViolations += $violations
867 }
868
869 Write-PinningLog "Found $($allViolations.Count) dependency pinning violations" -Level Info
870
871 # Generate compliance report
872 $report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
873
874 # Export report
875 Export-ComplianceReport -Report $report -Format $Format -OutputPath $OutputPath
876
877 # Export CI/CD artifacts
878 Export-CICDArtifact -Report $report -ReportPath $OutputPath
879
880 # Display summary
881 Write-PinningLog "Compliance Analysis Complete!" -Level Success
882 Write-PinningLog "Compliance Score: $($report.ComplianceScore)%" -Level Info
883 Write-PinningLog "Total Dependencies: $($report.TotalDependencies)" -Level Info
884 Write-PinningLog "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
885
886 if ($report.UnpinnedDependencies -gt 0) {
887 Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
888
889 # Check threshold compliance
890 if ($report.ComplianceScore -lt $Threshold) {
891 Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
892
893 if ($FailOnUnpinned) {
894 Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
895 exit 1
896 }
897 else {
898 Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
899 }
900 }
901 else {
902 Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
903 }
904 }
905 else {
906 Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
907 exit 0
908 }
909 }
910 else {
911 Write-Error "Test Dependency Pinning failed: will not execute if dot-sourced"
912 exit 1
913 }
914}
915catch {
916 Write-PinningLog "Dependency pinning analysis failed: $($_.Exception.Message)" -Level Error
917 if ($env:GITHUB_ACTIONS -eq 'true') {
918 Write-Output "::error::$($_.Exception.Message)"
919 }
920 exit 1
921}
922
923#endregion
924