[CmdletBinding()] param() $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Assert-Condition { param( [Parameter(Mandatory = $true)] [bool]$Condition, [Parameter(Mandatory = $true)] [string]$Message ) if (-not $Condition) { throw $Message } } function Get-RequiredCommandPath { param( [Parameter(Mandatory = $true)] [string]$Name ) $command = Get-Command -Name $Name -ErrorAction SilentlyContinue if ($null -eq $command) { throw "Required command not found on PATH: $Name" } return $command.Source } function Test-RequiredHeaderLine { param( [Parameter(Mandatory = $true)] [string]$Content, [Parameter(Mandatory = $true)] [string]$ExpectedLine ) $normalizedLines = @( $Content -split "\r?\n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } ) return $ExpectedLine -in $normalizedLines } function Assert-ScriptRegex { param( [Parameter(Mandatory = $true)] [System.IO.FileInfo]$ScriptFile, [Parameter(Mandatory = $true)] [string]$Content, [Parameter(Mandatory = $true)] [string]$Pattern, [Parameter(Mandatory = $true)] [string]$Message ) if (-not [regex]::IsMatch($Content, $Pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)) { throw "$Message`: $($ScriptFile.Name)" } } $workspaceRoot = Split-Path -Parent $PSScriptRoot $settingsPath = Join-Path $workspaceRoot 'PSScriptAnalyzerSettings.psd1' $scriptFiles = @(Get-ChildItem -LiteralPath $PSScriptRoot -Filter '*.ps1' -File | Sort-Object Name) $requiredHeaderLines = @( "`$ErrorActionPreference = 'Stop'", 'Set-StrictMode -Version Latest' ) $scriptContentByName = @{} Assert-Condition (Test-Path -LiteralPath $settingsPath) "Missing ScriptAnalyzer settings file: $settingsPath" [void](Get-RequiredCommandPath -Name 'Invoke-ScriptAnalyzer') foreach ($scriptFile in $scriptFiles) { $scriptContentByName[$scriptFile.Name] = @{ File = $scriptFile Content = Get-Content -Raw -LiteralPath $scriptFile.FullName } } foreach ($scriptFile in $scriptFiles) { if ($scriptFile.Name -eq 'toolbox-commands.ps1') { continue } $content = $scriptContentByName[$scriptFile.Name].Content foreach ($requiredLine in $requiredHeaderLines) { Assert-Condition ( Test-RequiredHeaderLine -Content $content -ExpectedLine $requiredLine ) "PowerShell script must include required header line '$requiredLine': $($scriptFile.Name)" } } $validationMatrix = @( @{ Script = 'install-toolbox.ps1' Pattern = 'param\([\s\S]*?\[ValidateNotNullOrEmpty\(\)\]\s*\[string\]\$InstallRoot' Message = 'Installer must reject an empty InstallRoot argument during parameter binding' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'param\([\s\S]*?\[ValidateNotNullOrEmpty\(\)\]\s*\[string\]\$InstallRoot' Message = 'Package installer must reject an empty InstallRoot argument during parameter binding' }, @{ Script = 'uninstall-toolbox.ps1' Pattern = 'param\([\s\S]*?\[ValidateNotNullOrEmpty\(\)\]\s*\[string\]\$InstallRoot' Message = 'Uninstaller must reject an empty InstallRoot argument during parameter binding' }, @{ Script = 'uninstall-package-toolbox.ps1' Pattern = 'param\([\s\S]*?\[ValidateNotNullOrEmpty\(\)\]\s*\[string\]\$InstallRoot' Message = 'Package uninstaller must reject an empty InstallRoot argument during parameter binding' }, @{ Script = 'package-toolbox.ps1' Pattern = 'param\([\s\S]*?\[ValidateNotNullOrEmpty\(\)\]\s*\[string\]\$OutputRoot' Message = 'Packager must reject an empty OutputRoot argument during parameter binding' }, @{ Script = 'package-toolbox.ps1' Pattern = 'Assert-SingleDirectoryName[\s\S]*IsPathRooted[\s\S]*GetFileName[\s\S]*''\.\'', ''\.\.''' Message = 'Packager must reject PackageName values that are rooted paths, nested paths, dot, or dot-dot' }, @{ Script = 'package-toolbox.ps1' Pattern = 'Resolved package root must stay inside OutputRoot' Message = 'Packager must reject resolved package roots that escape OutputRoot' }, @{ Script = 'package-toolbox.ps1' Pattern = 'OutputRoot must not be a reparse point' Message = 'Packager must reject reparse-point OutputRoot values before writing output' }, @{ Script = 'package-toolbox.ps1' Pattern = 'Get-RequiredCommandPath -Name ''rustc''[\s\S]*Failed to query rustc host target with command' Message = 'Packager must resolve rustc explicitly and report the failing rustc probe command' }, @{ Script = 'package-toolbox.ps1' Pattern = 'Assert-ArchiveCreated[\s\S]*Compress-Archive did not create the expected archive[\s\S]*Compress-Archive created an empty archive' Message = 'Packager must verify that Compress-Archive produced a non-empty archive' }, @{ Script = 'install-toolbox.ps1' Pattern = 'Assert-ExistingPathNotReparsePoint -Path \$InstallRoot' Message = 'Installer must reject reparse-point InstallRoot values before mutation' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'Assert-ExistingPathNotReparsePoint -Path \$InstallRoot' Message = 'Package installer must reject reparse-point InstallRoot values before mutation' }, @{ Script = 'uninstall-toolbox.ps1' Pattern = 'Assert-ExistingPathNotReparsePoint -Path \$InstallRoot' Message = 'Uninstaller must reject reparse-point InstallRoot values before removal' }, @{ Script = 'uninstall-package-toolbox.ps1' Pattern = 'Assert-ExistingPathNotReparsePoint -Path \$InstallRoot' Message = 'Package uninstaller must reject reparse-point InstallRoot values before removal' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'Checksum path escapes package root' Message = 'Package installer must reject checksum entries that escape the package root' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'Duplicate checksum entry for package file' Message = 'Package installer must reject duplicate checksum entries' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'Package contains file missing from SHA256SUMS' Message = 'Package installer must reject package files missing from SHA256SUMS' }, @{ Script = 'install-toolbox.ps1' Pattern = 'Failed to create current install junction[\s\S]*NTFS-compatible install root[\s\S]*Current install junction was not created' Message = 'Installer must explain Windows junction creation failures and verify the current link exists' }, @{ Script = 'install-package-toolbox.ps1' Pattern = 'Failed to create current install junction[\s\S]*NTFS-compatible install root[\s\S]*Current install junction was not created' Message = 'Package installer must explain Windows junction creation failures and verify the current link exists' }, @{ Script = 'toolbox-commands.ps1' Pattern = 'Format-ToolboxNativeCommand[\s\S]*Command failed with exit code[\s\S]*Get-Location' Message = 'Shared native command runner must include a quoted command line and cwd in failures' }, @{ Script = 'setup-gitea-runner.ps1' Pattern = 'HttpResponseMessage[\s\S]*ErrorDetails[\s\S]*ReadAsStringAsync[\s\S]*GetResponseStream' Message = 'Gitea runner setup must handle PowerShell 7 and legacy HTTP error responses' }, @{ Script = 'publish-gitea-release.ps1' Pattern = 'HttpResponseMessage[\s\S]*ErrorDetails[\s\S]*ReadAsStringAsync[\s\S]*GetResponseStream' Message = 'Gitea release publisher must handle PowerShell 7 and legacy HTTP error responses' }, @{ Script = 'invoke-gitea-git.ps1' Pattern = 'credential\.helper=[\s\S]*http\.sslBackend=openssl[\s\S]*http\.extraHeader=Authorization: Basic' Message = 'Gitea Git helper must use transient Basic auth while disabling credential-manager lookup' }, @{ Script = 'invoke-gitea-git.ps1' Pattern = 'GIT_TERMINAL_PROMPT[\s\S]*GIT_TRACE_CURL[\s\S]*GIT_CURL_VERBOSE' Message = 'Gitea Git helper must disable prompts and curl tracing while the transient header is in scope' }, @{ Script = 'invoke-gitea-git.ps1' Pattern = 'Write-GitOutput[\s\S]*http\\\.c:\\d\+[\s\S]*2>&1' Message = 'Gitea Git helper must filter libcurl trace output from captured git stderr' }, @{ Script = 'invoke-gitea-git.ps1' Pattern = 'InsecureSkipTlsVerify[\s\S]*http\.sslVerify=false' Message = 'Gitea Git helper may skip TLS verification only through an explicit opt-in switch' }, @{ Script = 'invoke-gitea-git.ps1' Pattern = 'deliberately does not call Git Credential Manager' Message = 'Gitea Git helper must fail closed instead of falling back to Git Credential Manager' }, @{ Script = 'check-gitea-ci.ps1' Pattern = 'actions/workflows/\$WorkflowId[\s\S]*actions/runners[\s\S]*actions/runs' Message = 'Gitea CI checker must inspect workflow, runner, and run state through the Gitea API' }, @{ Script = 'check-gitea-ci.ps1' Pattern = 'DispatchIfMissing[\s\S]*dispatches[\s\S]*Wait' Message = 'Gitea CI checker must support dispatch fallback and wait for a conclusive run' }, @{ Script = 'check-ai-prompt.ps1' Pattern = 'Invoke-GeneratorCheck[\s\S]*Invoke-GeneratorWrite[\s\S]*Invoke-GeneratorCheck' Message = 'AI prompt checker must regenerate once and recheck before failing generated asset drift' }, @{ Script = 'check-ai-skill.ps1' Pattern = 'Invoke-GeneratorCheck[\s\S]*Invoke-GeneratorWrite[\s\S]*Invoke-GeneratorCheck' Message = 'AI skill checker must regenerate once and recheck before failing generated asset drift' } ) foreach ($validationCase in $validationMatrix) { Assert-Condition ($scriptContentByName.ContainsKey($validationCase.Script)) "Validation matrix references missing script: $($validationCase.Script)" $scriptRecord = $scriptContentByName[$validationCase.Script] Assert-ScriptRegex -ScriptFile $scriptRecord.File -Content $scriptRecord.Content -Pattern $validationCase.Pattern -Message $validationCase.Message } $diagnostics = @(Invoke-ScriptAnalyzer -Path $PSScriptRoot -Recurse -Settings $settingsPath) if ($diagnostics.Count -gt 0) { $diagnostics | Select-Object RuleName, Severity, ScriptName, Line, Message | Format-Table -AutoSize | Out-String | Write-Output throw "PSScriptAnalyzer reported $($diagnostics.Count) diagnostic(s)." } Write-Host "PowerShell gate passed for $($scriptFiles.Count) script file(s)"