diff --git a/modules/ingestor/src/graph/cvss.rs b/modules/ingestor/src/graph/cvss.rs index 6ed34c4dd..a1177c4e4 100644 --- a/modules/ingestor/src/graph/cvss.rs +++ b/modules/ingestor/src/graph/cvss.rs @@ -92,20 +92,20 @@ 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() + let full_score = cvss + .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 { 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(), } } } @@ -281,6 +281,73 @@ 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); + } + + /// 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.