microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/580-dt-start-project

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

976lines · 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 $color = switch ($Level) {
323 'Info' { 'Cyan' }
324 'Warning' { 'Yellow' }
325 'Error' { 'Red' }
326 'Success' { 'Green' }
327 }
328 Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
329
330 # Surface warnings and errors as CI annotations so they appear in the Actions/ADO UI
331 if ($Level -eq 'Warning') {
332 Write-CIAnnotation -Message $Message -Level Warning
333 }
334 elseif ($Level -eq 'Error') {
335 Write-CIAnnotation -Message $Message -Level Error
336 }
337}
338
339function Get-FilesToScan {
340 <#
341 .SYNOPSIS
342 Discovers files to scan based on dependency type patterns.
343 #>
344 [CmdletBinding()]
345 param(
346 [string]$ScanPath,
347 [string[]]$Types,
348 [string[]]$ExcludePatterns,
349 [switch]$Recursive
350 )
351
352 $allFiles = @()
353
354 foreach ($type in $Types) {
355 if ($DependencyPatterns.ContainsKey($type)) {
356 $patterns = $DependencyPatterns[$type].FilePatterns
357
358 foreach ($pattern in $patterns) {
359 # Convert glob pattern to PowerShell-compatible path
360 $searchPath = Join-Path $ScanPath $pattern
361
362 try {
363 if ($Recursive) {
364 $files = Get-ChildItem -Path $searchPath -Recurse -File -ErrorAction SilentlyContinue
365 }
366 else {
367 $files = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue
368 }
369
370 # Apply exclusion filters
371 if ($ExcludePatterns) {
372 foreach ($exclude in $ExcludePatterns) {
373 $files = $files | Where-Object { $_.FullName -notlike "*$exclude*" }
374 }
375 }
376
377 $allFiles += $files | ForEach-Object {
378 @{
379 Path = $_.FullName
380 Type = $type
381 RelativePath = [System.IO.Path]::GetRelativePath($ScanPath, $_.FullName)
382 }
383 }
384 }
385 catch {
386 Write-PinningLog "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
387 }
388 }
389 }
390 }
391
392 return $allFiles | Sort-Object Path -Unique
393}
394
395function Test-SHAPinning {
396 <#
397 .SYNOPSIS
398 Tests if a version reference is properly SHA-pinned.
399 #>
400 [CmdletBinding()]
401 param(
402 [string]$Version,
403 [string]$Type
404 )
405
406 if ($DependencyPatterns.ContainsKey($Type) -and $DependencyPatterns[$Type].SHAPattern) {
407 $shaPattern = $DependencyPatterns[$Type].SHAPattern
408 return $Version -match $shaPattern
409 }
410
411 return $false
412}
413
414function Get-DependencyViolation {
415 <#
416 .SYNOPSIS
417 Scans a file for dependency pinning violations.
418 #>
419 [CmdletBinding()]
420 param(
421 [hashtable]$FileInfo
422 )
423
424 $violations = @()
425 $filePath = $FileInfo.Path
426 $fileType = $FileInfo.Type
427
428 if (!(Test-Path $filePath)) {
429 return $violations
430 }
431
432 # Check if this type uses a validation function instead of regex patterns
433 if ($null -ne $DependencyPatterns[$fileType].ValidationFunc) {
434 $funcName = $DependencyPatterns[$fileType].ValidationFunc
435 $rawViolations = & $funcName -FileInfo $FileInfo
436
437 if ($null -eq $rawViolations) {
438 return @()
439 }
440
441 foreach ($v in $rawViolations) {
442 if ($null -eq $v) {
443 continue
444 }
445
446 if (-not ($v -is [DependencyViolation])) {
447 $actualType = $v.GetType().FullName
448 throw "Validation function '$funcName' must return [DependencyViolation] objects, got '$actualType'."
449 }
450
451 if (-not $v.File) {
452 $v.File = $FileInfo.RelativePath
453 }
454
455 if ($v.Line -lt 1) {
456 $v.Line = 0
457 }
458
459 if (-not $v.Type) {
460 $v.Type = $fileType
461 }
462 }
463
464 return $rawViolations
465 }
466
467 try {
468 $content = Get-Content -Path $filePath -Raw
469 $lines = Get-Content -Path $filePath
470
471 $patterns = $DependencyPatterns[$fileType].VersionPatterns
472
473 foreach ($patternInfo in $patterns) {
474 $pattern = $patternInfo.Pattern
475 $description = $patternInfo.Description
476
477 $regexMatches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
478
479 foreach ($match in $regexMatches) {
480 # Find line number
481 $lineNumber = 1
482 $position = $match.Index
483 for ($i = 0; $i -lt $position; $i++) {
484 if ($content[$i] -eq "`n") {
485 $lineNumber++
486 }
487 }
488
489 # Extract dependency information
490 $dependencyName = $match.Groups[1].Value
491 $version = $match.Groups[2].Value
492
493 # Check if properly pinned
494 if (!(Test-SHAPinning -Version $version -Type $fileType)) {
495 $violation = [DependencyViolation]::new()
496 $violation.File = $FileInfo.RelativePath
497 $violation.Line = $lineNumber
498 $violation.Type = $fileType
499 $violation.Name = $dependencyName
500 $violation.Version = $version
501 $violation.CurrentRef = $match.Value
502 $violation.Description = "Unpinned dependency: $description"
503 $violation.Severity = if ($fileType -eq 'github-actions') { 'High' } else { 'Medium' }
504 $violation.Metadata['PatternDescription'] = $description
505 $violation.Metadata['LineContent'] = $lines[$lineNumber - 1]
506
507 $violations += $violation
508 }
509 }
510 }
511 }
512 catch {
513 Write-PinningLog "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
514 }
515
516 return $violations
517}
518
519function Get-RemediationSuggestion {
520 <#
521 .SYNOPSIS
522 Generates remediation suggestions for unpinned dependencies.
523 #>
524 [CmdletBinding()]
525 param(
526 [DependencyViolation]$Violation,
527
528 [switch]$Remediate
529 )
530
531 $type = $Violation.Type
532 $name = $Violation.Name
533 $version = $Violation.Version
534
535 if (!$Remediate) {
536 return "Enable -Remediate flag for specific SHA suggestions"
537 }
538
539 try {
540 switch ($type) {
541 'github-actions' {
542 # For GitHub Actions, resolve tag to commit SHA
543 $apiUrl = "https://api.github.com/repos/$name/commits/$version"
544 $headers = @{}
545
546 if ($env:GITHUB_TOKEN) {
547 $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN"
548 }
549
550 $response = Invoke-RestMethod -Uri $apiUrl -Headers $headers -TimeoutSec 30
551 $sha = $response.sha
552
553 if ($sha) {
554 return "Pin to SHA: uses: $name@$sha # $version"
555 }
556 }
557
558 default {
559 return "Research and pin to specific commit SHA or content hash for $type dependencies"
560 }
561 }
562 }
563 catch {
564 Write-PinningLog "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
565 }
566
567 return "Manually research and pin to immutable reference"
568}
569
570function Get-ComplianceReportData {
571 <#
572 .SYNOPSIS
573 Generates a comprehensive compliance report.
574 #>
575 [CmdletBinding()]
576 param(
577 [DependencyViolation[]]$Violations,
578 [hashtable[]]$ScannedFiles,
579 [string]$ScanPath,
580 [switch]$Remediate
581 )
582
583 $report = [ComplianceReport]::new()
584 $report.ScanPath = $ScanPath
585 $report.ScannedFiles = $ScannedFiles.Count
586 $report.Violations = $Violations
587
588 # Calculate metrics
589 $totalDeps = @($Violations).Count
590 $unpinnedDeps = @($Violations | Where-Object { $_.Severity -ne 'Info' }).Count
591 $pinnedDeps = $totalDeps - $unpinnedDeps
592
593 $report.TotalDependencies = $totalDeps
594 $report.PinnedDependencies = $pinnedDeps
595 $report.UnpinnedDependencies = $unpinnedDeps
596
597 if ($totalDeps -gt 0) {
598 $report.ComplianceScore = [math]::Round(($pinnedDeps / $totalDeps) * 100, 2)
599 }
600 else {
601 $report.ComplianceScore = 100.0
602 }
603
604 # Generate summary by type
605 $report.Summary = @{}
606 foreach ($type in @($Violations | Group-Object Type)) {
607 $report.Summary[$type.Name] = @{
608 Total = $type.Count
609 High = @($type.Group | Where-Object { $_.Severity -eq 'High' }).Count
610 Medium = @($type.Group | Where-Object { $_.Severity -eq 'Medium' }).Count
611 Low = @($type.Group | Where-Object { $_.Severity -eq 'Low' }).Count
612 }
613 }
614
615 # Add metadata
616 $report.Metadata = @{
617 PowerShellVersion = $PSVersionTable.PSVersion.ToString()
618 Platform = $PSVersionTable.Platform
619 ScanTimestamp = $report.Timestamp.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
620 IncludedTypes = $IncludeTypes
621 ExcludedPaths = $ExcludePaths
622 RemediationEnabled = $Remediate.IsPresent
623 ComplianceThreshold = $Threshold
624 }
625
626 return $report
627}
628
629function Export-ComplianceReport {
630 <#
631 .SYNOPSIS
632 Exports compliance report in specified format.
633 #>
634 [CmdletBinding()]
635 param(
636 # Use duck typing to avoid class type collision during code coverage instrumentation
637 $Report,
638 [string]$Format,
639 [string]$OutputPath
640 )
641
642 # Validate required properties on duck-typed $Report parameter (ComplianceReport schema)
643 $requiredProperties = @('ComplianceScore', 'Violations', 'TotalDependencies', 'UnpinnedDependencies', 'Metadata')
644 foreach ($prop in $requiredProperties) {
645 if ($null -eq $Report.PSObject.Properties[$prop]) {
646 throw "Report object missing required property: $prop"
647 }
648 }
649
650 # Ensure parent directory exists
651 $parentDir = Split-Path -Path $OutputPath -Parent
652 if ($parentDir -and -not (Test-Path $parentDir)) {
653 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
654 }
655
656 switch ($Format.ToLower()) {
657 'json' {
658 $Report | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
659 }
660
661 'sarif' {
662 $sarif = @{
663 version = "2.1.0"
664 "`$schema" = "https://json.schemastore.org/sarif-2.1.0.json"
665 runs = @(@{
666 tool = @{
667 driver = @{
668 name = "dependency-pinning-analyzer"
669 version = "1.0.0"
670 informationUri = "https://github.com/microsoft/hve-core"
671 }
672 }
673 results = @($Report.Violations | ForEach-Object {
674 @{
675 ruleId = "dependency-not-pinned"
676 level = switch ($_.Severity) { 'High' { 'error' } 'Medium' { 'warning' } default { 'note' } }
677 message = @{ text = $_.Description }
678 locations = @(@{
679 physicalLocation = @{
680 artifactLocation = @{ uri = $_.File }
681 region = @{ startLine = $_.Line }
682 }
683 })
684 properties = @{
685 dependencyName = $_.Name
686 currentVersion = $_.Version
687 remediation = $_.Remediation
688 }
689 }
690 })
691 })
692 }
693 $sarif | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
694 }
695
696 'csv' {
697 $Report.Violations | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
698 }
699
700 'markdown' {
701 $markdown = @"
702# Dependency Pinning Compliance Report
703
704**Scan Date:** $($Report.Timestamp.ToString('yyyy-MM-dd HH:mm:ss'))
705**Scan Path:** $($Report.ScanPath)
706**Compliance Score:** $($Report.ComplianceScore)%
707
708## Summary
709
710| Metric | Count |
711|--------|--------|
712| Total Files Scanned | $($Report.ScannedFiles) |
713| Total Dependencies | $($Report.TotalDependencies) |
714| Pinned Dependencies | $($Report.PinnedDependencies) |
715| Unpinned Dependencies | $($Report.UnpinnedDependencies) |
716
717## Violations by Type
718
719"@
720 foreach ($type in $Report.Summary.Keys) {
721 $summary = $Report.Summary[$type]
722 $markdown += @"
723
724### $type
725- **Total:** $($summary.Total)
726- **High Severity:** $($summary.High)
727- **Medium Severity:** $($summary.Medium)
728- **Low Severity:** $($summary.Low)
729
730"@
731 }
732
733 if ($Report.Violations.Count -gt 0) {
734 $markdown += @"
735
736## Detailed Violations
737
738| File | Line | Type | Dependency | Current Version | Severity | Remediation |
739|------|------|------|------------|----------------|----------|-------------|
740"@
741 foreach ($violation in $Report.Violations) {
742 $markdown += "|$($violation.File)|$($violation.Line)|$($violation.Type)|$($violation.Name)|$($violation.Version)|$($violation.Severity)|$($violation.Remediation)|`n"
743 }
744 }
745
746 $markdown | Out-File -FilePath $OutputPath -Encoding UTF8
747 }
748
749 'table' {
750 # Display formatted table to console and save simple text format
751 if ($Report.Violations.Count -gt 0) {
752 $Report.Violations | Format-Table -Property File, Line, Type, Name, Version, Severity -AutoSize | Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
753 }
754 else {
755 "No dependency pinning violations found." | Out-File -FilePath $OutputPath -Encoding UTF8
756 }
757 }
758 }
759
760 Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
761}
762
763function Export-CICDArtifact {
764 <#
765 .SYNOPSIS
766 Exports compliance report as CI/CD artifacts for both GitHub Actions and Azure DevOps.
767 #>
768 [CmdletBinding()]
769 param(
770 [ComplianceReport]$Report,
771 [string]$ReportPath
772 )
773
774 Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
775
776 $platform = Get-CIPlatform
777 Write-PinningLog "Detected $platform environment - setting up artifacts" -Level Info
778
779 # Set CI outputs (works for both GitHub Actions and Azure DevOps)
780 Set-CIOutput -Name 'dependency-report' -Value $ReportPath -IsOutput
781 Set-CIOutput -Name 'compliance-score' -Value $Report.ComplianceScore -IsOutput
782 Set-CIOutput -Name 'unpinned-count' -Value $Report.UnpinnedDependencies -IsOutput
783
784 # Create summary content
785 $summaryContent = @"
786# 📌 Dependency Pinning Analysis
787
788**Compliance Score:** $($Report.ComplianceScore)%
789**Unpinned Dependencies:** $($Report.UnpinnedDependencies)
790**Total Dependencies Scanned:** $($Report.TotalDependencies)
791
792$(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!" })
793"@
794
795 # Write step summary
796 Write-CIStepSummary -Content $summaryContent
797
798 # Publish artifact
799 Publish-CIArtifact -Path $ReportPath -Name 'dependency-pinning-report' -ContainerFolder 'dependency-pinning'
800
801 # Set up local artifact directory for GitHub Actions upload-artifact action
802 if ($platform -eq 'github') {
803 $artifactDir = Join-Path $PWD "dependency-pinning-artifacts"
804 New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
805 Copy-Item -Path $ReportPath -Destination $artifactDir -Force
806 }
807
808 Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
809}
810
811function Invoke-DependencyPinningAnalysis {
812 <#
813 .SYNOPSIS
814 Orchestrates dependency pinning compliance analysis.
815 #>
816 [CmdletBinding()]
817 [OutputType([void])]
818 param(
819 [Parameter()]
820 [string]$Path = ".",
821
822 [Parameter()]
823 [switch]$Recursive,
824
825 [Parameter()]
826 [string]$IncludeTypes = "github-actions,npm,pip,shell-downloads",
827
828 [Parameter()]
829 [string]$ExcludePaths = "",
830
831 [Parameter()]
832 [string]$Format = 'json',
833
834 [Parameter()]
835 [string]$OutputPath = 'logs/dependency-pinning-results.json',
836
837 [Parameter()]
838 [switch]$FailOnUnpinned,
839
840 [Parameter()]
841 [int]$Threshold = 95,
842
843 [Parameter()]
844 [switch]$Remediate
845 )
846
847 Write-PinningLog "Starting dependency pinning compliance analysis..." -Level Info
848 Write-PinningLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
849 Write-PinningLog "Platform: $($PSVersionTable.Platform)" -Level Info
850
851 # Parse include types and exclude paths
852 $typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
853 $excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
854
855 Write-PinningLog "Scanning path: $Path" -Level Info
856 Write-PinningLog "Include types: $($typesToCheck -join ', ')" -Level Info
857 if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
858
859 # Discover files to scan
860 $filesToScan = @(Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive)
861 Write-PinningLog "Found $(@($filesToScan).Count) files to scan" -Level Info
862
863 # Scan for violations
864 $allViolations = @()
865 foreach ($fileInfo in $filesToScan) {
866 Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
867 $violations = @(Get-DependencyViolation -FileInfo $fileInfo)
868
869 # Add remediation suggestions
870 foreach ($violation in $violations) {
871 $violation.Remediation = Get-RemediationSuggestion -Violation $violation -Remediate:$Remediate
872 }
873
874 $allViolations += $violations
875 }
876
877 Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
878
879 # Emit per-violation CI annotations and console output
880 if ($allViolations.Count -gt 0) {
881 Write-Host "`n❌ Found $($allViolations.Count) unpinned dependencies:" -ForegroundColor Red
882 $groupedByFile = $allViolations | Group-Object -Property File
883 foreach ($fileGroup in $groupedByFile) {
884 Write-Host "`n📄 $($fileGroup.Name)" -ForegroundColor Cyan
885 foreach ($dep in $fileGroup.Group) {
886 $annotationLevel = switch ($dep.Severity) {
887 'High' { 'Error' }
888 'Medium' { 'Warning' }
889 default { 'Notice' }
890 }
891 $icon = switch ($dep.Severity) {
892 'High' { '❌' }
893 'Medium' { '⚠️' }
894 default { 'ℹ️' }
895 }
896 $color = switch ($dep.Severity) {
897 'High' { 'Red' }
898 'Medium' { 'Yellow' }
899 default { 'Cyan' }
900 }
901 Write-Host " $icon [$($dep.Severity)] $($dep.Name)@$($dep.Version): $($dep.Description) (Line $($dep.Line))" -ForegroundColor $color
902 Write-CIAnnotation `
903 -Message "[$($dep.ViolationType)] $($dep.Name): $($dep.Description)" `
904 -Level $annotationLevel `
905 -File $dep.File `
906 -Line $dep.Line
907 }
908 }
909 }
910 else {
911 Write-Host "`n✅ All dependencies are properly SHA-pinned." -ForegroundColor Green
912 }
913
914 # Generate compliance report
915 $report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
916
917 # Export report
918 Export-ComplianceReport -Report $report -Format $Format -OutputPath $OutputPath
919
920 # Export CI/CD artifacts
921 Export-CICDArtifact -Report $report -ReportPath $OutputPath
922
923 # Display summary
924 Write-PinningLog "Compliance Analysis Complete!" -Level Success
925 Write-PinningLog "Compliance Score: $($report.ComplianceScore)%" -Level Info
926 Write-PinningLog "Total Dependencies: $($report.TotalDependencies)" -Level Info
927 Write-PinningLog "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
928
929 if ($report.UnpinnedDependencies -gt 0) {
930 Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
931
932 # Check threshold compliance
933 if ($report.ComplianceScore -lt $Threshold) {
934 Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
935
936 if ($FailOnUnpinned) {
937 Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
938 throw "Compliance score $($report.ComplianceScore)% is below threshold $Threshold% (-FailOnUnpinned enabled)"
939 }
940 else {
941 Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
942 }
943 }
944 else {
945 Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
946 }
947 }
948 else {
949 Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
950 }
951}
952
953#endregion Functions
954
955#region Main Execution
956if ($MyInvocation.InvocationName -ne '.') {
957 try {
958 Invoke-DependencyPinningAnalysis `
959 -Path $Path `
960 -Recursive:$Recursive `
961 -IncludeTypes $IncludeTypes `
962 -ExcludePaths $ExcludePaths `
963 -Format $Format `
964 -OutputPath $OutputPath `
965 -FailOnUnpinned:$FailOnUnpinned `
966 -Threshold $Threshold `
967 -Remediate:$Remediate
968 exit 0
969 }
970 catch {
971 Write-Error -ErrorAction Continue "Test-DependencyPinning failed: $($_.Exception.Message)"
972 Write-CIAnnotation -Message $_.Exception.Message -Level Error
973 exit 1
974 }
975}
976#endregion Main Execution
977