feat(llm-security)!: v8 Phase 3 step 1 - remove the deprecated mode env-vars
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
This commit is contained in:
parent
7336250c60
commit
b6af9b46df
10 changed files with 430 additions and 299 deletions
|
|
@ -1,45 +1,73 @@
|
|||
// audit-trail.test.mjs — Tests for structured JSONL audit trail
|
||||
// v8.0.0: configured via policy.json `audit.log_path`, not LLM_SECURITY_AUDIT_LOG.
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
|
||||
import { writeFileSync, readFileSync, unlinkSync, existsSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { writeAuditEvent, isAuditEnabled, _resetForTest } from '../../scanners/lib/audit-trail.mjs';
|
||||
import { _resetCacheForTest } from '../../scanners/lib/policy-loader.mjs';
|
||||
|
||||
const TEST_LOG = join(tmpdir(), `llm-security-audit-test-${Date.now()}.jsonl`);
|
||||
|
||||
// v8.0.0: the audit-log path comes from the policy.json key `audit.log_path`
|
||||
// (LLM_SECURITY_AUDIT_LOG was removed). audit-trail runs in-process, so the
|
||||
// policy is supplied through a throwaway CLAUDE_PROJECT_ROOT.
|
||||
let projectRoot = null;
|
||||
|
||||
/** Point audit.log_path at `logPath` for the next writeAuditEvent/isAuditEnabled. */
|
||||
function enableAudit(logPath) {
|
||||
writeFileSync(
|
||||
join(projectRoot, '.llm-security', 'policy.json'),
|
||||
JSON.stringify({ audit: { log_path: logPath } })
|
||||
);
|
||||
_resetCacheForTest();
|
||||
_resetForTest();
|
||||
}
|
||||
|
||||
/** Leave audit.log_path unset (the default). */
|
||||
function disableAudit() {
|
||||
writeFileSync(join(projectRoot, '.llm-security', 'policy.json'), JSON.stringify({}));
|
||||
_resetCacheForTest();
|
||||
_resetForTest();
|
||||
}
|
||||
|
||||
describe('audit-trail', () => {
|
||||
beforeEach(() => {
|
||||
_resetForTest();
|
||||
projectRoot = mkdtempSync(join(tmpdir(), 'llmsec-audit-root-'));
|
||||
mkdirSync(join(projectRoot, '.llm-security'), { recursive: true });
|
||||
process.env.CLAUDE_PROJECT_ROOT = projectRoot;
|
||||
disableAudit();
|
||||
// Clean up test file
|
||||
try { unlinkSync(TEST_LOG); } catch {}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetForTest();
|
||||
delete process.env.LLM_SECURITY_AUDIT_LOG;
|
||||
_resetCacheForTest();
|
||||
delete process.env.CLAUDE_PROJECT_ROOT;
|
||||
if (projectRoot) rmSync(projectRoot, { recursive: true, force: true });
|
||||
projectRoot = null;
|
||||
try { unlinkSync(TEST_LOG); } catch {}
|
||||
});
|
||||
|
||||
it('is disabled when env var not set', () => {
|
||||
delete process.env.LLM_SECURITY_AUDIT_LOG;
|
||||
it('is disabled when audit.log_path is not set', () => {
|
||||
assert.equal(isAuditEnabled(), false);
|
||||
});
|
||||
|
||||
it('is enabled when env var is set to writable path', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG;
|
||||
it('is enabled when audit.log_path is a writable path', () => {
|
||||
enableAudit(TEST_LOG);
|
||||
assert.equal(isAuditEnabled(), true);
|
||||
});
|
||||
|
||||
it('no-op when env var not set', () => {
|
||||
delete process.env.LLM_SECURITY_AUDIT_LOG;
|
||||
it('no-op when audit.log_path is not set', () => {
|
||||
writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test' });
|
||||
assert.equal(existsSync(TEST_LOG), false);
|
||||
});
|
||||
|
||||
it('writes valid JSONL when enabled', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG;
|
||||
enableAudit(TEST_LOG);
|
||||
writeAuditEvent({
|
||||
event_type: 'trifecta_warning',
|
||||
severity: 'high',
|
||||
|
|
@ -62,7 +90,7 @@ describe('audit-trail', () => {
|
|||
});
|
||||
|
||||
it('appends multiple events as separate lines', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG;
|
||||
enableAudit(TEST_LOG);
|
||||
writeAuditEvent({ event_type: 'event1', severity: 'info', source: 'test' });
|
||||
writeAuditEvent({ event_type: 'event2', severity: 'medium', source: 'test' });
|
||||
writeAuditEvent({ event_type: 'event3', severity: 'high', source: 'test' });
|
||||
|
|
@ -77,7 +105,7 @@ describe('audit-trail', () => {
|
|||
});
|
||||
|
||||
it('events contain all required fields', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG;
|
||||
enableAudit(TEST_LOG);
|
||||
writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test-hook' });
|
||||
|
||||
const entry = JSON.parse(readFileSync(TEST_LOG, 'utf8').trim());
|
||||
|
|
@ -88,7 +116,7 @@ describe('audit-trail', () => {
|
|||
});
|
||||
|
||||
it('provides defaults for optional fields', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = TEST_LOG;
|
||||
enableAudit(TEST_LOG);
|
||||
writeAuditEvent({ event_type: 'minimal' });
|
||||
|
||||
const entry = JSON.parse(readFileSync(TEST_LOG, 'utf8').trim());
|
||||
|
|
@ -100,7 +128,7 @@ describe('audit-trail', () => {
|
|||
});
|
||||
|
||||
it('does not crash on invalid path', () => {
|
||||
process.env.LLM_SECURITY_AUDIT_LOG = '/nonexistent/dir/audit.jsonl';
|
||||
enableAudit('/nonexistent/dir/audit.jsonl');
|
||||
// Should not throw — gracefully logs to stderr
|
||||
assert.doesNotThrow(() => {
|
||||
writeAuditEvent({ event_type: 'test', severity: 'info', source: 'test' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue