chore(release): v8.1.2 — node_modules and plugin dir foreign, posture credit honest

Version sync: package.json, .claude-plugin/plugin.json, README badge +
Recent versions row, CLAUDE.md header + v8.1.2 highlights, CHANGELOG
[8.1.2]. The own-working-tree header and CHANGELOG no longer claim that
node_modules and the plugin dir are the only install locations foreign
code lands in (independent review: Python venv, vendor/ still own) —
listed as Known open instead.

Gates on the staged tree: suite 2328 / 2322 pass / 0 fail / 6 skipped,
hooks 370/0, golden 109/7/4 61/61, av-surface 6/6, claude plugin
validate pass (1 known CLAUDE.md warning).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-22 21:39:19 +02:00
commit 0bc4c8a6be
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
6 changed files with 70 additions and 10 deletions

View file

@ -1,5 +1,5 @@
{
"name": "llm-security",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.",
"version": "8.1.1"
"version": "8.1.2"
}

View file

@ -6,6 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [8.1.2] - 2026-09-22
Patch that closes both items v8.1.1 left open. Detection tables, golden
baseline and hooks are unchanged. Windows/Defender quarantine is still not
measured. 2328 tests, 2322 pass, 6 skipped, 0 fail.
### Fixed
- **An installed package and a Claude Code plugin copy are no longer "own
working tree".** A target without a `.git` of its own shares the working
directory's git root, so v8.1.1 still read its `.llm-security-ignore` and
`.llm-security/policy.json` (including `sig.custom_rules_path`). A target is
now also foreign when the path from the working directory down to it has a
`node_modules` folder in it, or when it lies under Claude Code's plugin
directory — `$CLAUDE_CONFIG_DIR/plugins`, or `~/.claude/plugins` when the
variable is unset (`cache/` and `marketplaces/` alike)
(`scanners/lib/own-working-tree.mjs`). These are the two install locations
the rule names; it does not cover every place foreign code can land (see
Known open). A general "has no `.git` of its own" rule was not taken,
because it would also ignore the configuration of ordinary folders inside
your own repository.
**Behaviour change:** scanning a package under `node_modules/`, or a plugin
under the plugin directory, no longer applies that folder's ignore file or
policy; it gets the defaults and one stderr line per ignored file. Only the
path *below* the working directory is checked for `node_modules`, so a
package you have `cd`'d into and scan as `.` is still your own tree. A target
under the plugin directory is foreign even from inside it. Scanning your
repository or a plain folder inside it is unchanged, and so are hooks, which
read the policy from the project root.
- **The posture scanner no longer gives NIST "Govern" credit for a
`policy.json` the scan would not honour.** The credit used to follow the
file's existence; it now also requires the target to be your own working
tree (`scanners/posture-scanner.mjs`).
### Known open
- **A `git archive` export or an unpacked tarball under the working
directory still counts as your own tree.** It has no `.git`, no
`node_modules` folder and no plugin-directory path, so nothing tells it apart
from a folder of your own; its ignore file and policy are read.
- **Other install locations still count as your own tree:** a package in a
Python virtual environment (`.venv/lib/…/site-packages/`), a composer or
bundler `vendor/` folder, and skills copied into `~/.claude/skills` when you
scan from inside a git-tracked `~/.claude`. Only `node_modules` and the
plugin directory are named by the rule.
- The plugin-directory check knows one config directory: the one
`CLAUDE_CONFIG_DIR` points at, or `~/.claude` when it is unset. With the
variable pointing at another profile, a copy under `~/.claude/plugins` counts
as own again.
- Two cases stay foreign on purpose (safe direction, more findings, not
fewer): a git worktree of your repository checked out inside it, scanned
from the main checkout; and a target path typed with different letter case
than the folder on disk (macOS APFS is case-insensitive, but the path check
compares exactly).
## [8.1.1] - 2026-09-22
Patch that closes the v8.1.0 follow-up list. Detection tables, golden baseline

View file

@ -1,9 +1,11 @@
# LLM Security Plugin (v8.1.1)
# LLM Security Plugin (v8.1.2)
Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI, 2026 edition), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 2250+ unit, integration, and end-to-end tests (`tests/e2e/` covers the multi-hook attack chain, multi-session state simulation, and the full scan-orchestrator pipeline); mutation-testing coverage not published.
Release notes for v7.0.0 → v7.8.2: see `docs/version-history.md` — read on demand.
**v8.1.2 highlights** — Patch closing both v8.1.1 known-open items; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) is additionally foreign when the path from cwd to the target has a `node_modules` segment (only the part below cwd counts — a package you `cd`'d into is own) or when the target lies under Claude Code's plugin dir (`$CLAUDE_CONFIG_DIR/plugins`, default `~/.claude/plugins`; foreign even from inside it). PM rule (order 20260922T192716Z), chosen over a general "no `.git` of its own" rule, which would shut out plain subdirs of the own repo (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/foreign-under-cwd-scope.test.mjs`). posture-scanner's NIST Govern credit for `policy.json` now requires `isOwnWorkingTree(projectRoot)` (`tests/scanners/posture-policy-scope.test.mjs`). Known open: a `git archive` export / unpacked tarball under cwd, and install locations the rule does not name (Python venv site-packages, composer/bundler `vendor/`, skills in a git-tracked `~/.claude`), still count as own; the plugin-dir check knows one config dir. Kept on purpose (safe direction): an own worktree under the repo and a case-mismatched target path are foreign. 2328 tests.
**v8.1.1 highlights** — Patch closing the v8.1.0 follow-up list; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) now also requires the target to share cwd's git root (nearest `.git`, dir or file; none for both), so a clone, submodule or worktree *under* cwd is foreign and its `.llm-security-ignore` / `policy.json` / custom SIG rules are ignored — closes v8.1.0's known-open item (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/nested-clone-scope.test.mjs`). `ci.failOn` / `ci.compact` from `policy.json` got a real test (the old one never read the policy). README and `.llm-security-ignore` no longer state self-scan counts that drift per commit (git-forensics' 50-commit window). `/security scan` description names the accepted URLs. Known open: a target with no `.git` of its own (`node_modules/`, plugin-cache copy, tarball export) under cwd still counts as own. Windows/Defender quarantine remains **not measured** (no Windows machine). 2306 tests.
**v8.1.0 highlights** — Antivirus surface: a Windows user can clone and install without Defender quarantining a file, and detection is unchanged. No payload sits on disk as a file that would run it — test fixtures (signature, memory-poisoning, trigger, `ast-scan/*.py`) are built in a temp dir at test time from split strings/rot13, the malicious-skill demo and `poisoned-claude-md` ship as encoded archives (`materialize.mjs`), zero-width test lines use `String.fromCodePoint`; sha256 of every removed file asserted. Gate: `tests/av-surface.test.mjs`, probes (a)/(a2) SIG payloads, (b) base64→shell command, (c) Tag/zero-width outside the conformance corpus, (d) malicious trees on disk, (e) runnable literals in session-loaded files. Skill-scanner detection lists moved from `agents/skill-scanner-agent.md` to `knowledge/skill-threat-patterns.md` § Detection Rules (a quarantined agent file breaks the plugin). README § Antivirus false positives (restore, report, sparse checkout; no exclusions recommended). Scope fixes: `.llm-security-ignore` and `.llm-security/policy.json` (incl. `sig.custom_rules_path`, `ast.python_path`, `ci.failOn`) are honoured only for the caller's own working tree (`scanners/lib/own-working-tree.mjs`: target = cwd or below, never under tmpdir) — a foreign target's file is ignored with one stderr line. `git-clone.mjs` accepts HTTPS on any host. Signature tables and golden baseline untouched. Known open: a foreign clone *under* cwd still counts as own tree. 2286 tests.

View file

@ -6,7 +6,7 @@ Security scanning, auditing, and threat modeling for Claude Code projects. OWASP
*AI-generated: all code produced by Claude Code through dialog-driven development. Every change is human-directed, reviewed, and validated before commit. Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user; this plugin is licensed MIT.*
![Version](https://img.shields.io/badge/version-8.1.1-blue)
![Version](https://img.shields.io/badge/version-8.1.2-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Scanners](https://img.shields.io/badge/scanners-22-cyan)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
@ -779,6 +779,7 @@ line if you run those. A sparse checkout of your own clone does not change what
| Version | Date | Highlights |
|---------|------|------------|
| **8.1.2** | 2026-09-22 | **Installed packages and plugin copies are foreign.** A target reached through a `node_modules` folder below the working directory, or one under the Claude Code plugin directory (`$CLAUDE_CONFIG_DIR/plugins`, default `~/.claude/plugins`), no longer counts as the caller's own tree, so its `.llm-security-ignore` and `.llm-security/policy.json` are ignored. The posture scanner gives NIST "Govern" credit for a `policy.json` only when the scan would honour it. Known open: a `git archive` export or unpacked tarball under the working directory, and install locations the rule does not name (a Python virtual environment, a `vendor/` folder), still count as own. Detection tables and hooks unchanged. 2328 tests, 0 fail. |
| **8.1.1** | 2026-09-22 | **Own-tree rule narrowed, v8.1.0 follow-ups.** A clone, submodule or worktree that sits *under* the working directory no longer counts as the caller's own tree, so its `.llm-security-ignore` and `.llm-security/policy.json` (incl. custom SIG rules) are ignored — the target must share the working directory's git root. `ci.failOn` / `ci.compact` in `policy.json` now have a real test. The README no longer states self-scan counts that change with every commit. Windows/Defender quarantine is still not measured. Detection tables and hooks unchanged. 2306 tests, 0 fail. |
| **8.1.0** | 2026-09-22 | **Antivirus surface.** Clone and install on Windows without Defender quarantining a file, with detection unchanged. No payload sits on disk as a file that would run it: test fixtures are built in a temp directory at test time, the malicious-skill demo ships encoded and is materialized on demand, and `tests/av-surface.test.mjs` fails the suite if a payload comes back. Skill-scanner detection lists moved from the agent file to `knowledge/`. New section [Antivirus false positives](#antivirus-false-positives). Scope fixes: a scanned repository's own `.llm-security-ignore` and `.llm-security/policy.json` are honoured only when it is your own working tree, so a foreign clone cannot suppress its findings or raise its thresholds. Remote scans accept HTTPS URLs on any host. Signature tables and golden baseline untouched. 2286 tests. |
| **8.0.0** | 2026-09-18 | **Breaking cleanup + detection tables on vendored commons data.** BREAKING: the four `LLM_SECURITY_*` mode env-vars deprecated in v7.3.0 are removed (`.llm-security/policy.json` is the only source; a removed variable is inert) and `riskScoreV1()` is deleted — see [Migrating to v8.0.0](#migrating-to-v800). The injection lexicon (83 patterns), codepoint carriers, `OWASP_MAP`, the 19 fixed secret shapes and the 7 SIG signatures are now built from the pull-only `scanners/commons/` subtree, each swap proven content-identical against a golden baseline; an unresolvable lexicon fails loudly on stderr instead of scanning with zero patterns. Fixes: `<script>` recall hole, two quadratic `hybrid-xss` patterns (whole-table ReDoS gate added), Yarn Berry + nested lockfile-v1 misses in `supply-check`, a second-occurrence strip miss in `content-extractor.mjs`, bare legacy OpenAI keys, a malformed-SIG-rule false-positive matcher, SARIF multi-OWASP tags. Commons conformance spec 1.1 implemented. 2253 tests. |

View file

@ -1,6 +1,6 @@
{
"name": "llm-security",
"version": "8.1.1",
"version": "8.1.2",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects",
"type": "module",
"bin": {

View file

@ -21,17 +21,17 @@
// 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). Two
// concrete places put such code under a user's working directory, and both
// are now foreign: (1) any `node_modules` segment on the path from cwd to the
// (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: a `git archive` export (or an unpacked
// tarball) under cwd has neither marker and cannot be told apart from an own
// subdirectory — it still counts as own.
// caller's own repo. Known limits — these still count as own: a `git archive`
// export or unpacked tarball under cwd (no marker at all), and other install
// locations this rule does not name (a Python venv's site-packages, a
// composer/bundler `vendor/` dir, skills copied into a git-tracked ~/.claude).
import { resolve, sep, join, dirname, relative } from 'node:path';
import { realpathSync, existsSync } from 'node:fs';