diff --git a/.pi/extensions/sensitive-file-guard/extractCommandNames.test.ts b/.pi/extensions/sensitive-file-guard/extractCommandNames.test.ts index 83e19bff..bc8df3e3 100644 --- a/.pi/extensions/sensitive-file-guard/extractCommandNames.test.ts +++ b/.pi/extensions/sensitive-file-guard/extractCommandNames.test.ts @@ -1,217 +1,312 @@ +/** + * Tests for extractCommandNames from ast.ts using Node.js built-in node:test runner. + * + * Run with: node --experimental-strip-types --test extractCommandNames.test.ts + */ + +import { describe, it } from "node:test"; +import * as assert from "node:assert/strict"; import { extractCommandNames } from "./ast.ts"; -// --- Test runner --- +// ── Test helpers ──────────────────────────────────────────────────────────── + const blocked = new Set(["printenv", "env"]); -const tests: { - cmd: string; - expected: "BLOCKED" | "ALLOWED"; - reason: string; -}[] = [ - // Simple commands - { cmd: "printenv", expected: "BLOCKED", reason: "Simple command match" }, - { cmd: "env FOO=bar", expected: "BLOCKED", reason: "Simple command match" }, - { - cmd: "FOO=bar printenv", - expected: "BLOCKED", - reason: "Assignment prefix — printenv is still the command", - }, - { - cmd: "FOO=bar env", - expected: "BLOCKED", - reason: "Assignment prefix — env is still the command", - }, - - // Command substitution — blocked commands inside $() or backticks run - { cmd: "$(env)", expected: "BLOCKED", reason: "Command substitution" }, - { cmd: "$(printenv)", expected: "BLOCKED", reason: "Command substitution" }, - { - cmd: "`env`", - expected: "BLOCKED", - reason: "Backtick command substitution", - }, - { - cmd: "echo $(env)", - expected: "BLOCKED", - reason: "env inside command substitution in echo argument", - }, - { - cmd: "FOO=$(env) echo hi", - expected: "BLOCKED", - reason: "env inside command substitution in assignment value", - }, - - // Subshell - { cmd: "(env)", expected: "BLOCKED", reason: "Subshell — env runs inside" }, - - // Pipeline — any member triggers a block - { - cmd: "env | grep FOO", - expected: "BLOCKED", - reason: "Pipeline — env is first command", - }, - { - cmd: "grep FOO | env", - expected: "BLOCKED", - reason: "Pipeline — env is second command", - }, - { - cmd: "printenv | env", - expected: "BLOCKED", - reason: "Both blocked commands in pipeline", - }, - - // Compound lists and control flow - { - cmd: "for f in *; do env; done", - expected: "BLOCKED", - reason: "env in for loop body", - }, - { - cmd: "for f in $(env); do echo $f; done", - expected: "BLOCKED", - reason: "env in for wordlist command substitution", - }, - { - cmd: "for ((i=0;i<10;i++)); do env; done", - expected: "BLOCKED", - reason: "env in arithmetic for", - }, - { - cmd: "while true; do env; done", - expected: "BLOCKED", - reason: "env in while body", - }, - { - cmd: "until false; do env; done", - expected: "BLOCKED", - reason: "env in until body (parsed as While with kind:until)", - }, - { - cmd: "if true; then env; fi", - expected: "BLOCKED", - reason: "env in if-then", - }, - { - cmd: "case x in a) env;; esac", - expected: "BLOCKED", - reason: "env in case body", - }, - { - cmd: "case $(env) in a) echo hi;; esac", - expected: "BLOCKED", - reason: "env in Case.word substitution", - }, - { cmd: "true && env", expected: "BLOCKED", reason: "env in AndOr (&&)" }, - { - cmd: "select f in $(env); do echo $f; done", - expected: "BLOCKED", - reason: "env in select wordlist", - }, - - // Function definition, brace group, coproc - { - cmd: "foo() { env; }", - expected: "BLOCKED", - reason: "env in function body", - }, - { cmd: "{ env; }", expected: "BLOCKED", reason: "env in brace group" }, - { cmd: "coproc env", expected: "BLOCKED", reason: "env in coproc body" }, - - // --- ALLOWED: blocked command name is NOT at an executable position --- - - { - cmd: "echo printenv", - expected: "ALLOWED", - reason: "printenv is an argument to echo, not a command", - }, - { cmd: "# printenv", expected: "ALLOWED", reason: "Comment — not executed" }, - { - cmd: 'echo "use env to..."', - expected: "ALLOWED", - reason: "Inside double-quoted string", - }, - { - cmd: "echo 'printenv'", - expected: "ALLOWED", - reason: "Inside single-quoted string", - }, - { - cmd: "(echo printenv)", - expected: "ALLOWED", - reason: "printenv is argument to echo inside subshell", - }, - { - cmd: "$(echo env)", - expected: "ALLOWED", - reason: "env is argument to echo inside command substitution", - }, - { - cmd: 'echo "use env" | grep FOO', - expected: "ALLOWED", - reason: "env in double-quoted string in pipeline", - }, - - // --- ALLOWED: command is not in the blocked set --- - { - cmd: "export FOO=bar", - expected: "ALLOWED", - reason: "export is not in blocked_commands", - }, - { - cmd: "ls -la", - expected: "ALLOWED", - reason: "ls is not in blocked_commands", - }, - { - cmd: "echo hi; echo there", - expected: "ALLOWED", - reason: "Semicolon-separated harmless commands", - }, - { - cmd: "echo hi || echo fail", - expected: "ALLOWED", - reason: "Or-separated harmless commands", - }, - { - cmd: "[[ -f file ]]", - expected: "ALLOWED", - reason: "TestCommand — no commands executed", - }, - - // --- ALLOWED: no command at all --- - { cmd: "", expected: "ALLOWED", reason: "Empty command" }, - { - cmd: "FOO=bar", - expected: "ALLOWED", - reason: "Bare assignment, no command name", - }, - { - cmd: ">/dev/null", - expected: "ALLOWED", - reason: "Redirection only, no command name", - }, -]; - -let passed = 0; -let failed = 0; - -for (const { cmd, expected, reason } of tests) { +function checkBlocked( + cmd: string, + expected: "BLOCKED" | "ALLOWED", + _reason: string, +): void { const names = extractCommandNames(cmd); const hasBlocked = [...names].some((n) => blocked.has(n)); const actual = hasBlocked ? "BLOCKED" : "ALLOWED"; - - if (actual === expected) { - passed++; - console.log(`✅ ${expected.padEnd(7)} | ${cmd.padEnd(45)} | ${reason}`); - } else { - failed++; - console.log( - `❌ expected ${expected.padEnd(7)} got ${actual.padEnd(7)} | ${cmd.padEnd(45)} | ${reason}`, - ); - console.log(` Command names found: [${[...names].join(", ")}]`); - } + assert.equal( + actual, + expected, + `cmd: "${cmd}" — names found: [${[...names].join(", ")}]`, + ); } -console.log(`\n${passed} passed, ${failed} failed`); -if (failed > 0) process.exit(1); +// ── BLOCKED: Simple commands ──────────────────────────────────────────────── + +describe("extractCommandNames — simple commands", () => { + it("blocks a simple blocked command", () => { + checkBlocked("printenv", "BLOCKED", "Simple command match"); + }); + + it("blocks env with prefix assignment", () => { + checkBlocked("env FOO=bar", "BLOCKED", "Simple command match"); + }); + + it("blocks printenv with assignment prefix", () => { + checkBlocked( + "FOO=bar printenv", + "BLOCKED", + "Assignment prefix — printenv is still the command", + ); + }); + + it("blocks env with assignment prefix", () => { + checkBlocked( + "FOO=bar env", + "BLOCKED", + "Assignment prefix — env is still the command", + ); + }); +}); + +// ── BLOCKED: Command substitution ────────────────────────────────────────── + +describe("extractCommandNames — command substitution", () => { + it("blocks $(env)", () => { + checkBlocked("$(env)", "BLOCKED", "Command substitution"); + }); + + it("blocks $(printenv)", () => { + checkBlocked("$(printenv)", "BLOCKED", "Command substitution"); + }); + + it("blocks backtick env", () => { + checkBlocked("`env`", "BLOCKED", "Backtick command substitution"); + }); + + it("blocks env inside command substitution in echo argument", () => { + checkBlocked( + "echo $(env)", + "BLOCKED", + "env inside command substitution in echo argument", + ); + }); + + it("blocks env inside command substitution in assignment value", () => { + checkBlocked( + "FOO=$(env) echo hi", + "BLOCKED", + "env inside command substitution in assignment value", + ); + }); +}); + +// ── BLOCKED: Subshell ────────────────────────────────────────────────────── + +describe("extractCommandNames — subshell", () => { + it("blocks env in subshell", () => { + checkBlocked("(env)", "BLOCKED", "Subshell — env runs inside"); + }); +}); + +// ── BLOCKED: Pipeline ────────────────────────────────────────────────────── + +describe("extractCommandNames — pipeline", () => { + it("blocks env as first command in pipeline", () => { + checkBlocked( + "env | grep FOO", + "BLOCKED", + "Pipeline — env is first command", + ); + }); + + it("blocks env as second command in pipeline", () => { + checkBlocked( + "grep FOO | env", + "BLOCKED", + "Pipeline — env is second command", + ); + }); + + it("blocks both blocked commands in pipeline", () => { + checkBlocked( + "printenv | env", + "BLOCKED", + "Both blocked commands in pipeline", + ); + }); +}); + +// ── BLOCKED: Compound lists and control flow ─────────────────────────────── + +describe("extractCommandNames — compound lists and control flow", () => { + it("blocks env in for loop body", () => { + checkBlocked("for f in *; do env; done", "BLOCKED", "env in for loop body"); + }); + + it("blocks env in for wordlist command substitution", () => { + checkBlocked( + "for f in $(env); do echo $f; done", + "BLOCKED", + "env in for wordlist command substitution", + ); + }); + + it("blocks env in arithmetic for", () => { + checkBlocked( + "for ((i=0;i<10;i++)); do env; done", + "BLOCKED", + "env in arithmetic for", + ); + }); + + it("blocks env in while body", () => { + checkBlocked("while true; do env; done", "BLOCKED", "env in while body"); + }); + + it("blocks env in until body (parsed as While with kind:until)", () => { + checkBlocked( + "until false; do env; done", + "BLOCKED", + "env in until body (parsed as While with kind:until)", + ); + }); + + it("blocks env in if-then", () => { + checkBlocked("if true; then env; fi", "BLOCKED", "env in if-then"); + }); + + it("blocks env in case body", () => { + checkBlocked("case x in a) env;; esac", "BLOCKED", "env in case body"); + }); + + it("blocks env in Case.word substitution", () => { + checkBlocked( + "case $(env) in a) echo hi;; esac", + "BLOCKED", + "env in Case.word substitution", + ); + }); + + it("blocks env in AndOr (&&)", () => { + checkBlocked("true && env", "BLOCKED", "env in AndOr (&&)"); + }); + + it("blocks env in select wordlist", () => { + checkBlocked( + "select f in $(env); do echo $f; done", + "BLOCKED", + "env in select wordlist", + ); + }); +}); + +// ── BLOCKED: Function definition, brace group, coproc ────────────────────── + +describe("extractCommandNames — function, brace, coproc", () => { + it("blocks env in function body", () => { + checkBlocked("foo() { env; }", "BLOCKED", "env in function body"); + }); + + it("blocks env in brace group", () => { + checkBlocked("{ env; }", "BLOCKED", "env in brace group"); + }); + + it("blocks env in coproc body", () => { + checkBlocked("coproc env", "BLOCKED", "env in coproc body"); + }); +}); + +// ── ALLOWED: Blocked name at non-executable position ─────────────────────── + +describe("extractCommandNames — blocked name not at executable position", () => { + it("allows printenv as argument to echo", () => { + checkBlocked( + "echo printenv", + "ALLOWED", + "printenv is an argument to echo, not a command", + ); + }); + + it("allows printenv in comment", () => { + checkBlocked("# printenv", "ALLOWED", "Comment — not executed"); + }); + + it("allows env in double-quoted string", () => { + checkBlocked( + 'echo "use env to..."', + "ALLOWED", + "Inside double-quoted string", + ); + }); + + it("allows printenv in single-quoted string", () => { + checkBlocked("echo 'printenv'", "ALLOWED", "Inside single-quoted string"); + }); + + it("allows printenv as argument to echo inside subshell", () => { + checkBlocked( + "(echo printenv)", + "ALLOWED", + "printenv is argument to echo inside subshell", + ); + }); + + it("allows env as argument to echo inside command substitution", () => { + checkBlocked( + "$(echo env)", + "ALLOWED", + "env is argument to echo inside command substitution", + ); + }); + + it("allows env in double-quoted string in pipeline", () => { + checkBlocked( + 'echo "use env" | grep FOO', + "ALLOWED", + "env in double-quoted string in pipeline", + ); + }); +}); + +// ── ALLOWED: Command not in blocked set ──────────────────────────────────── + +describe("extractCommandNames — command not in blocked set", () => { + it("allows export", () => { + checkBlocked( + "export FOO=bar", + "ALLOWED", + "export is not in blocked_commands", + ); + }); + + it("allows ls", () => { + checkBlocked("ls -la", "ALLOWED", "ls is not in blocked_commands"); + }); + + it("allows semicolon-separated harmless commands", () => { + checkBlocked( + "echo hi; echo there", + "ALLOWED", + "Semicolon-separated harmless commands", + ); + }); + + it("allows OR-separated harmless commands", () => { + checkBlocked( + "echo hi || echo fail", + "ALLOWED", + "Or-separated harmless commands", + ); + }); + + it("allows TestCommand ([[ ... ]])", () => { + checkBlocked( + "[[ -f file ]]", + "ALLOWED", + "TestCommand — no commands executed", + ); + }); +}); + +// ── ALLOWED: No command at all ───────────────────────────────────────────── + +describe("extractCommandNames — no command at all", () => { + it("allows empty command", () => { + checkBlocked("", "ALLOWED", "Empty command"); + }); + + it("allows bare assignment with no command name", () => { + checkBlocked("FOO=bar", "ALLOWED", "Bare assignment, no command name"); + }); + + it("allows redirection only with no command name", () => { + checkBlocked(">/dev/null", "ALLOWED", "Redirection only, no command name"); + }); +});