78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
interface SecureConfigModule {
|
|
readBrainConfigSecure(path: string, root: string): string;
|
|
}
|
|
|
|
const roots: string[] = [];
|
|
|
|
async function loadSecureConfig(): Promise<SecureConfigModule> {
|
|
try {
|
|
return (await import('./brain-secure-config.js')) as SecureConfigModule;
|
|
} catch (error: unknown) {
|
|
const detail = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`MB-REQ-06 secure brain config reader is absent (${detail})`);
|
|
}
|
|
}
|
|
|
|
function fixture(): { readonly root: string; readonly directory: string; readonly file: string } {
|
|
const outer = mkdtempSync(join(tmpdir(), 'mosaic-brain-secure-config-'));
|
|
roots.push(outer);
|
|
const root = join(outer, 'mosaic');
|
|
const directory = join(root, 'brain');
|
|
const file = join(directory, 'owners.json');
|
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
writeFileSync(file, '{"version":1}\n', { mode: 0o600 });
|
|
return { root, directory, file };
|
|
}
|
|
|
|
afterEach((): void => {
|
|
vi.restoreAllMocks();
|
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('security-critical brain configuration reads', (): void => {
|
|
it('reads a principal-owned non-writable regular file through the secure descriptor path', async (): Promise<void> => {
|
|
const secure = await loadSecureConfig();
|
|
const config = fixture();
|
|
|
|
expect(secure.readBrainConfigSecure(config.file, config.root)).toBe('{"version":1}\n');
|
|
});
|
|
|
|
it('rejects a managed root owned by a UID other than the running principal', async (): Promise<void> => {
|
|
const secure = await loadSecureConfig();
|
|
const config = fixture();
|
|
if (typeof process.getuid !== 'function') throw new Error('test requires POSIX getuid');
|
|
const processWithUid = process as typeof process & { getuid: () => number };
|
|
const actualUid = processWithUid.getuid();
|
|
vi.spyOn(processWithUid, 'getuid').mockReturnValue(actualUid + 1);
|
|
|
|
expect(() => secure.readBrainConfigSecure(config.file, config.root)).toThrow(
|
|
/config-ancestor-owner-unsafe/,
|
|
);
|
|
});
|
|
|
|
it('rejects a group/world-writable policy file', async (): Promise<void> => {
|
|
const secure = await loadSecureConfig();
|
|
const config = fixture();
|
|
chmodSync(config.file, 0o666);
|
|
|
|
expect(() => secure.readBrainConfigSecure(config.file, config.root)).toThrow(
|
|
/config-file-permissions-unsafe/,
|
|
);
|
|
});
|
|
|
|
it('rejects a group/world-writable managed ancestor', async (): Promise<void> => {
|
|
const secure = await loadSecureConfig();
|
|
const config = fixture();
|
|
chmodSync(config.directory, 0o777);
|
|
|
|
expect(() => secure.readBrainConfigSecure(config.file, config.root)).toThrow(
|
|
/config-ancestor-permissions-unsafe/,
|
|
);
|
|
});
|
|
});
|