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:
Kjell Tore Guttormsen 2026-04-10 13:25:59 +02:00
commit 269c14445c
3 changed files with 241 additions and 0 deletions

View file

@ -42,6 +42,7 @@ import { tmpdir } from 'node:os';
import { createHash } from 'node:crypto';
import { extractMcpServer } from '../../scanners/lib/mcp-description-cache.mjs';
import { jensenShannonDivergence, buildDistribution } from '../../scanners/lib/distribution-stats.mjs';
import { writeAuditEvent } from '../../scanners/lib/audit-trail.mjs';
// ---------------------------------------------------------------------------
// Constants
@ -799,6 +800,14 @@ if (!(classes.length === 1 && (classes[0] === 'neutral' || classes[0] === 'deleg
const sensitiveExfil = checkSensitiveExfil(window);
messages.push(formatWarning(evidence, mcpInfo, sensitiveExfil));
appendEntry(stateFile, { type: 'warning', ts: Date.now() });
writeAuditEvent({
event_type: 'trifecta_warning',
severity: mcpInfo.concentrated || sensitiveExfil ? 'critical' : 'high',
source: 'post-session-guard',
details: { evidence, mcp_concentrated: mcpInfo.concentrated, sensitive_exfil: sensitiveExfil },
owasp: ['ASI01', 'ASI02', 'LLM01'],
action_taken: TRIFECTA_MODE === 'block' && (mcpInfo.concentrated || sensitiveExfil) ? 'blocked' : 'warned',
});
// --- Rule of Two: Block mode ---
// Block for high-confidence trifecta: MCP-concentrated OR sensitive path + exfil
@ -823,6 +832,14 @@ if (classes.includes('delegation')) {
if (escalation.detected && !hasEscalationWarning(window)) {
messages.push(formatEscalationWarning(detail, escalation.inputDetail));
appendEntry(stateFile, { type: 'escalation_warning', ts: Date.now() });
writeAuditEvent({
event_type: 'escalation_after_input',
severity: 'medium',
source: 'post-session-guard',
details: { tool: detail, input_source: escalation.inputDetail },
owasp: ['ASI01'],
action_taken: 'warned',
});
}
}
@ -840,6 +857,14 @@ if (!(classes.length === 1 && classes[0] === 'neutral')) {
if (detected) {
messages.push(formatDataFlowWarning(evidence, flowLink.sourceEntries));
appendEntry(stateFile, { type: 'data_flow_warning', ts: Date.now() });
writeAuditEvent({
event_type: 'data_flow_trifecta',
severity: 'high',
source: 'post-session-guard',
details: { evidence, flow_sources: flowLink.sourceEntries.length },
owasp: ['ASI01', 'ASI02'],
action_taken: 'warned',
});
}
}
}
@ -855,6 +880,14 @@ if (outputSize > 0) {
if (totalVolume >= bytes && !hasVolumeWarning(allEntries, bytes)) {
messages.push(formatVolumeWarning(totalVolume, label, severity));
appendEntry(stateFile, { type: 'volume_warning', ts: Date.now(), threshold: bytes });
writeAuditEvent({
event_type: 'volume_threshold',
severity: severity.toLowerCase(),
source: 'post-session-guard',
details: { total_bytes: totalVolume, threshold: label },
owasp: ['ASI02'],
action_taken: 'warned',
});
break; // only emit highest unwarned threshold
}
}
@ -869,6 +902,14 @@ if (outputSize > 0) {
if (slowBurn.detected && !hasSlowBurnWarning(longWindow)) {
messages.push(formatSlowBurnWarning(slowBurn.spread));
appendEntry(stateFile, { type: 'slow_burn_warning', ts: Date.now() });
writeAuditEvent({
event_type: 'slow_burn_trifecta',
severity: 'medium',
source: 'post-session-guard',
details: { spread: slowBurn.spread },
owasp: ['ASI06', 'ASI08'],
action_taken: 'warned',
});
}
// Behavioral drift: JSD on tool distribution (first vs last DRIFT_SAMPLE_SIZE)
@ -876,6 +917,14 @@ if (outputSize > 0) {
if (drift.drifted && !hasDriftWarning(longWindow)) {
messages.push(formatDriftWarning(drift.jsd, drift.firstTools, drift.lastTools));
appendEntry(stateFile, { type: 'drift_warning', ts: Date.now() });
writeAuditEvent({
event_type: 'behavioral_drift',
severity: 'medium',
source: 'post-session-guard',
details: { jsd: drift.jsd, first_tools: drift.firstTools, last_tools: drift.lastTools },
owasp: ['ASI06', 'ASI08'],
action_taken: 'warned',
});
}
}