// Row 210 (#1707, design D3 and D6): the address checks and the argv allowlist. Row 234 (#1740, r6 S1) // moved the registry and the grants to bus store records, so the business file refuses them. import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { BusinessError, validateBusinessDocument } from "../src/index.mjs"; import { HOST_KEY as HOST_KEY_PATTERN, REFUSED_COMMANDS, USER, checkAddress, checkCommands, commandAllowed, ownMachine, validArgv } from "../src/hosts.mjs"; import { businessDoc, rolesCopy, tmp, tokenFile, FAR_DATE } from "./helpers.mjs"; const HOST_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOv6Rq3Mvo9w2j3eYvYbqX0J1pJ0x9m8Zr0w1kJ8p0Qn"; function refuses(fn, pattern) { assert.throws(fn, (error) => { assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`); assert.match(error.message, pattern); return true; }); } // The fixture with coder's definition declaring the host credential and no hosts. function check(mutate, { declare = true } = {}) { const root = tmp(); const rolesDir = rolesCopy(root); if (declare) { const file = join(rolesDir, "coder.json"); const coder = JSON.parse(readFileSync(file, "utf8")); coder.credentials.push({ service: "host" }); writeFileSync(file, JSON.stringify(coder)); } const doc = businessDoc(root); mutate?.(doc, root); return validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir }); } // Row 234 (#1740, r6 S1): hosts and grants are bus store records, so the business file refuses both keys // and any host. key reference. The control loads. test("S1: the business file refuses hosts at the top level and in a role, and host. references", () => { assert.equal("hosts" in check(), false); assert.equal("hosts" in check().roles.coder, false); const entry = { address: "10.1.1.53", port: 22, user: "localadmin", hostKeys: [HOST_KEY] }; refuses(() => check((d) => { d.hosts = { web1: entry }; }), /acme\.json hosts lives in the bus store/); refuses(() => check((d) => { d.hosts = {}; }), /hosts lives in the bus store/); refuses(() => check((d) => { d.roles.coder.hosts = { web1: { commands: [["systemctl", "status"]] } }; }), /roles\.coder\.hosts lives in the bus store/); refuses(() => check((d) => { d.roles.coder.hosts = {}; }, { declare: false }), /roles\.coder\.hosts lives in the bus store/); const key = (d, root) => { d.roles.coder.credentials["host.web1"] = { file: tokenFile(join(root, "secrets"), "coder-host.key"), rotateBy: FAR_DATE }; }; refuses(() => check(key), /host\.web1/); }); test("T1: address, user and host key checks, which the enrollment raise uses", () => { const own = ["127.0.0.2", "0.0.0.0", "::1", "::", "0:0:0:0:0:0:0:1", "::ffff:127.0.0.1", "::ffff:0.0.0.0", "localhost", "a.localhost", "127.1", "2130706433", "0177.0.0.1", "0x7f.1", "bad_name", "a..b", "-a.b", "fe80::1%eth0", 7]; for (const address of own) refuses(() => checkAddress(address, "grant.address"), /grant\.address/); for (const address of ["10.1.1.53", "host.example", "2001:db8::1"]) assert.equal(checkAddress(address, "grant.address"), address); assert.equal(ownMachine("::ffff:7f00:1"), true); assert.equal(ownMachine("::ffff:10.0.0.1"), false); for (const user of ["Root", "a b", ""]) assert.equal(USER.test(user), false); assert.equal(USER.test("localadmin"), true); for (const key of [`${HOST_KEY} root@web1`, "ssh-dss AAAA"]) assert.equal(HOST_KEY_PATTERN.test(key), false); assert.equal(HOST_KEY_PATTERN.test(HOST_KEY), true); }); test("T26: no wildcard and no shell, runner or interpreter as a prefix", () => { const at = /grant\.commands/; for (const commands of ["any", "*", {}, [], [[]], [[7]], [["ls", ""]], [["ls"], ["ls"]], [["ls", "*"]], [["ls", "a?"]], [["ls", "[a]"]], [["bin/ls"]], [["ls\0"]]]) refuses(() => checkCommands(commands, "grant.commands"), at); const names = [...REFUSED_COMMANDS, "python", "python3", "python3.12", "awk", "gawk", "mawk", "nawk", "php", "php8.2", "/bin/bash", "/usr/bin/env"]; for (const name of names) refuses(() => checkCommands([[name, "x"]], "grant.commands"), at); for (const name of ["systemctl", "/usr/bin/journalctl", "tee"]) assert.deepEqual(checkCommands([[name]], "grant.commands"), [[name]]); }); test("D6: argv shape and the literal prefix match", () => { assert.equal(validArgv(["systemctl", "status", "nginx"]), true); for (const argv of [[], "ls", [""], ["a\0"], [7], ["bin/ls"], Array(257).fill("a"), ["a".repeat(4097)], Array(9).fill("a".repeat(4000))]) assert.equal(validArgv(argv), false, JSON.stringify(argv).slice(0, 40)); assert.equal(validArgv(["/usr/bin/ls"]), true); const commands = [["systemctl", "status"]]; assert.equal(commandAllowed(["systemctl", "status"], commands), true); assert.equal(commandAllowed(["systemctl", "status", "nginx"], commands), true); assert.equal(commandAllowed(["systemctl"], commands), false); assert.equal(commandAllowed(["systemctl", "restart", "nginx"], commands), false); assert.equal(commandAllowed(["systemctl", "status*"], commands), false); });