Skip to content

Commit 627a877

Browse files
refactor(scripts): consolidate duplicate logging into shared SecurityHelpers module (#655)
## Summary Consolidates duplicate logging functions into the shared `SecurityHelpers.psm1` module, eliminating code duplication across security scripts. Closes #321 ## Changes ### `SecurityHelpers.psm1` - Import `CIHelpers.psm1` for CI annotation support - Add `-CIAnnotation` switch parameter to `Write-SecurityLog` for opt-in CI annotation forwarding (Warning/Error levels) - Change Info color from White to Cyan to match script conventions ### `Test-DependencyPinning.ps1` - Import `SecurityHelpers.psm1` - Remove local `Write-PinningLog` function (32 lines) - Replace all ~25 `Write-PinningLog` call sites with `Write-SecurityLog -CIAnnotation` ### `Test-SHAStaleness.ps1` - Import `SecurityHelpers.psm1` - Remove local `Write-SecurityLog` function (33 lines) - Add `$PSDefaultParameterValues` to route `OutputFormat` and `LogPath` parameters transparently ### Test files - Update `Test-DependencyPinning.Tests.ps1` mocks and context names from `Write-PinningLog` to `Write-SecurityLog` - Add 4 CI annotation forwarding tests to `SecurityHelpers.Tests.ps1` ## Validation - **Tests**: 900 passed, 0 failed, 0 skipped - **Lint**: 52 files analyzed, 0 issues --- ♻️ - Generated by Copilot
1 parent cc92ef9 commit 627a877

6 files changed

Lines changed: 101 additions & 102 deletions

File tree

scripts/security/Modules/SecurityHelpers.psm1

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
#Requires -Version 7.0
1111

12+
# Omit -Force so the standalone CIHelpers export is not shadowed by a nested re-import.
13+
Import-Module (Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1')
14+
1215
function Write-SecurityLog {
1316
<#
1417
.SYNOPSIS
@@ -33,8 +36,14 @@ function Write-SecurityLog {
3336
.EXAMPLE
3437
Write-SecurityLog -Message "Scanning workflows" -Level Info
3538
39+
.PARAMETER CIAnnotation
40+
When set, forwards Warning and Error messages as CI annotations via Write-CIAnnotation.
41+
3642
.EXAMPLE
3743
Write-SecurityLog -Message "Stale SHA detected" -Level Warning -LogPath "./logs/security.log"
44+
45+
.EXAMPLE
46+
Write-SecurityLog -Message "Not pinned" -Level Warning -CIAnnotation
3847
#>
3948
[CmdletBinding()]
4049
param(
@@ -50,7 +59,10 @@ function Write-SecurityLog {
5059
[string]$LogPath,
5160

5261
[Parameter()]
53-
[string]$OutputFormat = 'console'
62+
[string]$OutputFormat = 'console',
63+
64+
[Parameter()]
65+
[switch]$CIAnnotation
5466
)
5567

5668
# Handle blank line requests
@@ -67,7 +79,7 @@ function Write-SecurityLog {
6779
# Console output with colors
6880
if ($OutputFormat -eq 'console') {
6981
$color = switch ($Level) {
70-
'Info' { 'White' }
82+
'Info' { 'Cyan' }
7183
'Warning' { 'Yellow' }
7284
'Error' { 'Red' }
7385
'Success' { 'Green' }
@@ -77,6 +89,11 @@ function Write-SecurityLog {
7789
Write-Host $logEntry -ForegroundColor $color
7890
}
7991

92+
# Forward warnings and errors as CI annotations
93+
if ($CIAnnotation -and ($Level -eq 'Warning' -or $Level -eq 'Error')) {
94+
Write-CIAnnotation -Message $Message -Level $Level
95+
}
96+
8097
# File logging if path provided
8198
if ($LogPath) {
8299
try {

scripts/security/Test-DependencyPinning.ps1

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ $ErrorActionPreference = 'Stop'
123123

124124
# Import CIHelpers for workflow command escaping
125125
Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
126+
Import-Module (Join-Path $PSScriptRoot 'Modules/SecurityHelpers.psm1') -Force
126127

127128
# Define dependency patterns for different ecosystems
128129
$DependencyPatterns = @{
@@ -307,35 +308,6 @@ function Get-NpmDependencyViolations {
307308
return $violations
308309
}
309310

310-
function 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-
339311
function Get-FilesToScan {
340312
<#
341313
.SYNOPSIS
@@ -383,7 +355,7 @@ function Get-FilesToScan {
383355
}
384356
}
385357
catch {
386-
Write-PinningLog "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
358+
Write-SecurityLog -CIAnnotation "Error scanning for $type files with pattern $pattern`: $($_.Exception.Message)" -Level Warning
387359
}
388360
}
389361
}
@@ -510,7 +482,7 @@ function Get-DependencyViolation {
510482
}
511483
}
512484
catch {
513-
Write-PinningLog "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
485+
Write-SecurityLog -CIAnnotation "Error scanning file $filePath`: $($_.Exception.Message)" -Level Warning
514486
}
515487

516488
return $violations
@@ -561,7 +533,7 @@ function Get-RemediationSuggestion {
561533
}
562534
}
563535
catch {
564-
Write-PinningLog "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
536+
Write-SecurityLog -CIAnnotation "Could not generate automatic remediation for $($Violation.Name): $($_.Exception.Message)" -Level Warning
565537
}
566538

567539
return "Manually research and pin to immutable reference"
@@ -757,7 +729,7 @@ function Export-ComplianceReport {
757729
}
758730
}
759731

760-
Write-PinningLog "Compliance report exported to: $OutputPath" -Level Success
732+
Write-SecurityLog -CIAnnotation "Compliance report exported to: $OutputPath" -Level Success
761733
}
762734

763735
function Export-CICDArtifact {
@@ -771,10 +743,10 @@ function Export-CICDArtifact {
771743
[string]$ReportPath
772744
)
773745

774-
Write-PinningLog "Preparing compliance artifacts for CI/CD systems..." -Level Info
746+
Write-SecurityLog -CIAnnotation "Preparing compliance artifacts for CI/CD systems..." -Level Info
775747

776748
$platform = Get-CIPlatform
777-
Write-PinningLog "Detected $platform environment - setting up artifacts" -Level Info
749+
Write-SecurityLog -CIAnnotation "Detected $platform environment - setting up artifacts" -Level Info
778750

779751
# Set CI outputs (works for both GitHub Actions and Azure DevOps)
780752
Set-CIOutput -Name 'dependency-report' -Value $ReportPath -IsOutput
@@ -805,7 +777,7 @@ $(if ($Report.UnpinnedDependencies -gt 0) { "⚠️ **Action Required:** $($Repo
805777
Copy-Item -Path $ReportPath -Destination $artifactDir -Force
806778
}
807779

808-
Write-PinningLog "Compliance artifacts prepared for CI/CD consumption" -Level Success
780+
Write-SecurityLog -CIAnnotation "Compliance artifacts prepared for CI/CD consumption" -Level Success
809781
}
810782

811783
function Invoke-DependencyPinningAnalysis {
@@ -844,26 +816,26 @@ function Invoke-DependencyPinningAnalysis {
844816
[switch]$Remediate
845817
)
846818

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
819+
Write-SecurityLog -CIAnnotation "Starting dependency pinning compliance analysis..." -Level Info
820+
Write-SecurityLog -CIAnnotation "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
821+
Write-SecurityLog -CIAnnotation "Platform: $($PSVersionTable.Platform)" -Level Info
850822

851823
# Parse include types and exclude paths
852824
$typesToCheck = $IncludeTypes.Split(',') | ForEach-Object { $_.Trim() }
853825
$excludePatterns = if ($ExcludePaths) { $ExcludePaths.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
854826

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 }
827+
Write-SecurityLog -CIAnnotation "Scanning path: $Path" -Level Info
828+
Write-SecurityLog -CIAnnotation "Include types: $($typesToCheck -join ', ')" -Level Info
829+
if ($excludePatterns) { Write-SecurityLog -CIAnnotation "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
858830

859831
# Discover files to scan
860832
$filesToScan = @(Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive)
861-
Write-PinningLog "Found $(@($filesToScan).Count) files to scan" -Level Info
833+
Write-SecurityLog -CIAnnotation "Found $(@($filesToScan).Count) files to scan" -Level Info
862834

863835
# Scan for violations
864836
$allViolations = @()
865837
foreach ($fileInfo in $filesToScan) {
866-
Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
838+
Write-SecurityLog -CIAnnotation "Scanning: $($fileInfo.RelativePath)" -Level Info
867839
$violations = @(Get-DependencyViolation -FileInfo $fileInfo)
868840

869841
# Add remediation suggestions
@@ -874,7 +846,7 @@ function Invoke-DependencyPinningAnalysis {
874846
$allViolations += $violations
875847
}
876848

877-
Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
849+
Write-SecurityLog -CIAnnotation "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
878850

879851
# Emit per-violation CI annotations and console output
880852
if ($allViolations.Count -gt 0) {
@@ -921,32 +893,32 @@ function Invoke-DependencyPinningAnalysis {
921893
Export-CICDArtifact -Report $report -ReportPath $OutputPath
922894

923895
# 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
896+
Write-SecurityLog -CIAnnotation "Compliance Analysis Complete!" -Level Success
897+
Write-SecurityLog -CIAnnotation "Compliance Score: $($report.ComplianceScore)%" -Level Info
898+
Write-SecurityLog -CIAnnotation "Total Dependencies: $($report.TotalDependencies)" -Level Info
899+
Write-SecurityLog -CIAnnotation "Unpinned Dependencies: $($report.UnpinnedDependencies)" -Level Info
928900

929901
if ($report.UnpinnedDependencies -gt 0) {
930-
Write-PinningLog "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
902+
Write-SecurityLog -CIAnnotation "$($report.UnpinnedDependencies) dependencies require SHA pinning for security compliance" -Level Warning
931903

932904
# Check threshold compliance
933905
if ($report.ComplianceScore -lt $Threshold) {
934-
Write-PinningLog "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
906+
Write-SecurityLog -CIAnnotation "Compliance score $($report.ComplianceScore)% is below threshold $Threshold%" -Level Error
935907

936908
if ($FailOnUnpinned) {
937-
Write-PinningLog "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
909+
Write-SecurityLog -CIAnnotation "Failing build due to compliance threshold violation (-FailOnUnpinned enabled)" -Level Error
938910
throw "Compliance score $($report.ComplianceScore)% is below threshold $Threshold% (-FailOnUnpinned enabled)"
939911
}
940912
else {
941-
Write-PinningLog "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
913+
Write-SecurityLog -CIAnnotation "Threshold violation detected but continuing (soft-fail mode)" -Level Warning
942914
}
943915
}
944916
else {
945-
Write-PinningLog "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
917+
Write-SecurityLog -CIAnnotation "Compliance score $($report.ComplianceScore)% meets threshold $Threshold%" -Level Info
946918
}
947919
}
948920
else {
949-
Write-PinningLog "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
921+
Write-SecurityLog -CIAnnotation "All dependencies are properly pinned! ✅ (100% compliance, exceeds $Threshold% threshold)" -Level Success
950922
}
951923
}
952924

scripts/security/Test-SHAStaleness.ps1

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -77,46 +77,15 @@ $ErrorActionPreference = 'Stop'
7777

7878
# Import CIHelpers for workflow command escaping
7979
Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
80+
Import-Module (Join-Path $PSScriptRoot 'Modules/SecurityHelpers.psm1') -Force
81+
82+
# Route Write-SecurityLog output through script-scoped format and log path
83+
$PSDefaultParameterValues['Write-SecurityLog:OutputFormat'] = $OutputFormat
84+
$PSDefaultParameterValues['Write-SecurityLog:LogPath'] = $LogPath
8085

8186
# Script-scope collection of stale dependencies (used by multiple functions)
8287
$script:StaleDependencies = @()
8388

84-
function Write-SecurityLog {
85-
param(
86-
[Parameter(Mandatory = $true)]
87-
[string]$Message,
88-
89-
[Parameter(Mandatory = $false)]
90-
[ValidateSet("Info", "Warning", "Error", "Success")]
91-
[string]$Level = "Info"
92-
)
93-
94-
if ([string]::IsNullOrWhiteSpace($Message)) {
95-
$Message = "Empty log message"
96-
}
97-
98-
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
99-
$logEntry = "[$timestamp] [$Level] $Message"
100-
101-
# Console output with colors (only in console mode)
102-
if ($OutputFormat -eq "console") {
103-
switch ($Level) {
104-
"Info" { Write-Host $logEntry -ForegroundColor Cyan }
105-
"Warning" { Write-Host $logEntry -ForegroundColor Yellow }
106-
"Error" { Write-Host $logEntry -ForegroundColor Red }
107-
"Success" { Write-Host $logEntry -ForegroundColor Green }
108-
}
109-
}
110-
111-
# File logging
112-
try {
113-
Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
114-
}
115-
catch {
116-
Write-Error "Failed to write to log file: $($_.Exception.Message)" -ErrorAction SilentlyContinue
117-
}
118-
}
119-
12089
function Test-GitHubToken {
12190
param(
12291
[Parameter(Mandatory = $false)]

scripts/tests/security/SecurityHelpers.Tests.ps1

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,33 @@ Describe 'Write-SecurityLog' -Tag 'Unit' {
103103
}
104104
}
105105
}
106+
107+
Context 'CI annotation forwarding' {
108+
BeforeAll {
109+
Mock Write-CIAnnotation {} -ModuleName SecurityHelpers
110+
Mock Write-Host {} -ModuleName SecurityHelpers
111+
}
112+
113+
It 'Forwards Warning messages as CI annotations when -CIAnnotation is set' {
114+
Write-SecurityLog -Message 'Test warning' -Level Warning -CIAnnotation
115+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -ParameterFilter { $Level -eq 'Warning' -and $Message -eq 'Test warning' } -Times 1 -Exactly
116+
}
117+
118+
It 'Forwards Error messages as CI annotations when -CIAnnotation is set' {
119+
Write-SecurityLog -Message 'Test error' -Level Error -CIAnnotation
120+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -ParameterFilter { $Level -eq 'Error' -and $Message -eq 'Test error' } -Times 1 -Exactly
121+
}
122+
123+
It 'Does not forward Info messages as CI annotations' {
124+
Write-SecurityLog -Message 'Test info' -Level Info -CIAnnotation
125+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -Times 0
126+
}
127+
128+
It 'Does not forward any messages when -CIAnnotation is not set' {
129+
Write-SecurityLog -Message 'No annotation' -Level Warning
130+
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -Times 0
131+
}
132+
}
106133
}
107134

108135
Describe 'New-SecurityIssue' -Tag 'Unit' {

0 commit comments

Comments
 (0)