fix(llm-security): SIG self-flagged the vendored commons it detects from

Scanning this repository with the SIG scanner produced 7 findings, 4 of them
on our own detection data: scanners/commons/CHANGELOG.md and
scanners/commons/signatures/malware-signatures.json. The ruleset that describes
xmrig and webshells is, byte for byte, a document containing those strings, so
the engine matched it as malware. EXCLUDED_PATH_RE already carried
knowledge/, tests/, docs/ and node_modules/ for exactly this reason; the
vendored commons arrived in v8 Phase 5 (bbada84) without being added.

One alternation branch closes it. Tests first: two cases added to
describe('signature-scanner: path exclusions'), both verified red against the
real scan() entry point before the regex changed.

Stated plainly, because it is a real cost and not a technicality: the branch is
`scanners\/commons` behind the existing `(^|\/)` prefix, so it matches that
two-segment path ANYWHERE in a target's relative path, not only at its root. A
webshell planted at vendor/scanners/commons/shell.php in a hostile repository is
therefore invisible to SIG. The second new test asserts that blind spot
deliberately, so it can never be discovered by accident. It is accepted because
anchoring at ^scanners/commons/ would miss the same payload one directory
deeper while re-opening the self-flag whenever the plugin is scanned from a
parent directory. TRG, AST, entropy and supply-chain still read these files;
only SIG identity-matching is blinded.

scanners/lib/supply-chain-data.mjs is NOT excluded. Its finding is a true
positive against real blocklist data.

Measured before: 7 findings. After: 3 (2 on STATE.md, 1 on
supply-chain-data.mjs). signature-scanner.test.mjs 23/23; custom-rules + e2e
54/54; golden-baseline 8/8 with suite-counts.json untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBJVYzwch63Rpk1hii1cNM
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 21:41:09 +02:00
commit eceb71bbb3
3 changed files with 59 additions and 2 deletions

View file

@ -234,6 +234,52 @@ describe('signature-scanner: path exclusions', () => {
rmSync(dir, { recursive: true, force: true });
}
});
// Self-flag gap measured 2026-08-13: scanning THIS repo produced 4 findings on
// the vendored commons (scanners/commons/CHANGELOG.md and
// signatures/malware-signatures.json) — the ruleset describing malware matched
// as malware. The exclusion is a two-segment path, not a bare directory name,
// so a target's own `commons/` stays scanned.
it('does not scan scanners/commons/ — the vendored ruleset describes what it detects', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-commons-'));
try {
mkdirSync(join(dir, 'scanners', 'commons', 'signatures'), { recursive: true });
// Shaped like the real vendored data: signature prose that is itself a match.
writeFileSync(join(dir, 'scanners', 'commons', 'CHANGELOG.md'), "- added rule for `xmrig --donate-level` miners\n");
writeFileSync(
join(dir, 'scanners', 'commons', 'signatures', 'malware-signatures.json'),
JSON.stringify({ rules: [{ id: 'x', pattern: 'xmrig --donate-level' }] }),
);
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
assert.equal(result.findings.length, 0, `scanners/commons/ should yield 0 findings, got ${result.findings.map(f => f.file).join('; ')}`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// The exclusion matches ANYWHERE in the relative path, not just at its root.
// That is a deliberate blind spot with a real cost: a webshell planted at
// `vendor/scanners/commons/shell.php` in a hostile repo is invisible to SIG.
// Accepted because the alternative — anchoring to `^scanners/commons/` — would
// still miss the same payload one directory deeper while adding the failure
// mode where our own vendored copy self-flags whenever the plugin is scanned
// from a parent directory. Other scanners (TRG/AST, entropy, supply-chain)
// still see these files; only SIG identity-matching is blinded.
it('excludes scanners/commons/ anywhere in the path, blinding SIG to a webshell hidden there', async () => {
const dir = mkdtempSync(join(tmpdir(), 'sig-commons-nested-'));
try {
mkdirSync(join(dir, 'vendor', 'scanners', 'commons'), { recursive: true });
writeFileSync(join(dir, 'vendor', 'scanners', 'commons', 'shell.php'), "<?php @eval($_POST['x']); ?>\n");
resetCounter();
const discovery = await discoverFiles(dir);
const result = await scan(dir, discovery);
assert.equal(result.findings.length, 0, `documents the accepted blind spot, got ${result.findings.map(f => f.file).join('; ')}`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------