import { afterEach, describe, expect, test } from "bun:test"; import { chmod, lstat, mkdir, mkdtemp, open, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { DIAGNOSTICS_ROOT, assertTrustedDirectory, cleanupDiagnosticsWorkspace, createDiagnosticsWorkspace, discardDiagnosticsArchive, ensureDiagnosticsStorageRoot } from "../src/lib/diagnosticsStorage"; import { disableReadOnlyGuard, enableReadOnlyGuard } from "../src/lib/guard"; const directories: string[] = []; afterEach(async () => { disableReadOnlyGuard(); await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); function fakeStats(options: { directory?: boolean; symlink?: boolean; uid?: number; gid?: number; mode?: number; }): import("node:fs").Stats { return { isDirectory: () => options.directory ?? true, isSymbolicLink: () => options.symlink ?? false, uid: options.uid ?? 0, gid: options.gid ?? 0, mode: options.mode ?? 0o40700 } as import("node:fs").Stats; } async function temporaryPolicy(): Promise<{ base: string; stateRoot: string; diagnosticsRoot: string; uid: number; gid: number; }> { const base = await mkdtemp(join(tmpdir(), "hy2xs-diagnostics-storage-")); directories.push(base); const stateRoot = join(base, "hy2xs"); const diagnosticsRoot = join(stateRoot, "diagnostics"); await mkdir(stateRoot, { mode: 0o755 }); await chmod(stateRoot, 0o755); const stats = await lstat(stateRoot); return { base, stateRoot, diagnosticsRoot, uid: stats.uid, gid: stats.gid }; } describe("граница привилегий diagnostics storage", () => { test("production path не находится внутри service-writable logDir", () => { expect(DIAGNOSTICS_ROOT).toBe("/var/lib/hy2xs/diagnostics"); expect(DIAGNOSTICS_ROOT.startsWith("/var/log/hy2xs/")).toBe(false); }); test("создание storage проходит через read-only guard", async () => { const policy = await temporaryPolicy(); enableReadOnlyGuard("test phase"); await expect(ensureDiagnosticsStorageRoot(policy)).rejects.toThrow("read-only guard violation"); }); test("symlink не принимается за доверенный каталог", () => { expect(() => assertTrustedDirectory("/var/lib/hy2xs/diagnostics", fakeStats({ symlink: true }), { uid: 0, gid: 0, exactMode: 0o700 }) ).toThrow("symbolic link запрещён"); }); test("чужой владелец и записываемый родитель отвергаются", () => { expect(() => assertTrustedDirectory("/var/lib/hy2xs", fakeStats({ uid: 1001, mode: 0o40755 }), { uid: 0, gid: 0, rejectGroupOrOtherWrite: true }) ).toThrow("ожидается владелец 0:0"); expect(() => assertTrustedDirectory("/var/lib/hy2xs", fakeStats({ mode: 0o40775 }), { uid: 0, gid: 0, rejectGroupOrOtherWrite: true }) ).toThrow("могут изменять root namespace"); }); test("diagnostics root требует точный режим 0700", () => { expect(() => assertTrustedDirectory("/var/lib/hy2xs/diagnostics", fakeStats({ mode: 0o40750 }), { uid: 0, gid: 0, exactMode: 0o700 }) ).toThrow("ожидается режим 0700"); }); test("существующая symlink вместо diagnostics root отвергается fail closed", async () => { const policy = await temporaryPolicy(); const target = join(policy.base, "attacker-controlled"); await mkdir(target); const sentinel = join(target, "root-file"); await writeFile(sentinel, "не изменять\n"); await symlink(target, policy.diagnosticsRoot, process.platform === "win32" ? "junction" : "dir"); await expect(ensureDiagnosticsStorageRoot(policy)).rejects.toThrow("symbolic link запрещён"); expect(await readFile(sentinel, "utf8")).toBe("не изменять\n"); }); test("рабочие каталоги уникальны, а archive path заранее занят через wx", async () => { const policy = await temporaryPolicy(); await ensureDiagnosticsStorageRoot(policy); const first = await createDiagnosticsWorkspace(policy); const second = await createDiagnosticsWorkspace(policy); expect(first.outDir).not.toBe(second.outDir); expect(first.operationId.startsWith("diag-")).toBe(true); await expect(open(first.archive, "wx")).rejects.toBeDefined(); await cleanupDiagnosticsWorkspace(first, policy); await cleanupDiagnosticsWorkspace(second, policy); await discardDiagnosticsArchive(first, policy); await discardDiagnosticsArchive(second, policy); }); test("очистка отвергает workspace вне доверенного diagnostics root", async () => { const policy = await temporaryPolicy(); await expect( cleanupDiagnosticsWorkspace( { operationId: "diag-ABC123", outDir: join(policy.base, "diag-ABC123"), archive: join(policy.base, "diag-ABC123.tar.gz") }, policy ) ).rejects.toThrow("небезопасные пути diagnostics workspace"); }); });