Skip to content

Commit e1eda3f

Browse files
committed
fix: add vulnerability details in purl details for product statuses
1 parent 0f1f780 commit e1eda3f

File tree

22 files changed

+376
-344
lines changed

22 files changed

+376
-344
lines changed

common/src/db/multi_model.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use migration::IntoIden;
12
use sea_orm::{
23
ColumnTrait, DbErr, EntityTrait, FromQueryResult, IntoIdentity, IntoSimpleExpr, Iterable,
34
QueryResult, QuerySelect, Select, SelectModel, Selector,
@@ -44,6 +45,12 @@ impl<T: QuerySelect> ColumnsPrefixed for T {
4445
pub trait SelectIntoMultiModel: Sized {
4546
fn try_model_columns<E: EntityTrait>(self, entity: E) -> Result<Self, DbErr>;
4647

48+
fn try_model_columns_excluding<O: EntityTrait>(
49+
self,
50+
entity: O,
51+
excluded: &[O::Column],
52+
) -> Result<Self, DbErr>;
53+
4754
fn try_model_columns_from_alias<E: EntityTrait>(
4855
self,
4956
entity: E,
@@ -62,6 +69,27 @@ impl<E: EntityTrait> SelectIntoMultiModel for Select<E> {
6269
self.try_columns_prefixed(&prefix, O::Column::iter())
6370
}
6471

72+
fn try_model_columns_excluding<O: EntityTrait>(
73+
self,
74+
entity: O,
75+
excluded: &[O::Column],
76+
) -> Result<Self, DbErr> {
77+
let excluded_names: Vec<_> = excluded
78+
.iter()
79+
.map(|col| (*col).into_iden().to_string())
80+
.collect();
81+
82+
let columns: Vec<_> = O::Column::iter()
83+
.filter(|col| {
84+
let col_name = (*col).into_iden().to_string();
85+
!excluded_names.contains(&col_name)
86+
})
87+
.collect();
88+
89+
let prefix = format!("{}$", entity.module_name());
90+
self.try_columns_prefixed(&prefix, columns)
91+
}
92+
6593
fn try_model_columns_from_alias<O: EntityTrait>(
6694
mut self,
6795
_entity: O,

entity/src/advisory.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use trustify_common::{
88
id::{Id, IdError, TryFilterForId},
99
};
1010

11-
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)]
11+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)]
1212
#[graphql(complex)]
1313
#[graphql(concrete(name = "Advisory", params()))]
1414
#[sea_orm(table_name = "advisory")]
@@ -30,6 +30,8 @@ pub struct Model {
3030
pub title: Option<String>,
3131
pub labels: Labels,
3232
pub source_document_id: Option<Uuid>,
33+
pub average_score: Option<f64>,
34+
pub average_severity: Option<super::cvss3::Severity>,
3335
}
3436

3537
#[ComplexObject]

entity/src/cvss3.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{advisory, advisory_vulnerability, vulnerability};
2+
use async_graphql::Enum;
23
use sea_orm::entity::prelude::*;
34
use std::fmt::{Display, Formatter};
45
use trustify_cvss::cvss3;
@@ -331,7 +332,7 @@ impl From<cvss3::Availability> for Availability {
331332
}
332333
}
333334

334-
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
335+
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Enum)]
335336
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "cvss3_severity")]
336337
pub enum Severity {
337338
#[sea_orm(string_value = "none")]
@@ -391,3 +392,18 @@ impl From<Severity> for cvss3::severity::Severity {
391392
}
392393
}
393394
}
395+
396+
impl std::str::FromStr for Severity {
397+
type Err = ();
398+
399+
fn from_str(s: &str) -> Result<Self, Self::Err> {
400+
match s {
401+
"none" => Ok(Severity::None),
402+
"low" => Ok(Severity::Low),
403+
"medium" => Ok(Severity::Medium),
404+
"high" => Ok(Severity::High),
405+
"critical" => Ok(Severity::Critical),
406+
_ => Err(()),
407+
}
408+
}
409+
}

entity/src/vulnerability.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use crate::{advisory, advisory_vulnerability, cvss3, vulnerability_description};
1+
use crate::cvss3;
2+
use crate::{advisory, advisory_vulnerability, vulnerability_description};
23
use async_graphql::SimpleObject;
34
use sea_orm::entity::prelude::*;
45
use time::OffsetDateTime;
56

6-
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)]
7+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)]
78
#[sea_orm(table_name = "vulnerability")]
89
#[graphql(concrete(name = "Vulnerability", params()))]
910
pub struct Model {
@@ -15,6 +16,8 @@ pub struct Model {
1516
pub modified: Option<OffsetDateTime>,
1617
pub withdrawn: Option<OffsetDateTime>,
1718
pub cwes: Option<Vec<String>>,
19+
pub average_score: Option<f64>,
20+
pub average_severity: Option<super::cvss3::Severity>,
1821
}
1922

2023
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod m0000070_perf_adv_vuln4;
1414
mod m0000080_get_purl_refactor;
1515
mod m0000090_release_perf;
1616
mod m0000100_perf_adv_vuln5;
17+
mod m0000110_alter_aggregate_scores;
1718
mod m0000970_alter_importer_add_heartbeat;
1819

1920
pub struct Migrator;
@@ -33,6 +34,7 @@ impl MigratorTrait for Migrator {
3334
Box::new(m0000080_get_purl_refactor::Migration),
3435
Box::new(m0000090_release_perf::Migration),
3536
Box::new(m0000100_perf_adv_vuln5::Migration),
37+
Box::new(m0000110_alter_aggregate_scores::Migration),
3638
]
3739
}
3840
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
// Add columns to advisory
10+
manager
11+
.alter_table(
12+
Table::alter()
13+
.table(Advisory::Table)
14+
.add_column(ColumnDef::new(Advisory::AverageScore).double())
15+
.add_column(
16+
ColumnDef::new(Advisory::AverageSeverity)
17+
.custom(Alias::new("cvss3_severity")),
18+
)
19+
.to_owned(),
20+
)
21+
.await?;
22+
23+
// Add columns to vulnerability
24+
manager
25+
.alter_table(
26+
Table::alter()
27+
.table(Vulnerability::Table)
28+
.add_column(ColumnDef::new(Vulnerability::AverageScore).double())
29+
.add_column(
30+
ColumnDef::new(Vulnerability::AverageSeverity)
31+
.custom(Alias::new("cvss3_severity")),
32+
)
33+
.to_owned(),
34+
)
35+
.await?;
36+
37+
manager
38+
.get_connection()
39+
.execute_unprepared(include_str!(
40+
"m000080_alter_aggregate_scores_fns/recalculate_cvss_aggregates.sql"
41+
))
42+
.await
43+
.map(|_| ())?;
44+
45+
manager
46+
.get_connection()
47+
.execute_unprepared(include_str!(
48+
"m000080_alter_aggregate_scores_fns/update_cvss_aggregates_on_change.sql"
49+
))
50+
.await
51+
.map(|_| ())?;
52+
Ok(())
53+
}
54+
55+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
56+
// Drop trigger
57+
manager
58+
.get_connection()
59+
.execute_unprepared("DROP TRIGGER IF EXISTS cvss3_insert_update_trigger ON cvss3")
60+
.await?;
61+
62+
// Drop functions
63+
manager
64+
.get_connection()
65+
.execute_unprepared("DROP FUNCTION IF EXISTS update_cvss_aggregates_on_change")
66+
.await?;
67+
68+
manager
69+
.get_connection()
70+
.execute_unprepared("DROP FUNCTION IF EXISTS recalculate_cvss_aggregates")
71+
.await?;
72+
73+
// Drop columns from vulnerability
74+
manager
75+
.alter_table(
76+
Table::alter()
77+
.table(Vulnerability::Table)
78+
.drop_column(Vulnerability::AverageScore)
79+
.drop_column(Vulnerability::AverageSeverity)
80+
.to_owned(),
81+
)
82+
.await?;
83+
84+
// Drop columns from advisory
85+
manager
86+
.alter_table(
87+
Table::alter()
88+
.table(Advisory::Table)
89+
.drop_column(Advisory::AverageScore)
90+
.drop_column(Advisory::AverageSeverity)
91+
.to_owned(),
92+
)
93+
.await?;
94+
95+
Ok(())
96+
}
97+
}
98+
99+
#[derive(Iden)]
100+
enum Advisory {
101+
Table,
102+
AverageScore,
103+
AverageSeverity,
104+
}
105+
106+
#[derive(Iden)]
107+
enum Vulnerability {
108+
Table,
109+
AverageScore,
110+
AverageSeverity,
111+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
CREATE OR REPLACE FUNCTION recalculate_cvss_aggregates()
2+
RETURNS void AS $$
3+
BEGIN
4+
-- Update advisories
5+
UPDATE advisory SET
6+
average_score = sub.avg_score,
7+
average_severity = cvss3_severity(sub.avg_score)
8+
FROM (
9+
SELECT advisory_id, AVG(score) AS avg_score
10+
FROM cvss3
11+
GROUP BY advisory_id
12+
) AS sub
13+
WHERE advisory.id = sub.advisory_id;
14+
15+
-- Update vulnerabilities
16+
UPDATE vulnerability SET
17+
average_score = sub.avg_score,
18+
average_severity = cvss3_severity(sub.avg_score)
19+
FROM (
20+
SELECT vulnerability_id, AVG(score) AS avg_score
21+
FROM cvss3
22+
GROUP BY vulnerability_id
23+
) AS sub
24+
WHERE vulnerability.id = sub.vulnerability_id;
25+
END;
26+
$$ LANGUAGE plpgsql;
27+
28+
SELECT recalculate_cvss_aggregates();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
CREATE OR REPLACE FUNCTION update_cvss_aggregates_on_change()
2+
RETURNS trigger AS $$
3+
BEGIN
4+
-- Update advisory aggregate
5+
IF NEW.advisory_id IS NOT NULL THEN
6+
UPDATE advisory SET
7+
average_score = sub.avg_score,
8+
average_severity = cvss3_severity(sub.avg_score)
9+
FROM (
10+
SELECT AVG(score) AS avg_score
11+
FROM cvss3
12+
WHERE advisory_id = NEW.advisory_id
13+
) AS sub
14+
WHERE advisory.id = NEW.advisory_id;
15+
END IF;
16+
17+
-- Update vulnerability aggregate
18+
IF NEW.vulnerability_id IS NOT NULL THEN
19+
UPDATE vulnerability SET
20+
average_score = sub.avg_score,
21+
average_severity = cvss3_severity(sub.avg_score)
22+
FROM (
23+
SELECT AVG(score) AS avg_score
24+
FROM cvss3
25+
WHERE vulnerability_id = NEW.vulnerability_id
26+
) AS sub
27+
WHERE vulnerability.id = NEW.vulnerability_id;
28+
END IF;
29+
30+
RETURN NULL;
31+
END;
32+
$$ LANGUAGE plpgsql
33+
PARALLEL SAFE;
34+
35+
CREATE TRIGGER cvss3_insert_update_trigger
36+
AFTER INSERT OR UPDATE OR DELETE ON cvss3
37+
FOR EACH ROW
38+
EXECUTE FUNCTION update_cvss_aggregates_on_change();

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

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::{Error, vulnerability::model::VulnerabilityHead};
22
use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, LoaderTrait, QueryFilter};
33
use serde::{Deserialize, Serialize};
44
use trustify_common::memo::Memo;
5+
use trustify_cvss::cvss3::Cvss3Base;
56
use trustify_cvss::cvss3::severity::Severity;
6-
use trustify_cvss::{cvss3::Cvss3Base, cvss3::score::Score};
77
use trustify_entity::{advisory, advisory_vulnerability, cvss3, vulnerability};
88
use utoipa::ToSchema;
99

@@ -34,14 +34,6 @@ impl AdvisoryVulnerabilityHead {
3434
vulnerability: &vulnerability::Model,
3535
tx: &C,
3636
) -> Result<Self, Error> {
37-
let cvss3 = cvss3::Entity::find()
38-
.filter(cvss3::Column::AdvisoryId.eq(advisory.id))
39-
.filter(cvss3::Column::VulnerabilityId.eq(&vulnerability.id))
40-
.all(tx)
41-
.await?;
42-
43-
let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from));
44-
4537
let advisory_vuln = advisory_vulnerability::Entity::find()
4638
.filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id))
4739
.filter(advisory_vulnerability::Column::VulnerabilityId.eq(&vulnerability.id))
@@ -57,8 +49,11 @@ impl AdvisoryVulnerabilityHead {
5749
};
5850
Ok(AdvisoryVulnerabilityHead {
5951
head,
60-
severity: score.severity(),
61-
score: score.value(),
52+
severity: advisory
53+
.average_severity
54+
.map(|sev| sev.into())
55+
.unwrap_or(Severity::None),
56+
score: advisory.average_score.unwrap_or(0.0),
6257
})
6358
} else {
6459
Err(Error::Data(
@@ -72,34 +67,24 @@ impl AdvisoryVulnerabilityHead {
7267
vulnerabilities: &[vulnerability::Model],
7368
tx: &C,
7469
) -> Result<Vec<Self>, Error> {
75-
let cvss3s = vulnerabilities
76-
.load_many(
77-
cvss3::Entity::find().filter(cvss3::Column::AdvisoryId.eq(advisory.id)),
78-
tx,
79-
)
80-
.await?;
81-
8270
let mut heads = Vec::new();
8371

84-
for (vuln, cvss3) in vulnerabilities.iter().zip(cvss3s.iter()) {
85-
let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from));
86-
72+
for vuln in vulnerabilities.iter() {
8773
let advisory_vuln = advisory_vulnerability::Entity::find()
8874
.filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id))
8975
.filter(advisory_vulnerability::Column::VulnerabilityId.eq(&vuln.id))
9076
.one(tx)
9177
.await?;
9278
if let Some(advisory_vuln) = advisory_vuln {
93-
let head = if vuln.title.is_some() {
94-
VulnerabilityHead::from_vulnerability_entity(vuln, Memo::NotProvided, tx)
95-
.await?
96-
} else {
97-
VulnerabilityHead::from_advisory_vulnerability_entity(&advisory_vuln, vuln)
98-
};
79+
let head =
80+
VulnerabilityHead::from_advisory_vulnerability_entity(&advisory_vuln, vuln);
9981
heads.push(AdvisoryVulnerabilityHead {
10082
head,
101-
severity: score.severity(),
102-
score: score.value(),
83+
severity: advisory
84+
.average_severity
85+
.map(|sev| sev.into())
86+
.unwrap_or(Severity::None),
87+
score: advisory.average_score.unwrap_or(0.0),
10388
});
10489
}
10590
}

0 commit comments

Comments
 (0)