The residual
plugins/powershell-format/hooks/powershell-format.sh decides whether a PSScriptAnalyzer custom rule's code-loading targets can be pinned to a trust approval. Named loaders are matched against an enumerated list:
$loaders = @("Import-Module", "ipmo", "Add-Type", "New-Module",
"Import-PowerShellDataFile")
Any cmdlet that loads code but is not on that list is not treated as a loader, so a non-constant argument to it is not refused. Two named examples:
Invoke-Command -FilePath $x
Start-Job -FilePath $x
Both execute a repository file whose path the scan cannot pin, and neither trips the loader test.
Why generalizing is not the answer
The obvious generalization — "refuse any command with a non-constant argument" — would refuse Write-Output $x, Where-Object { $_ -eq $y }, and essentially every real rule module. That disables the feature for every consuming repo, which is a worse outcome than the residual: a gate nobody can satisfy gets switched off, and then nothing is checked at all.
There is no obvious middle predicate either. "Loads code" is not a property the AST exposes; it is cmdlet semantics, which means any mechanical test is either an enumeration (this one, with an edge) or over-broad (unusable). Enumerating more names narrows the edge without removing it.
Why the reported repro is nonetheless covered
The finding that surfaced this class was . (Join-Path $PSScriptRoot "deps" "helper.ps1"), and that is structurally covered without reference to $loaders:
InvocationOperator identifies every . and & invocation regardless of name
- a target that is not a
StringConstantExpressionAst, an expandable string whose variables the scan can expand, or an inline script block refuses approval
So the dot-source and call-operator forms — the ones a rule module actually uses to pull in a sibling file — do not depend on the list at all. The list only governs named cmdlets.
Fail-closed confirmation
The residual fails closed in the common direction and open only in the narrow one:
- an unlisted cmdlet with a constant argument: the path is collected by the literal scan and pinned, so a change revokes the approval — correct
- an unlisted cmdlet with a non-constant argument: not refused, and the target is not pinned — the bypass
The exposure is therefore limited to a rule module that both uses one of these specific cmdlets and computes its path. That is narrow, but it is real, and it is recorded here rather than left implicit.
What would actually close it
Options, none free:
- Extend the enumeration and accept a permanently-thin edge. Cheapest; does not close the class.
- Enumerate on the parameter rather than the cmdlet — any command invoked with
-FilePath, -LiteralPath, -Path, -ScriptBlock and a non-constant value. Broader coverage, still an enumeration, but of a much smaller and more stable vocabulary than the cmdlet namespace.
- Invert: allow-list the cmdlets a custom rule may call at all, refuse everything else. Genuinely closes it, at the cost of refusing every rule module that calls anything unanticipated — likely unusable.
Option 2 looks like the best ratio if this is ever worth revisiting.
Provenance
Raised as a review finding on #1097, assessed there as a residual the design deliberately does not cover, and accepted as such on the record. Filed so the boundary is documented rather than rediscovered.
The residual
plugins/powershell-format/hooks/powershell-format.shdecides whether a PSScriptAnalyzer custom rule's code-loading targets can be pinned to a trust approval. Named loaders are matched against an enumerated list:Any cmdlet that loads code but is not on that list is not treated as a loader, so a non-constant argument to it is not refused. Two named examples:
Invoke-Command -FilePath $xStart-Job -FilePath $xBoth execute a repository file whose path the scan cannot pin, and neither trips the loader test.
Why generalizing is not the answer
The obvious generalization — "refuse any command with a non-constant argument" — would refuse
Write-Output $x,Where-Object { $_ -eq $y }, and essentially every real rule module. That disables the feature for every consuming repo, which is a worse outcome than the residual: a gate nobody can satisfy gets switched off, and then nothing is checked at all.There is no obvious middle predicate either. "Loads code" is not a property the AST exposes; it is cmdlet semantics, which means any mechanical test is either an enumeration (this one, with an edge) or over-broad (unusable). Enumerating more names narrows the edge without removing it.
Why the reported repro is nonetheless covered
The finding that surfaced this class was
. (Join-Path $PSScriptRoot "deps" "helper.ps1"), and that is structurally covered without reference to$loaders:InvocationOperatoridentifies every.and&invocation regardless of nameStringConstantExpressionAst, an expandable string whose variables the scan can expand, or an inline script block refuses approvalSo the dot-source and call-operator forms — the ones a rule module actually uses to pull in a sibling file — do not depend on the list at all. The list only governs named cmdlets.
Fail-closed confirmation
The residual fails closed in the common direction and open only in the narrow one:
The exposure is therefore limited to a rule module that both uses one of these specific cmdlets and computes its path. That is narrow, but it is real, and it is recorded here rather than left implicit.
What would actually close it
Options, none free:
-FilePath,-LiteralPath,-Path,-ScriptBlockand a non-constant value. Broader coverage, still an enumeration, but of a much smaller and more stable vocabulary than the cmdlet namespace.Option 2 looks like the best ratio if this is ever worth revisiting.
Provenance
Raised as a review finding on #1097, assessed there as a residual the design deliberately does not cover, and accepted as such on the record. Filed so the boundary is documented rather than rediscovered.