chore(release): v8.1.0 — antivirus surface

Version sync to 8.1.0: package.json, .claude-plugin/plugin.json, README
badge + Recent versions row + self-scan counts, CLAUDE.md header +
highlights (and the GitHub-only remote-scan wording git-clone no longer
has), CHANGELOG [8.1.0] covering S0-S3c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-22 20:29:54 +02:00
commit 7c5514e5dd
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 106 additions and 7 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.0.0"
"version": "8.1.0"
}

View file

@ -6,6 +6,102 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [8.1.0] - 2026-09-22
Antivirus surface. A Windows user should be able to clone the repository and
install the plugin without Defender or a similar product quarantining a file,
and the plugin should still find what it finds today. No payload now sits on
disk as a file that would run it: test fixtures and the malicious-plugin demo
are stored encoded or built from fragments, and exist only in a temp directory
while a test or the demo runs. Two scope fixes ride along: a scanned repository
no longer controls its own suppression or detection thresholds. Detection
tables untouched; golden baseline identical. 2286 tests, 2280 pass, 6 skipped,
0 fail.
### Added
- **`tests/av-surface.test.mjs`** — a gate that fails the suite if a payload
comes back to disk. Five probes over `git ls-files`: (a) files that match
the webshell / reverse-shell / cryptominer signatures in a file type that
would run them, plus (a2) the same families as literals in test sources;
(b) base64 blobs that decode to a shell command; (c) Unicode Tag and
zero-width characters outside the conformance corpus; (d) whole malicious
fixture trees on disk; (e) runnable payload literals in the files Claude
Code loads every session (agents, commands, skills, hooks, knowledge).
- **README § Antivirus false positives** — what the repository contains and
why, how to restore a quarantined file, where to report a false positive to
Microsoft, and a sparse-checkout recipe for users who only want the scanners
and hooks. No exclusion is recommended.
### Changed
- **Test payloads are built at test time, never stored.** The poisoned
fixtures for the signature, memory-poisoning and trigger scanners are
written into a temp directory from split strings or rot13; zero-width test
lines use `String.fromCodePoint`. `tests/fixtures/signature-scan/poisoned/`
is gone. The sha256 of every removed file is asserted against what the
builders produce, so the content is provably unchanged.
- **The malicious-skill demo is stored encoded.** `examples/malicious-skill-demo/`
and `examples/poisoned-claude-md/` ship as encoded archives and are written
to a temp directory when run (`materialize.mjs` prints the demo's path).
The README and the demo's security assessment describe the base64 payloads
instead of showing decodable lines.
- **`tests/fixtures/ast-scan/*.py` are materialized at test time** (all five).
- **Skill-scanner detection lists moved** from `agents/skill-scanner-agent.md`
to `knowledge/skill-threat-patterns.md` § Detection Rules, a file the four
scanning commands already hand the agent. A quarantined agent file would
break the plugin for the user; a knowledge file would not. Runnable
one-liners in `knowledge/*.md` are written as descriptions where that costs
no precision.
- **Two hook rule names split** so no download-into-shell literal sits on disk
in `hooks/scripts/pre-bash-destructive.mjs`. Hook stderr is byte-identical.
- **`.llm-security-ignore` now covers the MEM, SIG, AST, TRG and UNI
prefixes**, and the README's self-scan numbers are measured, not claimed:
a fresh clone scans WARNING 61/100 with 58 findings (the earlier "0 findings
(ALLOW)" was false).
### Fixed
- **`.llm-security-ignore` is honoured only for the caller's own working
tree.** A scanned repository with `**` in its own ignore file turned
WARNING (1 finding) into ALLOW (0). The file is now read only when the
target is the current working directory or below it, and never when the
target is under the system temp directory (where remote clones land).
Otherwise it is ignored with one stderr line.
- **`.llm-security/policy.json` — including `sig.custom_rules_path`,
`ast.python_path` and `ci.failOn` — is honoured only for the caller's own
working tree**, by the same rule (`scanners/lib/own-working-tree.mjs`). A
foreign target could raise its own entropy thresholds out of reach or add
signature rules. Entropy's `calibration.policy_source` now says `defaults`
when the file was present but ignored.
**Behaviour change:** running a scanner with a target outside the current
working directory no longer applies that target's `policy.json`. Hooks,
and CI that runs `scan .` from the repository root, are unaffected.
- **`scanners/lib/git-clone.mjs` accepts HTTPS repository URLs on any host**,
not only GitHub, so the README's Forgejo example works. SSH URLs remain
GitHub-only.
### Known open
- "Own working tree" means the target is the working directory or below it.
A foreign clone that sits under the user's working directory (for example
with the working directory at `$HOME`, or a cloned vendor folder inside a
project) still has its `policy.json`, ignore file and custom SIG rules read.
Clones under the temp directory and targets outside the working directory
are covered.
- The README's self-scan suppression count drifts by one per commit:
`git-forensics` looks at the last 50 commits, and each new commit pushes one
older finding out of that window.
## [8.0.0] - 2026-09-18
Major release. The breaking part is small and was announced in v7.3.0: four

View file

@ -1,9 +1,11 @@
# LLM Security Plugin (v8.0.0)
# LLM Security Plugin (v8.1.0)
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.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.
**v8.0.0 highlights** — Breaking cleanup + commons foundation. BREAKING: the four `LLM_SECURITY_*` env-vars deprecated in v7.3.0 (`INJECTION_MODE`, `TRIFECTA_MODE`, `ESCALATION_WINDOW`, `AUDIT_LOG`, plus `DEPRECATION_QUIET`) are removed — `.llm-security/policy.json` is the only source and a removed variable is **inert** (no warning, silently back to default); `riskScoreV1()` + `SEVERITY_WEIGHTS_V1` deleted (`riskScore()` v2 untouched). Structural, behaviour-preserving: five detection tables are now built from the pull-only vendored subtree `scanners/commons/` (`llm-security-commons`) instead of source literals — codepoint carriers (`scanners/lib/codepoints.mjs`), `OWASP_MAP` (`owasp-map.mjs`), the 83 injection patterns (`injection-lexicon.mjs`), the 19 fixed secret shapes (`secret-egress.mjs`), the 7 SIG rules (`malware-signatures.mjs`; `knowledge/signatures.json` removed). Each swap was measured position-by-position first and diffed record-for-record against the golden baseline (`scripts/golden-baseline.mjs`, `tests/golden/`). The commons root is resolved from the plugin directory only, **never** from a scanned target's policy (a substitutive override would let a hostile repo empty the detection corpus). **Loud failure is contract:** an unresolvable injection lexicon or SIG ruleset writes one stderr line and never throws — an empty table would be the v7.8.2 defect class (clean report, nothing run). Commons conformance spec 1.1 implemented (`tests/lib/conformance-corpus.test.mjs`; not-applicable verdict, derived declaration, anti-narrowing floor). Fixes: `<script>` recall hole + two quadratic `hybrid-xss` rows (whole-table ReDoS gate with inputs derived from each pattern's own prefix), Yarn Berry + nested lockfile-v1 misses in `supply-chain-recheck.mjs`, per-occurrence strip attribution in `content-extractor.mjs`, bare legacy OpenAI keys in `pre-edit-secrets.mjs`, `compileRules()` non-string coercion, SIG self-flag on vendored commons, SARIF multi-OWASP tags. Known open: bounded HTML patterns evade on >256 chars of attribute padding. 2253 tests.
**v7.8.3 highlights** — Security/correctness patch, no feature changes. 47 verified fixes from the v7.8.1/v7.8.2 completion-review MEDIUM tier (52 findings triaged; 3 missing-detector feature-requests and 1 non-defect scoped out; the #11 persistence detector and #27 AST-taint f-string recall deferred to v8). No CRITICAL/HIGH — every review-claimed HIGH downgraded to MEDIUM on re-verification. Supply-chain gate bypasses closed (npm bare-install blocklist skip, nested-key name derivation, yarn.lock false-BLOCK + Yarn Berry miss, `pip audit`→`pip-audit`); pathguard now covers `Edit`; HTML-pattern ReDoS (28s→4ms) and an MCP-stdout memory-exhaustion DoS fixed; toxic-flow/TRG false positives and a bare-`if:` Dependabot-spoof false negative fixed; YAML block-scalar key-leak and embedded-base64→SIG decode closed; docs/counts synced (14 orchestrated scanners, 16 posture categories, 72 red-team scenarios, 2013 tests).
@ -25,7 +27,7 @@ Release notes for v7.0.0 → v7.8.2: see `docs/version-history.md` — read on d
| Command | Description |
|---------|-------------|
| `/security` | Router — lists sub-commands |
| `/security scan [path\|url]` | Scan skills/MCP/directories/GitHub repos (+ `--deep` for deterministic scanners) |
| `/security scan [path\|url]` | Scan skills/MCP/directories/remote git repos (+ `--deep` for deterministic scanners) |
| `/security deep-scan [path]` | 14 deterministic Node.js scanners (incl. supply chain, memory poisoning, toxic flow + trigger/signature/AST-taint) |
| `/security audit` | Full project audit, A-F grading |
| `/security plugin-audit [path\|url]` | Plugin trust assessment (local or GitHub URL) |
@ -78,7 +80,7 @@ Defense philosophy (v5.0), Opus 4.7 alignment, known limitations: see `docs/defe
## Remote Repo Support
`scan` and `plugin-audit` accept GitHub URLs directly. The command clones to a temp dir via `scanners/lib/git-clone.mjs`, scans locally, then cleans up. Use `--branch <name>` for non-default branches.
`scan` and `plugin-audit` accept remote git URLs directly (HTTPS on any host, SSH on GitHub only). The command clones to a temp dir via `scanners/lib/git-clone.mjs`, scans locally, then cleans up. Use `--branch <name>` for non-default branches.
**Clone sandboxing (v5.1):** Two layers of defense against `git clone` filter/smudge driver attacks:
1. **Git config flags (all platforms):** `core.hooksPath=/dev/null`, `core.symlinks=false`, `core.fsmonitor=false`, all LFS filter drivers disabled, `protocol.file.allow=never`, `transfer.fsckObjects=true`. Environment: `GIT_CONFIG_NOSYSTEM=1`, `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_ATTR_NOSYSTEM=1`, `GIT_TERMINAL_PROMPT=0`.

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.0.0-blue)
![Version](https://img.shields.io/badge/version-8.1.0-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)
@ -661,7 +661,7 @@ you start:
npm test # node --test 'tests/**/*.test.mjs'
```
Running `node scanners/scan-orchestrator.mjs .` on a fresh clone produces **WARNING 61/100 with 58 findings** (0 critical, 5 high, 15 medium, 2 low, 36 info; measured 2026-09-22), after `.llm-security-ignore` suppresses 383. Without the ignore file the same run reports BLOCK 95/100 with 441 findings. A security plugin that documents attack patterns, ships a malicious demo, and tests against deliberately evil code triggers its own scanners; every suppression is explained in the file. What is left is reported on purpose: high-entropy strings in docs and the HTML report renderer, example and research domains in docs, three CLI tools that write to a path given on the command line, and memory findings on `CLAUDE.md` — it documents the hooks, but it is the file a poisoning would target, so it is not suppressed. Remove the ignore file and re-run to see the unsuppressed picture.
Running `node scanners/scan-orchestrator.mjs .` on a fresh clone produces **WARNING 61/100 with 58 findings** (0 critical, 5 high, 15 medium, 2 low, 36 info; measured 2026-09-22), after `.llm-security-ignore` suppresses 380. Without the ignore file the same run reports BLOCK 95/100 with 438 findings. A security plugin that documents attack patterns, ships a malicious demo, and tests against deliberately evil code triggers its own scanners; every suppression is explained in the file. What is left is reported on purpose: high-entropy strings in docs and the HTML report renderer, example and research domains in docs, three CLI tools that write to a path given on the command line, and memory findings on `CLAUDE.md` — it documents the hooks, but it is the file a poisoning would target, so it is not suppressed. Remove the ignore file and re-run to see the unsuppressed picture.
The `examples/malicious-skill-demo/` directory contains a deliberately malicious "Project Health Dashboard" plugin and a [full security assessment](examples/malicious-skill-demo/security-assessment.md). The combined LLM + deterministic pipeline produced **85 findings** (24 critical, 24 high, 20 medium, 6 low, 11 info) and verdict **BLOCK 100/100** — both layers independently maxed the risk score. A human reviewing the plugin's `README.md` and `SKILL.md` would likely miss most of them; the Unicode Tag steganography is literally invisible.
@ -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.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. |
| **7.8.3** | 2026-07-18 | **Completion-review MEDIUM sweep — 47 verified fixes, no CRITICAL/HIGH.** 52 findings triaged (48 confirmed; 3 feature-requests + 1 non-defect scoped out; the #11 persistence detector and #27 AST-taint f-string recall deferred to v8). Supply-chain gate bypasses (npm bare-install blocklist skip, nested-key name derivation, yarn.lock false-BLOCK + Yarn Berry miss, `pip audit` no-op). Hook coverage (pathguard now `Edit|Write`; trifecta window no longer diluted by markers; pipe-to-shell interposition; bare provider-key patterns). Scanner robustness (HTML-pattern ReDoS 28s to 4ms; MCP-stdout memory exhaustion; VSIX redirect loop; scalar-policy TypeError; atomic cache writes). False positives/negatives (toxic-flow substring trifectas, TRG scoped-phrase FPs, leading-BOM HIGH, bare-`if:` Dependabot-spoof FN, reflog `reset` FP, diff duplicate-fingerprint mislabel, hex double-report). Parser divergence (YAML block-scalar key leak + indicators, ANSI-C octal/unicode, embedded-base64 to SIG, `.env.local` discovery). Docs consistency (scanner count 14, posture 16, red-team 72, SARIF version, dangling `ROADMAP.md`). Plus a live-protocol fix: `post-mcp-verify` now reads the PostToolUse `tool_response` field, so MCP-output injection scanning fires in live sessions. 2013 tests, 0 fail. |
| **7.8.2** | 2026-07-18 | **Silent-failure fixes from the completion review (HIGH).** Five defects, four sharing one failure mode: the check reported success without running. `hooks/scripts/pre-bash-destructive.mjs` did not block `rm -rf /` or `rm -rf ~` — the target alternation ended in a `\b` that cannot hold after a non-word character, so the bare forms the rule is named for fell through to WARN (exit 0, command executed) while `/etc` and `$HOME` blocked normally. `scanners/entropy-scanner.mjs` matched its test/fixture suppression against the **absolute** path, so any ancestor directory named `test`/`spec`/`fixture`/`mock` silenced every finding in the target and still returned status `ok`. `scanners/ide-extension-scanner.mjs` guarded only `parseVSCodeExtension`'s bare-`null` failure signal, not `parseIntelliJPlugin`'s truthy `{ manifest: null }`, so any malformed JetBrains plugin threw a TypeError that escaped `mapConcurrent`'s unguarded `Promise.all` and aborted the scan of every other extension. `scanners/content-extractor.mjs` detected obfuscated injections but did not remove them: a decoded-only `match[0]` never occurs in the raw text, so the literal replace was a no-op and the payload reached the LLM agent verbatim through `sanitized_content` — removal is now line-level, with unattributable multi-line payloads flagged `unstripped`. `scanners/lib/ide-extension-parser.mjs` emptied any plugin.xml field containing a character reference above `0x10FFFF`. No feature changes. 1901 tests, 0 fail. |

View file

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