Skip to content

Commit de43e73

Browse files
fix(scripts): ensure reliable array count operations in linting and security scripts (#395)
## Description This PR fixes PowerShell array handling issues in linting and security validation scripts by ensuring array assignments and count operations use proper array coercion with `@()`. These changes prevent potential errors when pipeline operations return null or single items instead of arrays. - **fix(linting)**: wrapped array assignments in Invoke-PSScriptAnalyzer, Invoke-YamlLint, and Markdown-Link-Check with `@()` to ensure consistent array behavior - Applied to file collection calls from `Get-ChangedFilesFromGit` and `Get-FilesRecursive` - Added array coercion to all `.Count` property accesses for reliable zero-count checks - **fix(linting)**: added array coercion to count checks in Validate-MarkdownFrontmatter - Wrapped `Get-ChangedMarkdownFileGroup` result with `@()` for consistent array handling - Applied to count checks to prevent null reference errors - **fix(security)**: improved array handling in Test-DependencyPinning script - Added `@()` wrapping to violation collection and grouping operations - Applied to measure-object count operations and group-by filtering - **fix(security)**: enhanced array coercion in Test-SHAStaleness script - Wrapped all count checks and array operations with `@()` for reliable behavior - Added initialization of `$script:StaleDependencies` as empty array - Applied to tool staleness detection and error collection ## Related Issue(s) Fixes [#394](#394) ## Type of Change Select all that apply: **Code & Documentation:** - [x] Bug fix (non-breaking change fixing an issue) - [ ] New feature (non-breaking change adding functionality) - [ ] Breaking change (fix or feature causing existing functionality to change) - [ ] Documentation update **Infrastructure & Configuration:** - [ ] GitHub Actions workflow - [ ] Linting configuration (markdown, PowerShell, etc.) - [ ] Security configuration - [ ] DevContainer configuration - [ ] Dependency update **AI Artifacts:** - [ ] Reviewed contribution with `prompt-builder` agent and addressed all feedback - [ ] Copilot instructions (`.github/instructions/*.instructions.md`) - [ ] Copilot prompt (`.github/prompts/*.prompt.md`) - [ ] Copilot agent (`.github/agents/*.agent.md`) **Other:** - [x] Script/automation (`.ps1`, `.sh`, `.py`) - [ ] Other (please describe): ## Testing Tested by running the affected scripts with various file count scenarios: - Zero files found (empty array handling) - Single file (automatic array coercion) - Multiple files (existing array behavior) All scripts now handle edge cases consistently without null reference errors. ## Checklist ### Required Checks - [ ] Documentation is updated (if applicable) - [x] Files follow existing naming conventions - [x] Changes are backwards compatible (if applicable) - [ ] Tests added for new functionality (if applicable) ### AI Artifact Contributions - [ ] Used `/prompt-analyze` to review contribution - [ ] Addressed all feedback from `prompt-builder` review - [ ] Verified contribution follows common standards and type-specific requirements ### Required Automated Checks The following validation commands must pass before merging: - [ ] Markdown linting: `npm run lint:md` - [x] Spell checking: `npm run spell-check` - [ ] Frontmatter validation: `npm run lint:frontmatter` - [ ] Link validation: `npm run lint:md-links` - [x] PowerShell analysis: `npm run lint:ps` ## Security Considerations - [x] This PR does not contain any sensitive or NDA information - [x] Any new dependencies have been reviewed for security issues - [x] Security-related scripts follow the principle of least privilege ## Additional Notes These changes follow PowerShell best practices for defensive array handling. The `@()` operator ensures that pipeline results are always treated as arrays, even when they return null or single items, preventing runtime errors in count operations and foreach loops. 🔧 - Generated by Copilot --------- Co-authored-by: Bill Berry <[email protected]>
1 parent d92c4e1 commit de43e73

10 files changed

Lines changed: 795 additions & 53 deletions

scripts/linting/Invoke-PSScriptAnalyzer.ps1

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,23 @@ $filesToAnalyze = @()
4242

4343
if ($ChangedFilesOnly) {
4444
Write-Host "Detecting changed PowerShell files..." -ForegroundColor Cyan
45-
$filesToAnalyze = Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.ps1', '*.psm1', '*.psd1')
45+
$filesToAnalyze = @(Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.ps1', '*.psm1', '*.psd1'))
4646
}
4747
else {
4848
Write-Host "Analyzing all PowerShell files..." -ForegroundColor Cyan
4949
$gitignorePath = Join-Path (git rev-parse --show-toplevel 2>$null) ".gitignore"
50-
$filesToAnalyze = Get-FilesRecursive -Path "." -Include @('*.ps1', '*.psm1', '*.psd1') -GitIgnorePath $gitignorePath
50+
$filesToAnalyze = @(Get-FilesRecursive -Path "." -Include @('*.ps1', '*.psm1', '*.psd1') -GitIgnorePath $gitignorePath)
5151
}
5252

53-
if ($filesToAnalyze.Count -eq 0) {
53+
if (@($filesToAnalyze).Count -eq 0) {
5454
Write-Host "✅ No PowerShell files to analyze" -ForegroundColor Green
5555
Set-GitHubOutput -Name "count" -Value "0"
5656
Set-GitHubOutput -Name "issues" -Value "0"
5757
exit 0
5858
}
5959

60-
Write-Host "Analyzing $($filesToAnalyze.Count) PowerShell files..." -ForegroundColor Cyan
61-
Set-GitHubOutput -Name "count" -Value $filesToAnalyze.Count
60+
Write-Host "Analyzing $(@($filesToAnalyze).Count) PowerShell files..." -ForegroundColor Cyan
61+
Set-GitHubOutput -Name "count" -Value @($filesToAnalyze).Count
6262

6363
#region Main Execution
6464
try {
@@ -104,11 +104,11 @@ try {
104104

105105
# Export results
106106
$summary = @{
107-
TotalFiles = $filesToAnalyze.Count
108-
TotalIssues = $allResults.Count
109-
Errors = ($allResults | Where-Object Severity -eq 'Error').Count
110-
Warnings = ($allResults | Where-Object Severity -eq 'Warning').Count
111-
Information = ($allResults | Where-Object Severity -eq 'Information').Count
107+
TotalFiles = @($filesToAnalyze).Count
108+
TotalIssues = @($allResults).Count
109+
Errors = @($allResults | Where-Object Severity -eq 'Error').Count
110+
Warnings = @($allResults | Where-Object Severity -eq 'Warning').Count
111+
Information = @($allResults | Where-Object Severity -eq 'Information').Count
112112
HasErrors = $hasErrors
113113
}
114114

scripts/linting/Invoke-YamlLint.ps1

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,25 @@ $filesToAnalyze = @()
6565

6666
if ($ChangedFilesOnly) {
6767
Write-Host "Detecting changed workflow files..." -ForegroundColor Cyan
68-
$changedFiles = Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.yml', '*.yaml')
69-
$filesToAnalyze = $changedFiles | Where-Object { $_ -like "$workflowPath/*" }
68+
$changedFiles = @(Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.yml', '*.yaml'))
69+
$filesToAnalyze = @($changedFiles | Where-Object { $_ -like "$workflowPath/*" })
7070
}
7171
else {
7272
Write-Host "Analyzing all workflow files..." -ForegroundColor Cyan
7373
if (Test-Path $workflowPath) {
74-
$filesToAnalyze = Get-ChildItem -Path $workflowPath -File | Where-Object { $_.Extension -in '.yml', '.yaml' } | ForEach-Object { $_.FullName }
74+
$filesToAnalyze = @(Get-ChildItem -Path $workflowPath -File | Where-Object { $_.Extension -in '.yml', '.yaml' } | ForEach-Object { $_.FullName })
7575
}
7676
}
7777

78-
if ($filesToAnalyze.Count -eq 0) {
78+
if (@($filesToAnalyze).Count -eq 0) {
7979
Write-Host "✅ No workflow files to analyze" -ForegroundColor Green
8080
Set-GitHubOutput -Name "count" -Value "0"
8181
Set-GitHubOutput -Name "issues" -Value "0"
8282
exit 0
8383
}
8484

85-
Write-Host "Analyzing $($filesToAnalyze.Count) workflow files..." -ForegroundColor Cyan
86-
Set-GitHubOutput -Name "count" -Value $filesToAnalyze.Count
85+
Write-Host "Analyzing $(@($filesToAnalyze).Count) workflow files..." -ForegroundColor Cyan
86+
Set-GitHubOutput -Name "count" -Value @($filesToAnalyze).Count
8787

8888
#region Main Execution
8989
try {

scripts/linting/Markdown-Link-Check.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ try {
197197
$repoRootPath = Split-Path -Path $scriptRootParent -Parent
198198
$repoRoot = Resolve-Path -LiteralPath $repoRootPath
199199
$config = Resolve-Path -LiteralPath $ConfigPath -ErrorAction Stop
200-
$filesToCheck = Get-MarkdownTarget -InputPath $Path
200+
$filesToCheck = @(Get-MarkdownTarget -InputPath $Path)
201201

202-
if (-not $filesToCheck -or $filesToCheck.Count -eq 0) {
202+
if (-not $filesToCheck -or @($filesToCheck).Count -eq 0) {
203203
Write-Error 'No markdown files were found to validate.'
204204
exit 1
205205
}

scripts/linting/Validate-MarkdownFrontmatter.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,16 +504,16 @@ function Test-FrontmatterValidation {
504504
# Handle ChangedFilesOnly mode
505505
if ($ChangedFilesOnly) {
506506
Write-Host "🔍 Detecting changed markdown files from git diff..." -ForegroundColor Cyan
507-
$Files = Get-ChangedMarkdownFileGroup -BaseBranch $BaseBranch
508-
if ($Files.Count -eq 0) {
507+
$Files = @(Get-ChangedMarkdownFileGroup -BaseBranch $BaseBranch)
508+
if (@($Files).Count -eq 0) {
509509
Write-Host "No changed markdown files found - validation complete" -ForegroundColor Green
510510
# Return empty summary with TotalFiles=0 to accurately represent no files validated
511511
# The caller handles this as success when ChangedFilesOnly mode is used
512512
$emptySummary = & (Get-Module FrontmatterValidation) { [ValidationSummary]::new() }
513513
$null = $emptySummary.Complete()
514514
return $emptySummary
515515
}
516-
Write-Host "Found $($Files.Count) changed markdown files to validate" -ForegroundColor Cyan
516+
Write-Host "Found $(@($Files).Count) changed markdown files to validate" -ForegroundColor Cyan
517517
}
518518

519519
# Resolve files from paths if not provided directly

scripts/security/Test-DependencyPinning.ps1

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,8 @@ function Get-ComplianceReportData {
596596
$report.Violations = $Violations
597597

598598
# Calculate metrics
599-
$totalDeps = ($Violations | Measure-Object).Count
600-
$unpinnedDeps = ($Violations | Where-Object { $_.Severity -ne 'Info' } | Measure-Object).Count
599+
$totalDeps = @($Violations).Count
600+
$unpinnedDeps = @($Violations | Where-Object { $_.Severity -ne 'Info' }).Count
601601
$pinnedDeps = $totalDeps - $unpinnedDeps
602602

603603
$report.TotalDependencies = $totalDeps
@@ -613,12 +613,12 @@ function Get-ComplianceReportData {
613613

614614
# Generate summary by type
615615
$report.Summary = @{}
616-
foreach ($type in ($Violations | Group-Object Type)) {
616+
foreach ($type in @($Violations | Group-Object Type)) {
617617
$report.Summary[$type.Name] = @{
618618
Total = $type.Count
619-
High = ($type.Group | Where-Object { $_.Severity -eq 'High' } | Measure-Object).Count
620-
Medium = ($type.Group | Where-Object { $_.Severity -eq 'Medium' } | Measure-Object).Count
621-
Low = ($type.Group | Where-Object { $_.Severity -eq 'Low' } | Measure-Object).Count
619+
High = @($type.Group | Where-Object { $_.Severity -eq 'High' }).Count
620+
Medium = @($type.Group | Where-Object { $_.Severity -eq 'Medium' }).Count
621+
Low = @($type.Group | Where-Object { $_.Severity -eq 'Low' }).Count
622622
}
623623
}
624624

@@ -854,14 +854,14 @@ try {
854854
if ($excludePatterns) { Write-PinningLog "Exclude patterns: $($excludePatterns -join ', ')" -Level Info }
855855

856856
# Discover files to scan
857-
$filesToScan = Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive
858-
Write-PinningLog "Found $($filesToScan.Count) files to scan" -Level Info
857+
$filesToScan = @(Get-FilesToScan -ScanPath $Path -Types $typesToCheck -ExcludePatterns $excludePatterns -Recursive:$Recursive)
858+
Write-PinningLog "Found $(@($filesToScan).Count) files to scan" -Level Info
859859

860860
# Scan for violations
861861
$allViolations = @()
862862
foreach ($fileInfo in $filesToScan) {
863863
Write-PinningLog "Scanning: $($fileInfo.RelativePath)" -Level Info
864-
$violations = Get-DependencyViolation -FileInfo $fileInfo
864+
$violations = @(Get-DependencyViolation -FileInfo $fileInfo)
865865

866866
# Add remediation suggestions
867867
foreach ($violation in $violations) {
@@ -871,7 +871,7 @@ try {
871871
$allViolations += $violations
872872
}
873873

874-
Write-PinningLog "Found $($allViolations.Count) dependency pinning violations" -Level Info
874+
Write-PinningLog "Found $(@($allViolations).Count) dependency pinning violations" -Level Info
875875

876876
# Generate compliance report
877877
$report = Get-ComplianceReportData -Violations $allViolations -ScannedFiles $filesToScan -ScanPath $Path -Remediate:$Remediate

scripts/security/Test-SHAStaleness.ps1

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -529,12 +529,12 @@ function Test-GitHubActionsForStaleness {
529529
}
530530
}
531531

532-
if ($allActionRepos.Count -eq 0) {
532+
if (@($allActionRepos).Count -eq 0) {
533533
Write-SecurityLog "No SHA-pinned GitHub Actions found" -Level Info
534534
return
535535
}
536536

537-
Write-SecurityLog "Found $($allActionRepos.Count) unique repositories with $($shaToActionMap.Count) SHA-pinned actions" -Level Info
537+
Write-SecurityLog "Found $(@($allActionRepos).Count) unique repositories with $(@($shaToActionMap.Keys).Count) SHA-pinned actions" -Level Info
538538

539539
# Bulk query for all actions using GraphQL optimization
540540
try {
@@ -673,7 +673,7 @@ function Write-OutputResult {
673673
$JsonOutput = @{
674674
Timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"
675675
MaxAgeThreshold = $MaxAge
676-
TotalStaleItems = $Dependencies.Count
676+
TotalStaleItems = @($Dependencies).Count
677677
Dependencies = $Dependencies
678678
} | ConvertTo-Json -Depth 10
679679

@@ -703,11 +703,11 @@ function Write-OutputResult {
703703
Write-Output "::warning file=$escapedPath::$escapedMessage"
704704
}
705705

706-
if ($Dependencies.Count -eq 0) {
706+
if (@($Dependencies).Count -eq 0) {
707707
Write-Output "::notice::No stale dependencies detected"
708708
}
709709
else {
710-
$escapedCount = ConvertTo-GitHubActionsEscaped -Value "Found $($Dependencies.Count) stale dependencies that may pose security risks"
710+
$escapedCount = ConvertTo-GitHubActionsEscaped -Value "Found $(@($Dependencies).Count) stale dependencies that may pose security risks"
711711
Write-Output "::error::$escapedCount"
712712
}
713713
}
@@ -718,17 +718,17 @@ function Write-OutputResult {
718718
Write-Output $Message
719719
}
720720

721-
if ($Dependencies.Count -eq 0) {
721+
if (@($Dependencies).Count -eq 0) {
722722
Write-Output "##vso[task.logissue type=info]No stale dependencies detected"
723723
}
724724
else {
725-
Write-Output "##vso[task.logissue type=error]Found $($Dependencies.Count) stale dependencies that may pose security risks"
725+
Write-Output "##vso[task.logissue type=error]Found $(@($Dependencies).Count) stale dependencies that may pose security risks"
726726
Write-Output "##vso[task.complete result=SucceededWithIssues]"
727727
}
728728
}
729729

730730
"console" {
731-
if ($Dependencies.Count -eq 0) {
731+
if (@($Dependencies).Count -eq 0) {
732732
Write-SecurityLog "No stale dependencies detected!" -Level Success
733733
}
734734
else {
@@ -739,18 +739,18 @@ function Write-OutputResult {
739739
Write-SecurityLog " Message: $($Dep.Message)" -Level Info
740740
Write-Information "" -InformationAction Continue
741741
}
742-
Write-SecurityLog "Total stale dependencies: $($Dependencies.Count)" -Level Warning
742+
Write-SecurityLog "Total stale dependencies: $(@($Dependencies).Count)" -Level Warning
743743
}
744744
}
745745

746746
"Summary" {
747-
if ($Dependencies.Count -eq 0) {
747+
if (@($Dependencies).Count -eq 0) {
748748
Write-Output "No stale dependencies detected!"
749749
}
750750
else {
751751
Write-Output "=== SHA Staleness Summary ==="
752-
Write-Output "Total stale dependencies: $($Dependencies.Count)"
753-
$ByType = $Dependencies | Group-Object Type
752+
Write-Output "Total stale dependencies: $(@($Dependencies).Count)"
753+
$ByType = @($Dependencies | Group-Object Type)
754754
foreach ($Group in $ByType) {
755755
Write-Output "$($Group.Name): $($Group.Count)"
756756
}
@@ -882,17 +882,20 @@ try {
882882
Write-SecurityLog "GraphQL batch size: $GraphQLBatchSize queries per request" -Level Info
883883
Write-SecurityLog "Output format: $OutputFormat" -Level Info
884884

885+
# Initialize stale dependencies array
886+
$script:StaleDependencies = @()
887+
885888
# Run staleness check for GitHub Actions
886889
Test-GitHubActionsForStaleness
887890

888891
# Run staleness check for tools from tool-checksums.json
889892
Write-SecurityLog "Checking tool staleness from tool-checksums.json" -Level Info
890893

891-
$toolResults = Get-ToolStaleness
892-
if ($toolResults) {
893-
$staleTools = $toolResults | Where-Object { $_.IsStale -eq $true }
894-
if ($staleTools.Count -gt 0) {
895-
Write-SecurityLog "Found $($staleTools.Count) stale tool(s):" -Level Warning
894+
$toolResults = @(Get-ToolStaleness)
895+
if (@($toolResults).Count -gt 0) {
896+
$staleTools = @($toolResults | Where-Object { $_.IsStale -eq $true })
897+
if (@($staleTools).Count -gt 0) {
898+
Write-SecurityLog "Found $(@($staleTools).Count) stale tool(s):" -Level Warning
896899
foreach ($tool in $staleTools) {
897900
Write-SecurityLog " - $($tool.Tool): $($tool.CurrentVersion) -> $($tool.LatestVersion)" -Level Warning
898901

@@ -914,20 +917,20 @@ try {
914917
}
915918

916919
# Check for errors
917-
$errorTools = $toolResults | Where-Object { $null -ne $_.Error }
918-
if ($errorTools.Count -gt 0) {
919-
Write-SecurityLog "Failed to check $($errorTools.Count) tool(s)" -Level Warning
920+
$errorTools = @($toolResults | Where-Object { $null -ne $_.Error })
921+
if (@($errorTools).Count -gt 0) {
922+
Write-SecurityLog "Failed to check $(@($errorTools).Count) tool(s)" -Level Warning
920923
}
921924
}
922925

923926
# Output results
924927
Write-OutputResult -Dependencies $StaleDependencies -OutputFormat $OutputFormat -OutputPath $OutputPath
925928

926929
Write-SecurityLog "SHA staleness monitoring completed" -Level Success
927-
Write-SecurityLog "Stale dependencies found: $($StaleDependencies.Count)" -Level Info
930+
Write-SecurityLog "Stale dependencies found: $(@($StaleDependencies).Count)" -Level Info
928931

929932
# Exit with appropriate code based on findings and -FailOnStale parameter
930-
if ($StaleDependencies.Count -gt 0) {
933+
if (@($StaleDependencies).Count -gt 0) {
931934
if ($FailOnStale) {
932935
Write-SecurityLog "Exiting with status 1 due to stale dependencies (-FailOnStale specified)" -Level Warning
933936
exit 1

0 commit comments

Comments
 (0)