89 lines
2.8 KiB
JavaScript
89 lines
2.8 KiB
JavaScript
// 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;
|
|
}
|