feat(config-audit): severity-weighted scoreByArea (v5 F3)

Replace count-based pass-rate with severity-weighted penalty:
- penalty = sum(count[s] * WEIGHTS[s])
- maxBudget = max(10, findingCount * 4)
- passRate = max(0, 100 - penalty / maxBudget * 100)

A few lows no longer crater an area's grade; a single high or critical
consumes a large fraction of budget. Mirrors the operator intuition that
severity, not count, is the signal.

BREAKING (intentional): scoring semantics differ from v4 for non-clean
configs. Add scoringVersion: 'v5' to the returned struct so consumers
can detect the version. baseline-all-a remains all-A (no critical/high
on that fixture).

Tests: +6 cases for severity weighting; existing "many findings" test
updated to use highs (where v5 still drops the grade as expected).
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 06:20:08 +02:00
commit a65c7f4080
2 changed files with 96 additions and 11 deletions

View file

@ -59,6 +59,29 @@ function makeScannerResult(scanner, findingCount) {
};
}
function makeScannerResultWithSeverities(scanner, severities) {
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
const findings = severities.map((sev, i) => {
if (counts[sev] !== undefined) counts[sev]++;
return {
id: `CA-${scanner}-${String(i + 1).padStart(3, '0')}`,
scanner,
severity: sev,
title: `Finding ${i + 1}`,
category: scanner === 'GAP' ? 't2' : null,
recommendation: 'Fix',
};
});
return {
scanner,
status: 'ok',
files_scanned: 5,
duration_ms: 10,
findings,
counts,
};
}
// ========================================
// calculateUtilization
// ========================================
@ -279,8 +302,8 @@ describe('scoreByArea', () => {
assert.equal(result.areas[0].score, 100);
});
it('many findings → lower grade', () => {
const scanners = [makeScannerResult('CML', 8)];
it('many high-severity findings → lower grade (v5 severity-weighted)', () => {
const scanners = [makeScannerResultWithSeverities('CML', ['high', 'high', 'high'])];
const result = scoreByArea(scanners);
assert.ok(result.areas[0].score < 50);
});
@ -327,6 +350,52 @@ describe('scoreByArea', () => {
assert.ok('score' in area);
assert.ok('findingCount' in area);
});
it('exposes scoringVersion: v5', () => {
const result = scoreByArea([makeScannerResult('CML', 0)]);
assert.equal(result.scoringVersion, 'v5');
});
});
// ========================================
// scoreByArea — severity weighting (v5 F3)
// ========================================
describe('scoreByArea — severity weighting (v5 F3)', () => {
it('clean scanner → 100/A', () => {
const result = scoreByArea([makeScannerResultWithSeverities('CML', [])]);
assert.equal(result.areas[0].score, 100);
assert.equal(result.areas[0].grade, 'A');
});
it('5 lows scores higher than 1 critical', () => {
const fiveLows = scoreByArea([makeScannerResultWithSeverities('CML', ['low', 'low', 'low', 'low', 'low'])]);
const oneCritical = scoreByArea([makeScannerResultWithSeverities('CML', ['critical'])]);
assert.ok(fiveLows.areas[0].score > oneCritical.areas[0].score,
`5 lows (${fiveLows.areas[0].score}) should score higher than 1 critical (${oneCritical.areas[0].score})`);
});
it('1 critical → grade is D or F (penalty exceeds budget)', () => {
const result = scoreByArea([makeScannerResultWithSeverities('CML', ['critical'])]);
assert.ok(['D', 'F'].includes(result.areas[0].grade),
`1 critical produced grade ${result.areas[0].grade}, expected D or F`);
});
it('a few lows still score A (low impact respected)', () => {
const result = scoreByArea([makeScannerResultWithSeverities('CML', ['low', 'low', 'low'])]);
assert.ok(result.areas[0].score >= 75,
`3 lows scored ${result.areas[0].score}, expected >= 75 (B+ range)`);
});
it('info-only findings are not penalized', () => {
const result = scoreByArea([makeScannerResultWithSeverities('CML', ['info', 'info', 'info'])]);
assert.equal(result.areas[0].score, 100);
});
it('1 high → grade is C or worse', () => {
const result = scoreByArea([makeScannerResultWithSeverities('CML', ['high'])]);
assert.ok(['C', 'D', 'F'].includes(result.areas[0].grade),
`1 high produced grade ${result.areas[0].grade}, expected C/D/F`);
});
});
// ========================================