From ae224a6aa9f3e544d25c2feb0e3c140d27acd00f Mon Sep 17 00:00:00 2001 From: mrrajan <86094767+mrrajan@users.noreply.github.com.> Date: Tue, 11 Aug 2026 20:12:09 +0530 Subject: [PATCH 1/2] fix(cvss): honor Exploit Maturity threat metric in CVSS v4.0 scoring Use calculated_full_score() instead of calculated_base_score() for v4.0 so E:P/E:U are not stripped. No change for CVEs without an E metric. Implements TC-5626 Assisted-by: Claude Code --- modules/ingestor/src/graph/cvss.rs | 54 ++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/modules/ingestor/src/graph/cvss.rs b/modules/ingestor/src/graph/cvss.rs index 6ed34c4dd..5adf4d53b 100644 --- a/modules/ingestor/src/graph/cvss.rs +++ b/modules/ingestor/src/graph/cvss.rs @@ -93,11 +93,11 @@ impl From<(String, v3::CvssV3)> for ScoreInformation { impl From<(String, v4_0::CvssV4)> for ScoreInformation { fn from((vulnerability_id, cvss): (String, v4_0::CvssV4)) -> Self { let base_score = cvss - .calculated_base_score() + .calculated_full_score() .or_else(|| { v4_0::CvssV4::from_str(&cvss.vector_string) .ok() - .and_then(|p| p.calculated_base_score()) + .and_then(|p| p.calculated_full_score()) }) .unwrap_or(cvss.base_score); Self { @@ -281,6 +281,56 @@ mod test { assert_eq!(info.severity, Severity::None); } + /// Verifies that CVSS v4.0 with E:P (ProofOfConcept) produces score 9.3, not 10.0. + #[test] + fn score_information_from_v4_exploit_maturity_proof_of_concept() { + // Given a CVSS v4.0 vector with all-high metrics and E:P + let cvss = v4_0::CvssV4::from_str( + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:P", + ) + .expect("valid CVSS v4 vector"); + + // When converting to ScoreInformation + let info: ScoreInformation = ("CVE-2026-18236".to_string(), cvss).into(); + + // Then the score includes the E:P threat metric (CVSS-BT = 9.3) + assert_eq!(info.r#type, ScoreType::V4_0); + assert_eq!(info.score, 9.3_f32); + assert_eq!(info.severity, Severity::Critical); + } + + /// Verifies that CVSS v4.0 without an E metric still produces 10.0 (no regression). + #[test] + fn score_information_from_v4_no_exploit_maturity() { + // Given a CVSS v4.0 vector with all-high metrics and no E metric + let cvss = v4_0::CvssV4::from_str( + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + ) + .expect("valid CVSS v4 vector"); + + // When converting to ScoreInformation + let info: ScoreInformation = ("CVE-2024-99999".to_string(), cvss).into(); + + // Then the score defaults E to Attacked, giving 10.0 (unchanged behavior) + assert_eq!(info.score, 10.0_f32); + } + + /// Verifies that CVSS v4.0 with E:U (Unreported) lowers the score below 10.0. + #[test] + fn score_information_from_v4_exploit_maturity_unreported() { + // Given a CVSS v4.0 vector with all-high metrics and E:U + let cvss = v4_0::CvssV4::from_str( + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:U", + ) + .expect("valid CVSS v4 vector"); + + // When converting to ScoreInformation + let info: ScoreInformation = ("CVE-2024-99998".to_string(), cvss).into(); + + // Then the score is lower than 10.0 (E:U → EQ5=2, lookup (0,0,0,1,2,0) → 9.1) + assert_eq!(info.score, 9.1_f32); + } + #[test] fn score_creator_extend() { // Exercises ScoreCreator::extend() by verifying items are appended to the internal list. From 49d32f1b93d7c1d6af47bf75703ee752a535e0d4 Mon Sep 17 00:00:00 2001 From: mrrajan <86094767+mrrajan@users.noreply.github.com.> Date: Wed, 12 Aug 2026 13:58:56 +0530 Subject: [PATCH 2/2] test(cvss): add E:A test coverage and fix variable reference Add missing test for CVSS v4.0 with E:A (Attacked) exploit maturity metric as requested in PR review. Also fix variable name inconsistency where the local variable was renamed to full_score but still referenced cvss.full_score instead of cvss.base_score in the fallback. Addresses review feedback on PR #2569 Co-Authored-By: Claude Code --- modules/ingestor/src/graph/cvss.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/ingestor/src/graph/cvss.rs b/modules/ingestor/src/graph/cvss.rs index 5adf4d53b..a1177c4e4 100644 --- a/modules/ingestor/src/graph/cvss.rs +++ b/modules/ingestor/src/graph/cvss.rs @@ -92,7 +92,7 @@ impl From<(String, v3::CvssV3)> for ScoreInformation { impl From<(String, v4_0::CvssV4)> for ScoreInformation { fn from((vulnerability_id, cvss): (String, v4_0::CvssV4)) -> Self { - let base_score = cvss + let full_score = cvss .calculated_full_score() .or_else(|| { v4_0::CvssV4::from_str(&cvss.vector_string) @@ -104,8 +104,8 @@ impl From<(String, v4_0::CvssV4)> for ScoreInformation { vulnerability_id, r#type: ScoreType::V4_0, vector: cvss.vector_string, - score: base_score as f32, - severity: (base_score, ScoreType::V4_0).into(), + score: full_score as f32, + severity: (full_score, ScoreType::V4_0).into(), } } } @@ -331,6 +331,23 @@ mod test { assert_eq!(info.score, 9.1_f32); } + /// Verifies that CVSS v4.0 with E:A (Attacked) produces the maximum score of 10.0. + #[test] + fn score_information_from_v4_exploit_maturity_attacked() { + // Given a CVSS v4.0 vector with all-high metrics and E:A + let cvss = v4_0::CvssV4::from_str( + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A", + ) + .expect("valid CVSS v4 vector"); + + // When converting to ScoreInformation + let info: ScoreInformation = ("CVE-2024-99997".to_string(), cvss).into(); + + // Then the score is 10.0 (E:A → EQ5=0, lookup (0,0,0,1,0,0) → 10.0) + assert_eq!(info.score, 10.0_f32); + assert_eq!(info.severity, Severity::Critical); + } + #[test] fn score_creator_extend() { // Exercises ScoreCreator::extend() by verifying items are appended to the internal list.