// 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); }