Version 8.1.3 in package.json, .claude-plugin/plugin.json, README badge and changelog list, CLAUDE.md header and highlights, CHANGELOG. No tag: release-plugin.mjs is the operator's push round. Also in this commit: file content no longer carries order IDs or pointers into the local-only plan (comments and test names in the own-working-tree, av-surface and watch-cron tests; `punkt N` -> `(N)`). README's antivirus section now says v8.1.3 touched it. Suite after `git add`: 2344 tests, 2338 pass, 0 fail, 6 skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5.4 KiB
JavaScript
124 lines
5.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.
|
|
//
|
|
// v8.1.2 closed the gap for foreign code with NO `.git` of its own under cwd
|
|
// (it shares cwd's git root, so the v8.1.1 rule alone called it own) for two
|
|
// install locations, which are now foreign: (1) any `node_modules` segment on the path from cwd to the
|
|
// target (an installed package; only the path BELOW cwd counts, so a package
|
|
// the user has cd'd into is own, like a clone they cd'd into), and (2) any
|
|
// target under Claude Code's plugin dir — `$CLAUDE_CONFIG_DIR/plugins`, default
|
|
// `~/.claude/plugins` (cache/ and marketplaces/). A general "no `.git` of its
|
|
// own" rule was not taken: it would shut out ordinary subdirectories of the
|
|
// caller's own repo. Known limit in v8.1.2: other install locations still
|
|
// counted as own.
|
|
//
|
|
// v8.1.3 (PM decisions) widened both checks:
|
|
// (1) a `site-packages` (Python venv) or `vendor` (composer/bundler/Go)
|
|
// segment below cwd is foreign like `node_modules`, and a Claude Code config
|
|
// dir's `skills/` is foreign like its `plugins/` (third-party skills copied
|
|
// into a git-tracked ~/.claude share its git root); (2) the config dirs are
|
|
// BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR`, whose leading `~` is expanded
|
|
// (Anthropic's docs do not say whether Claude Code expands it — not verified,
|
|
// so both readings count as foreign); (3) paths are realpath'd with
|
|
// `realpathSync.native`, which canonicalizes case on a case-insensitive
|
|
// volume: `NODE_MODULES/x` is the `node_modules` it names, and a
|
|
// case-mismatched path to an own dir is own. Still own (known limits): a
|
|
// `git archive` export or unpacked tarball under cwd (no marker at all), and
|
|
// cwd inside `node_modules` with a sibling package as target.
|
|
|
|
import { resolve, sep, join, dirname, relative } from 'node:path';
|
|
import { realpathSync, existsSync } from 'node:fs';
|
|
import { tmpdir, homedir } 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;
|
|
}
|
|
}
|
|
|
|
// Path segments below cwd that mark installed third-party code.
|
|
const INSTALL_SEGMENTS = ['node_modules', 'site-packages', 'vendor'];
|
|
|
|
// Subdirs of a Claude Code config dir that hold third-party code.
|
|
const CONFIG_INSTALL_DIRS = ['plugins', 'skills'];
|
|
|
|
/** Realpath with case canonicalized; the input itself when it does not exist. */
|
|
function realOrSelf(p) {
|
|
try {
|
|
return realpathSync.native(p);
|
|
} catch {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Claude Code's install dirs: `plugins/` and `skills/` under `~/.claude` and,
|
|
* when set, under `$CLAUDE_CONFIG_DIR` (a leading `~` is expanded, a relative
|
|
* value is resolved against cwd).
|
|
* @returns {string[]}
|
|
*/
|
|
function configInstallDirs() {
|
|
const configDirs = [join(homedir(), '.claude')];
|
|
const env = process.env.CLAUDE_CONFIG_DIR;
|
|
if (env) {
|
|
const expanded = env === '~' || env.startsWith('~/') ? join(homedir(), env.slice(1)) : env;
|
|
configDirs.push(resolve(expanded));
|
|
}
|
|
return configDirs.flatMap(dir => CONFIG_INSTALL_DIRS.map(sub => realOrSelf(join(dir, sub))));
|
|
}
|
|
|
|
/**
|
|
* @param {string} targetPath
|
|
* @returns {boolean}
|
|
*/
|
|
export function isOwnWorkingTree(targetPath) {
|
|
let resolvedTarget;
|
|
let resolvedCwd;
|
|
let resolvedTmp;
|
|
try {
|
|
resolvedTarget = realpathSync.native(resolve(targetPath));
|
|
resolvedCwd = realpathSync.native(process.cwd());
|
|
resolvedTmp = realpathSync.native(tmpdir());
|
|
} catch {
|
|
return false;
|
|
}
|
|
if (resolvedTarget === resolvedTmp || resolvedTarget.startsWith(resolvedTmp + sep)) {
|
|
return false;
|
|
}
|
|
for (const dir of configInstallDirs()) {
|
|
if (resolvedTarget === dir || resolvedTarget.startsWith(dir + sep)) return false;
|
|
}
|
|
const underCwd = resolvedTarget === resolvedCwd || resolvedTarget.startsWith(resolvedCwd + sep);
|
|
if (!underCwd) return false;
|
|
const below = relative(resolvedCwd, resolvedTarget).split(sep);
|
|
if (below.some(segment => INSTALL_SEGMENTS.includes(segment))) return false;
|
|
return gitRoot(resolvedTarget) === gitRoot(resolvedCwd);
|
|
}
|