384 lines
21 KiB
JavaScript
384 lines
21 KiB
JavaScript
// Row 240 (I4, #1746): scripts/case1-hostcheck.sh, the Case 1 host's drift stop for bwrap, and its
|
|
// pins and boot unit. Brief: docs/plans/2026-10-20_installer-rows.md "I4 hostcheck"; design r6 B2
|
|
// (agents/darkwing/work/queue-195/DESIGN.md at b7239bb66). Every case copies the script into a scratch
|
|
// lib directory with a fixture case1-pins.sh beside it and runs it with --root on a scratch host tree.
|
|
// Fake aa-status, dpkg, dpkg-statoverride, apparmor_parser, systemd-run, stat, chown, chmod, apt-get
|
|
// and dpkg-deb on PATH record their argv. Nothing here reads or writes the real /etc, /proc or /usr.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { tmp } from "./helpers.mjs";
|
|
|
|
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const SCRIPT = join(REPO, "scripts", "case1-hostcheck.sh");
|
|
const PINS = join(REPO, "scripts", "case1-pins.sh");
|
|
const UNIT = join(REPO, "packages", "cli", "systemd", "mosaic-hostcheck.service");
|
|
const PROFILE = "/etc/apparmor.d/bwrap-userns-restrict";
|
|
const LABEL = "bwrap//&unpriv_bwrap (enforce)";
|
|
// A made-up profile; the fixture pins carry its sha256.
|
|
const PROFILE_TEXT = "abi <abi/4.0>,\ninclude <tunables/global>\nprofile bwrap /usr/bin/bwrap flags=(attach_disconnected,mediate_deleted) {\n allow capability,\n change_profile /** -> &unpriv_bwrap,\n}\nprofile unpriv_bwrap flags=(attach_disconnected,mediate_deleted) {\n audit deny capability,\n}\n";
|
|
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
const AA_OK = { version: "2", profiles: { "/usr/bin/man": "enforce", bwrap: "enforce", unpriv_bwrap: "enforce", "lsb_release": "enforce" }, processes: {} };
|
|
const SYSTEMD_RUN = ["--wait", "--pipe", "--uid=mosaic", "--gid=mosaic", "-p", "NoNewPrivileges=yes", "-p", "ProtectSystem=strict",
|
|
"-p", "ProtectHome=yes", "-p", "PrivateTmp=yes", "/usr/bin/bwrap", "--unshare-user", "--unshare-pid", "--ro-bind", "/", "/",
|
|
"cat", "/proc/self/attr/current"];
|
|
|
|
function put(file, text, mode) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, text);
|
|
if (mode) chmodSync(file, mode);
|
|
}
|
|
|
|
// Each fake appends `name argv...` to calls.log and answers from a file in the state directory.
|
|
const FAKES = {
|
|
"aa-status": `[ -f "$HC/aa-rc" ] && exit "$(cat "$HC/aa-rc")"; cat "$HC/aa-status.json"`,
|
|
dpkg: `case "$1" in
|
|
-S) [ -f "$HC/owner" ] && cat "$HC/owner"; exit "$(cat "$HC/dpkg-S-rc")" ;;
|
|
-s) [ "$2" = bubblewrap ] && printf 'Package: bubblewrap\\nStatus: install ok installed\\nVersion: %s\\n' "$(cat "$HC/bwrap-version")" ;;
|
|
*) exit 2 ;;
|
|
esac`,
|
|
"dpkg-statoverride": `[ "$1" = --list ] && cat "$HC/statoverride"`,
|
|
apparmor_parser: `exit 0`,
|
|
"systemd-run": `cat "$HC/label"`,
|
|
stat: `cat "$HC/stat"`,
|
|
chown: `exit 0`,
|
|
chmod: `exit 0`,
|
|
"apt-get": `[ -f "$HC/apt-rc" ] && exit "$(cat "$HC/apt-rc")"; [ "$1 $2" = "download apparmor-profiles" ] && cp "$HC/deb" "./apparmor-profiles_9.9_all.deb"`,
|
|
"dpkg-deb": `case "$1" in
|
|
-f) [ "$3" = Version ] && echo 9.9-upstream ;;
|
|
-x) mkdir -p "$3/usr/share/apparmor/extra-profiles"; [ ! -f "$HC/upstream-profile" ] || cp "$HC/upstream-profile" "$3/usr/share/apparmor/extra-profiles/bwrap-userns-restrict" ;;
|
|
esac`,
|
|
};
|
|
|
|
// A scratch host where all seven items hold.
|
|
function host(t) {
|
|
const dir = tmp(t, "case1-hostcheck-");
|
|
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin"), lib = join(dir, "lib");
|
|
put(join(root, PROFILE), PROFILE_TEXT);
|
|
put(join(root, "etc", "apparmor.d", "abstractions", "base"), " /etc/ld.so.cache r,\n");
|
|
put(join(root, "etc", "apparmor.d", "tunables", "global"), "@{HOME}=/home/*/\n");
|
|
put(join(root, "proc", "sys", "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
|
|
put(join(root, "proc", "sys", "fs", "protected_hardlinks"), "1\n");
|
|
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
|
|
put(join(root, "etc", "systemd", "system", "mosaic-webui.service"),
|
|
"[Service]\nExecStart=/opt/mosaic-stack/node/bin/node /srv/mosaic/copies/x/packages/webui/src/cli.mjs serve --port 732 --business mosaic-stack --tester-port 7333\n");
|
|
put(join(root, "usr", "bin", "bwrap"), "", 0o755);
|
|
put(join(hc, "aa-status.json"), JSON.stringify(AA_OK));
|
|
put(join(hc, "dpkg-S-rc"), "1");
|
|
put(join(hc, "bwrap-version"), "0.9.0-1ubuntu0.3");
|
|
put(join(hc, "statoverride"), "root mosaic 0750 /usr/bin/bwrap\n");
|
|
put(join(hc, "label"), `${LABEL}\n`);
|
|
put(join(hc, "stat"), "root mosaic 750\n");
|
|
put(join(hc, "deb"), "fake deb\n");
|
|
put(join(hc, "upstream-profile"), PROFILE_TEXT);
|
|
for (const [name, body] of Object.entries(FAKES)) {
|
|
put(join(bin, name), `#!/usr/bin/env bash\nprintf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"\n${body}\n`, 0o755);
|
|
}
|
|
mkdirSync(lib);
|
|
copyFileSync(SCRIPT, join(lib, "hostcheck.sh"));
|
|
chmodSync(join(lib, "hostcheck.sh"), 0o755);
|
|
put(join(lib, "case1-pins.sh"), `PIN_PROFILES_VERSION=4.0.1-fixture\nPIN_PROFILE_SHA256=${sha256(PROFILE_TEXT)}\nPIN_BUBBLEWRAP_VERSION=0.9.0-1ubuntu0.3\n`);
|
|
return { dir, root, hc, bin, lib, script: join(lib, "hostcheck.sh") };
|
|
}
|
|
|
|
function run(h, ...args) {
|
|
const r = spawnSync(h.script, [...args, "--root", h.root], {
|
|
encoding: "utf8",
|
|
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, TMPDIR: h.dir },
|
|
});
|
|
return { status: r.status, out: r.stdout, err: r.stderr, lines: r.stdout.split("\n").filter(Boolean) };
|
|
}
|
|
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
|
|
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
|
|
const fails = (r) => r.lines.filter((l) => l.startsWith("FAIL "));
|
|
const drift = (h) => join(h.root, "etc", "mosaic-stack", "DRIFT");
|
|
const hlog = (h) => join(h.root, "var", "log", "mosaic-stack", "hostcheck.log");
|
|
const aa = (h, profiles) => put(join(h.hc, "aa-status.json"), JSON.stringify({ version: "2", profiles, processes: {} }));
|
|
|
|
// One fault per item; each makes that item, and only that one, fail.
|
|
const FAULTS = {
|
|
1: (h) => put(join(h.root, PROFILE), `${PROFILE_TEXT}# drift\n`),
|
|
2: (h) => aa(h, { ...AA_OK.profiles, bwrap: "complain" }),
|
|
3: (h) => put(join(h.root, "etc", "apparmor.d", "usr.bin.other"), "profile other /usr/bin/other {\n /usr/bin/bwrap ix,\n}\n"),
|
|
4: (h) => put(join(h.hc, "label"), "unconfined\n"),
|
|
5: (h) => put(join(h.hc, "statoverride"), "root root 0755 /usr/bin/bwrap\n"),
|
|
6: (h) => put(join(h.root, "proc", "sys", "kernel", "apparmor_restrict_unprivileged_userns"), "0\n"),
|
|
7: (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n"),
|
|
};
|
|
|
|
test("row 240 check: exits 0 with all seven items and a note that bubblewrap is the install pin", (t) => {
|
|
const h = host(t);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
assert.deepEqual(r.lines.map((l) => l.split(" ").slice(0, 2).join(" ")), ["ok 1", "ok 2", "ok 3", "ok 4", "ok 5", "ok 6", "ok 7", "note bubblewrap"]);
|
|
assert.match(r.lines[6], /is 1024, above the Console's --port 732$/);
|
|
assert.equal(r.lines[7], "note bubblewrap 0.9.0-1ubuntu0.3 is the install pin");
|
|
});
|
|
|
|
test("row 240 check: exits 1 with each item failing alone, and its line names it", (t) => {
|
|
for (const [n, fault] of Object.entries(FAULTS)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 1, `item ${n}: ${r.out}`);
|
|
assert.deepEqual(fails(r).map((l) => l.split(" ")[1]), [n], `item ${n}: ${r.out}`);
|
|
}
|
|
});
|
|
|
|
test("row 240 check: item 1 fails when a package owns the profile, when dpkg -S errs, or with no pin", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "dpkg-S-rc"), "0");
|
|
put(join(h.hc, "owner"), `apparmor-profiles: ${PROFILE}\n`);
|
|
let r = run(h, "--check");
|
|
assert.equal(r.status, 1);
|
|
assert.deepEqual(fails(r), [`FAIL 1 ${PROFILE} is owned by a package: apparmor-profiles: ${PROFILE}`]);
|
|
assert.deepEqual(calls(h, "dpkg").filter((a) => a.startsWith("-S")), [`-S ${PROFILE}`]);
|
|
h = host(t);
|
|
put(join(h.hc, "dpkg-S-rc"), "2");
|
|
r = run(h, "--check");
|
|
assert.deepEqual(fails(r), [`FAIL 1 dpkg -S ${PROFILE} exited 2`]);
|
|
h = host(t);
|
|
rmSync(join(h.lib, "case1-pins.sh"));
|
|
r = run(h, "--check");
|
|
assert.equal(r.status, 1);
|
|
assert.deepEqual(fails(r), [`FAIL 1 no profile pin in ${h.lib}/case1-pins.sh`]);
|
|
});
|
|
|
|
test("row 240 check: item 2 fails on a loaded mosaic-bwrap, a missing unpriv_bwrap or a failed aa-status", (t) => {
|
|
for (const [fault, text] of [
|
|
[(h) => aa(h, { ...AA_OK.profiles, "mosaic-bwrap": "enforce" }), "a mosaic-bwrap profile"],
|
|
[(h) => aa(h, { bwrap: "enforce" }), "no unpriv_bwrap profile in enforce mode"],
|
|
[(h) => put(join(h.hc, "aa-rc"), "1"), "aa-status --json failed"],
|
|
]) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 1);
|
|
assert.equal(fails(r).length, 1, r.out);
|
|
assert.ok(fails(r)[0].startsWith("FAIL 2 ") && fails(r)[0].includes(text), r.out);
|
|
}
|
|
});
|
|
|
|
test("row 240 check: item 3 fails on a second file naming bwrap as a word, /{usr/,}bin/bwrap included", (t) => {
|
|
for (const text of ["profile x /{usr/,}bin/bwrap {\n}\n", "# runs bwrap\n", "/usr/bin/bwrap ix,\n"]) {
|
|
const h = host(t);
|
|
put(join(h.root, "etc", "apparmor.d", "local", "extra"), text);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 1, text);
|
|
assert.deepEqual(fails(r).map((l) => l.split(" ")[1]), ["3"], text);
|
|
assert.ok(fails(r)[0].includes(join(h.root, "etc", "apparmor.d", "local", "extra")), r.out);
|
|
}
|
|
const h = host(t);
|
|
put(join(h.root, "etc", "apparmor.d", "local", "extra"), "# bwrapper and unpriv-bwrapx are other words\n");
|
|
assert.equal(run(h, "--check").status, 0);
|
|
});
|
|
|
|
test("row 240 check: item 4 reads the label under the units' no-new-privs and matches it exactly", (t) => {
|
|
let h = host(t);
|
|
assert.equal(run(h, "--check").status, 0);
|
|
assert.deepEqual(calls(h, "systemd-run"), [SYSTEMD_RUN.join(" ")]);
|
|
for (const label of ["unpriv_bwrap (enforce)", `${LABEL} `, `x${LABEL}`]) {
|
|
h = host(t);
|
|
put(join(h.hc, "label"), `${label}\n`);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 1, JSON.stringify(label));
|
|
assert.deepEqual(fails(r), [`FAIL 4 a bwrap child under no-new-privs has the label '${label}', not '${LABEL}'`]);
|
|
}
|
|
});
|
|
|
|
test("row 240 check: item 5 needs the statoverride and stat to agree", (t) => {
|
|
const h = host(t);
|
|
put(join(h.hc, "stat"), "root root 700\n");
|
|
const r = run(h, "--check");
|
|
assert.deepEqual(fails(r), ["FAIL 5 /usr/bin/bwrap is 'root root 700', not 'root mosaic 750'"]);
|
|
assert.deepEqual(calls(h, "dpkg-statoverride"), ["--list /usr/bin/bwrap"]);
|
|
assert.deepEqual(calls(h, "stat"), [`-c %U %G %a ${h.root}/usr/bin/bwrap`]);
|
|
});
|
|
|
|
test("row 240 check: item 6 fails on protected_hardlinks 0; item 7 fails with the floor at 732 and reads 1023 with no unit", (t) => {
|
|
let h = host(t);
|
|
put(join(h.root, "proc", "sys", "fs", "protected_hardlinks"), "0\n");
|
|
assert.deepEqual(fails(run(h, "--check")).map((l) => l.split(" ")[1]), ["6"]);
|
|
h = host(t);
|
|
put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "732\n");
|
|
assert.deepEqual(fails(run(h, "--check")), ["FAIL 7 net.ipv4.ip_unprivileged_port_start is 732, not above the Console's --port 732"]);
|
|
put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "733\n");
|
|
assert.equal(run(h, "--check").status, 0);
|
|
rmSync(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"));
|
|
assert.deepEqual(fails(run(h, "--check")), ["FAIL 7 net.ipv4.ip_unprivileged_port_start is 733, not above 1023, because no Console unit is installed"]);
|
|
put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
|
|
assert.equal(run(h, "--check").status, 0);
|
|
put(join(h.root, "etc", "systemd", "system", "mosaic-webui.service"), "[Service]\nExecStart=/x serve --business mosaic-stack --tester-port 7333\n");
|
|
assert.deepEqual(fails(run(h, "--check")), ["FAIL 7 /etc/systemd/system/mosaic-webui.service has no --port"]);
|
|
});
|
|
|
|
test("row 240 check: a bubblewrap version other than the install pin is a note, not a failure", (t) => {
|
|
const h = host(t);
|
|
put(join(h.hc, "bwrap-version"), "0.9.0-1ubuntu0.4");
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 0);
|
|
assert.equal(r.lines.at(-1), "note bubblewrap 0.9.0-1ubuntu0.4 is not the install pin 0.9.0-1ubuntu0.3, which is not a drift item");
|
|
});
|
|
|
|
test("row 240 enforce: a pass with no DRIFT changes nothing and exits 0", (t) => {
|
|
const h = host(t);
|
|
const r = run(h, "--enforce");
|
|
assert.equal(r.status, 0);
|
|
assert.equal(r.lines.at(-1), "hostcheck: all seven hold");
|
|
assert.deepEqual([calls(h, "apparmor_parser"), calls(h, "chown"), calls(h, "chmod")], [[], [], []]);
|
|
assert.equal(existsSync(drift(h)) || existsSync(hlog(h)), false);
|
|
});
|
|
|
|
test("row 240 enforce: each failure exits 0, unloads the profile, closes bwrap, writes DRIFT and one log line", (t) => {
|
|
for (const [n, fault] of Object.entries(FAULTS)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h, "--enforce");
|
|
assert.equal(r.status, 0, `item ${n}: ${r.out}${r.err}`);
|
|
assert.deepEqual(calls(h, "apparmor_parser"), [`-R ${h.root}${PROFILE}`], `item ${n}`);
|
|
assert.deepEqual(calls(h, "chown"), [`root:root ${h.root}/usr/bin/bwrap`], `item ${n}`);
|
|
assert.deepEqual(calls(h, "chmod"), [`0700 ${h.root}/usr/bin/bwrap`], `item ${n}`);
|
|
const d = readFileSync(drift(h), "utf8").split("\n").filter(Boolean);
|
|
assert.equal(d.length, 1, `item ${n}`);
|
|
assert.match(d[0], new RegExp(`^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ ${n} `), `item ${n}`);
|
|
const l = readFileSync(hlog(h), "utf8").split("\n").filter(Boolean);
|
|
assert.equal(l.length, 1, `item ${n}`);
|
|
assert.match(l[0], new RegExp(`^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ enforce stop items=${n} drift-before=0 unload=rc=0 chown=0 chmod=0$`), `item ${n}`);
|
|
}
|
|
});
|
|
|
|
test("row 240 enforce: with DRIFT present and every item passing, it still closes bwrap", (t) => {
|
|
const h = host(t);
|
|
put(drift(h), "2026-10-20T00:00:00Z 1 an earlier drift\n");
|
|
const r = run(h, "--enforce");
|
|
assert.equal(r.status, 0);
|
|
assert.equal(fails(r).length, 0);
|
|
assert.deepEqual(calls(h, "chown"), [`root:root ${h.root}/usr/bin/bwrap`]);
|
|
assert.deepEqual(calls(h, "chmod"), [`0700 ${h.root}/usr/bin/bwrap`]);
|
|
assert.equal(readFileSync(drift(h), "utf8"), "2026-10-20T00:00:00Z 1 an earlier drift\n");
|
|
assert.match(readFileSync(hlog(h), "utf8"), /Z enforce stop items=none drift-before=1 unload=rc=0 chown=0 chmod=0\n$/);
|
|
});
|
|
|
|
test("row 240 enforce: no unload when aa-status lists neither bwrap profile; never writes the statoverride", (t) => {
|
|
const h = host(t);
|
|
aa(h, { "/usr/bin/man": "enforce" });
|
|
const r = run(h, "--enforce");
|
|
assert.equal(r.status, 0);
|
|
assert.deepEqual(calls(h, "apparmor_parser"), []);
|
|
assert.deepEqual(calls(h, "chmod"), [`0700 ${h.root}/usr/bin/bwrap`]);
|
|
assert.match(readFileSync(hlog(h), "utf8"), /Z enforce stop items=2 drift-before=0 unload=skipped chown=0 chmod=0\n$/);
|
|
for (const [n, fault] of Object.entries(FAULTS)) {
|
|
const g = host(t);
|
|
fault(g);
|
|
run(g, "--enforce");
|
|
for (const a of calls(g, "dpkg-statoverride")) assert.doesNotMatch(a, /--(update|add|remove)/, `item ${n}`);
|
|
assert.deepEqual(calls(g, "dpkg-statoverride"), ["--list /usr/bin/bwrap"], `item ${n}`);
|
|
}
|
|
});
|
|
|
|
test("row 240 upstream: a candidate whose profile differs prints the difference and exits 0", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "upstream-profile"), `${PROFILE_TEXT} # upstream change\n`);
|
|
let r = run(h, "--upstream");
|
|
assert.equal(r.status, 0, r.err);
|
|
assert.match(r.out, new RegExp(`^upstream: DIFFERS: apparmor-profiles 9.9-upstream's bwrap-userns-restrict has sha256 ${sha256(`${PROFILE_TEXT} # upstream change\n`)}; the pin is ${sha256(PROFILE_TEXT)} from 4.0.1-fixture\\.`));
|
|
assert.match(r.out, /^\+ {2}# upstream change$/m);
|
|
assert.deepEqual(calls(h, "apt-get"), ["download apparmor-profiles"]);
|
|
assert.deepEqual([calls(h, "chown"), calls(h, "chmod"), existsSync(drift(h))], [[], [], false]);
|
|
h = host(t);
|
|
r = run(h, "--upstream");
|
|
assert.equal(r.status, 0);
|
|
assert.equal(r.out, "upstream: apparmor-profiles 9.9-upstream's bwrap-userns-restrict matches the pin from 4.0.1-fixture\n");
|
|
h = host(t);
|
|
rmSync(join(h.hc, "upstream-profile"));
|
|
r = run(h, "--upstream");
|
|
assert.equal(r.status, 0);
|
|
assert.match(r.out, /^upstream: DIFFERS: apparmor-profiles 9.9-upstream has no bwrap-userns-restrict;/);
|
|
h = host(t);
|
|
put(join(h.hc, "apt-rc"), "100");
|
|
r = run(h, "--upstream");
|
|
assert.equal(r.status, 1);
|
|
assert.equal(r.err, "hostcheck: upstream: apt-get download apparmor-profiles failed\n");
|
|
});
|
|
|
|
test("row 240 boot unit: a oneshot after apparmor and before the bus that runs --enforce", () => {
|
|
const text = readFileSync(UNIT, "utf8");
|
|
const lines = text.split("\n").filter((l) => l && !l.startsWith("#"));
|
|
assert.deepEqual(lines.filter((l) => /^(Type|After|Before|ExecStart|WantedBy)=/.test(l)), [
|
|
"After=apparmor.service",
|
|
"[email protected]",
|
|
"Type=oneshot",
|
|
"ExecStart=/usr/local/lib/mosaic-stack/hostcheck.sh --enforce",
|
|
"WantedBy=multi-user.target",
|
|
]);
|
|
});
|
|
|
|
test("row 240 pins and usage: the shipped pins are r6's, the script reads the pins beside it, and bad usage exits 2", (t) => {
|
|
assert.equal(readFileSync(PINS, "utf8").split("\n").filter((l) => /^PIN_/.test(l)).join("\n"),
|
|
"PIN_PROFILES_VERSION=4.0.1really4.0.1-0ubuntu0.24.04.9\nPIN_PROFILE_SHA256=11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9\nPIN_BUBBLEWRAP_VERSION=0.9.0-1ubuntu0.3");
|
|
const h = host(t);
|
|
const r = run({ ...h, script: SCRIPT }, "--check");
|
|
assert.equal(r.status, 1);
|
|
assert.deepEqual(fails(r), [`FAIL 1 ${PROFILE} sha256 is ${sha256(PROFILE_TEXT)}, not the pin 11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9`]);
|
|
for (const args of [[], ["--check", "--enforce"], ["--check", "--root", "relative"], ["--status"]]) {
|
|
const u = spawnSync(h.script, args, { encoding: "utf8", env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc } });
|
|
assert.equal(u.status, 2, args.join(" "));
|
|
}
|
|
if (process.getuid() !== 0) {
|
|
const u = spawnSync(h.script, ["--check"], { encoding: "utf8", env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc } });
|
|
assert.deepEqual([u.status, u.stderr], [2, "hostcheck: refusing: run it as root\n"]);
|
|
}
|
|
});
|
|
|
|
test("Vikunja 614 check: item 5 accepts dpkg's bare octal mode and leading-zero equivalents", (t) => {
|
|
for (const mode of ["750", "0750"]) {
|
|
const h = host(t);
|
|
put(join(h.hc, "statoverride"), `root mosaic ${mode} /usr/bin/bwrap\n`);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
assert.deepEqual(fails(r), []);
|
|
assert.ok(r.lines.some((line) => line.startsWith("ok 5 ")), r.out);
|
|
}
|
|
});
|
|
|
|
test("Vikunja 614 enforce: dpkg's bare 750 leaves bwrap open and writes no DRIFT", (t) => {
|
|
const h = host(t);
|
|
put(join(h.hc, "statoverride"), "root mosaic 750 /usr/bin/bwrap\n");
|
|
const r = run(h, "--enforce");
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
assert.deepEqual(fails(r), []);
|
|
assert.equal(r.lines.at(-1), "hostcheck: all seven hold");
|
|
assert.deepEqual([calls(h, "apparmor_parser"), calls(h, "chown"), calls(h, "chmod")], [[], [], []]);
|
|
assert.equal(existsSync(drift(h)), false);
|
|
assert.equal(existsSync(hlog(h)), false);
|
|
});
|
|
|
|
test("Vikunja 614 check: item 5 refuses other modes, owners, groups, paths and malformed overrides", (t) => {
|
|
const overrides = [
|
|
...["755", "0755", "4750", "75", "7500", "075", "00750", "000750", "0780", "8", "0o750", "+750", "-750", "7x0", "750+0", ""].map((mode) => `root mosaic ${mode} /usr/bin/bwrap\n`),
|
|
"root mosaic 750 /usr/bin/bwrap\n",
|
|
"root\tmosaic 750 /usr/bin/bwrap\n",
|
|
"mosaic mosaic 750 /usr/bin/bwrap\n",
|
|
"root root 750 /usr/bin/bwrap\n",
|
|
"root mosaic 750 /usr/bin/other\n",
|
|
"root mosaic 750 /usr/bin/bwrap extra\n",
|
|
"root mosaic 750\n",
|
|
"root mosaic 750 /usr/bin/bwrap\nroot mosaic 750 /usr/bin/bwrap\n",
|
|
"\n",
|
|
"",
|
|
null,
|
|
];
|
|
for (const override of overrides) {
|
|
const h = host(t);
|
|
if (override === null) rmSync(join(h.hc, "statoverride"));
|
|
else put(join(h.hc, "statoverride"), override);
|
|
const r = run(h, "--check");
|
|
assert.equal(r.status, 1, `${JSON.stringify(override)}: ${r.out}${r.err}`);
|
|
assert.deepEqual(fails(r).map((line) => line.split(" ")[1]), ["5"], r.out);
|
|
}
|
|
});
|