Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions modules/ingestor/src/graph/cvss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -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.
Comment thread
mrrajan marked this conversation as resolved.
#[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.
Expand Down
Loading