275 lines
11 KiB
PowerShell
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)"
|