Files

188 lines
5.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
type CommandWord,
classifyWrapperWords,
executedUnitOf,
} from "#src/access-intent/bash/wrapper-analysis";
/**
* Split a command unit into words the way the AST walk does: whitespace
* separated, but a quoted span is one word carrying its quotes — tree-sitter
* emits a `string`/`raw_string` argument as a single named child.
*
* `program.test.ts` pins the real node adapter end to end; this stands in for it
* so the extraction rules can be exercised without a parse.
*/
function words(unitText: string): CommandWord[] {
const out: CommandWord[] = [];
const pattern = /"[^"]*"|'[^']*'|\S+/g;
let match = pattern.exec(unitText);
while (match !== null) {
out.push({ text: match[0], offset: match.index });
match = pattern.exec(unitText);
}
return out;
}
describe("classifyWrapperWords", () => {
describe("opaque payloads", () => {
it.each([
"eval rm",
"bash -c rm",
"sh -c rm",
"dash -c rm",
"zsh -c rm",
"ksh -c rm",
"bash -ec rm",
"bash -xc rm",
"/bin/bash -c rm",
])("flags %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("opaque-payload");
});
it("does not flag a shell running a script file", () => {
expect(classifyWrapperWords(words("bash script.sh"))).toBeUndefined();
});
it("does not flag a -c cluster after the end-of-options marker", () => {
expect(classifyWrapperWords(words("bash -- -c"))).toBeUndefined();
});
});
describe("indirection wrappers", () => {
it.each([
"sudo aws s3 ls",
"env FOO=bar aws",
"xargs grep foo",
"timeout 10 grep foo",
"nice -n 5 make",
"doas ls",
"flock /tmp/lock ls",
])("flags %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("indirection");
});
it.each([
"find . -exec grep foo {} ;",
"find . -execdir rm {} ;",
"fd -x rm",
"fd --exec-batch rm",
])("flags the exec-conditional %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBe("indirection");
});
it("does not flag a bare search", () => {
expect(classifyWrapperWords(words("find . -name x"))).toBeUndefined();
});
});
describe("ordinary commands", () => {
it.each([
"ls -la",
"grep -c foo file",
"git status",
])("does not flag %s", (unit) => {
expect(classifyWrapperWords(words(unit))).toBeUndefined();
});
it("does not flag an empty word list", () => {
expect(classifyWrapperWords([])).toBeUndefined();
});
});
});
describe("executedUnitOf", () => {
/** Extract from a unit spelled as plain whitespace-separated words. */
function executedUnit(unitText: string): string | null {
return executedUnitOf(unitText, words(unitText));
}
describe("opaque payloads", () => {
it.each([
['bash -c "rm -rf /"', "rm -rf /"],
["bash -c 'rm -rf /'", "rm -rf /"],
['sh -ec "make build"', "make build"],
['/bin/bash -c "ls"', "ls"],
['eval "rm x"', "rm x"],
])("names the inner program of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("returns null when the payload argument is missing", () => {
expect(executedUnit("bash -c")).toBeNull();
});
});
describe("indirection wrappers", () => {
it.each([
["sudo aws s3 rm", "aws s3 rm"],
["sudo -u root aws s3 rm", "aws s3 rm"],
["sudo -- ls -la", "ls -la"],
["xargs grep foo", "grep foo"],
["xargs -0 -n1 grep foo", "grep foo"],
["xargs -I{} rm {}", "rm {}"],
["timeout 10 grep foo", "grep foo"],
["timeout -s KILL 10 grep foo", "grep foo"],
["nice -n 5 make build", "make build"],
["env FOO=bar grep foo", "grep foo"],
["flock /tmp/lock aws s3 ls", "aws s3 ls"],
["watch -n 2 ls", "ls"],
])("names the inner command of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("preserves the inner command's original spacing and quoting", () => {
expect(executedUnit("sudo grep 'a b' x")).toBe("grep 'a b' x");
});
it.each([
"xargs",
"sudo",
"sudo -u root",
"timeout 10",
])("returns null when %s names no inner command", (unit) => {
expect(executedUnit(unit)).toBeNull();
});
it("returns null rather than guessing past an unknown trailing option", () => {
expect(executedUnit("xargs --unknown-opt")).toBeNull();
});
});
describe("exec-conditional wrappers", () => {
it.each([
["find . -name x -exec grep foo {} ;", "grep foo {}"],
["find . -exec rm {} +", "rm {}"],
["find . -execdir grep foo {} ;", "grep foo {}"],
["fd -x rm", "rm"],
["fd --exec-batch rm -f", "rm -f"],
])("names the per-result command of %s", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
it("returns null when the exec flag ends the command", () => {
expect(executedUnit("find . -exec")).toBeNull();
});
});
describe("nested wrappers", () => {
it.each([
["sudo timeout 5 xargs grep foo", "grep foo"],
["sudo bash -c 'rm x'", "rm x"],
["timeout 10 sudo -u root aws s3 rm", "aws s3 rm"],
])("unwraps %s to its innermost command", (unit, expected) => {
expect(executedUnit(unit)).toBe(expected);
});
});
describe("nothing to add", () => {
it("returns null for an ordinary command", () => {
expect(executedUnit("grep foo")).toBeNull();
});
it("returns null for an empty word list", () => {
expect(executedUnitOf("", [])).toBeNull();
});
});
});