ktg-plugin-marketplace/plugins/llm-security/scanners/lib/audit-trail.mjs

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;
}