fix(scan-orchestrator): honor .llm-security-ignore only for the caller's own working tree
A foreign or freshly-cloned scan target could ship its own
.llm-security-ignore with `**` and silently turn a real finding into
ALLOW (0 findings) -- only the undocumented envelope.suppressed count
revealed it. Same defect class the v8.0.0 commons-root fix closed: a
hostile repo should never be able to empty its own detection output.
Valgt X fordi Y: honor the ignore file only when realpath(target)
equals or is nested under realpath(process.cwd()), and never when the
target resolves under os.tmpdir() -- defense-in-depth for the case a
caller's own cwd happens to sit under tmpdir, matching where
git-clone.mjs materializes clones. A foreign target with an ignore
file now gets one stderr line saying it was not honored (loud, not
silent).
Red test first (tests/scanners/ignore-file-scope.test.mjs): a known
HIGH entropy finding (random-bytes base64, built at test time) that
must survive on a foreign/cloned target and stay suppressed on the
caller's own working tree. Verified red on b6edfa3, green after the
fix. Self-scan invariant reverified on three independent fresh clones
today: WARNING 61/100, 58 findings, 382 suppressed -- identical on
patched and unpatched clones, so the own-tree path is unchanged.
Suite 2276/2270 pass/0 fail/6 skip (jetbrains-parser after-hook flake
listed, not counted -- known). Golden unchanged (109/7/4, 61/61).
av-surface 6/6 green, unchanged.
Open, not fixed this session (scope was the ignore file only):
policy-loader.mjs's loadPolicy() also reads .llm-security/policy.json
from the scanned target, independently re-read by entropy-scanner.mjs
and signature-scanner.mjs -- same defect class, not yet measured red.
Logged in PLAN.md S3b for a follow-up order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6edfa3ceb
commit
0d37f5a628
2 changed files with 180 additions and 2 deletions
|
|
@ -4,8 +4,8 @@
|
|||
// shares file discovery, outputs JSON envelope to stdout.
|
||||
// Zero external dependencies.
|
||||
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
||||
import { resolve, join, dirname, sep } from 'node:path';
|
||||
import { existsSync, readFileSync, writeFileSync, appendFileSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { discoverFiles } from './lib/file-discovery.mjs';
|
||||
|
|
@ -22,10 +22,45 @@ const FAIL_ON_LEVELS = ['critical', 'high', 'medium', 'low'];
|
|||
// SCANNER:glob — ignore findings from SCANNER matching file glob
|
||||
// glob — ignore findings from ALL scanners matching file glob
|
||||
// Globs use minimatch-style: * matches within path segment, ** across segments.
|
||||
//
|
||||
// S3b (v8.1.0, 2026-09-22): the ignore file is read from the SCANNED TARGET,
|
||||
// so a foreign/cloned target could ship `.llm-security-ignore` with `**` and
|
||||
// silently empty its own findings — a hostile repo overriding the scan of
|
||||
// itself. Same defect class the v8.0.0 commons-root fix closed. The rule:
|
||||
// honor the ignore file ONLY when the target is the caller's own working
|
||||
// directory (or a subdirectory of it), and NEVER when the target resolves
|
||||
// under the OS temp directory (where git-clone.mjs materializes clones) —
|
||||
// the second check is defense-in-depth for the case a caller's own cwd sits
|
||||
// under tmpdir. Loud, not silent: a foreign target with an ignore file logs
|
||||
// one stderr line saying it was not honored.
|
||||
// ---------------------------------------------------------------------------
|
||||
function isOwnWorkingTree(targetPath) {
|
||||
let resolvedTarget;
|
||||
let resolvedCwd;
|
||||
let resolvedTmp;
|
||||
try {
|
||||
resolvedTarget = realpathSync(resolve(targetPath));
|
||||
resolvedCwd = realpathSync(process.cwd());
|
||||
resolvedTmp = realpathSync(tmpdir());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (resolvedTarget === resolvedTmp || resolvedTarget.startsWith(resolvedTmp + sep)) {
|
||||
return false;
|
||||
}
|
||||
return resolvedTarget === resolvedCwd || resolvedTarget.startsWith(resolvedCwd + sep);
|
||||
}
|
||||
|
||||
function loadIgnoreRules(targetPath) {
|
||||
const ignoreFile = join(targetPath, '.llm-security-ignore');
|
||||
if (!existsSync(ignoreFile)) return [];
|
||||
if (!isOwnWorkingTree(targetPath)) {
|
||||
console.error(
|
||||
`[deep-scan] ${ignoreFile}: .llm-security-ignore was found but is ignored — ` +
|
||||
`the scanned target is not the caller's own working directory (foreign/cloned target)\n`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const lines = readFileSync(ignoreFile, 'utf8').split('\n');
|
||||
const rules = [];
|
||||
for (const raw of lines) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue