refactor sensitive-file-guard tests to use node:test
This commit is contained in:
@@ -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";
|
import { extractCommandNames } from "./ast.ts";
|
||||||
|
|
||||||
// --- Test runner ---
|
// ── Test helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const blocked = new Set(["printenv", "env"]);
|
const blocked = new Set(["printenv", "env"]);
|
||||||
|
|
||||||
const tests: {
|
function checkBlocked(
|
||||||
cmd: string;
|
cmd: string,
|
||||||
expected: "BLOCKED" | "ALLOWED";
|
expected: "BLOCKED" | "ALLOWED",
|
||||||
reason: string;
|
_reason: string,
|
||||||
}[] = [
|
): void {
|
||||||
// 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 names = extractCommandNames(cmd);
|
||||||
const hasBlocked = [...names].some((n) => blocked.has(n));
|
const hasBlocked = [...names].some((n) => blocked.has(n));
|
||||||
const actual = hasBlocked ? "BLOCKED" : "ALLOWED";
|
const actual = hasBlocked ? "BLOCKED" : "ALLOWED";
|
||||||
|
assert.equal(
|
||||||
if (actual === expected) {
|
actual,
|
||||||
passed++;
|
expected,
|
||||||
console.log(`✅ ${expected.padEnd(7)} | ${cmd.padEnd(45)} | ${reason}`);
|
`cmd: "${cmd}" — names found: [${[...names].join(", ")}]`,
|
||||||
} 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`);
|
// ── BLOCKED: Simple commands ────────────────────────────────────────────────
|
||||||
if (failed > 0) process.exit(1);
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user