feat(governance): add structured JSONL audit trail with SIEM-ready schema
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>
This commit is contained in:
parent
2116e702df
commit
269c14445c
3 changed files with 241 additions and 0 deletions
83
plugins/llm-security/scanners/lib/audit-trail.mjs
Normal file
83
plugins/llm-security/scanners/lib/audit-trail.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// audit-trail.mjs — Structured JSONL audit trail writer
|
||||
// Writes SIEM-ready events to the path specified by LLM_SECURITY_AUDIT_LOG.
|
||||
// No-op when env var is not set. Zero external dependencies.
|
||||
|
||||
import { appendFileSync, writeFileSync, accessSync, constants } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
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;
|
||||
|
||||
const envPath = process.env.LLM_SECURITY_AUDIT_LOG;
|
||||
if (!envPath) return false;
|
||||
|
||||
try {
|
||||
// Ensure parent directory exists and is writable
|
||||
const dir = dirname(envPath);
|
||||
accessSync(dir, constants.W_OK);
|
||||
// Touch file if it doesn't exist
|
||||
try { accessSync(envPath); } catch { writeFileSync(envPath, ''); }
|
||||
auditPath = envPath;
|
||||
return true;
|
||||
} catch (err) {
|
||||
process.stderr.write(`[llm-security] Audit trail path not writable: ${envPath} (${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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue