// tests/kb-update/test-scan-adversarial-content.test.mjs // Layer B create-guard CLI (G6 §8 / R6 punkt d): scripts/kb-update/scan-adversarial-content.mjs // is the sibling to validate-kb-file.mjs — the gate any generator (kb-update, generate-skills) // calls over candidate files. A file that BLOCKs is never written / never committed; a file // that WARNs is flagged for the operator (never auto-committed). Exit: 0 clean, 1 block, 2 warn. // // scanPaths is dependency-injected (readFile + detect), so these tests feed synthetic content // and a fake detector — they never touch disk nor llm-security. FAIL-CLOSED behaviour (a read // error or an unavailable detector BLOCKS, never silently passes) is asserted here because it // is the whole point of a security gate. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { scanPaths } from '../../scripts/kb-update/scan-adversarial-content.mjs'; const CLEAN = '# T\n\n**Source:** https://learn.microsoft.com/azure/x\n\nProse.\n'; function reader(map) { return (p) => { if (!(p in map)) throw new Error(`ENOENT: ${p}`); return map[p]; }; } // Fake detectors keyed by returning canned raw findings regardless of content. const detectClean = async () => []; const detectBlock = async () => [{ class: 'injection', severity: 'critical', line: 5, evidence: 'spoofed tag: ' }]; const detectWarn = async () => [{ class: 'injection', severity: 'high', line: 5, evidence: 'imperative in prose' }]; test('scanPaths — all clean → ok:true, no block, no warn', async () => { const r = await scanPaths(['a.md', 'b.md'], { readFile: reader({ 'a.md': CLEAN, 'b.md': CLEAN }), detect: detectClean }); assert.equal(r.ok, true); assert.equal(r.blocked, false); assert.equal(r.warned, false); assert.equal(r.results.length, 2); assert.ok(r.results.every((x) => x.disposition === 'clean')); }); test('scanPaths — a blocked file fails the gate (ok:false, blocked:true)', async () => { const r = await scanPaths(['bad.md'], { readFile: reader({ 'bad.md': CLEAN }), detect: detectBlock }); assert.equal(r.ok, false); assert.equal(r.blocked, true); assert.equal(r.results[0].disposition, 'block'); }); test('scanPaths — a warn-only file is flagged, not clean (warned:true, blocked:false)', async () => { const r = await scanPaths(['warn.md'], { readFile: reader({ 'warn.md': CLEAN }), detect: detectWarn }); assert.equal(r.ok, false); assert.equal(r.blocked, false); assert.equal(r.warned, true); assert.equal(r.results[0].disposition, 'warn'); }); test('scanPaths — one blocked file in a batch fails the whole gate', async () => { const detect = async (content) => (content === 'POISON' ? detectBlock() : []); const r = await scanPaths(['ok.md', 'bad.md'], { readFile: reader({ 'ok.md': CLEAN, 'bad.md': 'POISON' }), detect }); assert.equal(r.blocked, true); assert.equal(r.results.find((x) => x.path === 'ok.md').disposition, 'clean'); assert.equal(r.results.find((x) => x.path === 'bad.md').disposition, 'block'); }); test('scanPaths — an unreadable path FAILS CLOSED (blocked), not thrown', async () => { const r = await scanPaths(['missing.md'], { readFile: () => { throw new Error('ENOENT'); }, detect: detectClean }); assert.equal(r.blocked, true); assert.equal(r.results[0].disposition, 'block'); assert.ok(r.results[0].findings.some((f) => /read|enoent/i.test(f.evidence || ''))); }); test('scanPaths — a detector that throws (llm-security unavailable) FAILS CLOSED (blocked)', async () => { const r = await scanPaths(['x.md'], { readFile: reader({ 'x.md': CLEAN }), detect: async () => { throw new Error('LLM_SECURITY_UNAVAILABLE'); }, }); assert.equal(r.blocked, true); assert.equal(r.results[0].disposition, 'block'); assert.ok(r.results[0].findings.some((f) => /llm_security|scanner/i.test(f.evidence || ''))); }); test('scanPaths — no paths → ok:true (nothing to gate)', async () => { const r = await scanPaths([], { readFile: reader({}), detect: detectClean }); assert.equal(r.ok, true); assert.deepEqual(r.results, []); });