@glassmkr/crucible 0.13.15 → 0.13.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alerts/__tests__/rules.test.js +34 -0
- package/dist/alerts/__tests__/rules.test.js.map +1 -1
- package/dist/alerts/rules.js +19 -2
- package/dist/alerts/rules.js.map +1 -1
- package/dist/collect/ethtool.js +2 -1
- package/dist/collect/ethtool.js.map +1 -1
- package/dist/collect/hardware-raid.js +6 -5
- package/dist/collect/hardware-raid.js.map +1 -1
- package/dist/collect/io-errors.js +2 -2
- package/dist/collect/io-errors.js.map +1 -1
- package/dist/collect/ipmi.js +6 -6
- package/dist/collect/ipmi.js.map +1 -1
- package/dist/collect/lvm.js +2 -7
- package/dist/collect/lvm.js.map +1 -1
- package/dist/collect/os-alerts.js +2 -1
- package/dist/collect/os-alerts.js.map +1 -1
- package/dist/collect/reboot-evidence.js +2 -2
- package/dist/collect/reboot-evidence.js.map +1 -1
- package/dist/collect/security.d.ts +3 -0
- package/dist/collect/security.js +87 -10
- package/dist/collect/security.js.map +1 -1
- package/dist/collect/smart.js +2 -2
- package/dist/collect/smart.js.map +1 -1
- package/dist/collect/zfs.js +3 -2
- package/dist/collect/zfs.js.map +1 -1
- package/dist/init.d.ts +16 -1
- package/dist/init.js +91 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/__tests__/capability.test.js +46 -34
- package/dist/lib/__tests__/capability.test.js.map +1 -1
- package/dist/lib/__tests__/privileged.test.d.ts +1 -0
- package/dist/lib/__tests__/privileged.test.js +110 -0
- package/dist/lib/__tests__/privileged.test.js.map +1 -0
- package/dist/lib/__tests__/run-privileged.test.d.ts +1 -0
- package/dist/lib/__tests__/run-privileged.test.js +50 -0
- package/dist/lib/__tests__/run-privileged.test.js.map +1 -0
- package/dist/lib/capability.d.ts +19 -4
- package/dist/lib/capability.js +48 -46
- package/dist/lib/capability.js.map +1 -1
- package/dist/lib/privileged.d.ts +31 -0
- package/dist/lib/privileged.js +168 -0
- package/dist/lib/privileged.js.map +1 -0
- package/dist/lib/types.d.ts +4 -1
- package/dist/metrics-server.js +1 -0
- package/dist/metrics-server.js.map +1 -1
- package/dist/notify/telegram.js +1 -1
- package/dist/notify/telegram.js.map +1 -1
- package/package.json +1 -1
- package/rule-ids.json +1 -0
|
@@ -1,56 +1,68 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { detectIpmiCapability, formatCapabilityLine } from "../capability.js";
|
|
2
|
+
import { detectIpmiCapability, formatCapabilityLine, isIpmitoolVersionVulnerable, } from "../capability.js";
|
|
3
|
+
describe("isIpmitoolVersionVulnerable (CVE-2020-5208, fixed in 1.8.19)", () => {
|
|
4
|
+
it("flags versions strictly below 1.8.19", () => {
|
|
5
|
+
expect(isIpmitoolVersionVulnerable("1.8.18")).toBe(true);
|
|
6
|
+
expect(isIpmitoolVersionVulnerable("1.8.11")).toBe(true);
|
|
7
|
+
expect(isIpmitoolVersionVulnerable("1.7.99")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it("clears 1.8.19 and above", () => {
|
|
10
|
+
expect(isIpmitoolVersionVulnerable("1.8.19")).toBe(false);
|
|
11
|
+
expect(isIpmitoolVersionVulnerable("1.8.20")).toBe(false);
|
|
12
|
+
expect(isIpmitoolVersionVulnerable("1.9.0")).toBe(false);
|
|
13
|
+
expect(isIpmitoolVersionVulnerable("2.0.0")).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it("does not gate an unknown/unparseable version (fail-open on identification)", () => {
|
|
16
|
+
expect(isIpmitoolVersionVulnerable(null)).toBe(false);
|
|
17
|
+
expect(isIpmitoolVersionVulnerable("")).toBe(false);
|
|
18
|
+
expect(isIpmitoolVersionVulnerable("unknown")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it("tolerates distro version suffixes", () => {
|
|
21
|
+
expect(isIpmitoolVersionVulnerable("1.8.18-11ubuntu2")).toBe(true);
|
|
22
|
+
expect(isIpmitoolVersionVulnerable("1.8.19-4")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
3
25
|
describe("detectIpmiCapability", () => {
|
|
4
26
|
it("returns no_ipmitool_binary when neither device nor binary exist (Pi)", async () => {
|
|
5
27
|
const cap = await detectIpmiCapability({
|
|
6
|
-
statDevice: async () => "enoent",
|
|
7
28
|
runIpmitool: async () => { const e = new Error("not found"); e.code = "ENOENT"; throw e; },
|
|
29
|
+
probeSensor: async () => null,
|
|
8
30
|
});
|
|
9
31
|
expect(cap).toEqual({ available: false, reason: "no_ipmitool_binary" });
|
|
10
32
|
});
|
|
11
|
-
it("
|
|
33
|
+
it("available when the WRAPPED sensor probe returns BMC data (CVE-safe version)", async () => {
|
|
34
|
+
// §2.1: availability comes from the wrapper (runs as root), not a direct
|
|
35
|
+
// /dev/ipmi0 stat the unprivileged service user can't do.
|
|
12
36
|
const cap = await detectIpmiCapability({
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
runIpmitool: async () => ({ stdout: "ipmitool version 1.8.19\n", stderr: "" }),
|
|
38
|
+
probeSensor: async () => "CPU Temp | 39.000 | degrees C | ok",
|
|
15
39
|
});
|
|
16
|
-
expect(cap).toEqual({ available: true, method: "ipmitool_in_band", ipmitool_version: "1.8.
|
|
40
|
+
expect(cap).toEqual({ available: true, method: "ipmitool_in_band", ipmitool_version: "1.8.19" });
|
|
17
41
|
});
|
|
18
|
-
it("
|
|
42
|
+
it("disables IPMI when ipmitool is below 1.8.19 even if the BMC answers (CVE-2020-5208)", async () => {
|
|
19
43
|
const cap = await detectIpmiCapability({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (args[0] === "-V")
|
|
23
|
-
return { stdout: "ipmitool version 1.8.18", stderr: "" };
|
|
24
|
-
const e = new Error("Could not open device");
|
|
25
|
-
e.stderr = "Could not open device at /dev/ipmi0: No such file or directory";
|
|
26
|
-
throw e;
|
|
27
|
-
},
|
|
44
|
+
runIpmitool: async () => ({ stdout: "ipmitool version 1.8.18\n", stderr: "" }),
|
|
45
|
+
probeSensor: async () => "CPU Temp | 39.000 | degrees C | ok",
|
|
28
46
|
});
|
|
29
47
|
expect(cap.available).toBe(false);
|
|
30
|
-
if (!cap.available)
|
|
31
|
-
expect(cap.reason).toBe("
|
|
48
|
+
if (!cap.available) {
|
|
49
|
+
expect(cap.reason).toBe("ipmitool_cve_2020_5208");
|
|
50
|
+
expect(cap.detail).toContain("1.8.18");
|
|
51
|
+
}
|
|
32
52
|
});
|
|
33
|
-
it("returns
|
|
53
|
+
it("no_bmc_device when the wrapped sensor probe returns nothing (no BMC / VM)", async () => {
|
|
34
54
|
const cap = await detectIpmiCapability({
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
runIpmitool: async () => ({ stdout: "ipmitool version 1.8.19", stderr: "" }),
|
|
56
|
+
probeSensor: async () => null,
|
|
37
57
|
});
|
|
38
58
|
expect(cap.available).toBe(false);
|
|
39
|
-
if (!cap.available)
|
|
40
|
-
expect(cap.reason).toBe("
|
|
41
|
-
expect(cap.detail).toContain("ipmi");
|
|
42
|
-
}
|
|
59
|
+
if (!cap.available)
|
|
60
|
+
expect(cap.reason).toBe("no_bmc_device");
|
|
43
61
|
});
|
|
44
|
-
it("
|
|
62
|
+
it("no_bmc_device on empty (whitespace-only) sensor output", async () => {
|
|
45
63
|
const cap = await detectIpmiCapability({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (args[0] === "-V")
|
|
49
|
-
return { stdout: "ipmitool version 1.8.19", stderr: "" };
|
|
50
|
-
const e = new Error("could not open");
|
|
51
|
-
e.stderr = "Could not open device";
|
|
52
|
-
throw e;
|
|
53
|
-
},
|
|
64
|
+
runIpmitool: async () => ({ stdout: "ipmitool version 1.8.19", stderr: "" }),
|
|
65
|
+
probeSensor: async () => " \n ",
|
|
54
66
|
});
|
|
55
67
|
expect(cap.available).toBe(false);
|
|
56
68
|
if (!cap.available)
|
|
@@ -58,8 +70,8 @@ describe("detectIpmiCapability", () => {
|
|
|
58
70
|
});
|
|
59
71
|
it("captures ipmitool version when present", async () => {
|
|
60
72
|
const cap = await detectIpmiCapability({
|
|
61
|
-
statDevice: async (p) => p === "/dev/ipmi0" ? "ok" : "enoent",
|
|
62
73
|
runIpmitool: async () => ({ stdout: "ipmitool version 1.8.21-rc1\n", stderr: "" }),
|
|
74
|
+
probeSensor: async () => "CPU Temp | 39.000 | degrees C | ok",
|
|
63
75
|
});
|
|
64
76
|
expect(cap.available).toBe(true);
|
|
65
77
|
if (cap.available)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/capability.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,
|
|
1
|
+
{"version":3,"file":"capability.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/capability.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isAllowedSmartDevice, isAllowedIface, WRAPPER_SCRIPT, SUDOERS_CONTENT, WRAPPER_PATH, SUDOERS_PATH, SERVICE_USER, } from "../privileged.js";
|
|
3
|
+
import { setupPrivilegeSeparation } from "../../init.js";
|
|
4
|
+
function mockDeps(execImpl) {
|
|
5
|
+
const files = new Map();
|
|
6
|
+
const warns = [];
|
|
7
|
+
const deps = {
|
|
8
|
+
fs: {
|
|
9
|
+
existsSync: (p) => files.has(p),
|
|
10
|
+
mkdirSync: () => { },
|
|
11
|
+
writeFileSync: (p, data, o) => { files.set(p, { data, mode: o?.mode ?? 0o644 }); },
|
|
12
|
+
chmodSync: (p, mode) => { const f = files.get(p); if (f)
|
|
13
|
+
f.mode = mode; },
|
|
14
|
+
renameSync: (from, to) => {
|
|
15
|
+
const f = files.get(from);
|
|
16
|
+
if (!f)
|
|
17
|
+
throw new Error(`ENOENT: ${from}`);
|
|
18
|
+
files.set(to, f);
|
|
19
|
+
files.delete(from);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
exec: execImpl,
|
|
23
|
+
hostname: () => "h", log: () => { }, warn: (m) => warns.push(m), error: () => { },
|
|
24
|
+
fetch: async () => ({ status: 200 }), readStdin: async () => "",
|
|
25
|
+
};
|
|
26
|
+
return { deps, files, warns };
|
|
27
|
+
}
|
|
28
|
+
// A fresh box: `id glassmkr` fails (user absent), everything else succeeds.
|
|
29
|
+
const okExec = (cmd) => cmd === "id"
|
|
30
|
+
? { stdout: "", status: 1 }
|
|
31
|
+
: { stdout: "", status: 0 };
|
|
32
|
+
describe("setupPrivilegeSeparation", () => {
|
|
33
|
+
it("succeeds on a clean box: creates user, installs wrapper 0755 + sudoers 0440", () => {
|
|
34
|
+
const { deps, files } = mockDeps(okExec);
|
|
35
|
+
expect(setupPrivilegeSeparation(deps, "/etc/glassmkr/crucible.yaml")).toBe(true);
|
|
36
|
+
expect(files.get(WRAPPER_PATH)?.mode).toBe(0o755);
|
|
37
|
+
expect(files.get(SUDOERS_PATH)?.mode).toBe(0o440);
|
|
38
|
+
expect(files.has(`${SUDOERS_PATH}.tmp`)).toBe(false); // renamed, not left behind
|
|
39
|
+
});
|
|
40
|
+
it("fail-safe: visudo rejection returns false and never installs the sudoers file", () => {
|
|
41
|
+
const { deps, files } = mockDeps((cmd) => cmd === "id" ? { stdout: "", status: 1 } :
|
|
42
|
+
cmd === "visudo" ? { stdout: "", status: 1 } :
|
|
43
|
+
{ stdout: "", status: 0 });
|
|
44
|
+
expect(setupPrivilegeSeparation(deps, "/etc/glassmkr/crucible.yaml")).toBe(false);
|
|
45
|
+
expect(files.has(SUDOERS_PATH)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("fail-safe: useradd failure returns false (caller keeps User=root)", () => {
|
|
48
|
+
const { deps } = mockDeps((cmd) => cmd === "id" ? { stdout: "", status: 1 } :
|
|
49
|
+
cmd === "useradd" ? { stdout: "", status: 1 } :
|
|
50
|
+
{ stdout: "", status: 0 });
|
|
51
|
+
expect(setupPrivilegeSeparation(deps, "/etc/glassmkr/crucible.yaml")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("isAllowedSmartDevice", () => {
|
|
55
|
+
it("accepts real block/nvme device nodes", () => {
|
|
56
|
+
for (const d of ["/dev/sda", "/dev/sdb", "/dev/sdaa", "/dev/hda", "/dev/nvme0n1", "/dev/nvme12n3", "/dev/nvme0", "/dev/bus/0"]) {
|
|
57
|
+
expect(isAllowedSmartDevice(d)).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
it("rejects traversal, injection, and non-device paths", () => {
|
|
61
|
+
for (const d of ["/etc/shadow", "/dev/../etc/shadow", "/dev/sda; rm -rf /", "/dev/sda foo", "", "sda", "/dev/sda\n/dev/sdb", "/dev/mapper/x"]) {
|
|
62
|
+
expect(isAllowedSmartDevice(d)).toBe(false);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("isAllowedIface", () => {
|
|
67
|
+
it("accepts real interface names", () => {
|
|
68
|
+
for (const i of ["eth0", "enp1s0", "eno1", "bond0", "bond0.100", "br-lan", "en_p0"]) {
|
|
69
|
+
expect(isAllowedIface(i)).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it("rejects empty, over-long, spaced, and injection-y names", () => {
|
|
73
|
+
for (const i of ["", "a".repeat(16), "eth0 x", "eth0;rm", "../eth0", "eth0|cat", "$(id)"]) {
|
|
74
|
+
expect(isAllowedIface(i)).toBe(false);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("WRAPPER_SCRIPT (the sudo facade)", () => {
|
|
79
|
+
it("is POSIX sh with strict mode", () => {
|
|
80
|
+
expect(WRAPPER_SCRIPT.startsWith("#!/bin/sh")).toBe(true);
|
|
81
|
+
expect(WRAPPER_SCRIPT).toContain("set -eu");
|
|
82
|
+
});
|
|
83
|
+
it("has no argument passthrough sink ($@ / $* into a command)", () => {
|
|
84
|
+
// The only $-expansions should be the validated $dev / $ifc / $action /
|
|
85
|
+
// ${#1}. A bare exec of "$@" or "$*" would be a passthrough hole.
|
|
86
|
+
expect(WRAPPER_SCRIPT).not.toMatch(/exec[^\n]*"\$@"/);
|
|
87
|
+
expect(WRAPPER_SCRIPT).not.toMatch(/exec[^\n]*\$\*/);
|
|
88
|
+
});
|
|
89
|
+
it("validates the two parameterised actions before exec", () => {
|
|
90
|
+
expect(WRAPPER_SCRIPT).toContain("valid_device");
|
|
91
|
+
expect(WRAPPER_SCRIPT).toContain("valid_iface");
|
|
92
|
+
});
|
|
93
|
+
it("hard-codes the expected privileged commands", () => {
|
|
94
|
+
expect(WRAPPER_SCRIPT).toContain("exec ipmitool sensor");
|
|
95
|
+
expect(WRAPPER_SCRIPT).toContain("exec smartctl --json --all");
|
|
96
|
+
expect(WRAPPER_SCRIPT).toContain("exec zpool status");
|
|
97
|
+
expect(WRAPPER_SCRIPT).toContain("exec iptables -L -n");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("SUDOERS_CONTENT", () => {
|
|
101
|
+
it("grants only the wrapper, NOPASSWD, to the service user", () => {
|
|
102
|
+
expect(SUDOERS_CONTENT).toContain(`${SERVICE_USER} ALL=(root) NOPASSWD: ${WRAPPER_PATH}`);
|
|
103
|
+
});
|
|
104
|
+
it("contains no wildcard and resets the environment", () => {
|
|
105
|
+
expect(SUDOERS_CONTENT).not.toContain("*");
|
|
106
|
+
expect(SUDOERS_CONTENT).toContain("env_reset");
|
|
107
|
+
expect(SUDOERS_CONTENT).toContain("secure_path");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=privileged.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privileged.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/privileged.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// Mock the fs existsSync (wrapper presence) and the exec run() so we can
|
|
3
|
+
// exercise runPrivileged's wrapper-vs-root-direct branching without touching
|
|
4
|
+
// the real filesystem or spawning processes. Keep the rest of each module real.
|
|
5
|
+
vi.mock("fs", async (orig) => ({ ...(await orig()), existsSync: vi.fn() }));
|
|
6
|
+
vi.mock("../exec.js", async (orig) => ({ ...(await orig()), run: vi.fn() }));
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { run } from "../exec.js";
|
|
9
|
+
import { runPrivileged, WRAPPER_PATH } from "../privileged.js";
|
|
10
|
+
const existsMock = existsSync;
|
|
11
|
+
const runMock = run;
|
|
12
|
+
describe("runPrivileged: wrapper vs root-direct fallback", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
existsMock.mockReset();
|
|
15
|
+
runMock.mockReset();
|
|
16
|
+
runMock.mockResolvedValue("output");
|
|
17
|
+
});
|
|
18
|
+
it("uses the sudo wrapper when it is installed", async () => {
|
|
19
|
+
existsMock.mockReturnValue(true);
|
|
20
|
+
await runPrivileged("ipmi-sensor");
|
|
21
|
+
expect(runMock).toHaveBeenCalledWith("sudo", ["-n", WRAPPER_PATH, "ipmi-sensor"], expect.any(Number));
|
|
22
|
+
});
|
|
23
|
+
it("falls back to the direct command when the wrapper is absent and we are root", async () => {
|
|
24
|
+
existsMock.mockReturnValue(false);
|
|
25
|
+
const getuid = vi.spyOn(process, "getuid").mockReturnValue(0);
|
|
26
|
+
await runPrivileged("ipmi-sensor");
|
|
27
|
+
expect(runMock).toHaveBeenCalledWith("ipmitool", ["sensor"], expect.any(Number));
|
|
28
|
+
getuid.mockRestore();
|
|
29
|
+
});
|
|
30
|
+
it("validates the smart device path on the direct fallback", async () => {
|
|
31
|
+
existsMock.mockReturnValue(false);
|
|
32
|
+
const getuid = vi.spyOn(process, "getuid").mockReturnValue(0);
|
|
33
|
+
await runPrivileged("smart", ["/dev/sda"]);
|
|
34
|
+
expect(runMock).toHaveBeenCalledWith("smartctl", ["--json", "--all", "/dev/sda"], expect.any(Number));
|
|
35
|
+
runMock.mockClear();
|
|
36
|
+
const rejected = await runPrivileged("smart", ["/etc/shadow"]);
|
|
37
|
+
expect(rejected).toBeNull();
|
|
38
|
+
expect(runMock).not.toHaveBeenCalled();
|
|
39
|
+
getuid.mockRestore();
|
|
40
|
+
});
|
|
41
|
+
it("returns null when the wrapper is absent and we are NOT root (no escalation)", async () => {
|
|
42
|
+
existsMock.mockReturnValue(false);
|
|
43
|
+
const getuid = vi.spyOn(process, "getuid").mockReturnValue(1000);
|
|
44
|
+
const r = await runPrivileged("ipmi-sensor");
|
|
45
|
+
expect(r).toBeNull();
|
|
46
|
+
expect(runMock).not.toHaveBeenCalled();
|
|
47
|
+
getuid.mockRestore();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=run-privileged.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-privileged.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/run-privileged.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,yEAAyE;AACzE,6EAA6E;AAC7E,gFAAgF;AAChF,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAuB,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AACjG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAA+B,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAE1G,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE/D,MAAM,UAAU,GAAG,UAAiD,CAAC;AACrE,MAAM,OAAO,GAAG,GAA0C,CAAC;AAE3D,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC9D,UAAU,CAAC,GAAG,EAAE;QACd,UAAU,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QACjF,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QACtG,OAAO,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;QAC/D,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5B,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACvC,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;QAC7C,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACvC,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/lib/capability.d.ts
CHANGED
|
@@ -4,17 +4,32 @@ export type IpmiCapability = {
|
|
|
4
4
|
ipmitool_version: string | null;
|
|
5
5
|
} | {
|
|
6
6
|
available: false;
|
|
7
|
-
reason: "no_ipmitool_binary" | "no_bmc_device" | "execution_failed" | "permission_denied";
|
|
7
|
+
reason: "no_ipmitool_binary" | "no_bmc_device" | "execution_failed" | "permission_denied" | "ipmitool_cve_2020_5208";
|
|
8
8
|
detail?: string;
|
|
9
9
|
};
|
|
10
|
+
export declare const MIN_SAFE_IPMITOOL_VERSION = "1.8.19";
|
|
11
|
+
/**
|
|
12
|
+
* True iff `version` is a parseable ipmitool version strictly below
|
|
13
|
+
* MIN_SAFE_IPMITOOL_VERSION. Unknown/unparseable versions return false
|
|
14
|
+
* (we do not disable a capability we cannot positively identify as
|
|
15
|
+
* vulnerable). Pure; unit-tested.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isIpmitoolVersionVulnerable(version: string | null): boolean;
|
|
10
18
|
interface DetectDeps {
|
|
11
|
-
/** Override for tests.
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
/** Override for tests. Runs `ipmitool -V` (direct; works unprivileged).
|
|
20
|
+
* Returns stdout or throws with err.code. Used only for the version /
|
|
21
|
+
* CVE gate. */
|
|
14
22
|
runIpmitool?: (args: string[]) => Promise<{
|
|
15
23
|
stdout: string;
|
|
16
24
|
stderr: string;
|
|
17
25
|
}>;
|
|
26
|
+
/** Override for tests. The WRAPPED sensor probe (`sudo crucible-collect
|
|
27
|
+
* ipmi-sensor`). Returns stdout, or null on any failure. This is the
|
|
28
|
+
* availability signal: under the §2.1 unprivileged model the agent user
|
|
29
|
+
* cannot stat /dev/ipmi0 or run ipmitool directly, so a direct
|
|
30
|
+
* device-node probe is meaningless; the wrapper (which runs as root) is
|
|
31
|
+
* the real reachability test. */
|
|
32
|
+
probeSensor?: () => Promise<string | null>;
|
|
18
33
|
}
|
|
19
34
|
export declare function detectIpmiCapability(deps?: DetectDeps): Promise<IpmiCapability>;
|
|
20
35
|
export declare function formatCapabilityLine(cap: IpmiCapability): string;
|
package/dist/lib/capability.js
CHANGED
|
@@ -8,50 +8,49 @@
|
|
|
8
8
|
//
|
|
9
9
|
// This module probes once at startup and caches the result. collectIpmi()
|
|
10
10
|
// reads the cached capability and short-circuits when unavailable.
|
|
11
|
-
import { promises as fs } from "node:fs";
|
|
12
11
|
import { execFile } from "node:child_process";
|
|
13
12
|
import { promisify } from "node:util";
|
|
13
|
+
import { runPrivileged } from "./privileged.js";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
// CVE-2020-5208 (GHSA-g659-9qxw-p7cp): heap overflow in ipmitool's
|
|
16
|
+
// read_fru_area_section and related parsers when handling data received
|
|
17
|
+
// from a remote LAN party (a malicious or compromised BMC). Fixed in
|
|
18
|
+
// ipmitool 1.8.19. The agent runs ipmitool in-band against the host BMC,
|
|
19
|
+
// so a compromised BMC parsing path is a real local-blast-radius primitive
|
|
20
|
+
// (audit §2.1 / catalog T-202). Below the fix version we mark IPMI
|
|
21
|
+
// unavailable rather than feed BMC output to a vulnerable parser.
|
|
22
|
+
export const MIN_SAFE_IPMITOOL_VERSION = "1.8.19";
|
|
23
|
+
/**
|
|
24
|
+
* True iff `version` is a parseable ipmitool version strictly below
|
|
25
|
+
* MIN_SAFE_IPMITOOL_VERSION. Unknown/unparseable versions return false
|
|
26
|
+
* (we do not disable a capability we cannot positively identify as
|
|
27
|
+
* vulnerable). Pure; unit-tested.
|
|
28
|
+
*/
|
|
29
|
+
export function isIpmitoolVersionVulnerable(version) {
|
|
30
|
+
if (!version)
|
|
31
|
+
return false;
|
|
32
|
+
const min = [1, 8, 19];
|
|
33
|
+
const parts = version.split(/[.\-+]/).map((p) => parseInt(p, 10));
|
|
34
|
+
if (parts.length === 0 || Number.isNaN(parts[0]))
|
|
35
|
+
return false; // unparseable
|
|
36
|
+
for (let i = 0; i < min.length; i++) {
|
|
37
|
+
const p = Number.isNaN(parts[i]) ? 0 : (parts[i] ?? 0);
|
|
38
|
+
if (p < min[i])
|
|
39
|
+
return true;
|
|
40
|
+
if (p > min[i])
|
|
41
|
+
return false;
|
|
25
42
|
}
|
|
43
|
+
return false; // equal or greater
|
|
26
44
|
}
|
|
27
45
|
async function defaultRunIpmitool(args) {
|
|
28
46
|
const { stdout, stderr } = await execFileAsync("ipmitool", args, { timeout: 2000 });
|
|
29
47
|
return { stdout, stderr };
|
|
30
48
|
}
|
|
31
49
|
export async function detectIpmiCapability(deps = {}) {
|
|
32
|
-
const statDevice = deps.statDevice ?? defaultStatDevice;
|
|
33
50
|
const runIpmitool = deps.runIpmitool ?? defaultRunIpmitool;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const path of DEVICE_CANDIDATES) {
|
|
38
|
-
const result = await statDevice(path);
|
|
39
|
-
if (result === "ok") {
|
|
40
|
-
deviceFound = true;
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
if (result === "eacces") {
|
|
44
|
-
permissionDenied = true;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (permissionDenied && !deviceFound) {
|
|
48
|
-
return {
|
|
49
|
-
available: false,
|
|
50
|
-
reason: "permission_denied",
|
|
51
|
-
detail: "/dev/ipmi0 exists but is not readable; run as root or add user to ipmi group",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
// Step 2: probe ipmitool binary.
|
|
51
|
+
const probeSensor = deps.probeSensor ?? (() => runPrivileged("ipmi-sensor"));
|
|
52
|
+
// Step 1: probe the ipmitool binary + version. `ipmitool -V` needs no BMC
|
|
53
|
+
// access, so it works even as the unprivileged service user.
|
|
55
54
|
let ipmitoolVersion = null;
|
|
56
55
|
try {
|
|
57
56
|
const { stdout } = await runIpmitool(["-V"]);
|
|
@@ -68,26 +67,28 @@ export async function detectIpmiCapability(deps = {}) {
|
|
|
68
67
|
detail: String(err?.stderr ?? err?.message ?? err).split("\n")[0]?.slice(0, 200),
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
|
-
// Step
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Step 2: version gate (CVE-2020-5208): refuse to feed BMC output to a
|
|
71
|
+
// vulnerable ipmitool parser. Fail-closed for the IPMI capability only;
|
|
72
|
+
// every other collector is unaffected. Skipped when the version could
|
|
73
|
+
// not be parsed (we do not disable a capability we cannot positively
|
|
74
|
+
// identify as vulnerable).
|
|
75
|
+
if (isIpmitoolVersionVulnerable(ipmitoolVersion)) {
|
|
76
|
+
return {
|
|
77
|
+
available: false,
|
|
78
|
+
reason: "ipmitool_cve_2020_5208",
|
|
79
|
+
detail: `ipmitool ${ipmitoolVersion} < ${MIN_SAFE_IPMITOOL_VERSION}; upgrade to close CVE-2020-5208`,
|
|
80
|
+
};
|
|
74
81
|
}
|
|
75
|
-
// Step
|
|
76
|
-
//
|
|
82
|
+
// Step 3: reachability via the WRAPPED sensor probe. Non-empty output means
|
|
83
|
+
// the BMC answered; empty/failure means no usable BMC on this host.
|
|
77
84
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const errOut = (stderr || "").toLowerCase();
|
|
81
|
-
if (out.trim().length > 0 && !errOut.includes("could not open")) {
|
|
85
|
+
const out = await probeSensor();
|
|
86
|
+
if (out && out.trim().length > 0) {
|
|
82
87
|
return { available: true, method: "ipmitool_in_band", ipmitool_version: ipmitoolVersion };
|
|
83
88
|
}
|
|
84
89
|
return { available: false, reason: "no_bmc_device" };
|
|
85
90
|
}
|
|
86
91
|
catch (err) {
|
|
87
|
-
const stderr = String(err?.stderr ?? "").toLowerCase();
|
|
88
|
-
if (stderr.includes("could not open")) {
|
|
89
|
-
return { available: false, reason: "no_bmc_device" };
|
|
90
|
-
}
|
|
91
92
|
return {
|
|
92
93
|
available: false,
|
|
93
94
|
reason: "execution_failed",
|
|
@@ -105,6 +106,7 @@ export function formatCapabilityLine(cap) {
|
|
|
105
106
|
case "no_bmc_device": return "IPMI: not available (no /dev/ipmi*, BMC not detected)";
|
|
106
107
|
case "permission_denied": return `IPMI: not available (${cap.detail ?? "permission denied"})`;
|
|
107
108
|
case "execution_failed": return `IPMI: not available (execution failed${cap.detail ? `: ${cap.detail}` : ""})`;
|
|
109
|
+
case "ipmitool_cve_2020_5208": return `IPMI: disabled (${cap.detail ?? "ipmitool too old (CVE-2020-5208)"})`;
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
//# sourceMappingURL=capability.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../../src/lib/capability.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,sEAAsE;AACtE,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,wEAAwE;AACxE,EAAE;AACF,0EAA0E;AAC1E,mEAAmE;AAEnE,OAAO,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../../src/lib/capability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const SERVICE_USER = "glassmkr";
|
|
2
|
+
export declare const WRAPPER_PATH = "/usr/local/sbin/crucible-collect";
|
|
3
|
+
export declare const SUDOERS_PATH = "/etc/sudoers.d/glassmkr-crucible";
|
|
4
|
+
export type PrivilegedAction = "ipmi-sensor" | "ipmi-sel-info" | "ipmi-sel-elist" | "ipmi-fan" | "smart" | "zpool" | "raid-perccli" | "raid-storcli" | "raid-ssacli" | "raid-arcconf" | "dmesg-errcrit" | "dmesg-io" | "iptables" | "nft" | "ufw" | "firewall-cmd" | "pve-firewall" | "sshd" | "lvs" | "ethtool" | "last";
|
|
5
|
+
/** SMART device paths the wrapper accepts. Mirrors the sh `valid_device`
|
|
6
|
+
* case in WRAPPER_SCRIPT; kept in TS so it is unit-testable. Blocks path
|
|
7
|
+
* traversal / arbitrary-file reads via `smartctl -a <path>`. */
|
|
8
|
+
export declare function isAllowedSmartDevice(dev: string): boolean;
|
|
9
|
+
/** ethtool interface names the wrapper accepts. Linux ifnames are <=15
|
|
10
|
+
* chars from a restricted set; no slashes/spaces so no arg injection. */
|
|
11
|
+
export declare function isAllowedIface(name: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Invoke a privileged collection action. Preferred path is the fixed-argv
|
|
14
|
+
* sudo wrapper (the unprivileged `glassmkr` service user escalates only
|
|
15
|
+
* through it). Returns the command stdout, or null on any failure - callers
|
|
16
|
+
* treat null as "capability unavailable", so a missing tool degrades
|
|
17
|
+
* gracefully rather than crashing.
|
|
18
|
+
*
|
|
19
|
+
* Root fallback: if the wrapper is NOT installed (e.g. an upgrade via
|
|
20
|
+
* `npm i -g` without re-running `init`, or a host still on User=root that
|
|
21
|
+
* never migrated) AND we are running as root, run the underlying command
|
|
22
|
+
* directly. As root that is exactly what the wrapper would exec, so it
|
|
23
|
+
* restores collection instead of silently returning null for every
|
|
24
|
+
* hardware/security probe. This honors init.ts's documented "User=root
|
|
25
|
+
* never loses collection" invariant, which the wrapper-only path (audit
|
|
26
|
+
* §2.1) had regressed on wrapper-less root hosts. A non-root agent without
|
|
27
|
+
* the wrapper still gets null (it cannot and must not escalate).
|
|
28
|
+
*/
|
|
29
|
+
export declare function runPrivileged(action: PrivilegedAction, args?: string[], timeoutMs?: number): Promise<string | null>;
|
|
30
|
+
export declare const WRAPPER_SCRIPT = "#!/bin/sh\n# Glassmkr Crucible privileged-collection facade (audit \u00A72.1).\n# Installed root:root 0755. The sole sudo grant for the unprivileged\n# 'glassmkr' service user. Fixed action -> fixed argv; no passthrough.\n# Managed by glassmkr-crucible init; do not edit by hand.\nset -eu\n\naction=\"${1:-}\"\n[ -n \"$action\" ] || { echo \"crucible-collect: missing action\" >&2; exit 64; }\nshift 2>/dev/null || true\n\nvalid_device() {\n case \"$1\" in\n /dev/sd[a-z]|/dev/sd[a-z][a-z]|/dev/hd[a-z]|/dev/hd[a-z][a-z]) return 0 ;;\n /dev/nvme[0-9]*n[0-9]*|/dev/nvme[0-9]*) return 0 ;;\n /dev/bus/[0-9]*) return 0 ;;\n *) return 1 ;;\n esac\n}\nvalid_iface() {\n case \"$1\" in\n \"\" | *[!A-Za-z0-9._:-]*) return 1 ;;\n *) [ \"${#1}\" -le 15 ] ;;\n esac\n}\n\ncase \"$action\" in\n ipmi-sensor) exec ipmitool sensor ;;\n ipmi-sel-info) exec ipmitool sel info ;;\n ipmi-sel-elist) exec ipmitool sel elist ;;\n ipmi-fan) exec ipmitool sdr type Fan ;;\n smart)\n dev=\"${1:-}\"\n valid_device \"$dev\" || { echo \"crucible-collect: rejected device: $dev\" >&2; exit 65; }\n exec smartctl --json --all \"$dev\" ;;\n zpool) exec zpool status ;;\n raid-perccli) exec perccli /c0 show all J ;;\n raid-storcli) exec storcli /call show all J ;;\n raid-ssacli) exec ssacli ctrl all show status ;;\n raid-arcconf) exec arcconf list ;;\n dmesg-errcrit) exec dmesg --level=err,crit --since \"5 min ago\" ;;\n dmesg-io) exec sh -c 'dmesg -T --since \"10 minutes ago\" 2>/dev/null | grep -i \"I/O error\\\\|Buffer I/O error\\\\|blk_update_request.*error\"' ;;\n iptables) exec iptables -L -n ;;\n nft) exec nft list ruleset ;;\n ufw) exec ufw status ;;\n firewall-cmd) exec firewall-cmd --state ;;\n pve-firewall) exec pve-firewall status ;;\n sshd) exec sshd -T ;;\n lvs) exec lvs --reportformat=json --options=lv_name,vg_name,lv_attr,data_percent,metadata_percent --units=b --noheadings ;;\n ethtool)\n ifc=\"${1:-}\"\n valid_iface \"$ifc\" || { echo \"crucible-collect: rejected iface: $ifc\" >&2; exit 65; }\n exec ethtool \"$ifc\" ;;\n last) exec last -x -F ;;\n *) echo \"crucible-collect: unknown action: $action\" >&2; exit 64 ;;\nesac\n";
|
|
31
|
+
export declare const SUDOERS_CONTENT = "# Glassmkr Crucible (audit \u00A72.1). Managed by glassmkr-crucible init.\n# The service user may run ONLY the fixed-argv collection facade as root.\nDefaults!/usr/local/sbin/crucible-collect env_reset\nDefaults!/usr/local/sbin/crucible-collect secure_path=\"/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin\"\nglassmkr ALL=(root) NOPASSWD: /usr/local/sbin/crucible-collect\n";
|