Files
music_library/.pi/extensions/sensitive-file-guard/extractCommandNames.test.ts
T
Claudio Ortolina a88c704fe5 ML-175: parse bash commands structurally via unbash
Replace regex-based command blocking with structural AST parsing
using unbash (zero-dependency TypeScript bash parser). The walker
collects command names only from executable positions (simple
commands, pipelines, subshells, command substitutions, control-flow
bodies), excluding arguments, comments, and quoted strings.

- Convert to directory-style extension with pinned unbash@3.0.0
- Add AST walker (ast.ts) covering 15 shell construct types
- Update blocked_commands to plain names instead of regexes
- Keep regex fallback for parse failures (degradation path)
- Path-based blocking unchanged for non-bash tools
- 41 tests cover blocked/allowed cases including false positives
2026-05-11 17:42:48 +01:00

218 lines
5.5 KiB
TypeScript

import { extractCommandNames } from "./ast.ts";
// --- Test runner ---
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) {
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(", ")}]`);
}
}
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);