mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 07:48:24 -04:00
fix(shell-scan): handle PowerShell line endings
This commit is contained in:
@@ -50,6 +50,6 @@ Add one syntax class only when representative commands show meaningful opacity.
|
||||
| Replace Bash tree-sitter | 1.38 MB grammar, ~69k parity scans/sec | Pure TS, ~149k parity scans/sec | Keep |
|
||||
| Replace PowerShell tree-sitter | 0.98 MB grammar + 0.21 MB shared runtime | Pure TS; no Core parser assets | Keep |
|
||||
| Combined scanner bundle | 2.57 MB parser assets | 10.1 KB minified, 3.6 KB gzip | Keep |
|
||||
| Security hardening | Initial portable subset | 397 scanner tests, 685 assertions, zero known unsafe shell-grammar corpus cases | Keep |
|
||||
| Security hardening | Initial portable subset | 400 scanner tests, 689 assertions, zero known unsafe shell-grammar corpus cases | Keep |
|
||||
|
||||
The TUI's independent tree-sitter grammar remains for syntax highlighting. Core has no tree-sitter runtime dependency.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
{"target":"shell-permission-scan","hypothesis":"the same permission-focused scanner design can replace PowerShell tree-sitter","metric":"parser_asset_bytes","before":1188724,"after":0,"decision":"keep","notes":"removed the 983,236-byte PowerShell grammar and 205,488-byte shared runtime from Core; focused tests and CLI asset tests pass"}
|
||||
{"target":"shell-permission-scan","hypothesis":"PowerShell safety hardening remains negligible relative to removed parser assets","metric":"scanner_minified_bytes","before":4203,"after":4846,"decision":"keep","notes":"added quote escaping, redirects, malformed input, dynamic execution, scripts/modules, and location guards; gzip is 1,950 bytes and scanner throughput is 349,777/sec"}
|
||||
{"target":"shell-permission-scan","hypothesis":"removing hot-path slices and repeated Set allocation improves throughput while simplifying classification","metric":"scans_per_second","before":349777,"after":514076,"decision":"keep","notes":"median of five complete 9-run benchmark invocations after simplification; 113 scanner tests remain green"}
|
||||
{"target":"shell-permission-scan","hypothesis":"mutation closure and grammar hardening remain fast and compact enough for permission analysis","metric":"scans_per_second","before":410594,"after":404948,"decision":"keep","notes":"final 9-run benchmark median; 397 scanner tests and 685 assertions pass; bundle is 10,147 bytes minified and 3,615 bytes gzip"}
|
||||
{"target":"shell-permission-scan","hypothesis":"mutation closure and grammar hardening remain fast and compact enough for permission analysis","metric":"scans_per_second","before":410594,"after":415493,"decision":"keep","notes":"final 9-run benchmark median; 400 scanner tests and 689 assertions pass; bundle is 10,147 bytes minified and 3,615 bytes gzip"}
|
||||
|
||||
@@ -519,8 +519,10 @@ export function scanPowerShell(input: string): Result {
|
||||
if (/^#requires\b/i.test(input.slice(index))) return { kind: "opaque", reason: "dynamic-execution" }
|
||||
finishCommand(index)
|
||||
comment = true
|
||||
const newline = input.indexOf("\n", index)
|
||||
const endings = [input.indexOf("\n", index), input.indexOf("\r", index)].filter((ending) => ending >= 0)
|
||||
const newline = endings.length > 0 ? Math.min(...endings) : -1
|
||||
if (newline === -1) break
|
||||
comment = false
|
||||
index = newline
|
||||
segment = newline + 1
|
||||
continue
|
||||
@@ -558,7 +560,7 @@ export function scanPowerShell(input: string): Result {
|
||||
}
|
||||
|
||||
if (quote) return { kind: "opaque", reason: "unterminated-quote" }
|
||||
if (!comment || input.includes("\n")) finishCommand(input.length)
|
||||
if (!comment) finishCommand(input.length)
|
||||
if (redirectTarget || invalid || dangling) return { kind: "opaque", reason: "invalid-structure" }
|
||||
if (
|
||||
dynamic ||
|
||||
@@ -605,7 +607,8 @@ function shellCommandName(word: string | undefined) {
|
||||
}
|
||||
|
||||
function knownPowerShellDirectory(word: string) {
|
||||
return /^(?:\$(?:PWD|HOME|PSHOME)|\$env:[A-Za-z_][A-Za-z0-9_]*|\$\{env:[^}]+\})(?:[\\/]|$)/i.test(word)
|
||||
const variable = /^(?:\$(?:PWD|HOME|PSHOME)|\$env:[A-Za-z_][A-Za-z0-9_]*|\$\{env:[^}]+\})(?:[\\/]|$)/i.exec(word)
|
||||
return Boolean(variable) && !word.slice(variable?.[0].length).includes("$")
|
||||
}
|
||||
|
||||
function powerShellRedirect(input: string, index: number) {
|
||||
|
||||
@@ -149,6 +149,8 @@ describe("ShellScan adversarial corpus", () => {
|
||||
"pushd Alias:; si harmless Remove-Item; harmless victim",
|
||||
"Invoke-`\nExpression 'Remove-Item victim'",
|
||||
"<# ignored #> Remove-Item victim",
|
||||
"Set-Location $HOME/$target; Get-ChildItem",
|
||||
"Push-Location $env:TEMP/$target; Get-ChildItem",
|
||||
])("fails closed for secondary PowerShell execution: %s", (input) => {
|
||||
expect(ShellScan.scanPowerShell(input).kind).toBe("opaque")
|
||||
})
|
||||
|
||||
@@ -173,6 +173,13 @@ describe("ShellScan PowerShell", () => {
|
||||
expect(result.commands.map((command) => command.words[0])).toEqual(["Get-ChildItem", "Remove-Item"])
|
||||
})
|
||||
|
||||
test("ends comments at carriage returns", () => {
|
||||
const result = ShellScan.scanPowerShell("# comment\rRemove-Item victim")
|
||||
expect(result.kind).toBe("scanned")
|
||||
if (result.kind === "opaque") return
|
||||
expect(result.commands.map((command) => command.words[0])).toEqual(["Remove-Item"])
|
||||
})
|
||||
|
||||
test("scans static commands and pipelines", () => {
|
||||
expect(ShellScan.scanPowerShell("Get-ChildItem; Write-Output 'done' | Out-File output.txt")).toEqual({
|
||||
kind: "scanned",
|
||||
|
||||
Reference in New Issue
Block a user