fix(policy): read policy.json only from the caller's own working tree
loadPolicy() read .llm-security/policy.json from whatever root it was
given, and every scanner passes the SCANNED TARGET: scan-orchestrator
(policyRoot = resolve(args.target)), entropy-scanner (thresholds and
suppression patterns), signature-scanner (sig.custom_rules_path and
enabled_families), trigger-scanner (phrase lists) and ast-taint-scanner
(enabled, python_path). A foreign/cloned target could raise its own
entropy thresholds, disable SIG families, supply its own SIG ruleset or
name the interpreter the AST scanner spawns — configuring the scan of
itself. Same defect class as S3b's .llm-security-ignore fix.
Chosen: move isOwnWorkingTree() to scanners/lib/own-working-tree.mjs (one
copy, reused by the orchestrator's ignore-file check) and make
loadPolicy() refuse an EXPLICIT root that is not the caller's own tree —
defaults plus one stderr line, same form as S3b — because one rule in one
function covers every scanner and a future call site cannot forget it.
The IMPLICIT root (CLAUDE_PROJECT_ROOT/cwd, what every hook uses) is the
caller's own project by construction and is read as before.
entropy-scanner's calibration.policy_source no longer reports an ignored
file as its source.
New tests/scanners/policy-scope.test.mjs was red on 0d37f5a (foreign
target: entropy finding silenced, custom SIG rule loaded, findings differ
from the same tree without policy.json, no stderr line) and is green now;
its own-tree scenario (known-positive) is green before and after. The 15
existing policy tests that placed own-tree fixtures under os.tmpdir() now
use tests/helpers/own-tree.mjs (fixture under $HOME, cwd set to it).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d37f5a628
commit
6d0f3c31fc
12 changed files with 364 additions and 50 deletions
|
|
@ -15,6 +15,7 @@ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|||
import { randomBytes } from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { mkOwnTreeDir, inOwnTree } from '../helpers/own-tree.mjs';
|
||||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||
import { discoverFiles } from '../../scanners/lib/file-discovery.mjs';
|
||||
import { scan } from '../../scanners/entropy-scanner.mjs';
|
||||
|
|
@ -212,7 +213,7 @@ describe('entropy-scanner context suppression (v7.0.0+)', () => {
|
|||
|
||||
describe('C. Policy-driven overrides', () => {
|
||||
it('user-policy suppress_line_patterns adds custom suppression', async () => {
|
||||
const fx = await newRoot('ent-policy-');
|
||||
const fx = mkOwnTreeDir('ent-policy-'); // S3c: policy is read only for the own tree
|
||||
await writeFixture(fx, 'secret.js', 'const vendor = "' + PAYLOAD + '"; // MY_VENDOR_MARKER');
|
||||
await writeFixture(fx, '.llm-security/policy.json', JSON.stringify({
|
||||
entropy: { suppress_line_patterns: ['MY_VENDOR_MARKER'] }
|
||||
|
|
@ -220,14 +221,14 @@ describe('entropy-scanner context suppression (v7.0.0+)', () => {
|
|||
resetCounter();
|
||||
_resetCacheForTest();
|
||||
const discovery = await discoverFiles(fx);
|
||||
const result = await scan(fx, discovery);
|
||||
const result = await inOwnTree(fx, () => scan(fx, discovery));
|
||||
assert.equal(result.findings.length, 0, 'expected user pattern to suppress');
|
||||
assert.equal(result.calibration.policy_source, 'policy.json');
|
||||
await rm(fx, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('user-policy suppress_paths skips files whose relPath contains the substring', async () => {
|
||||
const fx = await newRoot('ent-paths-');
|
||||
const fx = mkOwnTreeDir('ent-paths-'); // S3c: policy is read only for the own tree
|
||||
await writeFixture(fx, 'src/vendored/big.js', 'var x="' + PAYLOAD + '";');
|
||||
await writeFixture(fx, 'src/app.js', 'var y="' + PAYLOAD + '";');
|
||||
await writeFixture(fx, '.llm-security/policy.json', JSON.stringify({
|
||||
|
|
@ -236,14 +237,14 @@ describe('entropy-scanner context suppression (v7.0.0+)', () => {
|
|||
resetCounter();
|
||||
_resetCacheForTest();
|
||||
const discovery = await discoverFiles(fx);
|
||||
const result = await scan(fx, discovery);
|
||||
const result = await inOwnTree(fx, () => scan(fx, discovery));
|
||||
assert.equal(result.findings.length, 1, 'Expected 1 finding (app.js only), got ' + result.findings.length);
|
||||
assert.ok(result.calibration.files_skipped_by_path >= 1);
|
||||
await rm(fx, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('user-policy stricter thresholds suppress medium-strength payload', async () => {
|
||||
const fx = await newRoot('ent-thresh-');
|
||||
const fx = mkOwnTreeDir('ent-thresh-'); // S3c: policy is read only for the own tree
|
||||
await writeFixture(fx, 'cfg.js', 'const blob = "' + PAYLOAD + '";');
|
||||
await writeFixture(fx, '.llm-security/policy.json', JSON.stringify({
|
||||
entropy: {
|
||||
|
|
@ -257,7 +258,7 @@ describe('entropy-scanner context suppression (v7.0.0+)', () => {
|
|||
resetCounter();
|
||||
_resetCacheForTest();
|
||||
const discovery = await discoverFiles(fx);
|
||||
const result = await scan(fx, discovery);
|
||||
const result = await inOwnTree(fx, () => scan(fx, discovery));
|
||||
assert.equal(result.findings.length, 0, 'expected strict thresholds to suppress medium-strength payload');
|
||||
await rm(fx, { recursive: true, force: true });
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue