isOwnWorkingTree() treated any target at or below cwd as own, so a foreign clone under cwd (cwd = $HOME, a vendor clone in a project) still had its .llm-security-ignore, policy.json and custom SIG rules read. The target must now also share cwd's git root: nearest ancestor with a `.git` entry (dir for a clone, file for a submodule/worktree), or none for both. No git spawn. tmpdir stays foreign. Chosen per the PM order: it is exactly the line between "my repo" and "something I fetched", and the failure direction is safe (foreign => config ignored => more findings, never fewer). Red first: tests/lib/own-working-tree.test.mjs 4 fail / 5 pass (the 5 are known-positives), tests/scanners/nested-clone-scope.test.mjs 5 fail / 4 pass on the old rule. Green after; reverting the git-root comparison turns 9 red. Suite 2306 / 2300 pass / 0 fail / 6 skip; hooks 370/0 (implicit root untouched); golden 109/7/4, 61/61. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
2.4 KiB
JavaScript
62 lines
2.4 KiB
JavaScript
// own-working-tree.mjs — Is a scan target the caller's own working tree?
|
|
// Zero external dependencies.
|
|
//
|
|
// Configuration that lives INSIDE a scanned target (.llm-security-ignore,
|
|
// .llm-security/policy.json and the custom SIG ruleset it can point at) is
|
|
// honored only when the target is the caller's own working directory (or a
|
|
// subdirectory of it in the same git working tree), 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.
|
|
// Otherwise a foreign/cloned target could configure the scan of itself.
|
|
//
|
|
// S3b (v8.1.0, 2026-09-22) introduced this check in scan-orchestrator.mjs for
|
|
// the ignore file; S3c moved it here so policy-loader.mjs shares the one rule.
|
|
//
|
|
// v8.1.1 narrowed "at or below cwd": the target must also have the SAME git
|
|
// root as cwd — the nearest ancestor holding a `.git` (a directory for a
|
|
// clone, a file for a submodule or worktree), or no git root for either. A
|
|
// clone under cwd (cwd = $HOME, or a vendor clone inside a project) is
|
|
// therefore foreign. No git subprocess: the walk only stats `.git`. The
|
|
// failure direction is safe — foreign means the target's config is ignored,
|
|
// so more findings, never fewer.
|
|
|
|
import { resolve, sep, join, dirname } from 'node:path';
|
|
import { realpathSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
/**
|
|
* Nearest ancestor of `start` (inclusive) that holds a `.git` entry, or null.
|
|
* @param {string} start - a realpath
|
|
* @returns {string|null}
|
|
*/
|
|
function gitRoot(start) {
|
|
let dir = start;
|
|
for (;;) {
|
|
if (existsSync(join(dir, '.git'))) return dir;
|
|
const parent = dirname(dir);
|
|
if (parent === dir) return null;
|
|
dir = parent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} targetPath
|
|
* @returns {boolean}
|
|
*/
|
|
export 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;
|
|
}
|
|
const underCwd = resolvedTarget === resolvedCwd || resolvedTarget.startsWith(resolvedCwd + sep);
|
|
return underCwd && gitRoot(resolvedTarget) === gitRoot(resolvedCwd);
|
|
}
|