// audit-trail.mjs — Structured JSONL audit trail writer // Resolves the audit-log path via getPolicyValueWithEnvWarn so the env-var // LLM_SECURITY_AUDIT_LOG and policy.json key audit.log_path stay in sync, // with a one-time deprecation warning when both are explicitly set. // No-op when neither env nor policy provides a path. Zero external dependencies. import { appendFileSync, writeFileSync, accessSync, constants } from 'node:fs'; import { dirname } from 'node:path'; import { getPolicyValueWithEnvWarn } from './policy-loader.mjs'; let auditPath = null; let initialized = false; /** * Initialize audit trail. Validates the path is writable on first call. * @returns {boolean} true if audit trail is enabled and writable */ function initAuditTrail() { if (initialized) return auditPath !== null; initialized = true; // D3 (v7.3.0): env still wins, deprecation warning when policy also set. const resolved = getPolicyValueWithEnvWarn( 'audit', 'log_path', 'LLM_SECURITY_AUDIT_LOG', null ); if (!resolved) return false; try { // Ensure parent directory exists and is writable const dir = dirname(resolved); accessSync(dir, constants.W_OK); // Touch file if it doesn't exist try { accessSync(resolved); } catch { writeFileSync(resolved, ''); } auditPath = resolved; return true; } catch (err) { process.stderr.write(`[llm-security] Audit trail path not writable: ${resolved} (${err.message})\n`); return false; } } /** * Write a structured audit event as one JSON line. * No-op when LLM_SECURITY_AUDIT_LOG is not set. * * @param {object} event * @param {string} event.event_type - e.g. trifecta_warning, injection_detected * @param {string} event.severity - critical|high|medium|low|info * @param {string} event.source - hook or scanner name * @param {object} [event.details] - event-specific payload * @param {string[]} [event.owasp] - OWASP categories * @param {string} [event.action_taken] - blocked|warned|allowed */ export function writeAuditEvent(event) { if (!initAuditTrail()) return; const entry = { timestamp: new Date().toISOString(), session_id: String(process.ppid || process.pid), event_type: event.event_type || 'unknown', severity: event.severity || 'info', source: event.source || 'unknown', details: event.details || {}, owasp: event.owasp || [], action_taken: event.action_taken || 'warned', }; try { appendFileSync(auditPath, JSON.stringify(entry) + '\n'); } catch (err) { process.stderr.write(`[llm-security] Audit trail write failed: ${err.message}\n`); } } /** * Check whether audit trail is enabled (for guard clauses in hooks). * @returns {boolean} */ export function isAuditEnabled() { return initAuditTrail(); } /** * Reset internal state (for testing only). */ export function _resetForTest() { auditPath = null; initialized = false; }