// audit-trail.test.mjs — Tests for structured JSONL audit trail import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { writeAuditEvent, isAuditEnabled, _resetForTest } from '../../scanners/lib/audit-trail.mjs'; const TEST_LOG = join(tmpdir(), `llm-security-audit-test-${Date.now()}.jsonl`); describe('audit-trail', () => { beforeEach(() => { _resetForTest(); // Clean up test file try { unlinkSync(TEST_LOG); } catch {} }); afterEach(() => { _resetForTest(); delete process.env.LLM_SECURITY_AUDIT_LOG; try { unlinkSync(TEST_LOG); } catch {} }); it('is disabled when env var not set', () => { delete process.env.LLM_SECURITY_AUDIT_LOG; assert.equal(isAuditEnabled(), false); }); it('is enabled when env var is set to writable path', () => { process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG; assert.equal(isAuditEnabled(), true); }); it('no-op when env var not set', () => { delete process.env.LLM_SECURITY_AUDIT_LOG; writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test' }); assert.equal(existsSync(TEST_LOG), false); }); it('writes valid JSONL when enabled', () => { process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG; writeAuditEvent({ event_type: 'trifecta_warning', severity: 'high', source: 'post-session-guard', details: { window_size: 20 }, owasp: ['ASI01', 'ASI02'], action_taken: 'warned', }); const content = readFileSync(TEST_LOG, 'utf8').trim(); const entry = JSON.parse(content); assert.equal(entry.event_type, 'trifecta_warning'); assert.equal(entry.severity, 'high'); assert.equal(entry.source, 'post-session-guard'); assert.deepEqual(entry.owasp, ['ASI01', 'ASI02']); assert.equal(entry.action_taken, 'warned'); assert.ok(entry.timestamp.match(/^\d{4}-\d{2}-\d{2}T/), 'Expected ISO timestamp'); assert.ok(entry.session_id, 'Expected session_id'); }); it('appends multiple events as separate lines', () => { process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG; writeAuditEvent({ event_type: 'event1', severity: 'info', source: 'test' }); writeAuditEvent({ event_type: 'event2', severity: 'medium', source: 'test' }); writeAuditEvent({ event_type: 'event3', severity: 'high', source: 'test' }); const lines = readFileSync(TEST_LOG, 'utf8').trim().split('\n'); assert.equal(lines.length, 3); const e1 = JSON.parse(lines[0]); const e3 = JSON.parse(lines[2]); assert.equal(e1.event_type, 'event1'); assert.equal(e3.event_type, 'event3'); }); it('events contain all required fields', () => { process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG; writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test-hook' }); const entry = JSON.parse(readFileSync(TEST_LOG, 'utf8').trim()); const required = ['timestamp', 'session_id', 'event_type', 'severity', 'source', 'details', 'owasp', 'action_taken']; for (const field of required) { assert.ok(field in entry, `Missing required field: ${field}`); } }); it('provides defaults for optional fields', () => { process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG; writeAuditEvent({ event_type: 'minimal' }); const entry = JSON.parse(readFileSync(TEST_LOG, 'utf8').trim()); assert.equal(entry.severity, 'info'); assert.equal(entry.source, 'unknown'); assert.deepEqual(entry.details, {}); assert.deepEqual(entry.owasp, []); assert.equal(entry.action_taken, 'warned'); }); it('does not crash on invalid path', () => { process.env.LLM_SECURITY_AUDIT_LOG = '/nonexistent/dir/audit.jsonl'; // Should not throw — gracefully logs to stderr assert.doesNotThrow(() => { writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test' }); }); }); });