microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/security/Test-DependencyPinning.ps1

810lines · 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 # Use duck typing to avoid class type collision during code coverage instrumentation
535 $Report,
536 [string]$Format,
537 [string]$OutputPath
538 )
539
540 # Validate required properties on duck-typed $Report parameter (ComplianceReport schema)
541 $requiredProperties = @('ComplianceScore', 'Violations', 'TotalDependencies', 'UnpinnedDependencies', 'Metadata')
542 foreach ($prop in $requiredProperties) {
543 if ($null -eq $Report.PSObject.Properties[$prop]) {
544 throw "Report object missing required property: $prop"
545 }
546 }
547
548 # Ensure parent directory exists
549 $parentDir = Split-Path -Path $OutputPath -Parent
550 if ($parentDir -and -not (Test-Path $parentDir)) {
551 New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
552 }
553
554 switch ($Format.ToLower()) {
555 'json' {
556 $Report | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
557 }
558
559 'sarif' {
560 $sarif = @{
561 version = "2.1.0"
562 "`$schema" = "https://json.schemastore.org/sarif-2.1.0.json"
563 runs = @(@{
564 tool = @{
565 driver = @{
566 name = "dependency-pinning-analyzer"
567 version = "1.0.0"
568 informationUri = "https://github.com/microsoft/hve-core"
569 }
570 }
571 results = @($Report.Violations | ForEach-Object {
572 @{
573 ruleId = "dependency-not-pinned"
574 level = switch ($_.Severity) { 'High' { 'error' } 'Medium' { 'warning' } default { 'note' } }
575 message = @{ text = $_.Description }
576 locations = @(@{
577 physicalLocation = @{
578 artifactLocation = @{ uri = $_.File }
579 region = @{ startLine = $_.Line }
580 }
581 })
582 properties = @{
583 dependencyName = $_.Name
584 currentVersion = $_.Version
585 remediation = $_.Remediation
586 }
587 }
588 })
589 })
590 }
591 $sarif | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
592 }
593
594 'csv' {
595 $Report.Violations | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
596 }
597
598 'markdown' {
599 $markdown = @"
600# Dependency Pinning Compliance Report
601
602**Scan Date:** $($Report.Timestamp.ToString('yyyy-MM-dd HH:mm:ss'))
603**Scan Path:** $($Report.ScanPath)
604**Compliance Score:** $($Report.ComplianceScore)%
605
606## Summary
607
608| Metric | Count |
609|--------|--------|
610| Total Files Scanned | $($Report.ScannedFiles) |
611| Total Dependencies | $($Report.TotalDependencies) |
612| Pinned Dependencies | $($Report.PinnedDependencies) |
613| Unpinned Dependencies | $($Report.UnpinnedDependencies) |
614
615## Violations by Type
616
617"@
618 foreach ($type in $Report.Summary.Keys) {
619 $summary = $Report.Summary[$type]
620 $markdown += @"
621
622### $type
623- **Total:** $($summary.Total)
624- **High Severity:** $($summary.High)
625- **Medium Severity:** $($summary.Medium)
626- **Low Severity:** $($summary.Low)
627
628"@
629 }
630
631 if ($Report.Violations.Count -gt 0) {
632 $markdown += @"
633
634## Detailed Violations
635
636| File | Line | Type | Dependency | Current Version | Severity | Remediation |
637|------|------|------|------------|----------------|----------|-------------|
638"@
639 foreach ($violation in $Report.Violations) {
640 $markdown += "|$($violation.File)|$($violation.Line)|$($violation.Type)|$($violation.Name)|$($violation.Version)|$($violation.Severity)|$($violation.Remediation)|`n"
641 }
642 }
643
644 $markdown | Out-File -FilePath $OutputPath -Encoding UTF8
645 }
646
647 'table' {
648 # Display formatted table to console and save simple text format
649 if ($Report.Violations.Count -gt 0) {
650 $Report.Violations | Format-Table -Property File, Line, Type, Name, Version, Severity -AutoSize | Out-File -FilePath $OutputPath -Encoding UTF8 -Width 200
651 }
652 else {
653 "No dependency pinning violations found." | Out-File -FilePath $OutputPath -Encoding UTF8
654 }
655 }
656 }
657
658 Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
659}
660
661function Export-CICDArtifact {
662 <#
663 .SYNOPSIS
664 Exports compliance report as CI/CD artifacts for both GitHub Actions and Azure DevOps.
665 #>
666 param(
667 [ComplianceReport]$Report,
668 [string]$ReportPath
669 )
670
671 Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
672
673 # GitHub Actions artifact export
674 if ($env:GITHUB_ACTIONS -eq 'true') {
675 Write-PinningLog "Detected GitHub Actions environment - setting up artifacts" -Level Info
676
677 # Set outputs for GitHub Actions
678 if ($env:GITHUB_OUTPUT) {
679 "dependency-report=$ReportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
680 "compliance-score=$($Report.ComplianceScore)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
681 "unpinned-count=$($Report.UnpinnedDependencies)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
682 }
683
684 # Set up for actions/upload-artifact@v4
685 $artifactDir = Join-Path $PWD "dependency-pinning-artifacts"
686 New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
687 Copy-Item -Path $ReportPath -Destination $artifactDir -Force
688
689 # Create GitHub summary
690 $summaryPath = Join-Path $artifactDir "github-summary.md"
691 @"
692# 📌 Dependency Pinning Analysis
693
694**Compliance Score:** $($Report.ComplianceScore)%
695**Unpinned Dependencies:** $($Report.UnpinnedDependencies)
696**Total Dependencies Scanned:** $($Report.TotalDependencies)
697
698$(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!" })
699"@ | Out-File -FilePath $summaryPath -Encoding UTF8
700
701 if ($env:GITHUB_STEP_SUMMARY) {
702 Copy-Item -Path $summaryPath -Destination $env:GITHUB_STEP_SUMMARY -Force
703 }
704 }
705
706 # Azure DevOps artifact export
707 if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') {
708 Write-PinningLog "Detected Azure DevOps environment - setting up artifacts" -Level Info
709
710 # Set Azure DevOps variables
711 Write-Output "##vso[task.setvariable variable=dependencyReport;isOutput=true]$ReportPath"
712 Write-Output "##vso[task.setvariable variable=complianceScore;isOutput=true]$($Report.ComplianceScore)"
713 Write-Output "##vso[task.setvariable variable=unpinnedCount;isOutput=true]$($Report.UnpinnedDependencies)"
714
715 # Publish as pipeline artifact
716 Write-Output "##vso[artifact.upload containerfolder=dependency-pinning;artifactname=dependency-pinning-report]$ReportPath"
717
718 # Add to build summary
719 Write-Output "##[section]Dependency Pinning Compliance Report"
720 Write-Output "Compliance Score: $($Report.ComplianceScore)%"
721 Write-Output "Unpinned Dependencies: $($Report.UnpinnedDependencies)"
722 Write-Output "Total Dependencies: $($Report.TotalDependencies)"
723 }
724
725 Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
726}
727
728#region Script Entry Point
729
730# Only execute when invoked directly (not dot-sourced)
731if ($MyInvocation.InvocationName -ne '.') {
732 try {
733 Write-PinningLog "Starting dependency pinning compliance analysis..." -Level Info
734 Write-PinningLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
735 Write-PinningLog "Platform: $($PSVersionTable.Platform)" -Level Info
736
737 # Parse include types and exclude paths
738 $typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
739 $excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
740
741 Write-PinningLog "Scanning path: $Path" -Level Info
742 Write-PinningLog "Include types: $($typesToCheck -join ', ')" -Level Info
743 if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
744
745 # Discover files to scan
746 $filesToScan = Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive
747 Write-PinningLog "Found $($filesToScan.Count) files to scan" -Level Info
748
749 # Scan for violations
750 $allViolations = @()
751 foreach ($fileInfo in $filesToScan) {
752 Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
753 $violations = Get-DependencyViolation -FileInfo $fileInfo
754
755 # Add remediation suggestions
756 foreach ($violation in $violations) {
757 $violation.Remediation = Get-RemediationSuggestion -Violation $violation -Remediate:$Remediate
758 }
759
760 $allViolations += $violations
761 }
762
763 Write-PinningLog "Found $($allViolations.Count) dependency pinning violations" -Level Info
764
765 # Generate compliance report
766 $report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate
767
768 # Export report
769 Export-ComplianceReport -Report $report -Format $Format -OutputPath $OutputPath
770
771 # Export CI/CD artifacts
772 Export-CICDArtifact -Report $report -ReportPath $OutputPath
773
774 # Display summary
775 Write-PinningLog "Compliance Analysis Complete!" -Level Success
776 Write-PinningLog "Compliance Score: $($report.ComplianceScore)%" -Level Info
777 Write-PinningLog "Total Dependencies: $($report.TotalDependencies)" -Level Info
778 Write-PinningLog "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
779
780 if ($report.UnpinnedDependencies -gt 0) {
781 Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
782
783 # Check threshold compliance
784 if ($report.ComplianceScore -lt $Threshold) {
785 Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
786
787 if ($FailOnUnpinned) {
788 Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
789 exit 1
790 }
791 else {
792 Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
793 }
794 }
795 else {
796 Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
797 }
798 }
799 else {
800 Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
801 }
802 }
803 catch {
804 Write-PinningLog "Dependency pinning analysis failed: $($_.Exception.Message)" -Level Error
805 Write-PinningLog "Stack trace: $($_.ScriptStackTrace)" -Level Error
806 exit 1
807 }
808}
809
810#endregion
811