import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { ShellParse } from "../src/shell/parse.js" import { Wildcard } from "../src/util/wildcard.js" describe("native shell syntax compatibility", () => { test("PowerShell invocation approvals include the operator instead of saving an ineffective prefix", async () => { const command = "& $Command value" expect(await Effect.runPromise(ShellParse.scan(command, "pwsh", "/workspace"))).toEqual({ commands: [{ resource: command, save: "& $Command *" }], directories: [], }) }) test.each([ "ForEach-Object { Write-Output value }", "Write-Output before; ForEach-Object { Write-Output value }", "ForEach-Object { Write-Output value } | Write-Output done", "Write-Output before | ForEach-Object { Write-Output $_ }", "& ForEach-Object { Write-Output value }", "& 'ForEach-Object' { Write-Output value }", "% { Write-Output value }", "Where-Object { Write-Output value }", ])("PowerShell scriptblock callers preserve permission resources and usable approvals: %s", async (command) => { const native = await Effect.runPromise(ShellParse.scan(command, "pwsh", "/workspace")) for (const item of native.commands) expect(Wildcard.match(item.resource, item.save), item.resource).toBe(true) }) for (const shell of ["bash", "zsh"]) { test.each([ "cat <<'EOF'\n$(not_a_command)\nEOF", "cat < { const scanned = await Effect.runPromise(ShellParse.scan(command, shell, "/workspace")) expect(scanned.commands.length).toBeGreaterThanOrEqual(0) }) } test("does not invent commands from a quoted second heredoc body", async () => { const command = "cat < { expect( await Effect.runPromise( ShellParse.scan("true && cat <<'EOF' && git status\nhello\nEOF", "bash", "/workspace"), ), ).toEqual({ commands: [ { resource: "true", save: "true *" }, { resource: "cat <<'EOF'", save: "cat *" }, { resource: "git status", save: "git status *" }, ], directories: [], }) expect( await Effect.runPromise(ShellParse.scan("cat <<'EOF' && git status\nhello\nEOF", "bash", "/workspace")), ).toEqual({ commands: [ { resource: "cat <<'EOF'", save: "cat *" }, { resource: "git status", save: "git status *" }, ], directories: [], }) expect( await Effect.runPromise( ShellParse.scan("if cat <<'EOF' | grep -q hi; then echo ok; fi\nhi\nEOF", "bash", "/workspace"), ), ).toEqual({ commands: [ { resource: "cat <<'EOF'", save: "cat *" }, { resource: "grep -q hi", save: "grep *" }, { resource: "echo ok", save: "echo *" }, ], directories: [], }) expect( await Effect.runPromise( ShellParse.scan("true && git add -p file.rs <<< $'n\\ny\\n' >/dev/null", "bash", "/workspace"), ), ).toEqual({ commands: [ { resource: "true", save: "true *" }, { resource: "git add -p file.rs <<< $'n\\ny\\n' >/dev/null", save: "git add *" }, ], directories: [], }) }) test("scans Zsh glob qualifiers, indirect parameter flags, and disown operators while rejecting code qualifiers", async () => { expect( await Effect.runPromise( ShellParse.scan( 'for f in locks/waiter-*(N); do echo "${(P)f}"; done; ls -d src/**/target(N/) *.ts(.OL[1,3]); nohup sleep 1 >/dev/null 2>&1 &! ; echo started', "zsh", "/workspace", ), ), ).toEqual({ commands: [ { resource: 'echo "${(P)f}"', save: "echo *" }, { resource: "ls -d src/**/target(N/) *.ts(.OL[1,3])", save: "ls *" }, { resource: "nohup sleep 1 >/dev/null 2>&1", save: "nohup *" }, { resource: "echo started", save: "echo *" }, ], directories: [], }) for (const unsafe of [ "echo *(e:'rm -rf /':)", "echo *(+cmd)", "echo *(+1)", "echo *(P:-f:)", "echo *(u:root:)", "echo *(:Q)", "a=(1); r='a[$(printf pwn)]'; echo ${(P)r}", ]) { expect(await Effect.runPromise(Effect.result(ShellParse.scan(unsafe, "zsh", "/workspace")))).toMatchObject({ _tag: "Failure", failure: { message: "Portable shell scanner cannot analyze command: dynamic-execution" }, }) } }) test.each([ ["bash", "x=\"a$(printf '\\x5b')\\$(touch pwn)]\"; echo $((x))"], ["bash", "x=$(printf 'a\\x5b\\x24\\x28touch pwn\\x29]'); echo $((x))"], ["bash", "x=$(echo 'a{$(touch pwn)]' | tr '{' '\\133'); (( x ))"], ["bash", "F='$HOME/bin'; SIZE=$(stat -f %z file); N=$(( (SIZE + 1023) / 1024 )); echo $N"], ["zsh", "r=\"a$(printf '\\x5b')\\$(touch pwn)]\"; echo ${(P)r}"], ])("%s rejects variable sinks when a substitution can build the subscript: %s", async (shell, command) => { expect(await Effect.runPromise(Effect.result(ShellParse.scan(command, shell, "/workspace")))).toMatchObject({ _tag: "Failure", failure: { message: "Portable shell scanner cannot analyze command: dynamic-execution" }, }) }) test.each([ 'Write-Output "$(Get-Location)"', "$value = Get-Date; Write-Output $value", "if ($true) { Write-Output yes } else { Write-Output no }", "if (Test-Path file) { Get-Item file }", "foreach ($value in @('a','b')) { Write-Output $value }", "for ($i=0; $i -lt 2; $i++) { Write-Output $i }", "while (Test-Path file) { Get-Item file; break }", "function Show-Value { Write-Output value }; Show-Value", "Get-Item -Path (Join-Path src file)", 'Write-Output "line1`nline2"', "Write-Output `\n continued", "<# comment #> Write-Output done", "Write-Output @'\nhello\n'@", "git st`atus", ])("PowerShell extracts commands without rejecting ordinary syntax: %s", async (command) => { const scanned = await Effect.runPromise(ShellParse.scan(command, "pwsh", "/workspace")) for (const item of scanned.commands) expect(Wildcard.match(item.resource, item.save), item.resource).toBe(true) }) })