microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/address-powershell-test-comments

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

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