# Changelog All notable changes to the LLM Security Plugin are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ## [7.8.3] - 2026-07-18 Security and correctness patch. 47 verified fixes from the v7.8.1/v7.8.2 completion-review MEDIUM tier (52 findings triaged: 48 confirmed defects, with 3 feature-requests and 1 non-defect scoped out; capability work — the #11 persistence detector and #27 AST-taint f-string recall — deferred to v8). No new features, and no CRITICAL/HIGH: every review-claimed HIGH downgraded to MEDIUM on re-verification. 2013 tests, 0 fail. ### Fixed - **Supply-chain gate bypasses.** The offline npm blocklist was skipped for bare/range/tag installs (resolved version never re-checked); non-hoisted nested lockfile keys derived the wrong package name; the yarn.lock matcher both false-BLOCKed a legitimate package (unassociated substrings + unanchored `pkg@`) and missed Yarn Berry's `version:` format; `pip audit` (no such subcommand) made Python CVE detection a permanent silent no-op and, once corrected to `pip-audit`, audited the target instead of the scanner host. - **Hook coverage.** pathguard registered `Write` only, so an Edit to an existing protected file (settings, `.env`, `.ssh`, the hooks themselves) bypassed it — now `Edit|Write`. The trifecta window counted marker lines and could scroll a real leg out. The remote-pipe-to-shell block missed xargs/sudo/tee/env interposition. pre-edit-secrets missed bare provider keys (Anthropic, OpenAI, fine-grained GitHub PAT, Google, JWT). - **Scanner robustness / DoS.** Quadratic ReDoS in the HTML-obfuscation injection patterns (~28s to 4ms); unbounded readline on MCP-server stdout (memory exhaustion / uncaught RangeError); an uncapped same-host redirect loop in the VSIX fetcher; a scalar `policy.json` section override threw an uncaught TypeError; non-atomic writes to the MCP-description and skill-registry caches. - **False positives / negatives.** toxic-flow fabricated CRITICAL trifectas from substring keyword matches (`url` in `curl`); TRG-broad/-baiting fired on scoped phrasing and substrings; a legitimate leading UTF-8 BOM was flagged HIGH; the workflow actor-auth-bypass detector missed the bare `if:` form (Dependabot spoof); the git reflog force-push detector tripped on any `reset` in a commit subject; the diff engine mislabeled unchanged findings on duplicate fingerprints; memory-poisoning double-reported a hex token as base64 + hex. - **Parser divergence / evasion.** YAML block-scalar bodies were re-parsed as top-level keys (name/allowed_tools override) and indented/chomped block-scalar headers (`|2`, `>-`) were unrecognized; the bash normalizer decoded only `\xHH`, not ANSI-C octal/`\u`/`\U`; embedded base64 (opt-in) now reaches the SIG decode pipeline; `.env.local`/`.env.example` were silently skipped by discovery; `collapseLetterSpacing` (multi-space/tab) and `redact(_, _, 0)` (full-URL leak) were fixed; the SIG scanner now honours the documented `custom_rules_path` policy option. - **Docs / version consistency.** Orchestrated scanner count corrected to 14, posture categories to 16, red-team scenarios to 72, tests badge to 2013; SARIF `driver.version` now reflects the real plugin version; the pathguard matcher and persistence-detection documentation were corrected; dangling `ROADMAP.md` references removed. - **MCP output-injection scan was inert in live sessions** (`hooks/scripts/post-mcp-verify.mjs`). The hook read the PostToolUse `tool_output` field, but live Claude Code delivers the tool result as `tool_response`, so the indirect-injection scan on MCP output never ran outside the test harness; it now reads `tool_response` and falls back to `tool_output`. Found via a live-session check during the sweep, not part of the MEDIUM-tier triage. ### Deferred (to v8) - Persistence-command detection (cron/launchctl/rc-files/plist) and the AST-taint f-string/concat/alias recall gap — capability enhancements, not defects in shipped code. - Deterministic detectors for AST09 (bulk knowledge load), AST10/LLM10 (unbounded consumption), and MCP05 (path-traversal read sink) — each already covered by the LLM-agent layer. ## [7.8.2] - 2026-07-18 Security patch. Fixes five defects from the v7.8.1 completion review, four of which caused a scanner or hook to fail silently — reporting success while the check it was named for did not run. No feature changes. 1901 tests, 0 fail. ### Fixed - **HIGH — bare root/home targets bypassed the rm block** (`hooks/scripts/pre-bash-destructive.mjs`). The BLOCK rule's target alternation ended in a shared `\b`, and a word boundary cannot hold after `/` or `~` at end-of-command. `rm -rf /`, `rm -rf ~`, `rm -rf /*`, `rm -fr /` and the sudo-prefixed forms all fell through to WARN (exit 0) — advisory only, command executed. Only targets starting with a word character (`/etc`, `/usr`) were ever blocked. `\b` now applies to the `$HOME` alternative alone, where it is meaningful. - **HIGH — entropy suppression keyed off the absolute path** (`scanners/entropy-scanner.mjs`). The test/fixture suppression rule matched `/(test|spec|fixture|mock|__test__|__spec__)/i` against the **absolute** path, so any directory name above the scan root silenced every entropy finding in the entire target while still reporting status `ok`. The rule now keys off the path relative to the scan root. - **HIGH — one unparseable JetBrains plugin crashed the whole ide-scan** (`scanners/ide-extension-scanner.mjs`). The two manifest parsers disagree on how they signal failure: `parseVSCodeExtension` returns bare `null`, `parseIntelliJPlugin` returns a truthy `{ manifest: null, warnings }`. Only the bare-null form was guarded, so every JetBrains failure path dereferenced `manifest.hasSignature`; the TypeError escaped through `mapConcurrent`'s unguarded `Promise.all` and aborted the scan of every other installed extension. Guard widened, plus per-extension fault isolation at the call site. - **HIGH — obfuscated injections were reported but not stripped** (`scanners/content-extractor.mjs`). `stripInjection` scanned both the raw and the decoded text but removed matches only via a literal replace against the raw text. For a decoded-only match, `match[0]` is the decoded string, which by construction does not occur in the raw text — so the replace was a silent no-op and the encoded payload reached the LLM agent verbatim via `sanitized_content`, alongside a finding announcing it. Every obfuscation the normalizer exists to defeat was affected. Decoded-only matches are now removed by redacting the source line; multi-line payloads that cannot be attributed are flagged `unstripped: true` rather than left silently. - **Out-of-range numeric character reference emptied a plugin.xml field** (`scanners/lib/ide-extension-parser.mjs`). `decodeEntities` guarded `parseInt` with `Number.isFinite`, which bounds nothing, so any code point above `0x10FFFF` made `String.fromCodePoint` raise `RangeError`. The no-throw contract held (the per-field `safe()` wrapper catches it), but the affected field was discarded and replaced with `''`. Undecodable references are now left literal. Filed as HIGH; it is lower — such a document is not well-formed XML, so the plugin would not load in IntelliJ either. ### Changed - `stripInjection` (content-extractor) and `scanOneExtension` (ide-extension- scanner) are exported for testing; `content-extractor.mjs` runs `main()` behind the standard `isMain` guard so importing it no longer executes the CLI. The remote-scan injection boundary had no direct test coverage before this release. ### Removed - `tests/hooks/probe-rm.mjs` — a self-labelled temporary debug probe with no assertions, pointing at a hardcoded path into the installed marketplace copy. ## [7.8.1] - 2026-07-18 Security patch. Fixes a CRITICAL command-injection defect in the auto-cleaner that shipped in v7.8.0. No feature changes. 1865 tests, 0 fail. ### Fixed - **CRITICAL — command injection via scanned filename** (`scanners/auto-cleaner.mjs`). `validateContent()` syntax-checked `.mjs`/`.js`/`.cjs` candidates with ``execSync(`node --check "${tmpPath}"`)``, where `tmpPath` derives from the **untrusted scanned-repo filename**. The F-2 guard added in v7.8.0 checks path containment but neither strips nor quotes shell metacharacters, and a filename containing `"` closes the interpolated quote. A repository shipping a file named ``x";;".mjs`` therefore turned `/security clean` — whose live mode is the documented default — into arbitrary command execution on the operator's machine. Verified with a live proof-of-concept before the fix. Both subprocess call sites (the syntax check, and the CLI's scan-orchestrator fallback) now use `spawnSync` with an argv array, so no shell parses the path. - **Defense-in-depth:** `applyFixes()` now refuses findings whose `file` field carries shell or control metacharacters, reporting them as `skipped` rather than passing them to any sink. This guards against a future call site re-introducing string interpolation. ### Changed - `validateContent` is exported from `scanners/auto-cleaner.mjs` so the regression suite can exercise the subprocess sink directly, independently of the `applyFixes` guard that would otherwise mask it. ## [7.8.0] - 2026-06-20 Three new deterministic deep-scan scanners (TRG/SIG/AST) targeting the skills/agents attack surface. Each ships with its own finding prefix, OWASP/AST mapping, policy block, fixtures, and graceful-skip behaviour. Built behind a security-fix gate: the F-1/F-2/F-3 shell-injection and path-traversal fixes landed first. No existing scanner, hook, or command behaviour changes. 1863 tests, 0 fail. ### Added - **TRG — trigger/activation-abuse scanner** (`scanners/trigger-scanner.mjs`). Inspects command/agent/skill `name` + `description` frontmatter for three activation-surface abuses: `TRG-shadow` (name collides with a built-in command/tool and intercepts it), `TRG-baiting` (description uses maximally-activating phrases — "anything", "always", "all files" — to bait indiscriminate invocation), and `TRG-broad` (a generic name plus a universal-applicability claim, so the unit auto-activates beyond its stated purpose). Descriptions pass through the decode pipeline (zero-width strip → homoglyph fold → `normalizeForScan`) first, so obfuscated baiting still trips. Thresholds and the phrase/built-in lists are policy-overridable via `.llm-security/policy.json` (`trg`). OWASP LLM06 (excessive agency); AST04. - **SIG — known-bad-identity signature engine** (`scanners/signature-scanner.mjs`). Detects known-malware *identity* — webshells, reverse shells, cryptominers, hacktools — complementary to the shape-based entropy/taint scanners. Unlike a raw byte-matcher (e.g. YARA), every signature is tested against both the raw bytes and the project decode pipeline (base64/hex/url/entity/unicode decode, homoglyph fold, rot13), so obfuscated known-malware is still caught. Rules ship in `knowledge/signatures.json`; families are policy-selectable. OWASP LLM03 (supply chain) primary; LLM02. - **AST — Python taint analysis** (`scanners/ast-taint-scanner.mjs`). Shells out to a PARSE-ONLY `python3` helper (`scanners/lib/py-ast-taint.py`) for variable-level, scope-aware taint analysis of Python skill code — higher recall/precision than the ~70%-recall regex `taint-tracer.mjs`. The helper only `ast.parse`s the target and never executes it, so analysing hostile code is side-effect-free. Falls back to the regex tracer whenever `python3` is unavailable, so the scan never hard-fails. OWASP LLM01, LLM02; AST02. ### Changed - `TRG`/`SIG`/`AST` registered as valid finding prefixes (`scanners/lib/output.mjs`); all three wired into the scan orchestrator and policy loader. `knowledge/` added to the `package.json` `files` whitelist. ## [7.7.2] - 2026-05-19 Language consistency pass. Norwegian had crept into surface text across v7.5-v7.7. Per the `~/.claude/CLAUDE.md` convention (English for code and documentation, Norwegian for dialog only), surface text was translated to English. No scanner, hook, or behavior changes — purely surface text. ### Changed - **18 skill commands `commands/*.md`** — the "HTML Report"-step appended by each `/security ` flow now reads `> **HTML report:** [Open in browser](file:///abs/path.html)` (previously Norwegian). - **CLI canonical module `scripts/lib/report-renderers.mjs`** — translated KEY_STATS_CONFIG labels (`TOTALT` → `TOTAL`, `KRITISK` → `CRITICAL`, `HØY` → `HIGH`, `FUNN` → `FINDINGS`, `PROSJEKTER` → `PROJECTS`, `MASKINKLASSE` → `MACHINE GRADE`, `SVAKEST` → `WEAKEST`, `NÅ-GRADE` → `CURRENT GRADE`, `AKSJONER` → `ACTIONS`, `MODUS` → `MODE`), the 5-step maturity ladder descriptions, the suppressed-group desc, 4 table-header sets, the 6 renderer `lede` defaults (plugin-audit, mcp-audit, harden, diff, watch, clean), the action tier labels (Umiddelbar/Høy prioritet/Medium prioritet → Immediate/High priority/ Medium priority), the clean buckets, and the dry-run/apply text. JS comments translated for consistency. Preserved the regex alternations `/^high|^høy/` and `/resolution|løsning/i` — they intentionally match Norwegian-language report markdown. - **Playground `playground/llm-security-playground.html`** — the same display strings as the canonical module (kept bit-identical), plus playground-specific UI text: catalog row labels, search placeholder, breadcrumb aria-label, theme-toggle labels, primary nav aria-label, builder-modal hints, "no projects yet" guide-panel, delete-project confirmation, alert/copy-confirm strings, and the field-from-tag "felles" pill (now "shared"). The hardcoded `Plugin v7.7.1` in `renderHome` bumped to `Plugin v7.7.2`, and `prosjekter`/`kommandoer` there became `projects`/`commands`. Demo-state fixture content for the `dft-komplett-demo` project (intentional Norwegian persona) and regex tokens were preserved. - **Agent prompts `agents/skill-scanner-agent.md` + `agents/mcp-scanner-agent.md`** — translated the `Generaliseringsgrense` and `Parallell Read-strategi` sections (identical content in both files) to `Generalization boundary` and `Parallel Read strategy`. - **`README.md`** — translated the Recent versions table rows for v7.5.0 → v7.7.1 and the playground architecture prose (L495-553). Version badge bumped to 7.7.2. - **`CLAUDE.md`** — translated the v7.7.1 highlights paragraph and added a new v7.7.2 highlights paragraph. Header and "release notes" sentinel bumped to v7.7.2. - **Marketplace root `../../README.md`** — translated the v7.5.0 → v7.7.1 llm-security bullet entries (lines 39-43). Version label in the header bumped to v7.7.2. The voyage and ms-ai-architect entries on lines 90-91 / 192-197 were not touched (strict plugin scope). - **Marketplace root `../../CLAUDE.md`** — translated the llm-security catalog entry on line 13 and bumped its version to v7.7.2. - **`docs/scanner-reference.md`** — translated the six runnable-examples table cells (L114-122) and the surrounding paragraph. - **`docs/version-history.md`** — added a v7.7.2 entry describing this pass. The v7.5.0 → v7.7.1 narrative sections retain the Norwegian they were written in (deferred per operator decision). - **`package.json` + `.claude-plugin/plugin.json`** — version 7.7.1 → 7.7.2. ### Preserved (intentional Norwegian) - Demo-state `dft-komplett-demo` JSON `description`, `system_description`, and parsed-data `"label": "HØY"` / `"label": "NÅ-GRADE"` entries — intentional Norwegian persona for the public-sector reference scenario. - Regex alternations `/^high|^høy/` and `/resolution|løsning/i` in both the canonical renderer and the playground inline copy — they let reports written in Norwegian still parse and route correctly. - `knowledge/norwegian-context.md` and other knowledge files — out of scope. - The v7.5.0 → v7.7.1 entries in CHANGELOG.md and `docs/version-history.md` remain in the language they were written in; rewriting historical release notes was deferred. - `REMEMBER.md`, `TODO.md`, `ROADMAP.md`, `*.local.md`, commit messages, test fixtures, and the `playground/A11Y-RAPPORT.md` artifact. ## [7.7.1] - 2026-05-18 Playground UX-strip etter v7.7.0-operatør-feedback. Hjem-overflaten ledet med prosjekter (Re-onboard / Nytt prosjekt / Command-katalog) — katalog var tredje kort, sekundært bak prosjekt-tracks. Operatør ba om å fjerne onboarding + prosjekter og beholde katalog ("Vi legger til funksjonalitet senere"). Ingen scanner- eller hook-atferdsendringer. ### Changed - **Playground routing — katalog som eneste levende overflate.** `renderActive()` tvinger alltid `activeSurface` til `'catalog'`. `renderOnboardingSurface`/`renderHomeSurface`/`renderProjectSurface`- funksjonene er bevart i kildekoden, men ikke rutbare før funksjonalitet legges til igjen. Init-default endret fra `'home'` til `'catalog'`, også for migrerte states fra IndexedDB. - **Playground topbar — Hjem + Re-onboard-knappene fjernet.** Bare `Katalog`-knappen beholdt i primær navigasjon, sammen med Eksporter/Importer + tema-toggle. Project-state forblir i IndexedDB men ingen UI-vei dit. - **Playground topbar breadcrumb — orgName erstattet med `llm-security`.** Etter at onboarding ble fjernet fra routing var `shared.organization.name` (demo-state) fortsatt synlig i toppen ("Direktoratet for digital tjenesteutvikling · Katalog"). Erstattet med statisk `llm-security · Katalog` som nøytralt scope-anker. ### Fixed - **Hardkodet versjons-streng i `renderHome`.** v7.7.0-versjonsbumpen fanget ikke `'Plugin v7.6.1'` på linje 6933 i `llm-security-playground.html` (template-string-litteral, ikke matching regex-mønster). Bumpet til v7.7.1. ### Notes - v7.7.1 bumpet kun versjons-strenger i 7 filer (`package.json`, `.claude-plugin/plugin.json`, plugin `README.md` badge + Recent versions-tabell, plugin `CLAUDE.md` header + state-seksjon, `docs/version-history.md`, `playground/llm-security-playground.html`, rot `README.md` plugin-entry, rot `CLAUDE.md` plugin-katalog). - Onboarding-konseptet er nå dokumentert som v7.8.0-kandidat (per-kommando kontekst-injeksjon) i ROADMAP.md. ## [7.7.0] - 2026-05-18 HTML-rapport for alle 18 skill-kommandoer som produserer rapport. Hver `/security ` printer nå en klikkbar `file://`-lenke til en self-contained HTML-versjon. Levert over fem sesjoner (UX-arbeid + renderer-extract + CLI + skill-wiring + release). Ingen scanner- eller hook-atferdsendringer — purely additive surface. ### Added - **Playground katalog list-view + builder-pane** (sesjon 1, `0dc7ff4`). Katalog-overflaten fikk list-view (grid-toggle) + builder-pane med copy-knapp på alle 18 rapporter, så onboarding-flytene blir bredere og dypere uten å forlate playground-modusen. - **Playground prosjekt-surface opprydding** (sesjon 2, `86d6ecd`). Stub-screen-håndtering (rapport ikke ferdig parsed → tydelig placeholder i stedet for tom panel), topbar-splitt (navigasjons- trinn vs. eksport-handlinger), generell DS-justering for prosjekt- overflate. - **`scripts/lib/report-renderers.mjs`** (sesjon 3, `fa5fb48`). De 18 inline parserne + 18 inline rendererne i playground-HTML-fila flyttet til canonical ESM-modul. Ren overflate: `import { PARSERS, RENDERERS } from './lib/report-renderers.mjs'`. Playground beholder bit-identisk inline-kopi (ESM `import` fungerer ikke fra `file://` uten Chrome/Firefox-flags). Canonical kilde + playground inline = to overflater, samme atferd. - **`scripts/render-report.mjs` CLI** (sesjon 4, `db80854`). Zero-dep Node-CLI som tar `commandId` + `--in`/`--out`-flags og konverterer markdown-rapporter til self-contained HTML. Stdin/file/stdout-modus, kebab→camel commandId-routing (alle 18 PARSERS fungerer automatisk uten hardkoding). Output inliner 6 DS-stylesheets (`tokens`, `base`, `components`, `tier2`, `tier3`, `tier3-supplement`) + lokal `.report-table`-CSS. ~140 KB self-contained HTML; fonter ikke inlined (ville blåst opp 7x til ~1 MB), `tokens.css` har `-apple-system, BlinkMacSystemFont, system-ui` som fallback. Absolutte `file://`-paths i stdout for Ghostty cmd-click. Default output `reports/-.html` relativt til CWD. - **HTML-rapport for alle 18 skill-kommandoer** (sesjon 4-5). Sesjon 4 wired 4 skills (`scan`, `audit`, `posture`, `deep-scan`). Sesjon 5 wired de 14 resterende (`plugin-audit`, `mcp-audit`, `mcp-inspect`, `ide-scan`, `supply-check`, `dashboard`, `pre-deploy`, `diff`, `watch`, `registry`, `clean`, `harden`, `threat-model`, `red-team`). Hver skill-fil har en avsluttende "HTML Report"-step som instruerer Claude å (1) compute temp md-path, (2) Write hele markdown-rapporten verbatim, (3) kjøre CLI, (4) appende `> **HTML-rapport:** [Åpne i nettleser](file:///abs/sti.html)` til respons. ### Changed - Playground beholder inline-kopi av parserne og rendererne for å forbli single-file `file://`-distribuerbar — ESM `import` fungerer ikke fra `file://`-URLs uten Chrome/Firefox-flags. Canonical kilden i `scripts/lib/report-renderers.mjs` og playground inline-kopien er bit-identisk per release. ### Notes - Pre-existing `pre-compact-scan`-perf-flake (1000 ms terskel under last) gjenstår — defer til v7.7.x patch. - Sync-test mellom `scripts/lib/report-renderers.mjs` og playground inline-kopi planlagt som v7.7.x patch (krever scope-utvidelse til `tests/`). ## [7.6.1] - 2026-05-06 Playground v7.6.0 visuell-patch. Seks bugs fanget under maintainer- verifisering i nettleser; alle skyldes mismatch mellom DS-klasser og hvordan playground-rendrere brukte dem (eller manglende DS-implementasjoner av klasser playground-rendrere antok eksisterte). Ingen scanner- eller hook-behavior-changes. ### Fixed - **`renderFindingsBlock` brukte `.findings` outer-class** som DS har som 2-kolonners grid (`grid-template-columns: 360px 1fr`) for list+detail- panel-layout. Resultat: findings-headeren havnet i venstre 360px- kolonne og items i 1fr-kolonnen, brutt layout i alle 18 rapporter med findings. Erstattet med `
` + `

` + `findings__list > findings__group > findings__group-header + findings__items` (korrekt DS-mønster). - **`.report-table` mangler i DS** men brukes i 7+ rendrere (OWASP- kategorier, Supply chain, Scanner Risk Matrix, Plugin-meta, Permission- matrise, Live-meter, Siste runs, Godkjenninger, Mitigation roadmap). Lagt lokal CSS-implementasjon i playground-HTML `