microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/621-ai-artifacts

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

927lines · 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
171#region Functions
172
173function Test-ShellDownloadSecurity {
174 <#
175 .SYNOPSIS
176 Scans shell scripts for curl/wget downloads lacking checksum verification.
177
178 .DESCRIPTION
179 Analyzes shell scripts to detect download commands (curl/wget) that do not
180 have corresponding checksum verification (sha256sum/shasum) within the
181 following lines.
182
183 .PARAMETER FileInfo
184 Hashtable with Path, Type, and RelativePath keys from Get-FilesToScan.
185 #>
186 [CmdletBinding()]
187 param(
188 [Parameter(Mandatory)]
189 [hashtable]$FileInfo
190 )
191
192 $FilePath = $FileInfo.Path
193
194 if (-not (Test-Path $FilePath)) {
195 return @()
196 }
197
198 $lines = Get-Content $FilePath
199 $violations = @()
200
201 # Pattern to match curl/wget download commands
202 $downloadPattern = '(curl|wget)\s+.*https?://[^\s]+'
203 $checksumPattern = 'sha256sum|shasum|Get-FileHash|openssl\s+dgst\s+-sha256|sha256sum\s+-c'
204
205 for ($i = 0; $i -lt $lines.Count; $i++) {
206 $line = $lines[$i]
207 if ($line -match $downloadPattern) {
208 # Check next 5 lines for checksum verification
209 $hasChecksum = $false
210 $searchEnd = [Math]::Min($i + 5, $lines.Count - 1)
211
212 for ($j = $i; $j -le $searchEnd; $j++) {
213 if ($lines[$j] -match $checksumPattern) {
214 $hasChecksum = $true
215 break
216 }
217 }
218
219 if (-not $hasChecksum) {
220 $violation = [DependencyViolation]::new()
221 $violation.File = $FileInfo.RelativePath
222 $violation.Line = $i + 1
223 $violation.Type = $FileInfo.Type
224 $violation.Name = $line.Trim()
225 $violation.Severity = 'warning'
226 $violation.Description = 'Download without checksum verification'
227 $violation.Metadata = @{ Pattern = $line.Trim() }
228 $violations += $violation
229 }
230 }
231 }
232
233 return $violations
234}
235
236function Get-NpmDependencyViolations {
237 <#
238 .SYNOPSIS
239 Analyzes package.json files for unpinned npm dependencies.
240 .DESCRIPTION
241 Parses package.json as JSON and checks only actual dependency sections
242 (dependencies, devDependencies, peerDependencies, optionalDependencies)
243 for SHA-pinned versions. Ignores metadata fields like name, version,
244 description, contributes, scripts, repository, etc.
245 .PARAMETER FileInfo
246 Hashtable with Path, Type, and RelativePath keys from Get-FilesToScan.
247 .OUTPUTS
248 Array of PSCustomObjects representing dependency violations.
249 #>
250 [CmdletBinding()]
251 param(
252 [Parameter(Mandatory)]
253 [hashtable]$FileInfo
254 )
255
256 $filePath = $FileInfo.Path
257 $relativePath = $FileInfo.RelativePath
258 $type = $FileInfo.Type
259 $violations = @()
260
261 if (-not (Test-Path -Path $filePath -PathType Leaf)) {
262 return $violations
263 }
264
265 try {
266 $content = Get-Content -Path $filePath -Raw -ErrorAction Stop
267 $packageJson = $content | ConvertFrom-Json -ErrorAction Stop
268 }
269 catch {
270 Write-Warning "Failed to parse $relativePath as JSON: $_"
271 return $violations
272 }
273
274 $dependencySections = @('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies')
275
276 foreach ($section in $dependencySections) {
277 $deps = $packageJson.$section
278 if ($null -eq $deps) {
279 continue
280 }
281
282 foreach ($prop in $deps.PSObject.Properties) {
283 $packageName = $prop.Name
284 $version = $prop.Value
285
286 if ([string]::IsNullOrWhiteSpace($version)) {
287 continue
288 }
289
290 $isPinned = Test-SHAPinning -Version $version -Type $type
291
292 if (-not $isPinned) {
293 $violation = [DependencyViolation]::new()
294 $violation.File = $relativePath
295 $violation.Line = 0
296 $violation.Type = $type
297 $violation.Name = $packageName
298 $violation.Version = $version
299 $violation.Severity = 'warning'
300 $violation.Description = "Unpinned npm dependency in $section"
301 $violation.Metadata = @{ Section = $section }
302 $violations += $violation
303 }
304 }
305 }
306
307 return $violations
308}
309
310function Write-PinningLog {
311 [CmdletBinding()]
312 param(
313 [Parameter(Mandatory = $true)]
314 [string]$Message,
315
316 [Parameter(Mandatory = $false)]
317 [ValidateSet('Info', 'Warning', 'Error', 'Success')]
318 [string]$Level = 'Info'
319 )
320
321 $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
322 Write-Output "[$timestamp] [$Level] $Message"
323}
324
325function Get-FilesToScan {
326 <#
327 .SYNOPSIS
328 Discovers files to scan based on dependency type patterns.
329 #>
330 [CmdletBinding()]
331 param(
332 [string]$ScanPath,
333 [string[]]$Types,
334 [string[]]$ExcludePatterns,
335 [switch]$Recursive
336 )
337
338 $allFiles = @()
339
340 foreach ($type in $Types) {
341 if ($DependencyPatterns.ContainsKey($type)) {
342 $patterns = $DependencyPatterns[$type].FilePatterns
343
344 foreach ($pattern in $patterns) {
345 # Convert glob pattern to PowerShell-compatible path
346 $searchPath = Join-Path $ScanPath $pattern
347
348 try {
349 if ($Recursive) {
350 $files = Get-ChildItem -Path $searchPath -Recurse -File -ErrorAction SilentlyContinue
351 }
352 else {
353 $files = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue
354 }
355
356 # Apply exclusion filters
357 if ($ExcludePatterns) {
358 foreach ($exclude in $ExcludePatterns) {
359 $files = $files | Where-Object { $_.FullName -notlike "*$exclude*" }
360 }
361 }
362
363 $allFiles += $files | ForEach-Object {
364 @{
365 Path = $_.FullName
366 Type = $type
367 RelativePath = [System.IO.Path]::GetRelativePath($ScanPath, $_.FullName)
368 }
369 }
370 }
371 catch {
372 Write-PinningLog "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
373 }
374 }
375 }
376 }
377
378 return $allFiles | Sort-Object Path -Unique
379}
380
381function Test-SHAPinning {
382 <#
383 .SYNOPSIS
384 Tests if a version reference is properly SHA-pinned.
385 #>
386 [CmdletBinding()]
387 param(
388 [string]$Version,
389 [string]$Type
390 )
391
392 if ($DependencyPatterns.ContainsKey($Type) -and $DependencyPatterns[$Type].SHAPattern) {
393 $shaPattern = $DependencyPatterns[$Type].SHAPattern
394 return $Version -match $shaPattern
395 }
396
397 return $false
398}
399
400function Get-DependencyViolation {
401 <#
402 .SYNOPSIS
403 Scans a file for dependency pinning violations.
404 #>
405 [CmdletBinding()]
406 param(
407 [hashtable]$FileInfo
408 )
409
410 $violations = @()
411 $filePath = $FileInfo.Path
412 $fileType = $FileInfo.Type
413
414 if (!(Test-Path $filePath)) {
415 return $violations
416 }
417
418 # Check if this type uses a validation function instead of regex patterns
419 if ($null -ne $DependencyPatterns[$fileType].ValidationFunc) {
420 $funcName = $DependencyPatterns[$fileType].ValidationFunc
421 $rawViolations = & $funcName -FileInfo $FileInfo
422
423 if ($null -eq $rawViolations) {
424 return @()
425 }
426
427 foreach ($v in $rawViolations) {
428 if ($null -eq $v) {
429 continue
430 }
431
432 if (-not ($v -is [DependencyViolation])) {
433 $actualType = $v.GetType().FullName
434 throw "Validation function '$funcName' must return [DependencyViolation] objects, got '$actualType'."
435 }
436
437 if (-not $v.File) {
438 $v.File = $FileInfo.RelativePath
439 }
440
441 if ($v.Line -lt 1) {
442 $v.Line = 0
443 }
444
445 if (-not $v.Type) {
446 $v.Type = $fileType
447 }
448 }
449
450 return $rawViolations
451 }
452
453 try {
454 $content = Get-Content -Path $filePath -Raw
455 $lines = Get-Content -Path $filePath
456
457 $patterns = $DependencyPatterns[$fileType].VersionPatterns
458
459 foreach ($patternInfo in $patterns) {
460 $pattern = $patternInfo.Pattern
461 $description = $patternInfo.Description
462
463 $regexMatches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
464
465 foreach ($match in $regexMatches) {
466 # Find line number
467 $lineNumber = 1
468 $position = $match.Index
469 for ($i = 0; $i -lt $position; $i++) {
470 if ($content[$i] -eq "`n") {
471 $lineNumber++
472 }
473 }
474
475 # Extract dependency information
476 $dependencyName = $match.Groups[1].Value
477 $version = $match.Groups[2].Value
478
479 # Check if properly pinned
480 if (!(Test-SHAPinning -Version $version -Type $fileType)) {
481 $violation = [DependencyViolation]::new()
482 $violation.File = $FileInfo.RelativePath
483 $violation.Line = $lineNumber
484 $violation.Type = $fileType
485 $violation.Name = $dependencyName
486 $violation.Version = $version
487 $violation.CurrentRef = $match.Value
488 $violation.Description = "Unpinned dependency: $description"
489 $violation.Severity = if ($fileType -eq 'github-actions') { 'High' } else { 'Medium' }
490 $violation.Metadata['PatternDescription'] = $description
491 $violation.Metadata['LineContent'] = $lines[$lineNumber - 1]
492
493 $violations += $violation
494 }
495 }
496 }
497 }
498 catch {
499 Write-PinningLog "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
500 }
501
502 return $violations
503}
504
505function Get-RemediationSuggestion {
506 <#
507 .SYNOPSIS
508 Generates remediation suggestions for unpinned dependencies.
509 #>
510 [CmdletBinding()]
511 param(
512 [DependencyViolation]$Violation,
513
514 [switch]$Remediate
515 )
516
517 $type = $Violation.Type
518 $name = $Violation.Name
519 $version = $Violation.Version
520
521 if (!$Remediate) {
522 return "Enable -Remediate flag for specific SHA suggestions"
523 }
524
525 try {
526 switch ($type) {
527 'github-actions' {
528 # For GitHub Actions, resolve tag to commit SHA
529 $apiUrl = "https://api.github.com/repos/$name/commits/$version"
530 $headers = @{}
531
532 if ($env:GITHUB_TOKEN) {
533 $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN"
534 }
535
536 $response = Invoke-RestMethod -Uri $apiUrl -Headers $headers -TimeoutSec 30
537 $sha = $response.sha
538
539 if ($sha) {
540 return "Pin to SHA: uses: $name@$sha # $version"
541 }
542 }
543
544 default {
545 return "Research and pin to specific commit SHA or content hash for $type dependencies"
546 }
547 }
548 }
549 catch {
550 Write-PinningLog "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
551 }
552
553 return "Manually research and pin to immutable reference"
554}
555
556function Get-ComplianceReportData {
557 <#
558 .SYNOPSIS
559 Generates a comprehensive compliance report.
560 #>
561 [CmdletBinding()]
562 param(
563 [DependencyViolation[]]$Violations,
564 [hashtable[]]$ScannedFiles,
565 [string]$ScanPath,
566 [switch]$Remediate
567 )
568
569 $report = [ComplianceReport]::new()
570 $report.ScanPath = $ScanPath
571 $report.ScannedFiles = $ScannedFiles.Count
572 $report.Violations = $Violations
573
574 # Calculate metrics
575 $totalDeps = @($Violations).Count
576 $unpinnedDeps = @($Violations | Where-Object { $_.Severity -ne 'Info' }).Count
577 $pinnedDeps = $totalDeps - $unpinnedDeps
578
579 $report.TotalDependencies = $totalDeps
580 $report.PinnedDependencies = $pinnedDeps
581 $report.UnpinnedDependencies = $unpinnedDeps
582
583 if ($totalDeps -gt 0) {
584 $report.ComplianceScore = [math]::Round(($pinnedDeps / $totalDeps) * 100, 2)
585 }
586 else {
587 $report.ComplianceScore = 100.0
588 }
589
590 # Generate summary by type
591 $report.Summary = @{}
592 foreach ($type in @($Violations | Group-Object Type)) {
593 $report.Summary[$type.Name] = @{
594 Total = $type.Count
595 High = @($type.Group | Where-Object { $_.Severity -eq 'High' }).Count
596 Medium = @($type.Group | Where-Object { $_.Severity -eq 'Medium' }).Count
597 Low = @($type.Group | Where-Object { $_.Severity -eq 'Low' }).Count
598 }
599 }
600
601 # Add metadata
602 $report.Metadata = @{
603 PowerShellVersion = $PSVersionTable.PSVersion.ToString()
604 Platform = $PSVersionTable.Platform
605 ScanTimestamp = $report.Timestamp.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
606 IncludedTypes = $IncludeTypes
607 ExcludedPaths = $ExcludePaths
608 RemediationEnabled = $Remediate.IsPresent
609 ComplianceThreshold = $Threshold
610 }
611
612 return $report
613}
614
615function Export-ComplianceReport {
616 <#
617 .SYNOPSIS
618 Exports compliance report in specified format.
619 #>
620 [CmdletBinding()]
621 param(
622 # Use duck typing to avoid class type collision during code coverage instrumentation
623 $Report,
624 [string]$Format,
625 [string]$OutputPath
626 )
627
628 # Validate required properties on duck-typed $Report parameter (ComplianceReport schema)
629 $requiredProperties = @('ComplianceScore', 'Violations', 'TotalDependencies', 'UnpinnedDependencies', 'Metadata')
630 foreach ($prop in $requiredProperties) {
631 if ($null -eq $Report.PSObject.Properties[$prop]) {
632 throw "Report object missing required property: $prop"
633 }
634 }
635
636 # Ensure parent directory exists
637 $parentDir = Split-Path -Path $OutputPath -Parent
638 if ($parentDir -and -not (Test-Path $parentDir)) {
639 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
640 }
641
642 switch ($Format.ToLower()) {
643 'json' {
644 $Report | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
645 }
646
647 'sarif' {
648 $sarif = @{
649 version = "2.1.0"
650 "`$schema" = "https://json.schemastore.org/sarif-2.1.0.json"
651 runs = @(@{
652 tool = @{
653 driver = @{
654 name = "dependency-pinning-analyzer"
655 version = "1.0.0"
656 informationUri = "https://github.com/microsoft/hve-core"
657 }
658 }
659 results = @($Report.Violations | ForEach-Object {
660 @{
661 ruleId = "dependency-not-pinned"
662 level = switch ($_.Severity) { 'High' { 'error' } 'Medium' { 'warning' } default { 'note' } }
663 message = @{ text = $_.Description }
664 locations = @(@{
665 physicalLocation = @{
666 artifactLocation = @{ uri = $_.File }
667 region = @{ startLine = $_.Line }
668 }
669 })
670 properties = @{
671 dependencyName = $_.Name
672 currentVersion = $_.Version
673 remediation = $_.Remediation
674 }
675 }
676 })
677 })
678 }
679 $sarif | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
680 }
681
682 'csv' {
683 $Report.Violations | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
684 }
685
686 'markdown' {
687 $markdown = @"
688# Dependency Pinning Compliance Report
689
690**Scan Date:** $($Report.Timestamp.ToString('yyyy-MM-dd HH:mm:ss'))
691**Scan Path:** $($Report.ScanPath)
692**Compliance Score:** $($Report.ComplianceScore)%
693
694## Summary
695
696| Metric | Count |
697|--------|--------|
698| Total Files Scanned | $($Report.ScannedFiles) |
699| Total Dependencies | $($Report.TotalDependencies) |
700| Pinned Dependencies | $($Report.PinnedDependencies) |
701| Unpinned Dependencies | $($Report.UnpinnedDependencies) |
702
703## Violations by Type
704
705"@
706 foreach ($type in $Report.Summary.Keys) {
707 $summary = $Report.Summary[$type]
708 $markdown += @"
709
710### $type
711- **Total:** $($summary.Total)
712- **High Severity:** $($summary.High)
713- **Medium Severity:** $($summary.Medium)
714- **Low Severity:** $($summary.Low)
715
716"@
717 }
718
719 if ($Report.Violations.Count -gt 0) {
720 $markdown += @"
721
722## Detailed Violations
723
724| File | Line | Type | Dependency | Current Version | Severity | Remediation |
725|------|------|------|------------|----------------|----------|-------------|
726"@
727 foreach ($violation in $Report.Violations) {
728 $markdown += "|$($violation.File)|$($violation.Line)|$($violation.Type)|$($violation.Name)|$($violation.Version)|$($violation.Severity)|$($violation.Remediation)|`n"
729 }
730 }
731
732 $markdown | Out-File -FilePath $OutputPath -Encoding UTF8
733 }
734
735 'table' {
736 # Display formatted table to console and save simple text format
737 if ($Report.Violations.Count -gt 0) {
738 $Report.Violations | Format-Table -Property File, Line, Type, Name, Version, Severity -AutoSize | Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
739 }
740 else {
741 "No dependency pinning violations found." | Out-File -FilePath $OutputPath -Encoding UTF8
742 }
743 }
744 }
745
746 Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
747}
748
749function Export-CICDArtifact {
750 <#
751 .SYNOPSIS
752 Exports compliance report as CI/CD artifacts for both GitHub Actions and Azure DevOps.
753 #>
754 [CmdletBinding()]
755 param(
756 [ComplianceReport]$Report,
757 [string]$ReportPath
758 )
759
760 Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
761
762 $platform = Get-CIPlatform
763 Write-PinningLog "Detected $platform environment - setting up artifacts" -Level Info
764
765 # Set CI outputs (works for both GitHub Actions and Azure DevOps)
766 Set-CIOutput -Name 'dependency-report' -Value $ReportPath -IsOutput
767 Set-CIOutput -Name 'compliance-score' -Value $Report.ComplianceScore -IsOutput
768 Set-CIOutput -Name 'unpinned-count' -Value $Report.UnpinnedDependencies -IsOutput
769
770 # Create summary content
771 $summaryContent = @"
772# 📌 Dependency Pinning Analysis
773
774**Compliance Score:** $($Report.ComplianceScore)%
775**Unpinned Dependencies:** $($Report.UnpinnedDependencies)
776**Total Dependencies Scanned:** $($Report.TotalDependencies)
777
778$(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!" })
779"@
780
781 # Write step summary
782 Write-CIStepSummary -Content $summaryContent
783
784 # Publish artifact
785 Publish-CIArtifact -Path $ReportPath -Name 'dependency-pinning-report' -ContainerFolder 'dependency-pinning'
786
787 # Set up local artifact directory for GitHub Actions upload-artifact action
788 if ($platform -eq 'github') {
789 $artifactDir = Join-Path $PWD "dependency-pinning-artifacts"
790 New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
791 Copy-Item -Path $ReportPath -Destination $artifactDir -Force
792 }
793
794 Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
795}
796
797function Invoke-DependencyPinningAnalysis {
798 <#
799 .SYNOPSIS
800 Orchestrates dependency pinning compliance analysis.
801 #>
802 [CmdletBinding()]
803 [OutputType([void])]
804 param(
805 [Parameter()]
806 [string]$Path = ".",
807
808 [Parameter()]
809 [switch]$Recursive,
810
811 [Parameter()]
812 [string]$IncludeTypes = "github-actions,npm,pip,shell-downloads",
813
814 [Parameter()]
815 [string]$ExcludePaths = "",
816
817 [Parameter()]
818 [string]$Format = 'json',
819
820 [Parameter()]
821 [string]$OutputPath = 'logs/dependency-pinning-results.json',
822
823 [Parameter()]
824 [switch]$FailOnUnpinned,
825
826 [Parameter()]
827 [int]$Threshold = 95,
828
829 [Parameter()]
830 [switch]$Remediate
831 )
832
833 Write-PinningLog "Starting dependency pinning compliance analysis..." -Level Info
834 Write-PinningLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
835 Write-PinningLog "Platform: $($PSVersionTable.Platform)" -Level Info
836
837 # Parse include types and exclude paths
838 $typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
839 $excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
840
841 Write-PinningLog "Scanning path: $Path" -Level Info
842 Write-PinningLog "Include types: $($typesToCheck -join ', ')" -Level Info
843 if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
844
845 # Discover files to scan
846 $filesToScan = @(Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive)
847 Write-PinningLog "Found $(@($filesToScan).Count) files to scan" -Level Info
848
849 # Scan for violations
850 $allViolations = @()
851 foreach ($fileInfo in $filesToScan) {
852 Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
853 $violations = @(Get-DependencyViolation -FileInfo $fileInfo)
854
855 # Add remediation suggestions
856 foreach ($violation in $violations) {
857 $violation.Remediation = Get-RemediationSuggestion -Violation $violation -Remediate:$Remediate
858 }
859
860 $allViolations += $violations
861 }
862
863 Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
864
865 # Generate compliance report
866 $report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
867
868 # Export report
869 Export-ComplianceReport -Report $report -Format $Format -OutputPath $OutputPath
870
871 # Export CI/CD artifacts
872 Export-CICDArtifact -Report $report -ReportPath $OutputPath
873
874 # Display summary
875 Write-PinningLog "Compliance Analysis Complete!" -Level Success
876 Write-PinningLog "Compliance Score: $($report.ComplianceScore)%" -Level Info
877 Write-PinningLog "Total Dependencies: $($report.TotalDependencies)" -Level Info
878 Write-PinningLog "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
879
880 if ($report.UnpinnedDependencies -gt 0) {
881 Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
882
883 # Check threshold compliance
884 if ($report.ComplianceScore -lt $Threshold) {
885 Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
886
887 if ($FailOnUnpinned) {
888 Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
889 throw "Compliance score $($report.ComplianceScore)% is below threshold $Threshold% (-FailOnUnpinned enabled)"
890 }
891 else {
892 Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
893 }
894 }
895 else {
896 Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
897 }
898 }
899 else {
900 Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
901 }
902}
903
904#endregion Functions
905
906#region Main Execution
907if ($MyInvocation.InvocationName -ne '.') {
908 try {
909 Invoke-DependencyPinningAnalysis `
910 -Path $Path `
911 -Recursive:$Recursive `
912 -IncludeTypes $IncludeTypes `
913 -ExcludePaths $ExcludePaths `
914 -Format $Format `
915 -OutputPath $OutputPath `
916 -FailOnUnpinned:$FailOnUnpinned `
917 -Threshold $Threshold `
918 -Remediate:$Remediate
919 exit 0
920 }
921 catch {
922 Write-Error -ErrorAction Continue "Test-DependencyPinning failed: $($_.Exception.Message)"
923 Write-CIAnnotation -Message $_.Exception.Message -Level Error
924 exit 1
925 }
926}
927#endregion Main Execution
928