fix(llm-security): HIGH — entropy suppression keyed off the absolute path

Rule 4 in isFalsePositive ("test/fixture files intentionally contain
example secrets") matched /(test|spec|fixture|mock|__test__|__spec__)/i
against absPath — the ABSOLUTE path. Any directory name above the scan
root therefore silenced every entropy finding in the entire target: a
repo cloned to a CI workspace, checked out under a parent folder named
`testing`, or scanned from any path with one of those substrings
reported zero secrets and still exited status 'ok'. The failure is
silent — indistinguishable from a clean scan.

- isFalsePositive takes relPath and rule 4 keys off it. relPath was
  already computed and passed down to scanFileContent; only the
  suppression check was reading the wrong one.
- classifyFileContext keeps using absPath: it reads the basename
  extension only, so directory names above the root cannot affect it.
- Rules 1-3 and 5-13 are untouched.

Regression test drives scan() end-to-end against temp roots named
llmsec-test-*, llmsec-spec-*, llmsec-fixture-* and llmsec-mock-*, with a
neutral-root control proving the payload is detectable, plus two
guards that genuine suppression still works (a *.test.mjs file and a
file under a relative tests/ directory).

npm test: 1879/1879 green (1872 + 7 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 09:11:04 +02:00
commit f031dd496f
2 changed files with 110 additions and 3 deletions

View file

@ -282,9 +282,13 @@ function classifyFileContext(absPath, lines) {
* @param {string} absPath - Absolute file path
* @param {'shader-dominant'|'markup-dominant'|'code-dominant'|'mixed'} [context='mixed']
* File-level classification from classifyFileContext.
* @param {string} [relPath] - Path relative to the scan root. The test/fixture
* rule keys off this, never off absPath: a directory name ABOVE the scan root
* (clone target, parent folder, CI workspace) says nothing about whether the
* scanned file is a fixture, and matching it there silences the whole repo.
* @returns {boolean} - true if this string should be skipped
*/
function isFalsePositive(str, line, absPath, context = 'mixed') {
function isFalsePositive(str, line, absPath, context = 'mixed', relPath = '') {
// 1. URLs — entropy is misleading for long query strings / JWTs in URLs
if (str.startsWith('http://') || str.startsWith('https://')) return true;
@ -305,7 +309,8 @@ function isFalsePositive(str, line, absPath, context = 'mixed') {
}
// 4. Test/fixture files — intentionally contain example secrets, tokens, etc.
if (/(?:test|spec|fixture|mock|__test__|__spec__)/i.test(absPath)) return true;
// Scoped to the RELATIVE path: see the relPath note above.
if (/(?:test|spec|fixture|mock|__test__|__spec__)/i.test(relPath)) return true;
// 5. UUID patterns
if (UUID_PATTERN.test(str)) return true;
@ -477,7 +482,7 @@ function scanFileContent(content, absPath, relPath) {
if (!str || str.length < 10) continue;
// False positive suppression
if (isFalsePositive(str, line, absPath, fileContext)) continue;
if (isFalsePositive(str, line, absPath, fileContext, relPath)) continue;
const H = shannonEntropy(str);
let severity = classifyEntropy(H, str.length);