Skip to content

Commit e88c7a5

Browse files
dejanbctron
authored andcommitted
feat: implement osv vector parsing
Signed-off-by: Dejan Bosanac <dbosanac@redhat.com>
1 parent d63185e commit e88c7a5

File tree

5 files changed

+135
-114
lines changed

5 files changed

+135
-114
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

entity/src/advisory_vulnerability_score.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub struct Model {
1313

1414
pub r#type: ScoreType,
1515
pub vector: String,
16-
pub score: f64,
16+
pub score: f32,
1717
pub severity: Severity,
1818
}
1919

modules/ingestor/src/graph/cvss.rs

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use cvss::version::VersionV3;
22
use cvss::{Cvss, v2_0, v3, v4_0};
33
use sea_orm::{ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, Set};
4+
use trustify_cvss::cvss3::severity::Severity as CvssSeverity;
45
use trustify_entity::advisory_vulnerability_score::{self, ScoreType, Severity};
56
use uuid::Uuid;
67

@@ -16,7 +17,7 @@ pub struct ScoreInformation {
1617
pub vulnerability_id: String,
1718
pub r#type: ScoreType,
1819
pub vector: String,
19-
pub score: f64,
20+
pub score: f32,
2021
pub severity: Severity,
2122
}
2223

@@ -42,68 +43,72 @@ impl From<ScoreInformation> for advisory_vulnerability_score::ActiveModel {
4243
}
4344

4445
impl From<(String, v2_0::CvssV2)> for ScoreInformation {
45-
fn from((vulnerability_id, score): (String, v2_0::CvssV2)) -> Self {
46-
let v2_0::CvssV2 {
47-
vector_string,
48-
severity,
49-
base_score,
50-
..
51-
} = score;
46+
fn from((vulnerability_id, cvss): (String, v2_0::CvssV2)) -> Self {
47+
// Use calculated_base_score() to compute the actual score from metrics
48+
let base_score = cvss.calculated_base_score().unwrap_or(0.0);
49+
// Derive severity from calculated score using CVSS v2 scale (no "None" or "Critical")
50+
let severity = match base_score {
51+
x if x < 4.0 => Severity::Low,
52+
x if x < 7.0 => Severity::Medium,
53+
_ => Severity::High,
54+
};
5255

5356
Self {
5457
vulnerability_id,
5558
r#type: ScoreType::V2_0,
56-
vector: vector_string,
57-
score: base_score,
58-
severity: match severity {
59-
None => Severity::None,
60-
Some(v2_0::Severity::Low) => Severity::Low,
61-
Some(v2_0::Severity::Medium) => Severity::Medium,
62-
Some(v2_0::Severity::High) => Severity::High,
63-
},
59+
vector: cvss.vector_string,
60+
score: base_score as f32,
61+
severity,
6462
}
6563
}
6664
}
6765

6866
impl From<(String, v3::CvssV3)> for ScoreInformation {
69-
fn from((vulnerability_id, score): (String, v3::CvssV3)) -> Self {
70-
let v3::CvssV3 {
71-
version,
72-
vector_string,
73-
base_severity,
74-
base_score,
75-
..
76-
} = score;
67+
fn from((vulnerability_id, cvss): (String, v3::CvssV3)) -> Self {
68+
// Use calculated_base_score() to compute the actual score from metrics
69+
let base_score = cvss.calculated_base_score().unwrap_or(0.0);
70+
// Derive severity from calculated score using CVSS v3 scale
71+
let severity = match CvssSeverity::from_f64(base_score) {
72+
CvssSeverity::None => Severity::None,
73+
CvssSeverity::Low => Severity::Low,
74+
CvssSeverity::Medium => Severity::Medium,
75+
CvssSeverity::High => Severity::High,
76+
CvssSeverity::Critical => Severity::Critical,
77+
};
7778

7879
Self {
7980
vulnerability_id,
80-
r#type: match version {
81+
r#type: match cvss.version {
8182
Some(VersionV3::V3_0) => ScoreType::V3_0,
8283
Some(VersionV3::V3_1) => ScoreType::V3_1,
8384
None => ScoreType::V3_0, // Default to V3_0 if version is not specified
8485
},
85-
vector: vector_string,
86-
score: base_score,
87-
severity: base_severity.into(),
86+
vector: cvss.vector_string,
87+
score: base_score as f32,
88+
severity,
8889
}
8990
}
9091
}
9192

9293
impl From<(String, v4_0::CvssV4)> for ScoreInformation {
93-
fn from((vulnerability_id, score): (String, v4_0::CvssV4)) -> Self {
94-
let v4_0::CvssV4 {
95-
vector_string,
96-
base_severity,
97-
base_score,
98-
..
99-
} = score;
94+
fn from((vulnerability_id, cvss): (String, v4_0::CvssV4)) -> Self {
95+
// Use calculated_base_score() to compute the actual score from metrics
96+
let base_score = cvss.calculated_base_score().unwrap_or(0.0);
97+
// Derive severity from calculated score using CVSS v4 scale (same as v3)
98+
let severity = match CvssSeverity::from_f64(base_score) {
99+
CvssSeverity::None => Severity::None,
100+
CvssSeverity::Low => Severity::Low,
101+
CvssSeverity::Medium => Severity::Medium,
102+
CvssSeverity::High => Severity::High,
103+
CvssSeverity::Critical => Severity::Critical,
104+
};
100105

101106
Self {
102107
vulnerability_id,
103108
r#type: ScoreType::V4_0,
104-
vector: vector_string,
105-
score: base_score,
106-
severity: base_severity.into(),
109+
vector: cvss.vector_string,
110+
score: base_score as f32,
111+
severity,
107112
}
108113
}
109114
}

modules/ingestor/src/service/advisory/osv/loader.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,10 @@ mod test {
579579
use hex::ToHex;
580580
use osv::schema::Vulnerability;
581581
use rstest::rstest;
582+
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
582583
use test_context::test_context;
583584
use test_log::test;
585+
use trustify_entity::advisory_vulnerability_score;
584586
use trustify_test_context::{TrustifyContext, document};
585587

586588
#[test_context(TrustifyContext)]
@@ -642,6 +644,35 @@ mod test {
642644
.is_none()
643645
);
644646

647+
// Verify the advisory_vulnerability_score table has the calculated score
648+
let new_scores = advisory_vulnerability_score::Entity::find()
649+
.filter(
650+
advisory_vulnerability_score::Column::AdvisoryId.eq(loaded_advisory.advisory.id),
651+
)
652+
.all(&ctx.db)
653+
.await?;
654+
assert_eq!(1, new_scores.len());
655+
let new_score = &new_scores[0];
656+
assert_eq!(new_score.vulnerability_id, "CVE-2021-32714");
657+
assert_eq!(
658+
new_score.r#type,
659+
advisory_vulnerability_score::ScoreType::V3_1
660+
);
661+
assert_eq!(
662+
new_score.vector,
663+
"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H"
664+
);
665+
// Score should be 9.1 (calculated from the CVSS vector metrics)
666+
assert!(
667+
(new_score.score - 9.1_f32).abs() < 0.1,
668+
"Expected score ~9.1, got {}",
669+
new_score.score
670+
);
671+
assert_eq!(
672+
new_score.severity,
673+
advisory_vulnerability_score::Severity::Critical
674+
);
675+
645676
Ok(())
646677
}
647678

modules/ingestor/src/service/advisory/osv/mod.rs

Lines changed: 59 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ mod prefix;
33
pub mod loader;
44
pub mod translate;
55

6-
use crate::{
7-
graph::cvss::{ScoreCreator, ScoreInformation},
8-
service::Error,
9-
};
6+
use crate::{graph::cvss::ScoreCreator, service::Error};
7+
use cvss::{v2_0::CvssV2, v3::CvssV3, v4_0::CvssV4};
108
use osv::schema::{SeverityType, Vulnerability};
11-
use trustify_entity::advisory_vulnerability_score::{ScoreType, Severity};
9+
use std::str::FromStr;
1210

1311
/// Load a [`Vulnerability`] from YAML, using the "classic" enum representation.
1412
pub fn from_yaml(data: &[u8]) -> Result<Vulnerability, serde_yml::Error> {
@@ -49,78 +47,65 @@ pub fn extract_vulnerability_ids(osv: &Vulnerability) -> impl IntoIterator<Item
4947

5048
/// extract scores from OSV
5149
pub fn extract_scores(osv: &Vulnerability, creator: &mut ScoreCreator) {
52-
#[derive(Clone)]
53-
struct ScoreInfo {
54-
pub r#type: ScoreType,
55-
pub vector: String,
56-
pub score: f64,
57-
pub severity: Severity,
50+
// Get all vulnerability IDs upfront
51+
let ids: Vec<_> = extract_vulnerability_ids(osv).into_iter().collect();
52+
53+
// If no vulnerability IDs, nothing to do
54+
if ids.is_empty() {
55+
return;
5856
}
5957

60-
impl From<(String, ScoreInfo)> for ScoreInformation {
61-
fn from(
62-
(
63-
vulnerability_id,
64-
ScoreInfo {
65-
r#type,
66-
vector,
67-
score,
68-
severity,
69-
},
70-
): (String, ScoreInfo),
71-
) -> Self {
72-
Self {
73-
vulnerability_id,
74-
r#type,
75-
vector,
76-
score,
77-
severity,
58+
// Process each severity entry
59+
for severity in osv.severity.iter().flatten() {
60+
match severity.severity_type {
61+
SeverityType::CVSSv2 => match CvssV2::from_str(&severity.score) {
62+
Ok(cvss) => {
63+
for id in &ids {
64+
creator.add((id.to_string(), cvss.clone()));
65+
}
66+
}
67+
Err(e) => {
68+
log::warn!(
69+
"Failed to parse CVSSv2 vector '{}': {:?}",
70+
severity.score,
71+
e
72+
);
73+
}
74+
},
75+
76+
SeverityType::CVSSv3 => match CvssV3::from_str(&severity.score) {
77+
Ok(cvss) => {
78+
for id in &ids {
79+
creator.add((id.to_string(), cvss.clone()));
80+
}
81+
}
82+
Err(e) => {
83+
log::warn!(
84+
"Failed to parse CVSSv3 vector '{}': {:?}",
85+
severity.score,
86+
e
87+
);
88+
}
89+
},
90+
91+
SeverityType::CVSSv4 => match CvssV4::from_str(&severity.score) {
92+
Ok(cvss) => {
93+
for id in &ids {
94+
creator.add((id.to_string(), cvss.clone()));
95+
}
96+
}
97+
Err(e) => {
98+
log::warn!(
99+
"Failed to parse CVSSv4 vector '{}': {:?}",
100+
severity.score,
101+
e
102+
);
103+
}
104+
},
105+
106+
_ => {
107+
// Unknown severity type, skip
78108
}
79109
}
80110
}
81-
82-
// TODO: validate score type by prefix
83-
let scores = osv
84-
.severity
85-
.iter()
86-
.flatten()
87-
.flat_map(|severity| match severity.severity_type {
88-
SeverityType::CVSSv2 => Some(ScoreInfo {
89-
r#type: ScoreType::V2_0,
90-
vector: severity.score.clone(),
91-
score: 10f64, // TODO: replace with actual evaluated score
92-
severity: Severity::Critical, // TODO: replace with actual evaluated severity
93-
}),
94-
SeverityType::CVSSv3 => Some(ScoreInfo {
95-
r#type: match severity.score.starts_with("CVSS:3.1/") {
96-
true => ScoreType::V3_1,
97-
false => ScoreType::V3_0,
98-
},
99-
vector: severity.score.clone(),
100-
score: 10f64, // TODO: replace with actual evaluated score
101-
severity: Severity::Critical, // TODO: replace with actual evaluated severity
102-
}),
103-
SeverityType::CVSSv4 => Some(ScoreInfo {
104-
r#type: ScoreType::V4_0,
105-
vector: severity.score.clone(),
106-
score: 10f64, // TODO: replace with actual evaluated score
107-
severity: Severity::Critical, // TODO: replace with actual evaluated severity
108-
}),
109-
110-
_ => None,
111-
});
112-
113-
// get all vulnerability IDs
114-
115-
let ids = extract_vulnerability_ids(osv)
116-
.into_iter()
117-
.collect::<Vec<_>>();
118-
119-
// create scores for each vulnerability (alias)
120-
121-
creator.extend(
122-
scores
123-
.into_iter()
124-
.flat_map(|score| ids.iter().map(move |id| (id.to_string(), score.clone()))),
125-
);
126111
}

0 commit comments

Comments
 (0)