// tests/hooks/otel-export-validators.test.mjs // Step 11 validators: path, endpoint, field-allowlist. // CWE-22, CWE-918, CWE-212 mitigation. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { validateTextfilePath, FORBIDDEN_PREFIXES } from '../../lib/exporters/path-validator.mjs'; import { validateOtlpEndpoint } from '../../lib/exporters/endpoint-validator.mjs'; import { applyFieldAllowlist, POST_BASH_STATS_ALLOWED, EVENT_EMIT_PAYLOAD_ALLOWED, } from '../../lib/exporters/field-allowlist.mjs'; // ---- path-validator: CWE-22 mitigation ------------------------------------- test('path-validator: rejects ../etc/passwd traversal (CWE-22)', () => { const r = validateTextfilePath('../../etc/passwd'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PATH_TRAVERSAL')); }); test('path-validator: rejects /etc/voyage.prom (forbidden system prefix)', () => { const r = validateTextfilePath('/etc/voyage.prom'); assert.equal(r.valid, false); // Either forbidden-system or parent-missing (both are deny-paths) const denied = r.errors.find(e => e.code === 'PATH_FORBIDDEN_SYSTEM' || e.code === 'PATH_PARENT_MISSING'); assert.ok(denied, `expected deny, got: ${JSON.stringify(r.errors)}`); }); test('path-validator: rejects ~ home shorthand', () => { const r = validateTextfilePath('~/voyage.prom'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PATH_HOME_SHORTHAND')); }); test('path-validator: accepts path under allowedRoots', () => { const tmp = mkdtempSync(join(tmpdir(), 'voyage-path-allow-')); try { const target = join(tmp, 'voyage.prom'); const r = validateTextfilePath(target, { allowedRoots: [tmp] }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.match(r.parsed.path, /voyage\.prom$/); } finally { rmSync(tmp, { recursive: true, force: true }); } }); test('path-validator: rejects path outside allowedRoots', () => { const tmp = mkdtempSync(join(tmpdir(), 'voyage-path-deny-')); const otherTmp = mkdtempSync(join(tmpdir(), 'voyage-path-other-')); try { const target = join(otherTmp, 'voyage.prom'); const r = validateTextfilePath(target, { allowedRoots: [tmp] }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PATH_OUT_OF_ALLOWLIST')); } finally { rmSync(tmp, { recursive: true, force: true }); rmSync(otherTmp, { recursive: true, force: true }); } }); test('path-validator: FORBIDDEN_PREFIXES exports drift-pin', () => { // Ensure all the high-risk system paths are present for (const prefix of ['/etc/', '/proc/', '/sys/', '/var/', '/usr/']) { assert.ok(FORBIDDEN_PREFIXES.includes(prefix), `FORBIDDEN_PREFIXES missing critical path: ${prefix}`); } }); // ---- endpoint-validator: CWE-918 mitigation ------------------------------- test('endpoint-validator: rejects http://169.254.169.254/ link-local (cloud metadata, CWE-918)', () => { const r = validateOtlpEndpoint('http://169.254.169.254/v1/metrics', { env: {} }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'ENDPOINT_LINK_LOCAL_REJECTED')); }); test('endpoint-validator: rejects http://example.com/ (requires https)', () => { const r = validateOtlpEndpoint('http://example.com/v1/metrics', { env: {} }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HTTPS_REQUIRED')); }); test('endpoint-validator: rejects http://localhost without VOYAGE_OTEL_ALLOW_PRIVATE', () => { const r = validateOtlpEndpoint('http://localhost:4318/v1/metrics', { env: {} }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'ENDPOINT_LOOPBACK_REJECTED')); }); test('endpoint-validator: accepts http://localhost when VOYAGE_OTEL_ALLOW_PRIVATE=1 (home-lab opt-in)', () => { const r = validateOtlpEndpoint('http://localhost:4318/v1/metrics', { env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.equal(r.parsed.isPrivate, true); }); test('endpoint-validator: accepts https://example.com/v1/metrics (public)', () => { const r = validateOtlpEndpoint('https://otel.example.com/v1/metrics', { env: {} }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.equal(r.parsed.isPrivate, false); }); test('endpoint-validator: rejects RFC-1918 192.168.1.1 without opt-in', () => { const r = validateOtlpEndpoint('http://192.168.1.1:4318/v1/metrics', { env: {} }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'ENDPOINT_RFC1918_REJECTED')); }); test('endpoint-validator: rejects empty / non-string', () => { assert.equal(validateOtlpEndpoint('').valid, false); assert.equal(validateOtlpEndpoint(null).valid, false); assert.equal(validateOtlpEndpoint(undefined).valid, false); }); // ---- field-allowlist: CWE-212 mitigation ----------------------------------- test('field-allowlist: post-bash-stats DROPS command_excerpt + session_id (CWE-212)', () => { const record = { ts: '2026-05-09T08:00:00.000Z', session_id: 'uuid-12345', command_excerpt: 'git clone https://example.com/secret/repo', duration_ms: 152, success: true, }; const out = applyFieldAllowlist(record, 'post-bash-stats'); assert.equal('command_excerpt' in out, false, 'command_excerpt MUST be stripped'); assert.equal('session_id' in out, false, 'session_id MUST be stripped'); assert.equal(out.duration_ms, 152); assert.equal(out.success, true); assert.equal(out._schema_id, 'post-bash-stats'); }); test('field-allowlist: trekplan DROPS task / project_dir / brief_path (PII)', () => { const record = { ts: '2026-05-09T08:00:00.000Z', task: 'private user prose with PII', slug: 'add-auth', project_dir: '/home/user/secret/project', brief_path: '/home/user/secret/brief.md', codebase_files: 156, profile: 'premium', }; const out = applyFieldAllowlist(record, 'trekplan'); assert.equal('task' in out, false); assert.equal('project_dir' in out, false); assert.equal('brief_path' in out, false); assert.equal(out.slug, 'add-auth'); assert.equal(out.codebase_files, 156); assert.equal(out.profile, 'premium'); }); test('field-allowlist: event-emit applies sub-allowlist to payload', () => { const record = { ts: '2026-05-09T08:00:00.000Z', event: 'main-merge-gate', known_event: true, payload: { profile: 'balanced', profile_source: 'inheritance', command_excerpt: 'should be stripped from payload', raw_user_prose: 'should be stripped', }, }; const out = applyFieldAllowlist(record, 'event-emit'); assert.equal(out.event, 'main-merge-gate'); assert.equal(out.payload.profile, 'balanced'); assert.equal(out.payload.profile_source, 'inheritance'); assert.equal('command_excerpt' in out.payload, false); assert.equal('raw_user_prose' in out.payload, false); }); test('field-allowlist: unknown schema-type returns conservative {ts, _schema_id} only', () => { const out = applyFieldAllowlist( { ts: '2026-05-09T08:00:00.000Z', sensitive: 'secret' }, 'totally-unknown-schema', ); assert.equal('sensitive' in out, false); assert.equal(out.ts, '2026-05-09T08:00:00.000Z'); assert.equal(out._schema_id, 'totally-unknown-schema'); }); test('field-allowlist: Object.freeze on allowlists (drift-pin)', () => { assert.equal(Object.isFrozen(POST_BASH_STATS_ALLOWED), true, 'POST_BASH_STATS_ALLOWED must be frozen — runtime mutation prevention'); assert.equal(Object.isFrozen(EVENT_EMIT_PAYLOAD_ALLOWED), true); }); test('field-allowlist: null/undefined record handled safely', () => { assert.deepEqual(applyFieldAllowlist(null, 'trekplan'), {}); assert.deepEqual(applyFieldAllowlist(undefined, 'trekplan'), {}); });