Skip to content

Commit c60255e

Browse files
Fix CVE severity fallback when OSV omits CVSS v3.
Use ecosystem severity labels when present and default vulnerable deps to Medium so reporting and --high-only surface real dependency risk. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6ed95f3 commit c60255e

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

slices/v1-f/verify.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
cd "$(dirname "$0")/../.."
4+
npm test -- tests/scanners/dependencies.test.ts
5+
node -e "
6+
const { parseDependencies, lookupCves, detectPackageManagers } = require('./dist/scanners/dependencies');
7+
const path = require('path');
8+
const root = path.join(__dirname, 'test-data/vulnerable-deps');
9+
const files = require('fs').readdirSync(root).map(f => path.join(root, f));
10+
const mgrs = detectPackageManagers([path.join(root, 'package.json')], root);
11+
lookupCves(parseDependencies(mgrs)).then(r => {
12+
const bad = r.filter(d => d.vulnerabilities.length > 0 && d.maxSeverity === 'None');
13+
if (bad.length) process.exit(1);
14+
console.log('ok');
15+
});
16+
"

src/scanners/dependencies.ts

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,77 @@ function scoreToSeverity(score: number): FindingSeverity {
138138
return 'None'; // Should not happen if thresholds cover 0
139139
}
140140

141+
const SEVERITY_RANK: Record<FindingSeverity, number> = {
142+
None: 0,
143+
Info: 1,
144+
Low: 2,
145+
Medium: 3,
146+
High: 4,
147+
Critical: 5,
148+
};
149+
150+
function higherSeverity(a: FindingSeverity, b: FindingSeverity): FindingSeverity {
151+
return SEVERITY_RANK[a] >= SEVERITY_RANK[b] ? a : b;
152+
}
153+
154+
/**
155+
* Maps OSV/GitHub advisory severity labels (e.g. database_specific.severity) to FindingSeverity.
156+
*/
157+
function mapOsvSeverityLabel(label: string): FindingSeverity | null {
158+
switch (label.toUpperCase()) {
159+
case 'CRITICAL':
160+
return 'Critical';
161+
case 'HIGH':
162+
return 'High';
163+
case 'MODERATE':
164+
case 'MEDIUM':
165+
return 'Medium';
166+
case 'LOW':
167+
return 'Low';
168+
default:
169+
return null;
170+
}
171+
}
172+
173+
/**
174+
* Reads ecosystem severity from an OSV vuln when CVSS v3 is absent.
175+
*/
176+
function parseOsvEcosystemSeverity(vuln: OSVulnerability): FindingSeverity | null {
177+
const raw = vuln.database_specific?.severity;
178+
if (typeof raw !== 'string') {
179+
return null;
180+
}
181+
return mapOsvSeverityLabel(raw);
182+
}
183+
184+
/**
185+
* Computes max severity across vulns: CVSS v3 first, then OSV ecosystem severity,
186+
* else Medium when vulns exist but no score is available (batch API often omits CVSS).
187+
*/
188+
function computeMaxSeverityFromVulns(vulns: OSVulnerability[]): FindingSeverity {
189+
let maxCvss = 0;
190+
let maxOsvSeverity: FindingSeverity | null = null;
191+
192+
for (const vuln of vulns) {
193+
maxCvss = Math.max(maxCvss, getHighestCvssScore(vuln.severity));
194+
const osvSeverity = parseOsvEcosystemSeverity(vuln);
195+
if (osvSeverity) {
196+
maxOsvSeverity = maxOsvSeverity
197+
? higherSeverity(maxOsvSeverity, osvSeverity)
198+
: osvSeverity;
199+
}
200+
}
201+
202+
if (maxCvss > 0) {
203+
return scoreToSeverity(maxCvss);
204+
}
205+
if (maxOsvSeverity) {
206+
return maxOsvSeverity;
207+
}
208+
// OSV batch responses may omit CVSS; still surface known vulns at Medium minimum.
209+
return 'Medium';
210+
}
211+
141212
// Define the return type for the detection function
142213
type DetectedFilesMap = { [key in PackageManager]?: { manifest?: string, lock?: string } };
143214

@@ -339,12 +410,7 @@ export async function lookupCves(dependencies: DependencyInfo[]): Promise<Depend
339410
if (targetFinding) {
340411
if (result && result.vulns && result.vulns.length > 0) {
341412
targetFinding.vulnerabilities = result.vulns;
342-
// Calculate max severity for this dependency
343-
let maxCvss = 0;
344-
for (const vuln of result.vulns) {
345-
maxCvss = Math.max(maxCvss, getHighestCvssScore(vuln.severity));
346-
}
347-
targetFinding.maxSeverity = scoreToSeverity(maxCvss);
413+
targetFinding.maxSeverity = computeMaxSeverityFromVulns(result.vulns);
348414
} else {
349415
targetFinding.maxSeverity = 'None'; // Explicitly set to None if no vulns found
350416
}

tests/scanners/dependencies.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ describe('dependencies scanner', () => {
1717
);
1818

1919
expect(vulnerable.length).toBeGreaterThan(0);
20+
for (const finding of vulnerable) {
21+
expect(finding.maxSeverity).not.toBe('None');
22+
}
2023
});
2124
});

0 commit comments

Comments
 (0)