Files
MercuryToolbox/scripts/check-powershell.ps1
T

275 lines
11 KiB
PowerShell

[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)"