From 3cd68dc9fbb8b87301dda31f7ff67299f0ef23fc Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 29 Apr 2026 13:56:11 +0200 Subject: [PATCH] =?UTF-8?q?docs(severity):=20B3=20=E2=80=94=20document=20i?= =?UTF-8?q?nfo=20as=20scoring-inert=20(v7.2.0=20prep)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical-review §2 B3 finding: `riskScore({info: N}) = 0` silently masks info-volume findings. The behavior was correct (info is scoring-inert by design) but undocumented. Operators reading a report with N info findings had no way to know they contribute zero to verdict/band. Three coordinated edits: - scanners/lib/severity.mjs JSDoc — explicit "Info severity" subsection spelling out: scoring-inert, surfaced in owaspCategorize aggregates, treat as observability telemetry not verdict input. @param updated to mark info as accepted but ignored. - CLAUDE.md v7.0.0 risk-score-v2 line — one-sentence anchor pointing to severity.mjs JSDoc. - tests/lib/severity.test.mjs — anchor test alongside the existing 4-critical=93 anchor: asserts riskScore({info: 50}) === 0, riskScore({info: 1000}) === 0, verdict({info: 100}) === 'ALLOW', riskBand(riskScore({info: 500})) === 'Low'. Decision: skip the optional `infoScore()` helper from the brief. No current consumer would use it; doc-only fix keeps API surface minimal. Revisit if a consumer emerges. Tests: 1522 → 1523 (+1 anchor block, 4 assertions). All green. --- plugins/llm-security/CLAUDE.md | 2 +- plugins/llm-security/scanners/lib/severity.mjs | 13 ++++++++++++- plugins/llm-security/tests/lib/severity.test.mjs | 10 ++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/plugins/llm-security/CLAUDE.md b/plugins/llm-security/CLAUDE.md index 59f118e..24c93a5 100644 --- a/plugins/llm-security/CLAUDE.md +++ b/plugins/llm-security/CLAUDE.md @@ -4,7 +4,7 @@ Security scanning, auditing, and threat modeling for Claude Code projects. 5 fra **v7.0.0 — Severity-dominated risk scoring (v2 model, 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). +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). `info` findings are observability-only — counted in OWASP aggregates but contribute zero to risk_score, verdict, and riskBand (B3, v7.2.0 — was undocumented pre-7.2.0). See `severity.mjs` JSDoc for full contract. 2. **Rule-based entropy scanner with file-extension skip, 8 line-level suppression rules, and configurable policy** — extensions skipped (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`); 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.). diff --git a/plugins/llm-security/scanners/lib/severity.mjs b/plugins/llm-security/scanners/lib/severity.mjs index 0ed2dab..70cbe08 100644 --- a/plugins/llm-security/scanners/lib/severity.mjs +++ b/plugins/llm-security/scanners/lib/severity.mjs @@ -26,7 +26,18 @@ const SEVERITY_WEIGHTS_V1 = { critical: 25, high: 10, medium: 4, low: 1, info: 0 * Low only → 1-11 (1=4, 10=11) * None → 0 * - * @param {{ critical: number, high: number, medium: number, low: number, info: number }} counts + * Info severity (B3, v7.2.0): + * `info` counts are scoring-inert — accepted in the input shape but + * ignored by this formula. They contribute 0 to risk_score, do not + * affect verdict (BLOCK/WARNING/ALLOW), and do not affect riskBand + * (Low/Medium/High/Critical/Extreme). They ARE surfaced in + * `owaspCategorize` aggregates and in scanner report bodies for + * observability. Operators reading a report with N info findings + * should treat them as informational telemetry, not as input to + * the verdict. + * + * @param {{ critical: number, high: number, medium: number, low: number, info?: number }} counts + * `info` is accepted for shape completeness but ignored — see above. * @returns {number} 0-100 risk score */ export function riskScore(counts) { diff --git a/plugins/llm-security/tests/lib/severity.test.mjs b/plugins/llm-security/tests/lib/severity.test.mjs index 573b235..e1feb15 100644 --- a/plugins/llm-security/tests/lib/severity.test.mjs +++ b/plugins/llm-security/tests/lib/severity.test.mjs @@ -301,6 +301,16 @@ describe('verdict/riskBand co-monotonicity (v7.0.0 §5.4)', () => { // 70 + min(25, log2(5)*10) = 70 + 23.219... = 93.219 → round → 93. assert.equal(riskScore({ critical: 4 }), 93); }); + + it('info severity is scoring-inert (B3, v7.2.0)', () => { + // Documented contract: `info` counts contribute zero to risk_score, + // do not affect verdict, do not affect riskBand. Pinned here against + // any future change that would (intentionally or not) start scoring info. + assert.equal(riskScore({ info: 50 }), 0); + assert.equal(riskScore({ info: 1000 }), 0); + assert.equal(verdict({ info: 100 }), 'ALLOW'); + assert.equal(riskBand(riskScore({ info: 500 })), 'Low'); + }); }); // ---------------------------------------------------------------------------