// posture-trifecta-mode.test.mjs — B11: posture category 12 (Rule of Two) // recognises a policy-era session guard. // // Pre-v8 the check was `/TRIFECTA_MODE/i` over the hook source. That matched // the *identifier* `TRIFECTA_MODE`, not the configuration mechanism, so a hook // that resolves the mode from `.llm-security/policy.json` without naming a // constant that way scored PARTIAL despite being strictly more configurable. // With LLM_SECURITY_TRIFECTA_MODE removed in v8.0.0, that regex would push // every correctly-migrated project off PASS. import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, cpSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { scan } from '../../scanners/posture-scanner.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const GRADE_A_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-a-project'); const GUARD_REL = join('hooks', 'scripts', 'post-session-guard.mjs'); // A guard that is configurable, but names nothing `TRIFECTA_MODE`. const POLICY_ERA_GUARD = `#!/usr/bin/env node // post-session-guard.mjs — Runtime trifecta detection (Rule of Two) // v8.0.0: enforcement mode comes from .llm-security/policy.json. import { readFileSync } from 'node:fs'; import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs'; const mode = String(getPolicyValue('trifecta', 'mode', 'warn')).toLowerCase(); const input = JSON.parse(readFileSync('/dev/stdin', 'utf-8')); if (mode === 'off') { process.stdout.write(JSON.stringify({ decision: 'allow' })); process.exit(0); } const LONG_HORIZON_WINDOW = 100; process.stdout.write(JSON.stringify({ decision: 'allow', window: LONG_HORIZON_WINDOW })); `; // A guard with no configurable mode at all — hardcoded enforcement. const UNCONFIGURABLE_GUARD = `#!/usr/bin/env node // post-session-guard.mjs — Runtime trifecta detection, no configuration. import { readFileSync } from 'node:fs'; const input = JSON.parse(readFileSync('/dev/stdin', 'utf-8')); const LONG_HORIZON_WINDOW = 100; process.stdout.write(JSON.stringify({ decision: 'allow', window: LONG_HORIZON_WINDOW })); `; let root = null; /** Copy of grade-a-project whose session guard is replaced with `source`. */ function projectWithGuard(source) { root = mkdtempSync(join(tmpdir(), 'llmsec-posture-trifecta-')); cpSync(GRADE_A_FIXTURE, root, { recursive: true }); mkdirSync(join(root, 'hooks', 'scripts'), { recursive: true }); writeFileSync(join(root, GUARD_REL), source); return root; } function ruleOfTwo(result) { return result.categories.find((c) => c.id === 12); } /** Category findings live on the top-level `findings` array, not on the category. */ function ruleOfTwoFindings(result) { return result.findings.filter((f) => /trifecta mode|Rule of Two/i.test(f.title)); } describe('posture category 12 — Rule of Two mode detection (B11)', () => { afterEach(() => { if (root) rmSync(root, { recursive: true, force: true }); root = null; }); beforeEach(() => { resetCounter(); }); it('PASSes a policy-era guard that reads trifecta.mode from policy.json', async () => { const result = await scan(projectWithGuard(POLICY_ERA_GUARD)); const cat = ruleOfTwo(result); assert.equal( cat.status, 'PASS', 'a guard configured through policy.json is configurable; the identifier name is not the mechanism' ); assert.ok( cat.evidence.some((e) => /trifecta\.mode|policy\.json/i.test(e)), `expected evidence naming the policy key, got: ${JSON.stringify(cat.evidence)}` ); }); it('PASSes a legacy guard still using the pre-v8 env-var', async () => { // Third-party projects vendor their own hook copy and may not have migrated. // Their enforcement mode is still configurable, so the category still holds. const legacy = POLICY_ERA_GUARD.replace( "String(getPolicyValue('trifecta', 'mode', 'warn'))", "String(process.env.LLM_SECURITY_TRIFECTA_MODE || 'warn')" ); const result = await scan(projectWithGuard(legacy)); assert.equal(ruleOfTwo(result).status, 'PASS'); }); it('does not PASS a guard with no configurable mode', async () => { const result = await scan(projectWithGuard(UNCONFIGURABLE_GUARD)); const cat = ruleOfTwo(result); assert.equal(cat.status, 'PARTIAL', 'hardcoded enforcement is not a configurable mode'); assert.ok( ruleOfTwoFindings(result).some((f) => /configurable trifecta mode/i.test(f.title)), 'expected the PARTIAL finding to name the missing configurability' ); }); it('the PARTIAL finding recommends the policy key, not a removed env-var', async () => { const result = await scan(projectWithGuard(UNCONFIGURABLE_GUARD)); const f = ruleOfTwoFindings(result).find((x) => /configurable trifecta mode/i.test(x.title)); assert.ok(f, 'expected the PARTIAL finding'); const text = `${f.description} ${f.recommendation}`; assert.doesNotMatch( text, /LLM_SECURITY_TRIFECTA_MODE/, 'must not tell the user to set an env-var that v8 ignores' ); assert.match(text, /trifecta\.mode/, 'should name the policy key'); }); });