# 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] ### Removed — BREAKING - **The four `LLM_SECURITY_*` configuration env-vars deprecated in v7.3.0.** `.llm-security/policy.json` is now the only source. A removed variable is **inert**: it neither warns nor configures, so a project relying on one silently returns to the default. See the Migration section in `README.md` for the detection commands. | Removed env-var | Policy key | Default | |-----------------|------------|---------| | `LLM_SECURITY_INJECTION_MODE` | `injection.mode` | `block` | | `LLM_SECURITY_TRIFECTA_MODE` | `trifecta.mode` | `warn` | | `LLM_SECURITY_ESCALATION_WINDOW` | `trifecta.escalation_window` | `5` | | `LLM_SECURITY_AUDIT_LOG` | `audit.log_path` | unset | | `LLM_SECURITY_DEPRECATION_QUIET` | *(none)* | dies with the warning it silenced | Env-vars with no policy equivalent are unaffected: `LLM_SECURITY_PRECOMPACT_MODE`, `LLM_SECURITY_PRECOMPACT_MAX_BYTES`, `LLM_SECURITY_UPDATE_CHECK`, `LLM_SECURITY_MCP_CACHE_FILE`, `LLM_SECURITY_IDE_ROOTS`. `getPolicyValueWithEnvWarn` and its one-shot stderr deprecation line are deleted from `scanners/lib/policy-loader.mjs`; the four call sites collapse to `getPolicyValue`. Hook strings that advertised a removed variable as the escape hatch now name the policy key — a blocked user following the old text would have set a variable that does nothing. - **`riskScoreV1()`** in `scanners/lib/severity.mjs`, plus its `SEVERITY_WEIGHTS_V1` table. `@deprecated` since v7.0.0, kept for diff/comparison, with zero callers in code or tests. The v1 weights are recorded here for anyone re-deriving an old score: `critical 25, high 10, medium 4, low 1, info 0`, summed and capped at 100. `riskScore()` (v2) is untouched, so no score, band, or verdict moves. - **Two documented env-vars that were never implemented.** `LLM_SECURITY_SCR_OFFLINE` (`docs/ci-cd-guide.md`) and `LLM_SECURITY_OFFLINE` (`examples/supply-chain-attack/README.md`) were promised as OSV.dev / npm-audit kill-switches; no code has ever read either. The docs now say plainly that there is no kill-switch and that an air-gapped run must block egress at the network layer. The `LLM_SECURITY_AUDIT_*` wildcard phrasing (README, `docs/scanner-reference.md`, `docs/security-hardening-guide.md`) is narrowed to the single real key. ### Changed - **Posture category 12 (Rule of Two) no longer keys off the identifier `TRIFECTA_MODE`.** The check was `/TRIFECTA_MODE/i` over the session-guard source, which measured what a constant was *named* rather than whether enforcement was configurable at all. It now matches `getPolicyValue('trifecta', 'mode', …)`, and still accepts a pre-v8 vendored guard reading `LLM_SECURITY_TRIFECTA_MODE` — third-party projects carry their own hook copy and are equally configurable. Without this, every correctly migrated project would have dropped from PASS to PARTIAL. The PARTIAL finding now recommends the policy key instead of the removed env-var. ## [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 `