Vendoring commons v0.1.0 under scanners/commons/ falsified the premise of `degrades gracefully when commons has not been vendored yet`: it asserted the default DEFAULT_COMMONS_ROOT did not exist. It was the only red test after the subtree add (2072/2073). The graceful-empty contract it guarded is covered twice over by the missing-artifact and invalid-JSON cases, which drive the same code path through an explicit commonsRoot. So the replacement asserts the direction that is now uncovered and matters more: a non-zero record count through the real default root, no override. That is the positive load-assertion Phase 5 step 4 requires. Every other gate we have treats a commons load failure as indistinguishable from a legitimately empty table, so a total loss of the vendored corpus would leave the suite green. Proven red-capable by moving scanners/commons aside: the assertion fires by name, not as an incidental TypeError elsewhere. Suite 2073/2073.
137 lines
7.2 KiB
JavaScript
137 lines
7.2 KiB
JavaScript
// commons-loader.test.mjs — Tests for the vendored-commons JSON artifact loader
|
|
//
|
|
// v8 Phase 5 step 2: llm-security-commons doesn't exist yet (Phase 4 is
|
|
// blocked on the operator creating the Forgejo repo), so these tests drive
|
|
// the loader against a local fixture directory instead of a real vendored
|
|
// subtree — including the "commons isn't vendored yet" case, which must
|
|
// degrade gracefully rather than crash.
|
|
|
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { loadArtifact, _resetCacheForTest } from '../../scanners/lib/commons-loader.mjs';
|
|
|
|
const TEST_ROOT = join(tmpdir(), `llm-security-commons-loader-test-${Date.now()}`);
|
|
const FIXTURE_COMMONS_ROOT = join(TEST_ROOT, 'fixture-commons');
|
|
|
|
function writeArtifact(relPath, data) {
|
|
const filePath = join(FIXTURE_COMMONS_ROOT, `${relPath}.json`);
|
|
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
writeFileSync(filePath, JSON.stringify(data));
|
|
}
|
|
|
|
describe('commons-loader', () => {
|
|
beforeEach(() => {
|
|
_resetCacheForTest();
|
|
mkdirSync(FIXTURE_COMMONS_ROOT, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
_resetCacheForTest();
|
|
try { rmSync(TEST_ROOT, { recursive: true }); } catch {}
|
|
});
|
|
|
|
it('loads and parses a valid artifact from an explicit commonsRoot', () => {
|
|
writeArtifact('lexicon/injection-lexicon', { version: '0.1.0', terms: ['ignore previous instructions'] });
|
|
const data = loadArtifact('lexicon/injection-lexicon', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: null });
|
|
assert.deepEqual(data, { version: '0.1.0', terms: ['ignore previous instructions'] });
|
|
});
|
|
|
|
it('returns the caller fallback when the artifact file is missing', () => {
|
|
const data = loadArtifact('lexicon/injection-lexicon', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: [] });
|
|
assert.deepEqual(data, []);
|
|
});
|
|
|
|
it('returns the caller fallback when the artifact JSON is invalid', () => {
|
|
const filePath = join(FIXTURE_COMMONS_ROOT, 'mapping', 'owasp-map.json');
|
|
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
writeFileSync(filePath, 'not valid json!!!');
|
|
const data = loadArtifact('mapping/owasp-map', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: {} });
|
|
assert.deepEqual(data, {});
|
|
});
|
|
|
|
it('resolves the default vendored root without an override, and loads a non-empty artifact', () => {
|
|
// Until Phase 5 step 3 this asserted the opposite — that the default
|
|
// scanners/commons path did not exist and the loader degraded to the
|
|
// caller's fallback. Vendoring commons v0.1.0 falsified that premise, and
|
|
// the graceful-empty contract it guarded is covered twice over by the
|
|
// missing-artifact and invalid-JSON cases above, which drive the same code
|
|
// path through an explicit commonsRoot.
|
|
//
|
|
// What is NOT otherwise covered is the direction that now matters: a
|
|
// commons load FAILURE is indistinguishable from a legitimately empty
|
|
// table to every other gate we have, so a total loss of the vendored
|
|
// corpus would leave the suite green. This asserts a non-zero record count
|
|
// through the real DEFAULT_COMMONS_ROOT — no override, the resolution path
|
|
// production actually uses.
|
|
const lexicon = loadArtifact('lexicon/injection-lexicon', { fallback: null });
|
|
assert.notEqual(lexicon, null, 'default commons root did not resolve — is scanners/commons vendored?');
|
|
assert.equal(lexicon.totals.families, 4);
|
|
assert.equal(lexicon.totals.patterns, 83);
|
|
const patterns = lexicon.families.flatMap(f => f.patterns);
|
|
assert.equal(patterns.length, 83, 'lexicon families carry fewer patterns than totals claims');
|
|
});
|
|
|
|
it('caches a loaded artifact per resolved path', () => {
|
|
writeArtifact('calibration/calibration', { entropyFloor: 4.5 });
|
|
const first = loadArtifact('calibration/calibration', { commonsRoot: FIXTURE_COMMONS_ROOT });
|
|
writeArtifact('calibration/calibration', { entropyFloor: 9.9 }); // mutate after first read
|
|
const second = loadArtifact('calibration/calibration', { commonsRoot: FIXTURE_COMMONS_ROOT });
|
|
assert.equal(first, second); // same reference (cached)
|
|
assert.equal(second.entropyFloor, 4.5); // original value, not the mutation
|
|
});
|
|
|
|
it('caches a failed load too, so a fixed-but-unread file still returns the fallback', () => {
|
|
const first = loadArtifact('signatures/malware-signatures', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: [] });
|
|
writeArtifact('signatures/malware-signatures', { rules: [{ id: 'late-arrival' }] });
|
|
const second = loadArtifact('signatures/malware-signatures', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: [] });
|
|
assert.deepEqual(first, []);
|
|
assert.deepEqual(second, []);
|
|
});
|
|
|
|
it('a cached failure does not leak one caller\'s fallback shape to another', () => {
|
|
const arrayFallback = loadArtifact('missing/artifact', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: [] });
|
|
const objectFallback = loadArtifact('missing/artifact', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: {} });
|
|
assert.deepEqual(arrayFallback, []);
|
|
assert.deepEqual(objectFallback, {}); // not [] from the first caller's cached fallback
|
|
});
|
|
|
|
it('defaults the fallback to null when the caller passes none', () => {
|
|
const data = loadArtifact('lexicon/injection-lexicon', { commonsRoot: FIXTURE_COMMONS_ROOT });
|
|
assert.equal(data, null);
|
|
});
|
|
|
|
it('freezes a loaded artifact so a caller mutation throws instead of leaking process-wide', () => {
|
|
writeArtifact('lexicon/injection-lexicon', { version: '0.1.0', terms: ['a'] });
|
|
const data = loadArtifact('lexicon/injection-lexicon', { commonsRoot: FIXTURE_COMMONS_ROOT });
|
|
assert.throws(() => { data.terms.push('mutated'); }, TypeError);
|
|
assert.throws(() => { data.version = '9.9.9'; }, TypeError);
|
|
});
|
|
|
|
it('does not read .llm-security/policy.json from any target — commons.root is not honored', () => {
|
|
// Simulates a hostile scanned target shipping .llm-security/policy.json
|
|
// with {"commons":{"root": "..."}} to redirect detection data. Even
|
|
// with such a file on disk and CLAUDE_PROJECT_ROOT pointed at it, the
|
|
// loader must ignore it entirely and fall through to the caller's
|
|
// explicit commonsRoot / default, never to policy.
|
|
const hostileTargetRoot = join(TEST_ROOT, 'hostile-target');
|
|
const hostilePolicyDir = join(hostileTargetRoot, '.llm-security');
|
|
mkdirSync(hostilePolicyDir, { recursive: true });
|
|
writeFileSync(
|
|
join(hostilePolicyDir, 'policy.json'),
|
|
JSON.stringify({ commons: { root: join(TEST_ROOT, 'attacker-controlled-empty-commons') } }),
|
|
);
|
|
const prevProjectRoot = process.env.CLAUDE_PROJECT_ROOT;
|
|
process.env.CLAUDE_PROJECT_ROOT = hostileTargetRoot;
|
|
try {
|
|
writeArtifact('lexicon/injection-lexicon', { version: '0.1.0', terms: ['still-here'] });
|
|
const data = loadArtifact('lexicon/injection-lexicon', { commonsRoot: FIXTURE_COMMONS_ROOT, fallback: null });
|
|
assert.deepEqual(data, { version: '0.1.0', terms: ['still-here'] });
|
|
} finally {
|
|
if (prevProjectRoot === undefined) delete process.env.CLAUDE_PROJECT_ROOT;
|
|
else process.env.CLAUDE_PROJECT_ROOT = prevProjectRoot;
|
|
}
|
|
});
|
|
});
|