feat(llm-security)!: v7.0.0 commit 1 — severity-dominated log-scaled risk score
Replace sum-and-cap formula (every non-trivial scan → 100/Extreme) with severity-dominated, log-scaled-within-tier model. Discriminates actual risk: 1 critical = 80, 2 critical = 86, 17 high = 65. Hyperframes-class rendering codebases no longer collapse to Extreme just from shader noise. Changes: - scanners/lib/severity.mjs: new riskScore() v2; keep riskScoreV1() for reference; riskBand() cutoffs aligned (14/39/64/84). - scanners/posture-scanner.mjs: delete inline duplicate formula, import riskScore/riskBand/verdict from severity.mjs. Single source of truth. Breaking: aggregate.risk_score semantics change. Batched with entropy suppression (Commit 2+) under v7.0.0 bump in Commit 6. Do not release individually — JSON consumers depend on scoring band stability. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a86b897583
commit
d83424a782
2 changed files with 60 additions and 28 deletions
|
|
@ -9,20 +9,56 @@ export const SEVERITY = Object.freeze({
|
|||
INFO: 'info',
|
||||
});
|
||||
|
||||
const SEVERITY_WEIGHTS = { critical: 25, high: 10, medium: 4, low: 1, info: 0 };
|
||||
// Legacy weights — used only by riskScoreV1() for backwards-compat reference.
|
||||
const SEVERITY_WEIGHTS_V1 = { critical: 25, high: 10, medium: 4, low: 1, info: 0 };
|
||||
|
||||
/**
|
||||
* Calculate aggregate risk score from severity counts.
|
||||
* Calculate aggregate risk score from severity counts (v2 model — v7.0.0+).
|
||||
*
|
||||
* Severity-dominated, log-scaled within tier. Replaces the v1 sum-and-cap
|
||||
* formula which collapsed every non-trivial scan to 100/Extreme regardless
|
||||
* of actual risk distribution.
|
||||
*
|
||||
* Tiers:
|
||||
* Critical present → 70-95 (1=80, 2=86, 4=90, 10=95)
|
||||
* High only → 40-65 (1=48, 5=60, 17=65)
|
||||
* Medium only → 15-35 (1=20, 5=28, 50=33)
|
||||
* Low only → 1-11 (1=4, 10=11)
|
||||
* None → 0
|
||||
*
|
||||
* @param {{ critical: number, high: number, medium: number, low: number, info: number }} counts
|
||||
* @returns {number} 0-100 capped score
|
||||
* @returns {number} 0-100 risk score
|
||||
*/
|
||||
export function riskScore(counts) {
|
||||
const critical = counts.critical || 0;
|
||||
const high = counts.high || 0;
|
||||
const medium = counts.medium || 0;
|
||||
const low = counts.low || 0;
|
||||
|
||||
let base;
|
||||
if (critical > 0) base = 70 + Math.min(25, Math.log2(critical + 1) * 10);
|
||||
else if (high > 0) base = 40 + Math.min(25, Math.log2(high + 1) * 8);
|
||||
else if (medium > 0) base = 15 + Math.min(20, Math.log2(medium + 1) * 5);
|
||||
else if (low > 0) base = 1 + Math.min(10, Math.log2(low + 1) * 3);
|
||||
else base = 0;
|
||||
|
||||
return Math.round(Math.min(100, base));
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy v1 risk score formula — kept for diff/comparison only.
|
||||
* Not exported in production paths; reference for CI re-calibration.
|
||||
*
|
||||
* @param {{ critical: number, high: number, medium: number, low: number, info: number }} counts
|
||||
* @returns {number} 0-100 capped score (sum-and-cap model)
|
||||
*/
|
||||
export function riskScoreV1(counts) {
|
||||
const raw =
|
||||
(counts.critical || 0) * SEVERITY_WEIGHTS.critical +
|
||||
(counts.high || 0) * SEVERITY_WEIGHTS.high +
|
||||
(counts.medium || 0) * SEVERITY_WEIGHTS.medium +
|
||||
(counts.low || 0) * SEVERITY_WEIGHTS.low +
|
||||
(counts.info || 0) * SEVERITY_WEIGHTS.info;
|
||||
(counts.critical || 0) * SEVERITY_WEIGHTS_V1.critical +
|
||||
(counts.high || 0) * SEVERITY_WEIGHTS_V1.high +
|
||||
(counts.medium || 0) * SEVERITY_WEIGHTS_V1.medium +
|
||||
(counts.low || 0) * SEVERITY_WEIGHTS_V1.low +
|
||||
(counts.info || 0) * SEVERITY_WEIGHTS_V1.info;
|
||||
return Math.min(raw, 100);
|
||||
}
|
||||
|
||||
|
|
@ -41,14 +77,21 @@ export function verdict(counts) {
|
|||
|
||||
/**
|
||||
* Map a 0-100 risk score to a human-readable risk band.
|
||||
* Cutoffs aligned to v2 riskScore() tier structure (v7.0.0+):
|
||||
* 0-14 Low (no findings, or low-tier only)
|
||||
* 15-39 Medium (medium-tier dominant)
|
||||
* 40-64 High (high-tier dominant)
|
||||
* 65-84 Critical (1 critical, or many high)
|
||||
* 85-100 Extreme (multiple critical findings)
|
||||
*
|
||||
* @param {number} score - 0-100 risk score
|
||||
* @returns {'Low' | 'Medium' | 'High' | 'Critical' | 'Extreme'}
|
||||
*/
|
||||
export function riskBand(score) {
|
||||
if (score <= 20) return 'Low';
|
||||
if (score <= 40) return 'Medium';
|
||||
if (score <= 60) return 'High';
|
||||
if (score <= 80) return 'Critical';
|
||||
if (score <= 14) return 'Low';
|
||||
if (score <= 39) return 'Medium';
|
||||
if (score <= 64) return 'High';
|
||||
if (score <= 84) return 'Critical';
|
||||
return 'Extreme';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { readFile, readdir, stat, access } from 'node:fs/promises';
|
|||
import { join, resolve, relative, extname } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { scanForInjection } from './lib/injection-patterns.mjs';
|
||||
import { gradeFromPassRate, SEVERITY } from './lib/severity.mjs';
|
||||
import { gradeFromPassRate, riskScore, riskBand, verdict, SEVERITY } from './lib/severity.mjs';
|
||||
import { finding, scannerResult, resetCounter } from './lib/output.mjs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1489,21 +1489,10 @@ export async function scan(targetPath) {
|
|||
|
||||
const grade = gradeFromPassRate(passRate, failsInCritCats, counts.critical);
|
||||
|
||||
// Risk score
|
||||
const riskScoreValue = Math.min(
|
||||
counts.critical * 25 + counts.high * 10 + counts.medium * 4 + counts.low * 1,
|
||||
100,
|
||||
);
|
||||
|
||||
const riskBandValue =
|
||||
riskScoreValue <= 20 ? 'Low' :
|
||||
riskScoreValue <= 40 ? 'Medium' :
|
||||
riskScoreValue <= 60 ? 'High' :
|
||||
riskScoreValue <= 80 ? 'Critical' : 'Extreme';
|
||||
|
||||
const verdictValue =
|
||||
counts.critical >= 1 || riskScoreValue >= 61 ? 'BLOCK' :
|
||||
counts.high >= 1 || riskScoreValue >= 21 ? 'WARNING' : 'ALLOW';
|
||||
// Risk score (delegated to severity.mjs — single source of truth, v7.0.0+)
|
||||
const riskScoreValue = riskScore(counts);
|
||||
const riskBandValue = riskBand(riskScoreValue);
|
||||
const verdictValue = verdict(counts);
|
||||
|
||||
const durationMs = Date.now() - startMs;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue