BREAKING CHANGE: the four LLM_SECURITY_* configuration env-vars deprecated in v7.3.0 are removed. .llm-security/policy.json is now the only source: LLM_SECURITY_INJECTION_MODE -> injection.mode LLM_SECURITY_TRIFECTA_MODE -> trifecta.mode LLM_SECURITY_ESCALATION_WINDOW -> trifecta.escalation_window LLM_SECURITY_AUDIT_LOG -> audit.log_path LLM_SECURITY_DEPRECATION_QUIET -> dies with the mechanism it silenced Setting a removed var is now inert - it does not warn, and it does not configure. Env-vars with no policy equivalent (PRECOMPACT_MODE, PRECOMPACT_MAX_BYTES, UPDATE_CHECK, MCP_CACHE_FILE, IDE_ROOTS) are unaffected. getPolicyValueWithEnvWarn and its one-shot stderr warning are deleted from policy-loader.mjs, along with the module-scoped warned-var Set. The four call sites collapse to getPolicyValue. getPolicyValue's JSDoc claimed "environment variables ALWAYS take precedence" - it never read env itself, so that line described the shim, and it is corrected rather than deleted. User-facing hook strings that advertised a removed var as the escape hatch now name the policy key instead: the inject-scan block reason, its warn-mode note, both escalation-window advisories, and the trifecta block message. A blocked user following the old text would have set a var that does nothing. Tests. tests/lib/v8-env-removal.test.mjs is the regression gate and was written failing first (8 of 12 red before the change). It pins the NEGATIVE - setting a removed var does not alter the outcome - because that is the half that rots silently: a re-introduced process.env read would leave every migrated positive test green, since those configure through policy.json and never set the var at all. One assertion walks hooks/scripts and scanners for `process.env.<removed>` so the re-introduction is caught structurally, not only behaviourally. The 44 env-driven test occurrences (18 inject-scan, 13+4 session-guard, 9 audit-trail) migrate to a throwaway .llm-security/policy.json via a new runHookWithPolicy helper in hook-helper.mjs; audit-trail runs in-process, so it supplies the same policy through CLAUDE_PROJECT_ROOT. The D3 mechanism tests in policy-loader.test.mjs are deleted with the mechanism. Suite 2039 tests, 2037 pass (+12 gate, -7 D3 mechanism). The 2 failures are the known parallel-load timing flakes (pre-compact-scan size-cap, pre-install-supply-chain F-3); both green when run isolated. Remaining in Phase 3: posture-scanner TRIFECTA_MODE heuristic, riskScoreV1 removal, ghost-var cleanup, docs + migration note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BB4vXvwvtW4dxbPRd6vsez
86 lines
2.7 KiB
JavaScript
86 lines
2.7 KiB
JavaScript
// audit-trail.mjs — Structured JSONL audit trail writer
|
|
// Resolves the audit-log path from the policy.json key `audit.log_path`.
|
|
// v8.0.0: the LLM_SECURITY_AUDIT_LOG env-var was removed after its v7.3.0
|
|
// deprecation runway; policy.json is now the only source.
|
|
// No-op when policy provides no path. Zero external dependencies.
|
|
|
|
import { appendFileSync, writeFileSync, accessSync, constants } from 'node:fs';
|
|
import { dirname } from 'node:path';
|
|
import { getPolicyValue } 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;
|
|
|
|
const resolved = getPolicyValue('audit', 'log_path', 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 the policy.json key audit.log_path 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;
|
|
}
|