"use strict"; // System Monitor + chj.users on ONE isolated Linux test VM over real SSH (key login, sudo with password). // Never point this at a production server: it creates/deletes a swap file, a test account and changes sshd policy // (always reverted). Environment: CHJ_VM_HOST, CHJ_VM_FINGERPRINT (SHA256:...), CHJ_VM_KEY (private key path), // CHJ_VM_USER (default ai) and CHJ_VM_SUDO_PASSWORD or CHJ_VM_SUDO_PASSWORD_FILE + CHJ_VM_SUDO_PASSWORD_LINE // ("name = value" line read at runtime, never printed). const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { execFileSync, spawnSync } = require("node:child_process"); const { SessionManager } = require("../src/main/sessions/sessionManager"); const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec"); const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService"); const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService"); const { detectPlatform } = require("../src/main/sessions/platformDetect"); const env = process.env; const enabled = Boolean(env.CHJ_VM_HOST && env.CHJ_VM_FINGERPRINT && env.CHJ_VM_KEY && (env.CHJ_VM_SUDO_PASSWORD || env.CHJ_VM_SUDO_PASSWORD_FILE)); const skip = enabled ? false : "Set CHJ_VM_* to run against an isolated Linux test VM."; const sudoPassword = !enabled ? "" : env.CHJ_VM_SUDO_PASSWORD || fs.readFileSync(env.CHJ_VM_SUDO_PASSWORD_FILE, "utf8").split("\n")[Number(env.CHJ_VM_SUDO_PASSWORD_LINE || 1) - 1].split("=").slice(1).join("=").trim(); const vmUser = env.CHJ_VM_USER || "ai"; const host = env.CHJ_VM_HOST; const SWAP = "/swapfile-chjvmtest"; const USER = "chjvmtest"; let keyDir; let knownHosts; function keygen(name) { const file = path.join(keyDir, name); execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", `chj-vm-${name}`, "-f", file]); return { privatePath: file, publicKey: fs.readFileSync(`${file}.pub`, "utf8").trim() }; } function remote(command) { return execFileSync("ssh", ["-i", env.CHJ_VM_KEY, "-o", "BatchMode=yes", "-o", `UserKnownHostsFile=${knownHosts}`, "-o", "StrictHostKeyChecking=yes", `${vmUser}@${host}`, command], { encoding: "utf8" }).trim(); } function canLogin(user, key, address = host) { return spawnSync("ssh", ["-i", key, "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", "PreferredAuthentications=publickey", "-o", `UserKnownHostsFile=${knownHosts}`, "-o", "StrictHostKeyChecking=yes", "-o", "HostKeyAlias=" + host, "-o", "ConnectTimeout=8", `${user}@${address}`, "exit 0"], { encoding: "utf8" }).status === 0; } test("System Monitor and chj.users on an isolated Linux test VM", { skip, timeout: 1200000 }, async (t) => { keyDir = fs.mkdtempSync(path.join(os.tmpdir(), "chj-vm-")); knownHosts = path.join(keyDir, "known_hosts"); execFileSync("sh", ["-c", `ssh-keyscan -t ed25519 ${host} > '${knownHosts}' 2>/dev/null`]); assert.equal(execFileSync("ssh-keygen", ["-lf", knownHosts], { encoding: "utf8" }).split(" ")[1], env.CHJ_VM_FINGERPRINT, "documented host key"); const hostKeys = new Map(); const profileService = { get: () => ({ id: "vm", label: "vm", host, port: 22, username: vmUser, authMethod: "privateKey", privateKeyPath: env.CHJ_VM_KEY }), getHostKey: (h, p) => hostKeys.get(`${h}:${p}`) || null, trustHostKey: (h, p, fp) => hostKeys.set(`${h}:${p}`, fp), getStoredPassword: () => "", markUsed() {} }; const manager = new SessionManager({ profileService }); const latency = new Promise((resolve) => manager.on("state", (state) => { if (state.latency?.measuredAt) resolve(state.latency); })); try { await manager.connect({ sessionId: "vm", profileId: "vm" }); } catch (error) { if (error.code !== "HOST_KEY_UNKNOWN") throw error; assert.equal(`SHA256:${Buffer.from(error.fingerprint, "hex").toString("base64").replace(/=+$/, "")}`, env.CHJ_VM_FINGERPRINT, "app sees the documented host key"); manager.trustPending({ sessionId: "vm", profileId: "vm", fingerprint: error.fingerprint }); await manager.connect({ sessionId: "vm", profileId: "vm" }); } const executor = new PrivilegedExecutor({ sessionManager: manager }); const vm = new VirtualMemoryService({ sessionManager: manager, privilegedExecutor: executor }); const api = createUserAdminApi(new UserAdminService({ sessionManager: manager, privilegedExecutor: executor, detectPlatform })); const auth = { sudoPassword }; // systemd keeps the user manager alive for a few seconds after logout; CH-J correctly refuses USERS_USER_HAS_PROCESSES. async function deleteUser() { for (let attempt = 0; ; attempt += 1) { try { return await api.manage("vm", { action: "delete", username: USER, removeHome: true, ...auth }); } catch (error) { if (error.code !== "USERS_USER_HAS_PROCESSES" || attempt >= 30) throw error; await new Promise((resolve) => setTimeout(resolve, 1000)); } } } const aiKey = env.CHJ_VM_KEY; let fstabBefore = null; t.after(async () => { // Best-effort cleanup of everything this test may have created. for (const step of [ () => vm.manage("vm", { action: "delete", path: SWAP, ...auth }), () => vm.manage("vm", { action: "delete", path: `${SWAP}2`, ...auth }), () => api.sshPolicy("vm", { username: "root", policy: {}, ...auth }), () => api.sshPolicy("vm", { username: USER, policy: {}, ...auth }), () => deleteUser() ]) { try { await step(); } catch {} } try { const detail = await api.detail("vm", { username: "root", ...auth }); for (const file of detail.keys?.files || []) for (const entry of [...file.entries].reverse()) { if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth }); } } catch {} await manager.disconnectAll("test"); fs.rmSync(keyDir, { recursive: true, force: true }); assert.ok(canLogin(vmUser, aiKey) || true); }); await t.test("platform detection and metrics", async () => { const metrics = await manager.readSystemMetrics("vm"); t.diagnostic(`${metrics.platform} | kernel ${metrics.kernel} | RAM ${Math.round(metrics.memory.total / 1048576)} MiB`); assert.equal(metrics.os, "linux"); assert.equal(metrics.loadAverageSupported, true); assert.ok(metrics.cpu.logicalCores >= 1 && metrics.uptimeSeconds > 0); const d = metrics.memoryDetail; assert.ok(Math.abs(d.used + d.free + d.buffers + d.cached + d.slabReclaimable - d.total) < 1048576, "RAM slices add up without double counting"); assert.ok(metrics.disk.total > 0 && metrics.disk.mount === "/"); assert.ok(metrics.network.interfaces.some((entry) => entry.state === "up")); }); await t.test("ICMP ping and SSH RTT", async () => { const value = await Promise.race([latency, new Promise((resolve) => setTimeout(() => resolve(null), 40000))]); t.diagnostic(`ping ${value?.pingMs} ms (${value?.pingStatus}), ssh ${value?.sshRttMs} ms (${value?.sshRttMethod})`); assert.ok(value?.sshRttMs > 0); assert.ok(value.pingMs > 0 || ["timeout", "unreachable", "denied"].includes(value.pingStatus)); }); await t.test("swap file lifecycle on the real root file system", async () => { const before = await vm.read("vm", { privileged: true, ...auth }); if (before.swapAreas.some((entry) => entry.path === SWAP)) { await vm.manage("vm", { action: "delete", path: SWAP, ...auth }); t.diagnostic("removed leftover test swap file from an earlier run"); } fstabBefore = remote("cksum /etc/fstab"); t.diagnostic(`root fs: ${JSON.stringify(before.locations || before.fileSystems || null)}`); assert.equal(before.platform, "linux"); assert.equal(before.canManage, true); await assert.rejects(() => vm.manage("vm", { action: "create", path: "/etc/passwd", sizeMiB: 64, persist: false, ...auth }), { code: "VM_SWAP_PATH_INVALID" }); await assert.rejects(() => vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, persist: false, sudoPassword: "wrong-pass" }), { code: /SUDO_PASSWORD_INCORRECT/ }); let result = await vm.manage("vm", { action: "create", path: SWAP, sizeMiB: 128, priority: 5, persist: true, ...auth }); assert.ok(result.active.some((area) => area.path === SWAP && area.priority === 5), "created + active"); let state = await vm.read("vm", { privileged: true, ...auth }); const area = state.swapAreas.find((entry) => entry.path === SWAP); assert.equal(area.persistent, true); assert.equal(area.management, "managed"); t.diagnostic(`fs ${area.fsType}`); result = await vm.manage("vm", { action: "resize", path: SWAP, sizeMiB: 192, priority: 5, ...auth }); assert.ok(result.active.some((entry) => entry.path === SWAP && Math.abs(entry.size - 192 * 1048576) <= 65536)); result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 7, ...auth }); assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 7)); result = await vm.manage("vm", { action: "deactivate", path: SWAP, ...auth }); assert.equal(result.active.some((entry) => entry.path === SWAP), false); result = await vm.manage("vm", { action: "activate", path: SWAP, priority: 3, ...auth }); assert.ok(result.active.some((entry) => entry.path === SWAP && entry.priority === 3)); const concurrent = vm.manage("vm", { action: "persist", path: SWAP, persist: false, ...auth }); await assert.rejects(() => vm.manage("vm", { action: "delete", path: SWAP, ...auth }), { code: "VM_BUSY" }); await concurrent; await vm.manage("vm", { action: "delete", path: SWAP, ...auth }); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(state.swapAreas.some((entry) => entry.path === SWAP), false, "swap file removed"); assert.equal(remote("cksum /etc/fstab"), fstabBefore, "fstab back to original content"); }); await t.test("swap priorities: two swap files as main and backup, persistent in fstab", async () => { const SWAP2 = `${SWAP}2`, swaps = [SWAP, SWAP2]; const pick = (state, name) => state.swapAreas.find((entry) => entry.path === name); const fstabLine = (name) => remote(`grep -E '^${name}[[:space:]]' /etc/fstab || true`); const before = await vm.read("vm", { privileged: true, ...auth }); for (const name of swaps) if (before.swapAreas.some((entry) => entry.path === name)) await vm.manage("vm", { action: "delete", path: name, ...auth }); const fstabOriginal = remote("cksum /etc/fstab"); try { // 1-2: two swaps with automatic priorities (kernel assigns negative values). for (const name of swaps) await vm.manage("vm", { action: "create", path: name, sizeMiB: 128, priority: "auto", persist: true, ...auth }); let state = await vm.read("vm", { privileged: true, ...auth }); for (const name of swaps) { const area = pick(state, name); assert.ok(area.effectivePriority < 0, `${name} automatic negative`); assert.equal(area.priorityMode, "auto"); assert.equal(area.persistentPriority, null); } assert.equal(fstabLine(SWAP), `${SWAP} none swap sw 0 0 # CH-J chj.system-monitor`); // 3-5: explicit 100 (main) and 10 (backup), applied now and saved. let result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 100, apply: "now", ...auth }); assert.deepEqual(result.steps.filter((step) => step.step.startsWith("priority")).map((step) => [step.step, step.status]), [["priority-saved", "ok"], ["priority-runtime", "ok"]]); await vm.manage("vm", { action: "priority", path: SWAP2, priority: 10, ...auth }); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP).effectivePriority, 100); assert.equal(pick(state, SWAP2).effectivePriority, 10); assert.equal(pick(state, SWAP).precedence, 1); assert.equal(pick(state, SWAP2).precedence, 2); assert.equal(fstabLine(SWAP), `${SWAP} none swap sw,pri=100 0 0 # CH-J chj.system-monitor`); assert.equal(fstabLine(SWAP2), `${SWAP2} none swap sw,pri=10 0 0 # CH-J chj.system-monitor`); // 4: the kernel fills the higher priority first. Under memory pressure the 128 MiB main area takes pages before the backup. t.diagnostic(`proc/swaps:\n${remote("cat /proc/swaps")}`); // 7 + 18: changing an existing pri= replaces it; repeating it creates no duplicates. await vm.manage("vm", { action: "priority", path: SWAP, priority: 200, ...auth }); result = await vm.manage("vm", { action: "priority", path: SWAP, priority: 200, ...auth }); assert.ok(result.steps.some((step) => step.step === "priority-runtime" && /already/.test(step.message))); assert.equal(remote(`grep -c -E '^${SWAP}[[:space:]]' /etc/fstab`), "1"); // 6: back to automatic, saved only (applies at next activation): fstab loses pri=, runtime keeps 200 and is flagged pending. result = await vm.manage("vm", { action: "priority", path: SWAP, priority: "auto", apply: "later", ...auth }); assert.ok(result.steps.some((step) => step.step === "priority-runtime" && step.status === "pending")); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP).effectivePriority, 200); assert.equal(pick(state, SWAP).priorityPending, true); assert.equal(fstabLine(SWAP), `${SWAP} none swap sw 0 0 # CH-J chj.system-monitor`); // 10: new activation (as after a restart) takes the saved setting; other operations keep the user's priority. await vm.manage("vm", { action: "priority", path: SWAP, priority: 100, apply: "later", ...auth }); await vm.manage("vm", { action: "deactivate", path: SWAP, ...auth }); await vm.manage("vm", { action: "activate", path: SWAP, priority: "keep", ...auth }); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP).effectivePriority, 100); assert.equal(pick(state, SWAP).priorityPending, false); await vm.manage("vm", { action: "resize", path: SWAP2, sizeMiB: 96, priority: "keep", ...auth }); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP2).effectivePriority, 10, "resize keeps the configured priority"); assert.match(fstabLine(SWAP2), /pri=10/); // 15: equal explicit priorities are reported as shared (round-robin). await vm.manage("vm", { action: "priority", path: SWAP2, priority: 100, ...auth }); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP).sharedPriority, true); assert.equal(pick(state, SWAP2).precedence, 1); // 12: invalid input is refused before the server changes anything. for (const priority of [-2, 40000, "x"]) await assert.rejects(() => vm.manage("vm", { action: "priority", path: SWAP, priority, ...auth }), { code: "VM_SWAP_PRIORITY_INVALID" }); // 17: runtime-only swap (not in fstab) can be reprioritized without being written to fstab. await vm.manage("vm", { action: "persist", path: SWAP2, persist: false, ...auth }); result = await vm.manage("vm", { action: "priority", path: SWAP2, priority: 5, ...auth }); assert.ok(result.steps.some((step) => step.step === "priority-saved" && step.status === "none")); state = await vm.read("vm", { privileged: true, ...auth }); assert.equal(pick(state, SWAP2).effectivePriority, 5); assert.equal(pick(state, SWAP2).persistent, false); assert.equal(fstabLine(SWAP2), ""); await vm.manage("vm", { action: "persist", path: SWAP2, persist: true, priority: "keep", ...auth }); assert.equal(fstabLine(SWAP2), `${SWAP2} none swap sw,pri=5 0 0 # CH-J chj.system-monitor`, "persist keeps the explicit runtime priority"); } finally { for (const name of swaps) { try { await vm.manage("vm", { action: "delete", path: name, ...auth }); } catch {} } } assert.equal(remote("cksum /etc/fstab"), fstabOriginal, "fstab back to original content"); assert.equal(remote(`ls ${SWAP} ${SWAP2} 2>/dev/null || true`), ""); }); await t.test("chj.users: account, Ed25519 key and per-user SSH policy", async () => { const privileges = await api.privileges("vm"); t.diagnostic(`privileges ${privileges.mode}`); assert.equal(privileges.platform, "linux"); const inventory = await api.inventory("vm", auth); assert.ok(inventory.users.some((account) => account.username === "root")); if ((await api.inventory("vm", auth)).users.some((account) => account.username === USER)) await deleteUser(); await api.manage("vm", { action: "create", username: USER, password: "ChjVm-Test-Pass-9", comment: "CH-J VM test", ...auth }); const key = keygen("user"); await api.keys("vm", { action: "add", username: USER, publicKey: key.publicKey, ...auth }); assert.ok(canLogin(USER, key.privatePath), "new user logs in with the added Ed25519 key"); let result = await api.sshPolicy("vm", { username: USER, policy: { access: "deny" }, ...auth }); assert.equal(canLogin(USER, key.privatePath), false, "access denied by CH-J policy"); assert.ok(canLogin(vmUser, aiKey), "session account unaffected"); result = await api.sshPolicy("vm", { username: USER, policy: {}, ...auth }); assert.ok(canLogin(USER, key.privatePath), "inherit restores access"); await deleteUser(); assert.equal((await api.inventory("vm", auth)).users.some((account) => account.username === USER), false); }); await t.test("chj.users: root keys and PermitRootLogin via sudo; root stays protected", async () => { await assert.rejects(() => api.manage("vm", { action: "delete", username: "root", ...auth }), { code: "USERS_PROTECTED_USER" }); await assert.rejects(() => api.manage("vm", { action: "setShell", username: "root", shell: "/bin/sh", ...auth }), { code: "USERS_PROTECTED_USER" }); const rootKey = keygen("root"); try { await api.keys("vm", { action: "add", username: "root", publicKey: rootKey.publicKey, ...auth }); } catch (error) { // Proxmox: /root/.ssh/authorized_keys is a symlink into /etc/pve; CH-J must refuse rather than follow it. if (error.code !== "USERS_KEYS_FILE_UNMANAGEABLE") throw error; t.diagnostic(`root keys refused: ${error.message}`); await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth }); const effective = (await api.detail("vm", { username: "root", ...auth })); t.diagnostic(`policy after no: ${JSON.stringify(effective.sshPolicy?.managed || effective.ssh?.policy || null)}`); assert.ok(canLogin(vmUser, aiKey), "session account unaffected"); await api.sshPolicy("vm", { username: "root", policy: {}, ...auth }); return; } await assert.rejects(() => api.sshPolicy("vm", { username: "root", policy: { password: "yes" }, ...auth }), { code: "USERS_ROOT_PASSWORD_CONFIRMATION_REQUIRED" }); await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "prohibit-password" }, ...auth }); const allowed = canLogin("root", rootKey.privatePath); t.diagnostic(`root key login with prohibit-password: ${allowed}`); if (!allowed) { // OpenSSH without PAM (Alpine) refuses every login, keys included, for an account whose password field starts with "!". const rootAccount = (await api.inventory("vm", auth)).users.find((account) => account.username === "root"); t.diagnostic(`root passwordState=${rootAccount.passwordState} locked=${rootAccount.locked}`); assert.ok(rootAccount.locked || rootAccount.passwordState === "none", "root key login allowed by PermitRootLogin prohibit-password (unless the root account itself is locked)"); } await api.sshPolicy("vm", { username: "root", policy: { permitRootLogin: "no" }, ...auth }); assert.equal(canLogin("root", rootKey.privatePath), false, "PermitRootLogin no blocks root"); assert.ok(canLogin(vmUser, aiKey), "session account unaffected"); await api.sshPolicy("vm", { username: "root", policy: {}, ...auth }); const detail = await api.detail("vm", { username: "root", ...auth }); for (const file of detail.keys.files) for (const entry of [...file.entries].reverse()) { if (entry.comment === "chj-vm-root") await api.keys("vm", { action: "remove", username: "root", path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth }); } assert.equal(canLogin("root", rootKey.privatePath), false, "removed root key no longer works"); }); });