From 765bc74f52803def1a02ab101c728e8ecd876edb Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 19 Apr 2026 22:37:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(llm-security):=20v7.0.0=20commit=207=20?= =?UTF-8?q?=E2=80=94=20rule=2018=20(markdown=20image=20URL=20suppression)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E verification against content-heavy repo (`content-claude-code`) revealed 413 entropy findings (8 HIGH / 405 MEDIUM) from markdown image CDN URLs in JSON content indexes — e.g., `![Image 1: Title](https://www-cdn.anthropic.com/images/.../cf1dd2167fcf12f5882333ddc58a5bc1f0026952.svg)`. These are legitimate content-repo artifacts, not credentials. The 40-char hash segment in the CDN URL trips Shannon entropy (H=5.29 over 300 chars), and rule 13 (inline ) doesn't match since there's no literal `` tag — the `.svg` is just a URL path suffix. Added rule 18 `MARKDOWN_IMAGE = /!\[[^\]]*\]\(\s*https?:\/\//` — matches `![alt](http…)` / `![alt](https…)`. Line-level (not string-level) so URL is not over-specific. E2E impact on `content-claude-code`: - Before: BLOCK / 65 / 8H 437M 0L - After: WARNING / 56 / 3H 427M 0L Hyperframes unchanged: BLOCK / 80 / 1C 4H 92M — real CRITICAL SQL-injection and HIGH findings still detected. Tests: 2 new (positive + negative fixture) bringing entropy-context to 26, total suite 1485 → 1487. Docs updated to "rules 11-18" and "8 new line-suppression rules". Co-Authored-By: Claude Opus 4.7 --- README.md | 2 +- plugins/llm-security/CHANGELOG.md | 4 ++-- plugins/llm-security/CLAUDE.md | 4 ++-- plugins/llm-security/README.md | 2 +- .../docs/security-hardening-guide.md | 6 +++-- .../llm-security/scanners/entropy-scanner.mjs | 12 +++++++++- .../tests/scanners/entropy-context.test.mjs | 22 +++++++++++++++++++ 7 files changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 558bc07..0d8d6f9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Built on OWASP LLM Top 10 (2025), OWASP Agentic AI Top 10, and the AI Agent Trap Key commands: `/security posture`, `/security audit`, `/security scan`, `/security ide-scan`, `/security threat-model`, `/security plugin-audit` -6 specialized agents · 22 scanners · 9 hooks · 20 knowledge docs · 1485 tests +6 specialized agents · 22 scanners · 9 hooks · 20 knowledge docs · 1487 tests → [Full documentation](plugins/llm-security/README.md) diff --git a/plugins/llm-security/CHANGELOG.md b/plugins/llm-security/CHANGELOG.md index baecfaa..b31bea5 100644 --- a/plugins/llm-security/CHANGELOG.md +++ b/plugins/llm-security/CHANGELOG.md @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - **Context-aware entropy scanner** (`scanners/entropy-scanner.mjs`). Skip-lists and line-level rules drastically reduce false positives in shader/CSS/HTML/SQL-heavy codebases: - File-extension skip: `.glsl, .frag, .vert, .shader, .wgsl, .css, .scss, .sass, .less, .svg` + compound `.min.js, .min.css, .map` - - Line-level rules 11–17 in `isFalsePositive()`: GLSL keywords (`uniform`, `vec3`, `texture2D`...), CSS-in-JS templates (`styled.`), inline `` markup, ffmpeg `filter_complex` syntax, browser `User-Agent` strings, SQL DDL on dedicated lines (`^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|...)`), `throw new Error(\`…\`)` templates + - Line-level rules 11–18 in `isFalsePositive()`: GLSL keywords (`uniform`, `vec3`, `texture2D`...), CSS-in-JS templates (`styled.`), inline `` markup, ffmpeg `filter_complex` syntax, browser `User-Agent` strings, SQL DDL on dedicated lines (`^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|...)`), `throw new Error(\`…\`)` templates, markdown image syntax with external URLs (`![alt](https://cdn…)` — common in JSON content indexes) - Scanner envelope gains `calibration` block: `files_skipped_by_extension`, `files_skipped_by_path`, effective `thresholds`, and `policy_source` (`'default' | 'policy.json'`) - **Policy-driven entropy configuration** — `.llm-security/policy.json` `entropy` section accepts: - `thresholds.{critical,high,medium}.{entropy,minLen}` — override defaults per project @@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - npm: `knip`, `oxlint`, `tsx`, `nx`, `rimraf`, `glob`, `tar`, `zod`, `ky`, `ow`, `esm`, `ip`, `qs`, `url`, `prettier`, `vitest`, `vite`, `rollup`, `swc`, `turbo`, `bun`, `deno` - PyPI: `uv`, `ruff`, `rich`, `typer`, `anyio` - **Synthesizer "Scan Calibration" section** (`agents/deep-scan-synthesizer-agent.md`). Heuristic: omit if <5% files skipped, flag prominently if >80% skipped by path (signals over-aggressive user policy). Agent instructed to NEVER override scanner verdict with narrative opinion. -- **24 new unit tests** (`tests/scanners/entropy-context.test.mjs`): A. File-extension skip (4), B. Line-level rules 11–17 (8), C. Policy overrides (3); plus expanded `tests/lib/severity.test.mjs` with v2 scoring/band/verdict tables (70 tests total, was 52). **Total: 1485 tests (was 1461).** +- **26 new unit tests** (`tests/scanners/entropy-context.test.mjs`): A. File-extension skip (4), B. Line-level rules 11–18 (10), C. Policy overrides (3); plus expanded `tests/lib/severity.test.mjs` with v2 scoring/band/verdict tables (70 tests total, was 52). **Total: 1487 tests (was 1461).** ### Changed - `tests/lib/output.test.mjs:243` — "1 critical = score 80" under v2 (was 25 under v1). diff --git a/plugins/llm-security/CLAUDE.md b/plugins/llm-security/CLAUDE.md index c41cfa7..78bd0cc 100644 --- a/plugins/llm-security/CLAUDE.md +++ b/plugins/llm-security/CLAUDE.md @@ -1,11 +1,11 @@ # LLM Security Plugin (v7.0.0) -Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 1485 tests. +Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 1487 tests. **v7.0.0 — Trustworthy scoring (BREAKING).** Three changes target the false-positive cascade on real codebases (hyperframes.com gave `BLOCK / Extreme / 100`, ~70% noise): 1. **Risk-score v2 formula** (`scanners/lib/severity.mjs`) — severity-dominated, log-scaled within tier. Replaces v1 sum-and-cap that collapsed every non-trivial scan to 100/Extreme. Tiers: critical → 70–95, high only → 40–65, medium only → 15–35, low only → 1–11. Verdict cutoffs realigned to new bands (BLOCK ≥65, WARNING ≥15). -2. **Context-aware entropy scanner** — file-extension skip (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`) + 7 new line-suppression rules (GLSL keywords, CSS-in-JS, inline SVG, ffmpeg `filter_complex`, User-Agent strings, SQL DDL, `throw new Error(\`...\`)`). Configurable via `.llm-security/policy.json` `entropy` section (thresholds, `suppress_extensions`, `suppress_line_patterns`, `suppress_paths`). Envelope `calibration` block reports skip counters + effective thresholds + policy source. +2. **Context-aware entropy scanner** — file-extension skip (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`) + 8 new line-suppression rules (GLSL keywords, CSS-in-JS, inline SVG, ffmpeg `filter_complex`, User-Agent strings, SQL DDL, `throw new Error(\`...\`)`, markdown image URLs). Configurable via `.llm-security/policy.json` `entropy` section (thresholds, `suppress_extensions`, `suppress_line_patterns`, `suppress_paths`). Envelope `calibration` block reports skip counters + effective thresholds + policy source. 3. **DEP typosquat allowlist expansion** — 22 npm + 5 PyPI entries for short-name tools that tripped Levenshtein detection on every modern codebase (`knip`, `oxlint`, `tsx`, `nx`, `rimraf`, `uv`, `ruff`, etc.). See `docs/security-hardening-guide.md` §6 for the calibration story. diff --git a/plugins/llm-security/README.md b/plugins/llm-security/README.md index 8cbc095..6eec5ab 100644 --- a/plugins/llm-security/README.md +++ b/plugins/llm-security/README.md @@ -824,7 +824,7 @@ This plugin provides full-stack security hardening (static analysis + supply cha | Version | Date | Highlights | |---------|------|------------| -| **7.0.0** | 2026-04-19 | **Trustworthy scoring (BREAKING).** Three changes target the false-positive cascade on real codebases (scan of hyperframes.com gave `BLOCK / Extreme / 100` with ~70% noise). **1. Risk-score v2** (`scanners/lib/severity.mjs`) — severity-dominated, log-scaled within tier. Replaces sum-and-cap that collapsed every non-trivial scan to 100/Extreme. Tiers: critical → 70–95, high only → 40–65, medium only → 15–35, low only → 1–11. Verdict cutoffs realigned (BLOCK ≥65, WARNING ≥15) for band co-monotonicity. **2. Context-aware entropy scanner** — file-extension skip (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`) + 7 new line-suppression rules (GLSL keywords, CSS-in-JS templates, inline SVG, ffmpeg `filter_complex`, User-Agent strings, SQL DDL on dedicated lines, `throw new Error(\`...\`)`). Configurable via `.llm-security/policy.json` `entropy` section (thresholds, `suppress_extensions`, `suppress_line_patterns`, `suppress_paths`). Envelope `calibration` block reports skip counters + effective thresholds + policy source. **3. DEP typosquat allowlist expansion** — 22 npm + 5 PyPI entries for short-name tools that tripped Levenshtein on every modern codebase (`knip`, `oxlint`, `tsx`, `nx`, `rimraf`, `uv`, `ruff`, etc.). Synthesizer "Scan Calibration" section + "never override verdict" rule added. Legacy `riskScoreV1()` kept for reference. **CI pipelines with `--fail-on` thresholds may need recalibration.** 1485 tests (was 1461). | +| **7.0.0** | 2026-04-19 | **Trustworthy scoring (BREAKING).** Three changes target the false-positive cascade on real codebases (scan of hyperframes.com gave `BLOCK / Extreme / 100` with ~70% noise). **1. Risk-score v2** (`scanners/lib/severity.mjs`) — severity-dominated, log-scaled within tier. Replaces sum-and-cap that collapsed every non-trivial scan to 100/Extreme. Tiers: critical → 70–95, high only → 40–65, medium only → 15–35, low only → 1–11. Verdict cutoffs realigned (BLOCK ≥65, WARNING ≥15) for band co-monotonicity. **2. Context-aware entropy scanner** — file-extension skip (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`) + 8 new line-suppression rules (GLSL keywords, CSS-in-JS templates, inline SVG, ffmpeg `filter_complex`, User-Agent strings, SQL DDL on dedicated lines, `throw new Error(\`...\`)`, markdown image URLs). Configurable via `.llm-security/policy.json` `entropy` section (thresholds, `suppress_extensions`, `suppress_line_patterns`, `suppress_paths`). Envelope `calibration` block reports skip counters + effective thresholds + policy source. **3. DEP typosquat allowlist expansion** — 22 npm + 5 PyPI entries for short-name tools that tripped Levenshtein on every modern codebase (`knip`, `oxlint`, `tsx`, `nx`, `rimraf`, `uv`, `ruff`, etc.). Synthesizer "Scan Calibration" section + "never override verdict" rule added. Legacy `riskScoreV1()` kept for reference. **CI pipelines with `--fail-on` thresholds may need recalibration.** 1487 tests (was 1461). | | **6.6.0** | 2026-04-18 | **JetBrains/IntelliJ plugin scanning.** `/security ide-scan` now covers JetBrains IDEs (IntelliJ IDEA, PyCharm, GoLand, WebStorm, RubyMine, PhpStorm, CLion, DataGrip, RustRover, Rider, Aqua, Writerside, Android Studio) — Fleet and Toolbox excluded. OS-aware discovery of `~/Library/Application Support/JetBrains//plugins/` (macOS), `%APPDATA%\JetBrains\...` (Windows), `~/.config/JetBrains/...` (Linux). Zero-dep parsers for `META-INF/plugin.xml` and `META-INF/MANIFEST.MF` with nested-jar extraction. 7 JetBrains-specific checks: theme-with-code, broad activation (`application-components`), `Premain-Class` instrumentation (HIGH — javaagent retransform), native binaries (`.so`/`.dylib`/`.dll`/`.jnilib`), long `` chains (supply-chain pressure), typosquat vs top JetBrains plugins, shaded-jar advisory. URL fetch for `plugins.jetbrains.com/plugin/-` + direct `/plugin/download?pluginId=`; metadata resolves numericId → xmlId before download. `.kt`, `.groovy`, `.scala` added to `taint-tracer` code extensions. Reuses existing OS sandbox (`lib/vsix-sandbox.mjs` parameterized via `buildSandboxedWorker(..., workerPath)`). Knowledge: `knowledge/jetbrains-marketplace-api-notes.md`, expanded `knowledge/ide-extension-threat-patterns.md`, seeded `knowledge/top-jetbrains-plugins.json`. 1461 tests (was 1352). | | **6.5.0** | 2026-04-17 | **OS sandbox for `/security ide-scan `.** VSIX fetch + extract now runs in a sub-process wrapped by `sandbox-exec` (macOS) or `bwrap` (Linux), reusing the same primitives proven by the v5.1 git-clone sandbox. Defense-in-depth — even if `lib/zip-extract.mjs` ever has a bypass, the kernel refuses any write outside the per-scan temp directory. New: `lib/vsix-fetch-worker.mjs` (sub-process worker with deterministic JSON-line IPC) and `lib/vsix-sandbox.mjs` (`buildSandboxProfile` / `buildBwrapArgs` / `buildSandboxedWorker` / `runVsixWorker`, 35 s timeout, 1 MB stdout cap). New `scan(target, { useSandbox })` option (default `true` for CLI; tests use `false` since `globalThis.fetch` mocks do not cross processes). Windows fallback: in-process with `meta.warnings` advisory. Envelope `meta.source.sandbox` field: `'sandbox-exec' \| 'bwrap' \| 'none' \| 'in-process'`. 1352 tests (was 1344). | | **6.4.0** | 2026-04-17 | **`/security ide-scan ` — pre-install verification.** The IDE extension scanner now accepts URLs and fetches the VSIX before scanning. Supported: VS Code Marketplace (`https://marketplace.visualstudio.com/items?itemName=publisher.name`), OpenVSX (`https://open-vsx.org/extension/publisher/name[/version]`), and direct `.vsix` URLs. New libraries: `lib/vsix-fetch.mjs` (HTTPS-only fetch with 50MB cap, 30s timeout, SHA-256, manual host-whitelisted redirects) and `lib/zip-extract.mjs` (zero-dep ZIP parser, rejects zip-slip / symlinks / absolute paths / drive letters / encrypted entries / ZIP64; caps: 10 000 entries, 500MB uncompressed, 100x expansion ratio, depth 20). Temp dir always cleaned in `try/finally`. Envelope `meta.source` carries `{ type: "url", kind, url, finalUrl, sha256, size, publisher, name, version }`. New knowledge file: `marketplace-api-notes.md`. GitHub repo URLs intentionally not supported (would require a build step). 1344 tests (was 1296). | diff --git a/plugins/llm-security/docs/security-hardening-guide.md b/plugins/llm-security/docs/security-hardening-guide.md index 124787c..11c8c5d 100644 --- a/plugins/llm-security/docs/security-hardening-guide.md +++ b/plugins/llm-security/docs/security-hardening-guide.md @@ -192,11 +192,13 @@ suppression layers: .css, .scss, .sass, .less, .svg` + compound `.min.js, .min.css, .map`. A skip counter (`calibration.files_skipped_by_extension`) is reported in the scanner envelope. -2. **Line-level rules 11–17** — applied when a line contains any of: GLSL +2. **Line-level rules 11–18** — applied when a line contains any of: GLSL keywords (`uniform`, `vec3`, `texture2D`…), CSS-in-JS templates (`styled.…`), inline `` markup, ffmpeg `filter_complex` syntax, browser `User-Agent` strings, SQL DDL on a dedicated line - (`^\s*(SELECT|INSERT|…)`), or `throw new Error(\`…\`)` templates. + (`^\s*(SELECT|INSERT|…)`), `throw new Error(\`…\`)` templates, or + markdown image syntax with external URL (`![alt](https://cdn…)` — common + in JSON content indexes / article metadata). 3. **Per-project policy override** — `.llm-security/policy.json` `entropy` section supports: diff --git a/plugins/llm-security/scanners/entropy-scanner.mjs b/plugins/llm-security/scanners/entropy-scanner.mjs index cc9c0dd..bbd7ce7 100644 --- a/plugins/llm-security/scanners/entropy-scanner.mjs +++ b/plugins/llm-security/scanners/entropy-scanner.mjs @@ -169,6 +169,13 @@ const SQL_STATEMENT = /^\s*(?:SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|WITH|DROP /** Error-message templates with embedded HTML/markup (throw new Error("
...
")). */ const ERROR_TEMPLATE = /(?:throw\s+new\s+(?:Error|TypeError|RangeError|SyntaxError)|new\s+Error\s*\()\s*[`'"]/; +/** + * Markdown image syntax with external URL — `![alt](https://cdn.../hash.ext)`. + * Common in JSON data indexes / article metadata; CDN URL hash segments + * produce high Shannon entropy but are not credentials. + */ +const MARKDOWN_IMAGE = /!\[[^\]]*\]\(\s*https?:\/\//; + // --------------------------------------------------------------------------- // False-positive suppression helpers // --------------------------------------------------------------------------- @@ -248,7 +255,10 @@ function isFalsePositive(str, line, absPath) { // 17. Error-message templates (throw new Error("...")) if (ERROR_TEMPLATE.test(line)) return true; - // 18. User-policy regex patterns from .llm-security/policy.json + // 18. Markdown image syntax with external URL — CDN hash noise in content repos + if (MARKDOWN_IMAGE.test(line)) return true; + + // 19. User-policy regex patterns from .llm-security/policy.json for (const pattern of USER_SUPPRESS_LINE_PATTERNS) { if (pattern.test(line)) return true; } diff --git a/plugins/llm-security/tests/scanners/entropy-context.test.mjs b/plugins/llm-security/tests/scanners/entropy-context.test.mjs index d17ea58..bf84d4b 100644 --- a/plugins/llm-security/tests/scanners/entropy-context.test.mjs +++ b/plugins/llm-security/tests/scanners/entropy-context.test.mjs @@ -186,6 +186,28 @@ describe('entropy-scanner context suppression (v7.0.0+)', () => { assert.equal(result.findings.length, 0, 'expected throw new Error line to suppress'); await rm(fx, { recursive: true, force: true }); }); + + it('rule 18: markdown image with external URL suppresses finding', async () => { + const fx = await newRoot('ent-rule18-'); + await writeFixture(fx, 'index.json', + '{"summary": "![Image 1: Title](https://cdn.example.com/abc/' + PAYLOAD + '.svg)"}'); + resetCounter(); + const discovery = await discoverFiles(fx); + const result = await scan(fx, discovery); + assert.equal(result.findings.length, 0, 'expected markdown image line to suppress'); + await rm(fx, { recursive: true, force: true }); + }); + + it('rule 18 does NOT over-match plain URLs without image syntax', async () => { + const fx = await newRoot('ent-rule18b-'); + await writeFixture(fx, 'app.js', + 'const token = "' + PAYLOAD + '"; // not an image'); + resetCounter(); + const discovery = await discoverFiles(fx); + const result = await scan(fx, discovery); + assert.ok(result.findings.length >= 1, 'plain high-entropy string must still be detected'); + await rm(fx, { recursive: true, force: true }); + }); }); describe('C. Policy-driven overrides', () => {