Skip to content

Commit 2d72cb1

Browse files
committed
feat: migrate API consumers to read from advisory_vulnerability_score table
Update VulnerabilityService, PurlStatus, and VulnerabilitySummary to query CVSS scores from the new unified advisory_vulnerability_score table instead of the legacy cvss3 table. This completes the read-side migration for the score consolidation work started in PR #1913. The ingestion side continues to write to both tables (dual-write) to ensure backwards compatibility. Removal of the dual-write, deprecation of the legacy table, and any appropriate API changes will be addressed in follow-up PRs. Assisted-By: Claude
1 parent 29d9b15 commit 2d72cb1

File tree

12 files changed

+283
-171
lines changed

12 files changed

+283
-171
lines changed

cvss/src/cvss3/score.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,21 @@ impl FromIterator<Cvss3Base> for Score {
7474
}
7575
}
7676
}
77+
78+
impl FromIterator<f64> for Score {
79+
fn from_iter<I: IntoIterator<Item = f64>>(iter: I) -> Self {
80+
let mut count: usize = 0;
81+
let mut sum = 0.0;
82+
for v in iter {
83+
sum += v;
84+
count += 1;
85+
}
86+
if count > 0 {
87+
// Round to 1 decimal place (CVSS scores are displayed with 1 decimal precision)
88+
let avg = sum / (count as f64);
89+
Self::new((avg * 10.0).round() / 10.0)
90+
} else {
91+
Self::default()
92+
}
93+
}
94+
}

entity/src/advisory_vulnerability_score.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,26 @@ pub enum ScoreType {
7474
V4_0,
7575
}
7676

77+
impl ScoreType {
78+
/// Returns true if this is a CVSS v3.x score type (V3_0 or V3_1).
79+
///
80+
/// NOTE: This helper exists for backward compatibility during the migration
81+
/// from legacy cvss3 table. It will be removed once all consumers support
82+
/// all CVSS versions.
83+
#[inline]
84+
pub fn is_cvss3(self) -> bool {
85+
matches!(self, Self::V3_0 | Self::V3_1)
86+
}
87+
}
88+
89+
impl Model {
90+
/// Returns true if this score is a CVSS v3.x score (V3_0 or V3_1).
91+
#[inline]
92+
pub fn is_cvss3(&self) -> bool {
93+
self.r#type.is_cvss3()
94+
}
95+
}
96+
7797
// severity
7898

7999
#[derive(

entity/src/vulnerability.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
advisory, advisory_vulnerability,
2+
advisory, advisory_vulnerability, advisory_vulnerability_score,
33
cvss3::{self, Severity},
44
vulnerability_description,
55
};
@@ -40,6 +40,9 @@ pub enum Relation {
4040
#[sea_orm(has_many = "super::cvss3::Entity")]
4141
Cvss3,
4242

43+
#[sea_orm(has_many = "super::advisory_vulnerability_score::Entity")]
44+
AdvisoryVulnerabilityScore,
45+
4346
#[sea_orm(has_many = "super::purl_status::Entity")]
4447
PurlStatuses,
4548
}
@@ -76,6 +79,12 @@ impl Related<cvss3::Entity> for Entity {
7679
}
7780
}
7881

82+
impl Related<advisory_vulnerability_score::Entity> for Entity {
83+
fn to() -> RelationDef {
84+
Relation::AdvisoryVulnerabilityScore.def()
85+
}
86+
}
87+
7988
impl Related<vulnerability_description::Entity> for Entity {
8089
fn to() -> RelationDef {
8190
Relation::Descriptions.def()

modules/fundamental/src/advisory/endpoints/test.rs

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ use test_context::test_context;
1212
use test_log::test;
1313
use time::OffsetDateTime;
1414
use trustify_common::{hashing::Digests, id::Id, model::PaginatedResults};
15-
use trustify_cvss::cvss3::{
16-
AttackComplexity, AttackVector, Availability, Confidentiality, Cvss3Base, Integrity,
17-
PrivilegesRequired, Scope, UserInteraction,
18-
};
19-
use trustify_entity::labels::Labels;
15+
use trustify_entity::{advisory_vulnerability_score, labels::Labels};
2016
use trustify_module_ingestor::{
21-
graph::advisory::AdvisoryInformation, model::IngestResult, service::Format,
17+
graph::{advisory::AdvisoryInformation, cvss::ScoreCreator},
18+
model::IngestResult,
19+
service::Format,
2220
};
2321
use trustify_module_storage::service::{StorageBackend, StorageKey};
2422
use trustify_test_context::{TrustifyContext, call::CallService, document_bytes};
@@ -48,25 +46,20 @@ async fn all_advisories(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
4846
)
4947
.await?;
5048

51-
let advisory_vuln = advisory
49+
advisory
5250
.link_to_vulnerability("CVE-123", None, &ctx.db)
5351
.await?;
54-
advisory_vuln
55-
.ingest_cvss3_score(
56-
Cvss3Base {
57-
minor_version: 0,
58-
av: AttackVector::Network,
59-
ac: AttackComplexity::Low,
60-
pr: PrivilegesRequired::None,
61-
ui: UserInteraction::None,
62-
s: Scope::Unchanged,
63-
c: Confidentiality::None,
64-
i: Integrity::None,
65-
a: Availability::None,
66-
},
67-
&ctx.db,
68-
)
69-
.await?;
52+
53+
// Use ScoreCreator to write to advisory_vulnerability_score table
54+
let mut score_creator = ScoreCreator::new(advisory.advisory.id);
55+
score_creator.add(trustify_module_ingestor::graph::cvss::ScoreInformation {
56+
vulnerability_id: "CVE-123".to_string(),
57+
r#type: advisory_vulnerability_score::ScoreType::V3_0,
58+
vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N".to_string(),
59+
score: 0.0,
60+
severity: advisory_vulnerability_score::Severity::None,
61+
});
62+
score_creator.create(&ctx.db).await?;
7063

7164
ctx.graph
7265
.ingest_advisory(
@@ -156,25 +149,20 @@ async fn one_advisory(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
156149
)
157150
.await?;
158151

159-
let advisory_vuln = advisory2
152+
advisory2
160153
.link_to_vulnerability("CVE-123", None, &ctx.db)
161154
.await?;
162-
advisory_vuln
163-
.ingest_cvss3_score(
164-
Cvss3Base {
165-
minor_version: 0,
166-
av: AttackVector::Network,
167-
ac: AttackComplexity::Low,
168-
pr: PrivilegesRequired::High,
169-
ui: UserInteraction::None,
170-
s: Scope::Changed,
171-
c: Confidentiality::High,
172-
i: Integrity::None,
173-
a: Availability::None,
174-
},
175-
&ctx.db,
176-
)
177-
.await?;
155+
156+
// Use ScoreCreator to write to advisory_vulnerability_score table
157+
let mut score_creator = ScoreCreator::new(advisory2.advisory.id);
158+
score_creator.add(trustify_module_ingestor::graph::cvss::ScoreInformation {
159+
vulnerability_id: "CVE-123".to_string(),
160+
r#type: advisory_vulnerability_score::ScoreType::V3_0,
161+
vector: "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N".to_string(),
162+
score: 6.8,
163+
severity: advisory_vulnerability_score::Severity::Medium,
164+
});
165+
score_creator.create(&ctx.db).await?;
178166

179167
let uri = format!("/api/v2/advisory/urn:uuid:{}", advisory2.advisory.id);
180168

@@ -253,25 +241,20 @@ async fn one_advisory_by_uuid(ctx: &TrustifyContext) -> Result<(), anyhow::Error
253241

254242
let uuid = advisory.advisory.id;
255243

256-
let advisory_vuln = advisory
244+
advisory
257245
.link_to_vulnerability("CVE-123", None, &ctx.db)
258246
.await?;
259-
advisory_vuln
260-
.ingest_cvss3_score(
261-
Cvss3Base {
262-
minor_version: 0,
263-
av: AttackVector::Network,
264-
ac: AttackComplexity::Low,
265-
pr: PrivilegesRequired::High,
266-
ui: UserInteraction::None,
267-
s: Scope::Changed,
268-
c: Confidentiality::High,
269-
i: Integrity::None,
270-
a: Availability::None,
271-
},
272-
&ctx.db,
273-
)
274-
.await?;
247+
248+
// Use ScoreCreator to write to advisory_vulnerability_score table
249+
let mut score_creator = ScoreCreator::new(advisory.advisory.id);
250+
score_creator.add(trustify_module_ingestor::graph::cvss::ScoreInformation {
251+
vulnerability_id: "CVE-123".to_string(),
252+
r#type: advisory_vulnerability_score::ScoreType::V3_0,
253+
vector: "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N".to_string(),
254+
score: 6.8,
255+
severity: advisory_vulnerability_score::Severity::Medium,
256+
});
257+
score_creator.create(&ctx.db).await?;
275258

276259
let uri = format!("/api/v2/advisory/{}", uuid.urn());
277260

modules/fundamental/src/advisory/model/details/advisory_vulnerability.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, LoaderTrait, QueryFilte
33
use serde::{Deserialize, Serialize};
44
use tracing::instrument;
55
use trustify_common::memo::Memo;
6-
use trustify_cvss::cvss3::Cvss3Base;
76
use trustify_cvss::cvss3::severity::Severity;
8-
use trustify_entity::{advisory, advisory_vulnerability, cvss3, vulnerability};
7+
use trustify_entity::{
8+
advisory, advisory_vulnerability, advisory_vulnerability_score, vulnerability,
9+
};
910
use utoipa::ToSchema;
1011

1112
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
@@ -119,19 +120,22 @@ impl AdvisoryVulnerabilitySummary {
119120
vulnerabilities: &[vulnerability::Model],
120121
tx: &C,
121122
) -> Result<Vec<Self>, Error> {
122-
let mut cvss3s = vulnerabilities
123+
let mut all_scores = vulnerabilities
123124
.load_many(
124-
cvss3::Entity::find().filter(cvss3::Column::AdvisoryId.eq(advisory.id)),
125+
advisory_vulnerability_score::Entity::find()
126+
.filter(advisory_vulnerability_score::Column::AdvisoryId.eq(advisory.id)),
125127
tx,
126128
)
127129
.await?;
128130

129131
let mut summaries = Vec::new();
130132

131-
for (vuln, mut cvss3) in vulnerabilities.iter().zip(cvss3s.drain(..)) {
132-
let cvss3_scores = cvss3
133+
for (vuln, mut scores) in vulnerabilities.iter().zip(all_scores.drain(..)) {
134+
// Filter to V3.x scores and format as vector strings
135+
let cvss3_scores: Vec<String> = scores
133136
.drain(..)
134-
.map(|e| Cvss3Base::from(e).to_string())
137+
.filter(|s| s.is_cvss3())
138+
.map(|e| e.vector)
135139
.collect();
136140

137141
summaries.push(AdvisoryVulnerabilitySummary {

modules/fundamental/src/common/model/score.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use serde::{Deserialize, Serialize};
22
use serde_json::json;
33
use trustify_cvss::cvss3::severity::Severity;
4-
use trustify_entity::cvss3;
4+
use trustify_entity::{advisory_vulnerability_score, cvss3};
55
use utoipa::{
66
PartialSchema, ToSchema,
77
openapi::{
@@ -85,6 +85,35 @@ impl TryFrom<cvss3::Model> for Score {
8585
}
8686
}
8787

88+
impl From<advisory_vulnerability_score::Model> for Score {
89+
fn from(model: advisory_vulnerability_score::Model) -> Self {
90+
let r#type = match model.r#type {
91+
advisory_vulnerability_score::ScoreType::V2_0 => ScoreType::V2,
92+
advisory_vulnerability_score::ScoreType::V3_0 => ScoreType::V3,
93+
advisory_vulnerability_score::ScoreType::V3_1 => ScoreType::V3_1,
94+
advisory_vulnerability_score::ScoreType::V4_0 => ScoreType::V4,
95+
};
96+
97+
let severity = match model.severity {
98+
advisory_vulnerability_score::Severity::None => Severity::None,
99+
advisory_vulnerability_score::Severity::Low => Severity::Low,
100+
advisory_vulnerability_score::Severity::Medium => Severity::Medium,
101+
advisory_vulnerability_score::Severity::High => Severity::High,
102+
advisory_vulnerability_score::Severity::Critical => Severity::Critical,
103+
};
104+
105+
// Round to 1 decimal place to avoid f32->f64 precision artifacts
106+
// CVSS scores are typically displayed with 1 decimal precision
107+
let value = (model.score as f64 * 10.0).round() / 10.0;
108+
109+
Score {
110+
r#type,
111+
value,
112+
severity,
113+
}
114+
}
115+
}
116+
88117
#[cfg(test)]
89118
mod test {
90119
use super::*;

modules/fundamental/src/purl/model/details/purl.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ use trustify_common::{
2222
memo::Memo,
2323
purl::Purl,
2424
};
25-
use trustify_cvss::cvss3::{Cvss3Base, score::Score as Cvss3Score, severity::Severity};
25+
use trustify_cvss::cvss3::{score::Score as Cvss3Score, severity::Severity};
2626
use trustify_entity::{
27-
advisory, base_purl, cpe, cvss3, license, organization, product, product_status,
28-
product_version, product_version_range, purl_status, qualified_purl, sbom, sbom_package,
29-
sbom_package_license, sbom_package_purl_ref, status, version_range, versioned_purl,
30-
vulnerability,
27+
advisory, advisory_vulnerability_score, base_purl, cpe, license, organization, product,
28+
product_status, product_version, product_version_range, purl_status, qualified_purl, sbom,
29+
sbom_package, sbom_package_license, sbom_package_purl_ref, status, version_range,
30+
versioned_purl, vulnerability,
3131
};
3232
use trustify_module_ingestor::common::{Deprecation, DeprecationForExt};
3333
use utoipa::ToSchema;
@@ -340,14 +340,19 @@ impl PurlStatus {
340340
cpe: Option<String>,
341341
tx: &C,
342342
) -> Result<Self, Error> {
343+
// Query scores from the new advisory_vulnerability_score table
344+
let score_models = vuln
345+
.find_related(advisory_vulnerability_score::Entity)
346+
.all(tx)
347+
.await?;
348+
let average_score = Cvss3Score::from_iter(
349+
score_models
350+
.iter()
351+
.filter(|s| s.is_cvss3())
352+
.map(|s| s.score as f64),
353+
);
354+
let all_scores: Vec<Score> = score_models.into_iter().map(Score::from).collect();
343355
let issuer = Memo::Provided(advisory.find_related(organization::Entity).one(tx).await?);
344-
let cvss3 = vuln.find_related(cvss3::Entity).all(tx).await?;
345-
let average_score = Cvss3Score::from_iter(cvss3.iter().map(Cvss3Base::from));
346-
let all_scores = cvss3
347-
.iter()
348-
.cloned()
349-
.filter_map(|cvss3| Score::try_from(cvss3).ok())
350-
.collect();
351356

352357
Ok(Self {
353358
vulnerability: VulnerabilityHead::from_vulnerability_entity(
@@ -374,14 +379,15 @@ impl PurlStatus {
374379
status: String,
375380
version_range: Option<VersionRange>,
376381
cpe: Option<String>,
377-
scores: &[cvss3::Model],
382+
score_models: &[advisory_vulnerability_score::Model],
378383
) -> Result<Self, Error> {
379-
let average_score = Cvss3Score::from_iter(scores.iter().map(Cvss3Base::from));
380-
let all_scores = scores
381-
.iter()
382-
.cloned()
383-
.filter_map(|cvss3| Score::try_from(cvss3).ok())
384-
.collect();
384+
let average_score = Cvss3Score::from_iter(
385+
score_models
386+
.iter()
387+
.filter(|s| s.is_cvss3())
388+
.map(|s| s.score as f64),
389+
);
390+
let all_scores: Vec<Score> = score_models.iter().cloned().map(Score::from).collect();
385391

386392
Ok(Self {
387393
vulnerability: vuln_head,

0 commit comments

Comments
 (0)