274 lines
14 KiB
JavaScript
274 lines
14 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { validatePythonRevisionStates, validateRevisionState } from "./revision-state-policy.mjs";
|
|
|
|
function rejects(source, path) {
|
|
assert.throws(() => validateRevisionState(source, path), /revision-state/, source);
|
|
}
|
|
function passes(source, path) {
|
|
assert.doesNotThrow(() => validateRevisionState(source, path));
|
|
}
|
|
|
|
test("PowerShell scoped and braced revision variables remain executable", () => {
|
|
rejects('${revision}.state', "scripts/direct.ps1");
|
|
rejects('${workspaceRevision}["state"]', "scripts/bracket.ps1");
|
|
rejects('Write-Output "$(${selectedWorkspace}.state)"', "scripts/subexpression.ps1");
|
|
rejects('${script:revision}.state', "scripts/scoped.ps1");
|
|
rejects('${global:workspaceRevision}["state"]', "scripts/global.ps1");
|
|
for (const source of [
|
|
'$REVISION.STATE',
|
|
'${Revision}.state',
|
|
'$WORKSPACEREVISION["STATE"]',
|
|
'${GLOBAL:SELECTEDWORKSPACE}.State',
|
|
'$REVISION["ST" + "ATE"]',
|
|
'${Revision}[("sT" + "AtE")]',
|
|
'$record.REVISION.STATE',
|
|
'$record.WORKSPACEREVISION["STATE"]',
|
|
'$record["REVISION"].STATE',
|
|
]) rejects(source, "scripts/case.ps1");
|
|
passes('REVISION.STATE; revision.STATE; revision["ST" + "ATE"]; record.REVISION.STATE; record["REVISION"].state', "backend/src/case-sensitive.ts");
|
|
});
|
|
|
|
test("Bash jq command forms and associative revision parameters are active", () => {
|
|
for (const source of [
|
|
"value=$(jq -r '.revision.state' snapshot.json)",
|
|
"value=$(command jq -r '.workspaceRevision.state' snapshot.json)",
|
|
"/usr/bin/jq --arg x y '.selectedWorkspace.state' snapshot.json",
|
|
"env -i MODE=x jq -- '.revision.state' snapshot.json",
|
|
"env -u MODE /opt/tools/jq -r '.workspaceRevision.state' snapshot.json",
|
|
"echo safe\nvalue=`jq -r '.selectedWorkspace.state' snapshot.json`",
|
|
"sudo -u nobody /usr/bin/jq -r '.revision.state' snapshot.json",
|
|
"nice -n 5 jq -r '.workspaceRevision.state' snapshot.json",
|
|
"time jq -r '.selectedWorkspace.state' snapshot.json",
|
|
"printf x | xargs -n 1 jq -r '.revision.state'",
|
|
"timeout -k 2 5 jq -r '.revision.state' snapshot.json",
|
|
`stdbuf -o L jq -r '.["workspaceRevision"].state' snapshot.json`,
|
|
`stdbuf -oL jq -r '.["selectedWorkspace"]["state"]' snapshot.json`,
|
|
`nohup jq -r '.revision["state"]' snapshot.json`,
|
|
"< snapshot.json jq -r '.workspaceRevision.state'",
|
|
"sudo MODE=x jq -r '.selectedWorkspace.state' snapshot.json",
|
|
String.raw`jq -r '"x \(.revision.state)"' snapshot.json`,
|
|
"jq < snapshot.json -r '.revision.state'",
|
|
"jq -r < snapshot.json '.workspaceRevision.state'",
|
|
"jq --arg note safe < snapshot.json '.selectedWorkspace.state'",
|
|
"jq -r '.revision?.state' snapshot.json",
|
|
`jq -r '.["workspaceRevision"]?["state"]' snapshot.json`,
|
|
`jq -r '.["revision"]?.["state"]' snapshot.json`,
|
|
`jq -r '."revision".state' snapshot.json`,
|
|
`jq -r '."workspaceRevision"."state"' snapshot.json`,
|
|
"jq -r '$revision.state' snapshot.json",
|
|
"jq -r '($selectedWorkspace).state' snapshot.json",
|
|
`${"env ".repeat(17)}jq -r '.revision.state' snapshot.json`,
|
|
"jq<input.json -r '.revision.state'",
|
|
"jq 2>/dev/null -r '.workspaceRevision.state' snapshot.json",
|
|
"{ jq -r '.selectedWorkspace.state' snapshot.json; }",
|
|
"! jq -r '.revision.state' snapshot.json",
|
|
"if jq -r '.workspaceRevision.state' snapshot.json; then :; fi",
|
|
"if false; then :; elif jq -r '.selectedWorkspace.state' snapshot.json; then :; fi",
|
|
"while false; do jq -r '.revision.state' snapshot.json; done",
|
|
"until false; do jq -r '.workspaceRevision.state' snapshot.json; done",
|
|
"jq -r '.revision | .state' snapshot.json",
|
|
"jq -r '(.workspaceRevision | .state)' snapshot.json",
|
|
`jq -r '.["revi" + "sion"].state' snapshot.json`,
|
|
`jq -r '.["workspace" + "Revision"]["st" + "ate"]' snapshot.json`,
|
|
"jq 2>&1 -r '.revision.state' snapshot.json",
|
|
"jq 2>&- -r '.workspaceRevision.state' snapshot.json",
|
|
"jq 0<&3 -r '.selectedWorkspace.state' snapshot.json",
|
|
"jq &>/dev/null -r '.revision.state' snapshot.json",
|
|
"jq &>>log -r '.workspaceRevision.state' snapshot.json",
|
|
"jq >|output -r '.selectedWorkspace.state' snapshot.json",
|
|
"jq {fd}>output -r '.revision.state' snapshot.json",
|
|
"exec jq -r '.workspaceRevision.state' snapshot.json",
|
|
"coproc jq -r '.selectedWorkspace.state' snapshot.json",
|
|
"coproc worker jq -r '.revision.state' snapshot.json",
|
|
"coproc worker >out jq -r '.workspaceRevision.state' snapshot.json",
|
|
"coproc worker 2>/dev/null jq -r '.selectedWorkspace.state' snapshot.json",
|
|
"coproc worker VAR=x jq -r '.revision.state' snapshot.json",
|
|
`jq -r '.["revi" + ("sion")].state' snapshot.json`,
|
|
"jq -r '.revision | . | .state' snapshot.json",
|
|
"jq -r '(.workspaceRevision | (.) | .state)' snapshot.json",
|
|
"jq -r '.revision | select(.) | .state' snapshot.json",
|
|
"jq -r '.workspaceRevision | {value:.state}' snapshot.json",
|
|
"jq -r '.selectedWorkspace | [.state]' snapshot.json",
|
|
"jq -r '.revision + .state' snapshot.json",
|
|
"jq < <(cat snapshot.json) -r '.revision.state'",
|
|
"jq < <(cat <(printf snapshot.json)) -r '.workspaceRevision.state'",
|
|
"jq > >(cat >/dev/null) -r '.selectedWorkspace.state' snapshot.json",
|
|
`jq < <(printf '%s\n' "$((1 + (2)))") -r '.revision.state'`,
|
|
"jq < <(cat snapshot.json -r '.revision.state'",
|
|
`${"<(".repeat(65)}echo snapshot${")".repeat(65)} jq -r '.workspaceRevision.state'`,
|
|
"cat <(jq -r '.revision.state' snapshot.json)",
|
|
"cat snapshot.json > >(jq -r '.workspaceRevision.state')",
|
|
`echo "$(jq -r '.selectedWorkspace.state' snapshot.json)"`,
|
|
"value=$(jq -r '.revision.state' snapshot.json)",
|
|
`echo "\`jq -r '.workspaceRevision.state' snapshot.json\`"`,
|
|
`echo "$(cat <(jq -r '.selectedWorkspace.state' snapshot.json))"`,
|
|
`${"$(".repeat(33)}jq -r '.revision.state' snapshot.json${")".repeat(33)}`,
|
|
`echo "$(( $(jq -r '.revision.state' snapshot.json) + 0 ))"`,
|
|
"echo \"$(( `jq -r '.workspaceRevision.state' snapshot.json` + 0 ))\"",
|
|
"echo `echo \\`jq -r '.selectedWorkspace.state' snapshot.json\\``",
|
|
"echo \"`echo \\`jq -r '.revision.state' snapshot.json\\``\"",
|
|
`${"$(( ".repeat(33)}$(jq -r '.workspaceRevision.state' snapshot.json)${" + 0 ))".repeat(33)}`,
|
|
'old=${revision["state"]}',
|
|
"old=${workspaceRevision[state]}",
|
|
"old=${revision[state]:-missing}",
|
|
"old=${workspaceRevision['state']:=missing}",
|
|
"old=${selectedWorkspace[state]:1:2}",
|
|
]) rejects(source, "scripts/policy.sh");
|
|
const jqFilters = [
|
|
".revision?.state", '.["revision"]?.["state"]', '."revision".state',
|
|
'."workspaceRevision"."state"', "(.revision).state", "$revision.state",
|
|
".revision | .state", "(.workspaceRevision | .state)",
|
|
'.["revi" + "sion"].state', '.["revi" + ("sion")].state',
|
|
".revision | . | .state", "(.workspaceRevision | (.) | .state)",
|
|
".revision | select(.) | .state", ".workspaceRevision | {value:.state}",
|
|
'.revision | ["state"]', '(.workspaceRevision | (["state"]))', ".selectedWorkspace | $state",
|
|
];
|
|
for (const filter of jqFilters) {
|
|
const compiled = spawnSync("jq", ["-n", "--argjson", "revision", "{}", "--arg", "state", "x", filter], { encoding: "utf8" });
|
|
if (compiled.error?.code !== "ENOENT") assert.equal(compiled.status, 0, `${filter}: ${compiled.stderr}`);
|
|
}
|
|
passes("cat <<'EOF'\nrevision.state\nEOF\n", "scripts/literal.sh");
|
|
passes("echo '${revision[state]}'\n", "scripts/single-quoted-parameter.sh");
|
|
passes(`echo "<(jq '.revision.state')"\n`, "scripts/literal-process-text.sh");
|
|
passes(`echo "ordinary jq '.workspaceRevision.state' text"\n`, "scripts/literal-jq-text.sh");
|
|
passes(`echo '$(jq -r ".selectedWorkspace.state")'\n`, "scripts/single-quoted-command-text.sh");
|
|
passes(`# profile's harmless note
|
|
printf 'ok\n'
|
|
`, "scripts/comment-apostrophe.sh");
|
|
passes(`cat <( # profile's harmless note
|
|
printf 'snapshot\n'
|
|
)
|
|
`, "scripts/substitution-comment-apostrophe.sh");
|
|
passes(`echo "$(( 1 + (2 * 3) ))"\n`, "scripts/literal-arithmetic.sh");
|
|
passes(`echo $(( jq + revision + state ))\n`, "scripts/arithmetic-identifiers.sh");
|
|
passes("echo \\`jq -r '.revision.state' snapshot.json\\`\n", "scripts/escaped-literal-backticks.sh");
|
|
passes("echo \"\\`jq -r '.workspaceRevision.state' snapshot.json\\`\"\n", "scripts/double-quoted-literal-backticks.sh");
|
|
passes("echo `printf '%s' '\\`jq -r \".selectedWorkspace.state\" snapshot.json\\`'`\n", "scripts/quoted-nonexecuting-nested-backticks.sh");
|
|
passes("jq --arg note 'revision.state' '.' file\n", "scripts/jq-arg.sh");
|
|
passes(`jq --argjson note '"revision.state"' '.' file
|
|
`, "scripts/jq-argjson.sh");
|
|
passes("jq -r '.' revision.state.json\n", "scripts/jq-file.sh");
|
|
passes("jq -r '.revision.id' snapshot.json\n", "scripts/jq-simple-non-state.sh");
|
|
passes("jq -f revision.state.jq snapshot.json\n", "scripts/jq-from-file.sh");
|
|
passes("jq --from-file workspaceRevision.state.jq snapshot.json\n", "scripts/jq-long-from-file.sh");
|
|
passes(`jq -r '"revision.state"' snapshot.json
|
|
`, "scripts/jq-string.sh");
|
|
passes(`jq -r '{note:"selectedWorkspace.state"}' snapshot.json
|
|
`, "scripts/jq-object.sh");
|
|
passes(`jq -r '.revision | "state"' snapshot.json
|
|
`, "scripts/jq-pipe-literal-right.sh");
|
|
passes(`jq -r '"revision" | .state' snapshot.json
|
|
`, "scripts/jq-pipe-literal-left.sh");
|
|
passes(`jq -r '.revision | ["state"]' snapshot.json
|
|
`, "scripts/jq-pipe-array.sh");
|
|
passes(`jq -r '(.workspaceRevision | (["state"]))' snapshot.json
|
|
`, "scripts/jq-pipe-parenthesized-array.sh");
|
|
passes(`jq --arg state x '.selectedWorkspace | $state' snapshot.json
|
|
`, "scripts/jq-pipe-variable.sh");
|
|
for (const opener of ["'E'OF", "E'OF'", "E\\OF"]) {
|
|
passes(`cat <<${opener}
|
|
revision.state
|
|
EOF
|
|
`, "scripts/partial-quoted-heredoc.sh");
|
|
}
|
|
rejects("cat <<'E'OF\nrevision.state\nEOF\nworkspaceRevision.state\n", "scripts/after-heredoc.sh");
|
|
rejects("cat <<'EOF'\nrevision.state\n", "scripts/unclosed-heredoc.sh");
|
|
rejects(`echo "<<'EOF'"
|
|
jq -r '.revision.state' snapshot.json
|
|
`, "scripts/quoted-opener.sh");
|
|
});
|
|
|
|
test("Python helper resolves active AST expressions and static format bindings", () => {
|
|
const rejectsPython = (source) => assert.throws(
|
|
() => validatePythonRevisionStates([{ source, label: "backend/scripts/policy.py" }]),
|
|
/revision-state/,
|
|
);
|
|
for (const source of [
|
|
"old = revision.state",
|
|
'old = workspaceRevision["state"]',
|
|
'old = record["selectedWorkspace"].state',
|
|
'old = f"{revision.state}"',
|
|
'"{revision.state}".format(value)',
|
|
'"{0.state}".format(revision)',
|
|
'"{0[state]}".format(workspaceRevision)',
|
|
'"{item.state}".format(item=selectedWorkspace)',
|
|
'"{item[state]}".format_map({"item": revision})',
|
|
'("{0.state}").format(revision)',
|
|
'"{0:{1.state}}".format(value, revision)',
|
|
'old = revision["st" + "ate"]',
|
|
'old = record["revi" + "sion"].state',
|
|
'old = revision[f"state"]',
|
|
'old = record[f"revision"].state',
|
|
`old = revision[f"st{'a'}te"]`,
|
|
`old = revision[f"{'state'}"]`,
|
|
`old = record[f"revi{'sion'}"].state`,
|
|
`old = revision[f"{'st' + 'ate'}"]`,
|
|
`old = record[f"{'revi' + 'sion'}"].state`,
|
|
`old = revision[f"{'state':s}"]`,
|
|
'"{0.state}".format(*[revision])',
|
|
'"{0[state]}".format(*(revision,))',
|
|
'"{1[state]}".format(*[other, workspaceRevision])',
|
|
'"{item.state}".format(**{"item": selectedWorkspace})',
|
|
'"{item[state]}".format_map({**{"item": revision}})',
|
|
'"{.state}".format(revision)',
|
|
'"{[state]}".format(revision)',
|
|
'"{:{.state}}".format(value, revision)',
|
|
'"{.name} {[state]}".format(other, revision)',
|
|
'"{item.state}".format(item=revision, **values)',
|
|
]) rejectsPython(source);
|
|
validatePythonRevisionStates([
|
|
{ source: 'text = "{revision.state}"', label: "backend/scripts/literal.py" },
|
|
{ source: 'text = "{{revision.state}}".format(value)', label: "backend/scripts/escaped.py" },
|
|
{ source: 'text = "{0.state}".format(other)', label: "backend/scripts/unrelated.py" },
|
|
{ source: 'old = revision[f"st{suffix}"]', label: "backend/scripts/dynamic-key.py" },
|
|
{ source: 'text = "{.name} {[state]}".format(other, other)', label: "backend/scripts/multi-auto.py" },
|
|
{ source: 'text = "{item.state}".format(**values)', label: "backend/scripts/dynamic-map.py" },
|
|
]);
|
|
const hostile = mkdtempSync(join(tmpdir(), "revision-policy-hostile-"));
|
|
writeFileSync(join(hostile, "json.py"), "raise RuntimeError('shadowed')\n");
|
|
const previousPythonPath = process.env.PYTHONPATH;
|
|
try {
|
|
process.env.PYTHONPATH = hostile;
|
|
validatePythonRevisionStates([{ source: "value = 1", label: "backend/scripts/isolated.py" }]);
|
|
} finally {
|
|
if (previousPythonPath === undefined) delete process.env.PYTHONPATH;
|
|
else process.env.PYTHONPATH = previousPythonPath;
|
|
rmSync(hostile, { recursive: true, force: true });
|
|
}
|
|
assert.throws(
|
|
() => validatePythonRevisionStates([{ source: 'revision[f"{1:.1000000000f}"]', label: "backend/scripts/oversized.py" }]),
|
|
/revision-state helper failed/,
|
|
);
|
|
validatePythonRevisionStates([{ source: 'revision[f"{1:04d}"]', label: "backend/scripts/small-format.py" }]);
|
|
assert.throws(
|
|
() => validatePythonRevisionStates([{ source: "def broken(", label: "backend/scripts/invalid.py" }]),
|
|
/revision-state helper failed/,
|
|
);
|
|
});
|
|
|
|
test("YAML mappings and only active plain scalar expressions are rejected", () => {
|
|
for (const source of [
|
|
"value: { revision: { state: old } }\n",
|
|
"value:\n workspaceRevision:\n state: old\n",
|
|
'items:\n - "selectedWorkspace":\n "state": old\n',
|
|
"old: selectedWorkspace.state\n",
|
|
"url: https://host/x; old: selectedWorkspace.state\n",
|
|
"saved: &saved { state: old }\nvalue: { revision: *saved }\n",
|
|
"defaults: &defaults { workspaceRevision: { state: old } }\nvalue: { <<: *defaults }\n",
|
|
]) rejects(source, "scripts/policy.yaml");
|
|
rejects("a: &a [x,x,x,x,x,x,x,x,x]\nb: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]\nc: [*b,*b,*b,*b,*b,*b,*b,*b,*b]\n", "scripts/alias-bomb.yaml");
|
|
rejects("value: [\n", "scripts/invalid.yaml");
|
|
for (const source of [
|
|
"value: |\n revision.state\n",
|
|
"value: >\n workspaceRevision.state\n",
|
|
'value: "selectedWorkspace.state"\n',
|
|
"url: https://host/revision.state\n",
|
|
]) passes(source, "scripts/literal.yaml");
|
|
});
|