microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v1.1.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

796lines · 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', '**/package-lock.json')
135 VersionPatterns = @(
136 @{
137 Pattern = '"([^"]+)":\s*"([^@][^"]*)"'
138 Groups = @{ Package = 1; Version = 2 }
139 Description = 'NPM dependencies in package.json'
140 }
141 )
142 SHAPattern = '^[a-fA-F0-9]{40}$'
143 RemediationUrl = 'https://registry.npmjs.org/{0}/{1}'
144 }
145
146 'pip' = @{
147 FilePatterns = @('**/requirements*.txt', '**/Pipfile', '**/pyproject.toml', '**/setup.py')
148 VersionPatterns = @(
149 @{
150 Pattern = '([a-zA-Z0-9\-_]+)==([^#\s]+)'
151 Groups = @{ Package = 1; Version = 2 }
152 Description = 'Python pip requirements'
153 }
154 )
155 SHAPattern = '^[a-fA-F0-9]{40}$'
156 RemediationUrl = 'https://pypi.org/pypi/{0}/{1}/json'
157 }
158
159 'shell-downloads' = @{
160 FilePatterns = @('**/.devcontainer/scripts/*.sh', '**/scripts/*.sh')
161 ValidationFunc = 'Test-ShellDownloadSecurity'
162 Description = 'Shell script downloads must include checksum verification'
163 }
164}
165
166class DependencyViolation {
167 [string]$File
168 [int]$Line
169 [string]$Type
170 [string]$Name
171 [string]$Version
172 [string]$CurrentRef
173 [string]$Severity
174 [string]$Description
175 [string]$Remediation
176 [hashtable]$Metadata
177
178 DependencyViolation() {
179 $this.Metadata = @{}
180 }
181}
182
183class ComplianceReport {
184 [string]$ScanPath
185 [datetime]$Timestamp
186 [int]$TotalFiles
187 [int]$ScannedFiles
188 [int]$TotalDependencies
189 [int]$PinnedDependencies
190 [int]$UnpinnedDependencies
191 [decimal]$ComplianceScore
192 [DependencyViolation[]]$Violations
193 [hashtable]$Summary
194 [hashtable]$Metadata
195
196 ComplianceReport() {
197 $this.Timestamp = Get-Date
198 $this.Violations = @()
199 $this.Summary = @{}
200 $this.Metadata = @{}
201 }
202}
203
204function Test-ShellDownloadSecurity {
205 <#
206 .SYNOPSIS
207 Scans shell scripts for curl/wget downloads lacking checksum verification.
208
209 .DESCRIPTION
210 Analyzes shell scripts to detect download commands (curl/wget) that do not
211 have corresponding checksum verification (sha256sum/shasum) within the
212 following lines.
213
214 .PARAMETER FilePath
215 Path to the shell script file to scan.
216 #>
217 [CmdletBinding()]
218 param(
219 [Parameter(Mandatory)]
220 [string]$FilePath
221 )
222
223 if (-not (Test-Path $FilePath)) {
224 return @()
225 }
226
227 $lines = Get-Content $FilePath
228 $violations = @()
229
230 # Pattern to match curl/wget download commands
231 $downloadPattern = '(curl|wget)\s+.*https?://[^\s]+'
232 $checksumPattern = 'sha256sum|shasum|Get-FileHash|openssl\s+dgst\s+-sha256|sha256sum\s+-c'
233
234 for ($i = 0; $i -lt $lines.Count; $i++) {
235 $line = $lines[$i]
236 if ($line -match $downloadPattern) {
237 # Check next 5 lines for checksum verification
238 $hasChecksum = $false
239 $searchEnd = [Math]::Min($i + 5, $lines.Count - 1)
240
241 for ($j = $i; $j -le $searchEnd; $j++) {
242 if ($lines[$j] -match $checksumPattern) {
243 $hasChecksum = $true
244 break
245 }
246 }
247
248 if (-not $hasChecksum) {
249 $violations += [PSCustomObject]@{
250 File = $FilePath
251 Line = $i + 1
252 Pattern = $line.Trim()
253 Issue = 'Download without checksum verification'
254 Severity = 'warning'
255 Ecosystem = 'shell-downloads'
256 }
257 }
258 }
259 }
260
261 return $violations
262}
263
264function Write-PinningLog {
265 param(
266 [Parameter(Mandatory = $true)]
267 [string]$Message,
268
269 [Parameter(Mandatory = $false)]
270 [ValidateSet('Info', 'Warning', 'Error', 'Success')]
271 [string]$Level = 'Info'
272 )
273
274 $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
275 Write-Output "[$timestamp] [$Level] $Message"
276}
277
278function Get-FilesToScan {
279 <#
280 .SYNOPSIS
281 Discovers files to scan based on dependency type patterns.
282 #>
283 param(
284 [string]$ScanPath,
285 [string[]]$Types,
286 [string[]]$ExcludePatterns,
287 [switch]$Recursive
288 )
289
290 $allFiles = @()
291
292 foreach ($type in $Types) {
293 if ($DependencyPatterns.ContainsKey($type)) {
294 $patterns = $DependencyPatterns[$type].FilePatterns
295
296 foreach ($pattern in $patterns) {
297 # Convert glob pattern to PowerShell-compatible path
298 $searchPath = Join-Path $ScanPath $pattern
299
300 try {
301 if ($Recursive) {
302 $files = Get-ChildItem -Path $searchPath -Recurse -File -ErrorAction SilentlyContinue
303 }
304 else {
305 $files = Get-ChildItem -Path $searchPath -File -ErrorAction SilentlyContinue
306 }
307
308 # Apply exclusion filters
309 if ($ExcludePatterns) {
310 foreach ($exclude in $ExcludePatterns) {
311 $files = $files | Where-Object { $_.FullName -notlike "*$exclude*" }
312 }
313 }
314
315 $allFiles += $files | ForEach-Object {
316 @{
317 Path = $_.FullName
318 Type = $type
319 RelativePath = [System.IO.Path]::GetRelativePath($ScanPath, $_.FullName)
320 }
321 }
322 }
323 catch {
324 Write-PinningLog "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
325 }
326 }
327 }
328 }
329
330 return $allFiles | Sort-Object Path -Unique
331}
332
333function Test-SHAPinning {
334 <#
335 .SYNOPSIS
336 Tests if a version reference is properly SHA-pinned.
337 #>
338 param(
339 [string]$Version,
340 [string]$Type
341 )
342
343 if ($DependencyPatterns.ContainsKey($Type) -and $DependencyPatterns[$Type].SHAPattern) {
344 $shaPattern = $DependencyPatterns[$Type].SHAPattern
345 return $Version -match $shaPattern
346 }
347
348 return $false
349}
350
351function Get-DependencyViolation {
352 <#
353 .SYNOPSIS
354 Scans a file for dependency pinning violations.
355 #>
356 param(
357 [hashtable]$FileInfo
358 )
359
360 $violations = @()
361 $filePath = $FileInfo.Path
362 $fileType = $FileInfo.Type
363
364 if (!(Test-Path $filePath)) {
365 return $violations
366 }
367
368 try {
369 $content = Get-Content -Path $filePath -Raw
370 $lines = Get-Content -Path $filePath
371
372 $patterns = $DependencyPatterns[$fileType].VersionPatterns
373
374 foreach ($patternInfo in $patterns) {
375 $pattern = $patternInfo.Pattern
376 $description = $patternInfo.Description
377
378 $regexMatches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
379
380 foreach ($match in $regexMatches) {
381 # Find line number
382 $lineNumber = 1
383 $position = $match.Index
384 for ($i = 0; $i -lt $position; $i++) {
385 if ($content[$i] -eq "`n") {
386 $lineNumber++
387 }
388 }
389
390 # Extract dependency information
391 $dependencyName = $match.Groups[1].Value
392 $version = $match.Groups[2].Value
393
394 # Check if properly pinned
395 if (!(Test-SHAPinning -Version $version -Type $fileType)) {
396 $violation = [DependencyViolation]::new()
397 $violation.File = $FileInfo.RelativePath
398 $violation.Line = $lineNumber
399 $violation.Type = $fileType
400 $violation.Name = $dependencyName
401 $violation.Version = $version
402 $violation.CurrentRef = $match.Value
403 $violation.Description = "Unpinned dependency: $description"
404 $violation.Severity = if ($fileType -eq 'github-actions') { 'High' } else { 'Medium' }
405 $violation.Metadata['PatternDescription'] = $description
406 $violation.Metadata['LineContent'] = $lines[$lineNumber - 1]
407
408 $violations += $violation
409 }
410 }
411 }
412 }
413 catch {
414 Write-PinningLog "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
415 }
416
417 return $violations
418}
419
420function Get-RemediationSuggestion {
421 <#
422 .SYNOPSIS
423 Generates remediation suggestions for unpinned dependencies.
424 #>
425 param(
426 [DependencyViolation]$Violation,
427
428 [switch]$Remediate
429 )
430
431 $type = $Violation.Type
432 $name = $Violation.Name
433 $version = $Violation.Version
434
435 if (!$Remediate) {
436 return "Enable -Remediate flag for specific SHA suggestions"
437 }
438
439 try {
440 switch ($type) {
441 'github-actions' {
442 # For GitHub Actions, resolve tag to commit SHA
443 $apiUrl = "https://api.github.com/repos/$name/commits/$version"
444 $headers = @{}
445
446 if ($env:GITHUB_TOKEN) {
447 $headers['Authorization'] = "Bearer $env:GITHUB_TOKEN"
448 }
449
450 $response = Invoke-RestMethod -Uri $apiUrl -Headers $headers -TimeoutSec 30
451 $sha = $response.sha
452
453 if ($sha) {
454 return "Pin to SHA: uses: $name@$sha # $version"
455 }
456 }
457
458 default {
459 return "Research and pin to specific commit SHA or content hash for $type dependencies"
460 }
461 }
462 }
463 catch {
464 Write-PinningLog "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
465 }
466
467 return "Manually research and pin to immutable reference"
468}
469
470function Get-ComplianceReportData {
471 <#
472 .SYNOPSIS
473 Generates a comprehensive compliance report.
474 #>
475 param(
476 [DependencyViolation[]]$Violations,
477 [hashtable[]]$ScannedFiles,
478 [string]$ScanPath,
479 [switch]$Remediate
480 )
481
482 $report = [ComplianceReport]::new()
483 $report.ScanPath = $ScanPath
484 $report.ScannedFiles = $ScannedFiles.Count
485 $report.Violations = $Violations
486
487 # Calculate metrics
488 $totalDeps = ($Violations | Measure-Object).Count
489 $unpinnedDeps = ($Violations | Where-Object { $_.Severity -ne 'Info' } | Measure-Object).Count
490 $pinnedDeps = $totalDeps - $unpinnedDeps
491
492 $report.TotalDependencies = $totalDeps
493 $report.PinnedDependencies = $pinnedDeps
494 $report.UnpinnedDependencies = $unpinnedDeps
495
496 if ($totalDeps -gt 0) {
497 $report.ComplianceScore = [math]::Round(($pinnedDeps / $totalDeps) * 100, 2)
498 }
499 else {
500 $report.ComplianceScore = 100.0
501 }
502
503 # Generate summary by type
504 $report.Summary = @{}
505 foreach ($type in ($Violations | Group-Object Type)) {
506 $report.Summary[$type.Name] = @{
507 Total = $type.Count
508 High = ($type.Group | Where-Object { $_.Severity -eq 'High' } | Measure-Object).Count
509 Medium = ($type.Group | Where-Object { $_.Severity -eq 'Medium' } | Measure-Object).Count
510 Low = ($type.Group | Where-Object { $_.Severity -eq 'Low' } | Measure-Object).Count
511 }
512 }
513
514 # Add metadata
515 $report.Metadata = @{
516 PowerShellVersion = $PSVersionTable.PSVersion.ToString()
517 Platform = $PSVersionTable.Platform
518 ScanTimestamp = $report.Timestamp.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
519 IncludedTypes = $IncludeTypes
520 ExcludedPaths = $ExcludePaths
521 RemediationEnabled = $Remediate.IsPresent
522 ComplianceThreshold = $Threshold
523 }
524
525 return $report
526}
527
528function Export-ComplianceReport {
529 <#
530 .SYNOPSIS
531 Exports compliance report in specified format.
532 #>
533 param(
534 [ComplianceReport]$Report,
535 [string]$Format,
536 [string]$OutputPath
537 )
538
539 # Ensure parent directory exists
540 $parentDir = Split-Path -Path $OutputPath -Parent
541 if ($parentDir -and -not (Test-Path $parentDir)) {
542 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
543 }
544
545 switch ($Format.ToLower()) {
546 'json' {
547 $Report | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
548 }
549
550 'sarif' {
551 $sarif = @{
552 version = "2.1.0"
553 "`$schema" = "https://json.schemastore.org/sarif-2.1.0.json"
554 runs = @(@{
555 tool = @{
556 driver = @{
557 name = "dependency-pinning-analyzer"
558 version = "1.0.0"
559 informationUri = "https://github.com/microsoft/hve-core"
560 }
561 }
562 results = @($Report.Violations | ForEach-Object {
563 @{
564 ruleId = "dependency-not-pinned"
565 level = switch ($_.Severity) { 'High' { 'error' } 'Medium' { 'warning' } default { 'note' } }
566 message = @{ text = $_.Description }
567 locations = @(@{
568 physicalLocation = @{
569 artifactLocation = @{ uri = $_.File }
570 region = @{ startLine = $_.Line }
571 }
572 })
573 properties = @{
574 dependencyName = $_.Name
575 currentVersion = $_.Version
576 remediation = $_.Remediation
577 }
578 }
579 })
580 })
581 }
582 $sarif | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
583 }
584
585 'csv' {
586 $Report.Violations | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
587 }
588
589 'markdown' {
590 $markdown = @"
591# Dependency Pinning Compliance Report
592
593**Scan Date:** $($Report.Timestamp.ToString('yyyy-MM-dd HH:mm:ss'))
594**Scan Path:** $($Report.ScanPath)
595**Compliance Score:** $($Report.ComplianceScore)%
596
597## Summary
598
599| Metric | Count |
600|--------|--------|
601| Total Files Scanned | $($Report.ScannedFiles) |
602| Total Dependencies | $($Report.TotalDependencies) |
603| Pinned Dependencies | $($Report.PinnedDependencies) |
604| Unpinned Dependencies | $($Report.UnpinnedDependencies) |
605
606## Violations by Type
607
608"@
609 foreach ($type in $Report.Summary.Keys) {
610 $summary = $Report.Summary[$type]
611 $markdown += @"
612
613### $type
614- **Total:** $($summary.Total)
615- **High Severity:** $($summary.High)
616- **Medium Severity:** $($summary.Medium)
617- **Low Severity:** $($summary.Low)
618
619"@
620 }
621
622 if ($Report.Violations.Count -gt 0) {
623 $markdown += @"
624
625## Detailed Violations
626
627| File | Line | Type | Dependency | Current Version | Severity | Remediation |
628|------|------|------|------------|----------------|----------|-------------|
629"@
630 foreach ($violation in $Report.Violations) {
631 $markdown += "|$($violation.File)|$($violation.Line)|$($violation.Type)|$($violation.Name)|$($violation.Version)|$($violation.Severity)|$($violation.Remediation)|`n"
632 }
633 }
634
635 $markdown | Out-File -FilePath $OutputPath -Encoding UTF8
636 }
637
638 'table' {
639 # Display formatted table to console and save simple text format
640 if ($Report.Violations.Count -gt 0) {
641 $Report.Violations | Format-Table -Property File, Line, Type, Name, Version, Severity -AutoSize | Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
642 }
643 else {
644 "No dependency pinning violations found." | Out-File -FilePath $OutputPath -Encoding UTF8
645 }
646 }
647 }
648
649 Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
650}
651
652function Export-CICDArtifact {
653 <#
654 .SYNOPSIS
655 Exports compliance report as CI/CD artifacts for both GitHub Actions and Azure DevOps.
656 #>
657 param(
658 [ComplianceReport]$Report,
659 [string]$ReportPath
660 )
661
662 Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
663
664 # GitHub Actions artifact export
665 if ($env:GITHUB_ACTIONS -eq 'true') {
666 Write-PinningLog "Detected GitHub Actions environment - setting up artifacts" -Level Info
667
668 # Set outputs for GitHub Actions
669 if ($env:GITHUB_OUTPUT) {
670 "dependency-report=$ReportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
671 "compliance-score=$($Report.ComplianceScore)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
672 "unpinned-count=$($Report.UnpinnedDependencies)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
673 }
674
675 # Set up for actions/upload-artifact@v4
676 $artifactDir = Join-Path $PWD "dependency-pinning-artifacts"
677 New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
678 Copy-Item -Path $ReportPath -Destination $artifactDir -Force
679
680 # Create GitHub summary
681 $summaryPath = Join-Path $artifactDir "github-summary.md"
682 @"
683# 📌 Dependency Pinning Analysis
684
685**Compliance Score:** $($Report.ComplianceScore)%
686**Unpinned Dependencies:** $($Report.UnpinnedDependencies)
687**Total Dependencies Scanned:** $($Report.TotalDependencies)
688
689$(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!" })
690"@ | Out-File -FilePath $summaryPath -Encoding UTF8
691
692 if ($env:GITHUB_STEP_SUMMARY) {
693 Copy-Item -Path $summaryPath -Destination $env:GITHUB_STEP_SUMMARY -Force
694 }
695 }
696
697 # Azure DevOps artifact export
698 if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') {
699 Write-PinningLog "Detected Azure DevOps environment - setting up artifacts" -Level Info
700
701 # Set Azure DevOps variables
702 Write-Output "##vso[task.setvariable variable=dependencyReport;isOutput=true]$ReportPath"
703 Write-Output "##vso[task.setvariable variable=complianceScore;isOutput=true]$($Report.ComplianceScore)"
704 Write-Output "##vso[task.setvariable variable=unpinnedCount;isOutput=true]$($Report.UnpinnedDependencies)"
705
706 # Publish as pipeline artifact
707 Write-Output "##vso[artifact.upload containerfolder=dependency-pinning;artifactname=dependency-pinning-report]$ReportPath"
708
709 # Add to build summary
710 Write-Output "##[section]Dependency Pinning Compliance Report"
711 Write-Output "Compliance Score: $($Report.ComplianceScore)%"
712 Write-Output "Unpinned Dependencies: $($Report.UnpinnedDependencies)"
713 Write-Output "Total Dependencies: $($Report.TotalDependencies)"
714 }
715
716 Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
717}
718
719# Main execution
720try {
721 Write-PinningLog "Starting dependency pinning compliance analysis..." -Level Info
722 Write-PinningLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
723 Write-PinningLog "Platform: $($PSVersionTable.Platform)" -Level Info
724
725 # Parse include types and exclude paths
726 $typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
727 $excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
728
729 Write-PinningLog "Scanning path: $Path" -Level Info
730 Write-PinningLog "Include types: $($typesToCheck -join ', ')" -Level Info
731 if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
732
733 # Discover files to scan
734 $filesToScan = Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive
735 Write-PinningLog "Found $($filesToScan.Count) files to scan" -Level Info
736
737 # Scan for violations
738 $allViolations = @()
739 foreach ($fileInfo in $filesToScan) {
740 Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
741 $violations = Get-DependencyViolation -FileInfo $fileInfo
742
743 # Add remediation suggestions
744 foreach ($violation in $violations) {
745 $violation.Remediation = Get-RemediationSuggestion -Violation $violation -Remediate:$Remediate
746 }
747
748 $allViolations += $violations
749 }
750
751 Write-PinningLog "Found $($allViolations.Count) dependency pinning violations" -Level Info
752
753 # Generate compliance report
754 $report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
755
756 # Export report
757 Export-ComplianceReport -Report $report -Format $Format -OutputPath $OutputPath
758
759 # Export CI/CD artifacts
760 Export-CICDArtifact -Report $report -ReportPath $OutputPath
761
762 # Display summary
763 Write-PinningLog "Compliance Analysis Complete!" -Level Success
764 Write-PinningLog "Compliance Score: $($report.ComplianceScore)%" -Level Info
765 Write-PinningLog "Total Dependencies: $($report.TotalDependencies)" -Level Info
766 Write-PinningLog "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
767
768 if ($report.UnpinnedDependencies -gt 0) {
769 Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
770
771 # Check threshold compliance
772 if ($report.ComplianceScore -lt $Threshold) {
773 Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
774
775 if ($FailOnUnpinned) {
776 Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
777 exit 1
778 }
779 else {
780 Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
781 }
782 }
783 else {
784 Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
785 }
786 }
787 else {
788 Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
789 }
790
791}
792catch {
793 Write-PinningLog "Dependency pinning analysis failed: $($_.Exception.Message)" -Level Error
794 Write-PinningLog "Stack trace: $($_.ScriptStackTrace)" -Level Error
795 exit 1
796}