New audit-trail.mjs writes structured events to LLM_SECURITY_AUDIT_LOG path. Integrated into post-session-guard at 6 warning emission points: trifecta, escalation-after-input, data flow, volume threshold, slow-burn, behavioral drift. No-op when env var not set — zero overhead for existing users. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.9 KiB
JavaScript
109 lines
3.9 KiB
JavaScript
// 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' });
|
|
});
|
|
});
|
|
});
|