From 6eec33a5bee99241e59b7ec52ee7e65fb9b53d0f Mon Sep 17 00:00:00 2001 From: Dejan Bosanac Date: Thu, 14 Nov 2024 14:56:03 +0100 Subject: [PATCH 1/8] feat: provide scores in sbom details response --- .../src/ai/service/tools/sbom_info.rs | 4 +-- modules/fundamental/src/sbom/model/details.rs | 22 ++++++------ .../src/vulnerability/model/mod.rs | 3 +- modules/graphql/src/sbomstatus.rs | 35 +++++++++++++++---- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/modules/fundamental/src/ai/service/tools/sbom_info.rs b/modules/fundamental/src/ai/service/tools/sbom_info.rs index a4815079a..456a38275 100644 --- a/modules/fundamental/src/ai/service/tools/sbom_info.rs +++ b/modules/fundamental/src/ai/service/tools/sbom_info.rs @@ -192,10 +192,10 @@ The tool provides a list of advisories/CVEs affecting the SBOM. .status .iter() .map(|v| Vulnerability { - identifier: v.vulnerability_id.clone(), + identifier: v.vulnerability.head.identifier.clone(), link: format!( "http://localhost:3000/vulnerability/{}", - v.vulnerability_id + v.vulnerability.head.identifier ), }) .collect(), diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs index 311530ec0..cd6cf5f71 100644 --- a/modules/fundamental/src/sbom/model/details.rs +++ b/modules/fundamental/src/sbom/model/details.rs @@ -1,11 +1,7 @@ use super::SbomSummary; use crate::{ - advisory::model::AdvisoryHead, - purl::{model::details::purl::StatusContext, model::summary::purl::PurlSummary}, - sbom::{model::SbomPackage, service::sbom::QueryCatcher, service::SbomService}, - Error, + advisory::model::AdvisoryHead, purl::model::{details::purl::StatusContext, summary::purl::PurlSummary}, sbom::{model::SbomPackage, service::{sbom::QueryCatcher, SbomService}}, vulnerability::model::VulnerabilityDetails, Error }; -use async_graphql::SimpleObject; use cpe::uri::OwnedUri; use sea_orm::{ DbErr, EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QueryOrder, @@ -90,6 +86,7 @@ impl SbomDetails { ) .join(JoinType::Join, product_status::Relation::Status.def()) .join(JoinType::Join, product_status::Relation::Advisory.def()) + .join(JoinType::Join, product_status::Relation::Vulnerability.def()) .distinct_on([ (product_status::Entity, product_status::Column::ContextCpeId), (product_status::Entity, product_status::Column::StatusId), @@ -195,7 +192,7 @@ impl SbomAdvisory { let sbom_status = if let Some(status) = advisory.status.iter_mut().find(|status| { if status.status == each.status.slug - && status.vulnerability_id == each.vulnerability.id + && status.vulnerability.head.identifier == each.vulnerability.id { match (&status.context, &status_cpe) { (Some(StatusContext::Cpe(context_cpe)), Some(status_cpe)) => { @@ -211,7 +208,7 @@ impl SbomAdvisory { status } else { let status = SbomStatus { - vulnerability_id: each.vulnerability.id.clone(), + vulnerability: VulnerabilityDetails::from_entity(&each.vulnerability, Default::default(), tx).await?, status: each.status.slug.clone(), context: status_cpe .as_ref() @@ -256,7 +253,7 @@ impl SbomAdvisory { } let status = SbomStatus { - vulnerability_id: product.product_status.vulnerability_id.clone(), + vulnerability: VulnerabilityDetails::from_entity(&product.vulnerability.clone(), Default::default(), tx).await?, status: product.status.slug.clone(), context: advisory_cpe .as_ref() @@ -281,12 +278,10 @@ impl SbomAdvisory { } } -#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, SimpleObject)] -#[graphql(concrete(name = "SbomStatus", params()))] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct SbomStatus { - pub vulnerability_id: String, + pub vulnerability: VulnerabilityDetails, pub status: String, - #[graphql(skip)] pub context: Option, pub packages: Vec, } @@ -297,6 +292,7 @@ impl SbomStatus {} #[allow(dead_code)] //TODO sbom field is not used at the moment, but we will probably need it for graph search pub struct ProductStatusCatcher { advisory: advisory::Model, + vulnerability: trustify_entity::vulnerability::Model, product_status: product_status::Model, cpe: trustify_entity::cpe::Model, status: status::Model, @@ -307,6 +303,7 @@ impl FromQueryResult for ProductStatusCatcher { fn from_query_result(res: &QueryResult, _pre: &str) -> Result { Ok(Self { advisory: Self::from_query_result_multi_model(res, "", advisory::Entity)?, + vulnerability: Self::from_query_result_multi_model(res, "", trustify_entity::vulnerability::Entity)?, product_status: Self::from_query_result_multi_model(res, "", product_status::Entity)?, cpe: Self::from_query_result_multi_model(res, "", trustify_entity::cpe::Entity)?, status: Self::from_query_result_multi_model(res, "", status::Entity)?, @@ -319,6 +316,7 @@ impl FromQueryResultMultiModel for ProductStatusCatcher { fn try_into_multi_model(select: Select) -> Result, DbErr> { select .try_model_columns(advisory::Entity)? + .try_model_columns(trustify_entity::vulnerability::Entity)? .try_model_columns(product_status::Entity)? .try_model_columns(trustify_entity::cpe::Entity)? .try_model_columns(status::Entity)? diff --git a/modules/fundamental/src/vulnerability/model/mod.rs b/modules/fundamental/src/vulnerability/model/mod.rs index b566a3858..c9546440b 100644 --- a/modules/fundamental/src/vulnerability/model/mod.rs +++ b/modules/fundamental/src/vulnerability/model/mod.rs @@ -1,6 +1,7 @@ mod details; mod summary; +use async_graphql::SimpleObject; pub use details::*; use sea_orm::{ColumnTrait, LoaderTrait, ModelTrait, QueryFilter}; pub use summary::*; @@ -13,7 +14,7 @@ use trustify_common::memo::Memo; use trustify_entity::{advisory_vulnerability, vulnerability, vulnerability_description}; use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Debug, Clone, ToSchema, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema, PartialEq, Eq, SimpleObject)] pub struct VulnerabilityHead { #[schema(required)] pub normative: bool, diff --git a/modules/graphql/src/sbomstatus.rs b/modules/graphql/src/sbomstatus.rs index 78009a6c8..19682dc3d 100644 --- a/modules/graphql/src/sbomstatus.rs +++ b/modules/graphql/src/sbomstatus.rs @@ -1,13 +1,13 @@ -use async_graphql::{Context, FieldResult, Object}; +use async_graphql::{Context, FieldResult, Object, SimpleObject}; use std::{ops::Deref, sync::Arc}; use trustify_common::{ db::{self, Transactional}, id::Id, }; -use trustify_module_fundamental::sbom::{ - model::details::{SbomDetails, SbomStatus}, +use trustify_module_fundamental::{purl::model::details::purl::StatusContext, sbom::{ + model::{details::{SbomDetails, SbomStatus}, SbomPackage}, service::SbomService, -}; +}}; use uuid::Uuid; #[derive(Default)] @@ -15,7 +15,7 @@ pub struct SbomStatusQuery; #[Object] impl SbomStatusQuery { - async fn cves_by_sbom<'a>(&self, ctx: &Context<'a>, id: Uuid) -> FieldResult> { + async fn cves_by_sbom<'a>(&self, ctx: &Context<'a>, id: Uuid) -> FieldResult> { let db = ctx.data::>()?; let sbom_service = SbomService::new(db.deref().clone()); @@ -25,7 +25,30 @@ impl SbomStatusQuery { .unwrap_or_default(); Ok(sbom_details - .and_then(|mut sbom| sbom.advisories.pop().map(|advisory| advisory.status)) + .and_then(|mut sbom| sbom.advisories.pop().map(|advisory| advisory.status.into_iter().map(GraphQLSbomStatus::from).collect())) .unwrap_or_default()) } } + +#[derive(Clone, Debug, SimpleObject)] +#[graphql(concrete(name = "SbomStatus", params()))] +pub struct GraphQLSbomStatus { + pub vulnerability_id: String, + pub status: String, + #[graphql(skip)] + pub context: Option, + pub packages: Vec, +} + +impl GraphQLSbomStatus {} + +impl From for GraphQLSbomStatus { + fn from(sbom_status: SbomStatus) -> Self { + GraphQLSbomStatus { + vulnerability_id: sbom_status.vulnerability.head.identifier.clone(), + status: sbom_status.status, + context: sbom_status.context, + packages: sbom_status.packages, + } + } +} \ No newline at end of file From 939bce44cb1dd8a8b1a15c231bbb6a06cc28d0eb Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Wed, 20 Nov 2024 18:14:58 -0500 Subject: [PATCH 2/8] Formatting Signed-off-by: Jim Crossley --- modules/graphql/src/sbomstatus.rs | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/modules/graphql/src/sbomstatus.rs b/modules/graphql/src/sbomstatus.rs index 19682dc3d..8fb043ecd 100644 --- a/modules/graphql/src/sbomstatus.rs +++ b/modules/graphql/src/sbomstatus.rs @@ -4,10 +4,16 @@ use trustify_common::{ db::{self, Transactional}, id::Id, }; -use trustify_module_fundamental::{purl::model::details::purl::StatusContext, sbom::{ - model::{details::{SbomDetails, SbomStatus}, SbomPackage}, - service::SbomService, -}}; +use trustify_module_fundamental::{ + purl::model::details::purl::StatusContext, + sbom::{ + model::{ + details::{SbomDetails, SbomStatus}, + SbomPackage, + }, + service::SbomService, + }, +}; use uuid::Uuid; #[derive(Default)] @@ -15,7 +21,11 @@ pub struct SbomStatusQuery; #[Object] impl SbomStatusQuery { - async fn cves_by_sbom<'a>(&self, ctx: &Context<'a>, id: Uuid) -> FieldResult> { + async fn cves_by_sbom<'a>( + &self, + ctx: &Context<'a>, + id: Uuid, + ) -> FieldResult> { let db = ctx.data::>()?; let sbom_service = SbomService::new(db.deref().clone()); @@ -25,7 +35,15 @@ impl SbomStatusQuery { .unwrap_or_default(); Ok(sbom_details - .and_then(|mut sbom| sbom.advisories.pop().map(|advisory| advisory.status.into_iter().map(GraphQLSbomStatus::from).collect())) + .and_then(|mut sbom| { + sbom.advisories.pop().map(|advisory| { + advisory + .status + .into_iter() + .map(GraphQLSbomStatus::from) + .collect() + }) + }) .unwrap_or_default()) } } @@ -51,4 +69,4 @@ impl From for GraphQLSbomStatus { packages: sbom_status.packages, } } -} \ No newline at end of file +} From 30ae8e952cc6ec6934f646e5662b800727e08cc8 Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Wed, 20 Nov 2024 18:15:20 -0500 Subject: [PATCH 3/8] Use VulnerabilityHead in SbomStatus instead of Details Signed-off-by: Jim Crossley --- .../src/ai/service/tools/sbom_info.rs | 4 +-- modules/fundamental/src/sbom/model/details.rs | 34 +++++++++++++++---- .../src/vulnerability/service/test.rs | 2 +- modules/graphql/src/sbomstatus.rs | 2 +- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/modules/fundamental/src/ai/service/tools/sbom_info.rs b/modules/fundamental/src/ai/service/tools/sbom_info.rs index 456a38275..12b1fa7c9 100644 --- a/modules/fundamental/src/ai/service/tools/sbom_info.rs +++ b/modules/fundamental/src/ai/service/tools/sbom_info.rs @@ -192,10 +192,10 @@ The tool provides a list of advisories/CVEs affecting the SBOM. .status .iter() .map(|v| Vulnerability { - identifier: v.vulnerability.head.identifier.clone(), + identifier: v.vulnerability.identifier.clone(), link: format!( "http://localhost:3000/vulnerability/{}", - v.vulnerability.head.identifier + v.vulnerability.identifier ), }) .collect(), diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs index cd6cf5f71..2859dd8cd 100644 --- a/modules/fundamental/src/sbom/model/details.rs +++ b/modules/fundamental/src/sbom/model/details.rs @@ -1,6 +1,13 @@ use super::SbomSummary; use crate::{ - advisory::model::AdvisoryHead, purl::model::{details::purl::StatusContext, summary::purl::PurlSummary}, sbom::{model::SbomPackage, service::{sbom::QueryCatcher, SbomService}}, vulnerability::model::VulnerabilityDetails, Error + advisory::model::AdvisoryHead, + purl::model::{details::purl::StatusContext, summary::purl::PurlSummary}, + sbom::{ + model::SbomPackage, + service::{sbom::QueryCatcher, SbomService}, + }, + vulnerability::model::VulnerabilityHead, + Error, }; use cpe::uri::OwnedUri; use sea_orm::{ @@ -86,7 +93,10 @@ impl SbomDetails { ) .join(JoinType::Join, product_status::Relation::Status.def()) .join(JoinType::Join, product_status::Relation::Advisory.def()) - .join(JoinType::Join, product_status::Relation::Vulnerability.def()) + .join( + JoinType::Join, + product_status::Relation::Vulnerability.def(), + ) .distinct_on([ (product_status::Entity, product_status::Column::ContextCpeId), (product_status::Entity, product_status::Column::StatusId), @@ -192,7 +202,7 @@ impl SbomAdvisory { let sbom_status = if let Some(status) = advisory.status.iter_mut().find(|status| { if status.status == each.status.slug - && status.vulnerability.head.identifier == each.vulnerability.id + && status.vulnerability.identifier == each.vulnerability.id { match (&status.context, &status_cpe) { (Some(StatusContext::Cpe(context_cpe)), Some(status_cpe)) => { @@ -208,7 +218,10 @@ impl SbomAdvisory { status } else { let status = SbomStatus { - vulnerability: VulnerabilityDetails::from_entity(&each.vulnerability, Default::default(), tx).await?, + vulnerability: VulnerabilityHead::from_vulnerability_entity_and_description( + &each.vulnerability, + None, + ), status: each.status.slug.clone(), context: status_cpe .as_ref() @@ -253,7 +266,10 @@ impl SbomAdvisory { } let status = SbomStatus { - vulnerability: VulnerabilityDetails::from_entity(&product.vulnerability.clone(), Default::default(), tx).await?, + vulnerability: VulnerabilityHead::from_vulnerability_entity_and_description( + &product.vulnerability, + None, + ), status: product.status.slug.clone(), context: advisory_cpe .as_ref() @@ -280,7 +296,7 @@ impl SbomAdvisory { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct SbomStatus { - pub vulnerability: VulnerabilityDetails, + pub vulnerability: VulnerabilityHead, pub status: String, pub context: Option, pub packages: Vec, @@ -303,7 +319,11 @@ impl FromQueryResult for ProductStatusCatcher { fn from_query_result(res: &QueryResult, _pre: &str) -> Result { Ok(Self { advisory: Self::from_query_result_multi_model(res, "", advisory::Entity)?, - vulnerability: Self::from_query_result_multi_model(res, "", trustify_entity::vulnerability::Entity)?, + vulnerability: Self::from_query_result_multi_model( + res, + "", + trustify_entity::vulnerability::Entity, + )?, product_status: Self::from_query_result_multi_model(res, "", product_status::Entity)?, cpe: Self::from_query_result_multi_model(res, "", trustify_entity::cpe::Entity)?, status: Self::from_query_result_multi_model(res, "", status::Entity)?, diff --git a/modules/fundamental/src/vulnerability/service/test.rs b/modules/fundamental/src/vulnerability/service/test.rs index f90bca20f..6e09d435a 100644 --- a/modules/fundamental/src/vulnerability/service/test.rs +++ b/modules/fundamental/src/vulnerability/service/test.rs @@ -233,7 +233,7 @@ async fn product_statuses(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { let quarkus_adv = &quarkus_sbom.advisories[0].status[0]; assert_eq!(quarkus_adv.status, "fixed"); - assert_eq!(quarkus_adv.vulnerability_id, "CVE-2023-0044"); + assert_eq!(quarkus_adv.vulnerability.identifier, "CVE-2023-0044"); let vuln = vuln_service .fetch_vulnerability("CVE-2023-0044", Default::default(), Transactional::None) diff --git a/modules/graphql/src/sbomstatus.rs b/modules/graphql/src/sbomstatus.rs index 8fb043ecd..b9488bd69 100644 --- a/modules/graphql/src/sbomstatus.rs +++ b/modules/graphql/src/sbomstatus.rs @@ -63,7 +63,7 @@ impl GraphQLSbomStatus {} impl From for GraphQLSbomStatus { fn from(sbom_status: SbomStatus) -> Self { GraphQLSbomStatus { - vulnerability_id: sbom_status.vulnerability.head.identifier.clone(), + vulnerability_id: sbom_status.vulnerability.identifier.clone(), status: sbom_status.status, context: sbom_status.context, packages: sbom_status.packages, From 667f2fb2bfbc52ac0cc63525c199070183fd504d Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Wed, 20 Nov 2024 19:28:01 -0500 Subject: [PATCH 4/8] Removed some unused fn's and added a severity field to SbomStatus Signed-off-by: Jim Crossley --- modules/fundamental/src/sbom/model/details.rs | 26 +++- .../src/vulnerability/model/details/mod.rs | 128 +++--------------- .../src/vulnerability/model/mod.rs | 27 +--- 3 files changed, 40 insertions(+), 141 deletions(-) diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs index 2859dd8cd..feb43d1a2 100644 --- a/modules/fundamental/src/sbom/model/details.rs +++ b/modules/fundamental/src/sbom/model/details.rs @@ -6,7 +6,7 @@ use crate::{ model::SbomPackage, service::{sbom::QueryCatcher, SbomService}, }, - vulnerability::model::VulnerabilityHead, + vulnerability::model::{VulnerabilityDetails, VulnerabilityHead}, Error, }; use cpe::uri::OwnedUri; @@ -25,6 +25,7 @@ use trustify_common::{ }, memo::Memo, }; +use trustify_cvss::cvss3::severity::Severity; use trustify_entity::{ advisory, base_purl, product, product_status, product_version, purl_status, qualified_purl::{self}, @@ -217,11 +218,16 @@ impl SbomAdvisory { }) { status } else { + let (score, _) = + VulnerabilityDetails::average_score(&each.vulnerability, tx).await?; let status = SbomStatus { - vulnerability: VulnerabilityHead::from_vulnerability_entity_and_description( + vulnerability: VulnerabilityHead::from_vulnerability_entity( &each.vulnerability, - None, - ), + Memo::NotProvided, + tx, + ) + .await?, + severity: score.map(|v| v.severity()), status: each.status.slug.clone(), context: status_cpe .as_ref() @@ -265,11 +271,16 @@ impl SbomAdvisory { packages.push(package); } + let (score, _) = + VulnerabilityDetails::average_score(&product.vulnerability, tx).await?; let status = SbomStatus { - vulnerability: VulnerabilityHead::from_vulnerability_entity_and_description( + vulnerability: VulnerabilityHead::from_vulnerability_entity( &product.vulnerability, - None, - ), + Memo::NotProvided, + tx, + ) + .await?, + severity: score.map(|v| v.severity()), status: product.status.slug.clone(), context: advisory_cpe .as_ref() @@ -297,6 +308,7 @@ impl SbomAdvisory { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct SbomStatus { pub vulnerability: VulnerabilityHead, + pub severity: Option, pub status: String, pub context: Option, pub packages: Vec, diff --git a/modules/fundamental/src/vulnerability/model/details/mod.rs b/modules/fundamental/src/vulnerability/model/details/mod.rs index d2f628cc8..d476845d9 100644 --- a/modules/fundamental/src/vulnerability/model/details/mod.rs +++ b/modules/fundamental/src/vulnerability/model/details/mod.rs @@ -3,7 +3,7 @@ mod vulnerability_advisory; pub use vulnerability_advisory::*; use crate::{vulnerability::model::VulnerabilityHead, Error}; -use sea_orm::{ColumnTrait, EntityTrait, LoaderTrait, ModelTrait, QueryFilter}; +use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter}; use serde::{Deserialize, Serialize}; use trustify_common::{db::ConnectionOrTransaction, memo::Memo}; use trustify_cvss::cvss3::{score::Score, severity::Severity, Cvss3Base}; @@ -29,55 +29,6 @@ pub struct VulnerabilityDetails { } impl VulnerabilityDetails { - pub async fn from_advisory_vulnerabilities( - vulnerability: &vulnerability::Model, - advisory_vulnerabilities: &[advisory_vulnerability::Model], - vuln_cvss3s: &[cvss3::Model], - tx: &ConnectionOrTransaction<'_>, - ) -> Result { - let advisories = VulnerabilityAdvisorySummary::from_entities( - vulnerability, - advisory_vulnerabilities, - vuln_cvss3s, - tx, - ) - .await?; - - let cvss3 = cvss3::Entity::find() - .filter(trustify_entity::cvss3::Column::VulnerabilityId.eq(&vulnerability.id)) - .all(tx) - .await?; - - let total_score = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let average_score = total_score.map(|total| Score::new(total / cvss3.len() as f64)); - - Ok(VulnerabilityDetails { - head: VulnerabilityHead { - normative: true, - identifier: vulnerability.id.clone(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: Vec::default(), - }, - average_severity: average_score.map(|score| score.severity()), - average_score: average_score.map(|score| score.value()), - advisories, - }) - } - pub async fn from_entity( vulnerability: &vulnerability::Model, deprecation: Deprecation, @@ -89,10 +40,7 @@ impl VulnerabilityDetails { .all(tx) .await?; - let cvss3 = cvss3::Entity::find() - .filter(cvss3::Column::VulnerabilityId.eq(&vulnerability.id)) - .all(tx) - .await?; + let (score, cvss3) = Self::average_score(vulnerability, tx).await?; let advisories = VulnerabilityAdvisorySummary::from_entities( vulnerability, @@ -102,16 +50,6 @@ impl VulnerabilityDetails { ) .await?; - let total_score = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let average_score = total_score.map(|total| Score::new(total / cvss3.len() as f64)); - Ok(VulnerabilityDetails { head: VulnerabilityHead::from_vulnerability_entity( vulnerability, @@ -119,59 +57,31 @@ impl VulnerabilityDetails { tx, ) .await?, - average_severity: average_score.map(|score| score.severity()), - average_score: average_score.map(|score| score.value()), + average_severity: score.map(|v| v.severity()), + average_score: score.map(|v| v.value()), advisories, }) } - pub async fn from_entities( - vulnerabilities: &[vulnerability::Model], + pub async fn average_score( + vulnerability: &vulnerability::Model, tx: &ConnectionOrTransaction<'_>, - ) -> Result, Error> { - let advisory_vulnerabilities = vulnerabilities - .load_many(advisory_vulnerability::Entity::find(), tx) + ) -> Result<(Option, Vec), Error> { + let cvss3 = cvss3::Entity::find() + .filter(cvss3::Column::VulnerabilityId.eq(&vulnerability.id)) + .all(tx) .await?; - let mut details = Vec::new(); - - for (vulnerability, advisory_vulnerabilities) in - vulnerabilities.iter().zip(advisory_vulnerabilities.iter()) - { - let cvss3 = cvss3::Entity::find() - .filter(cvss3::Column::VulnerabilityId.eq(&vulnerability.id)) - .all(tx) - .await?; - - let total_score = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let average_score = total_score.map(|total| Score::new(total / cvss3.len() as f64)); - - details.push(VulnerabilityDetails { - head: VulnerabilityHead::from_vulnerability_entity( - vulnerability, - Memo::NotProvided, - tx, - ) - .await?, - average_severity: average_score.map(|score| score.severity()), - average_score: average_score.map(|score| score.value()), - advisories: VulnerabilityAdvisorySummary::from_entities( - vulnerability, - advisory_vulnerabilities, - &cvss3, - tx, - ) - .await?, + let total = cvss3 + .iter() + .map(|e| { + let base = Cvss3Base::from(e.clone()); + base.score().value() }) - } + .reduce(|accum, e| accum + e); + + let average = total.map(|t| Score::new(t / cvss3.len() as f64)); - Ok(details) + Ok((average, cvss3)) } } diff --git a/modules/fundamental/src/vulnerability/model/mod.rs b/modules/fundamental/src/vulnerability/model/mod.rs index c9546440b..bd125d58b 100644 --- a/modules/fundamental/src/vulnerability/model/mod.rs +++ b/modules/fundamental/src/vulnerability/model/mod.rs @@ -3,7 +3,7 @@ mod summary; use async_graphql::SimpleObject; pub use details::*; -use sea_orm::{ColumnTrait, LoaderTrait, ModelTrait, QueryFilter}; +use sea_orm::{ColumnTrait, ModelTrait, QueryFilter}; pub use summary::*; use crate::Error; @@ -89,7 +89,7 @@ impl VulnerabilityHead { )) } - pub fn from_vulnerability_entity_and_description( + fn from_vulnerability_entity_and_description( entity: &vulnerability::Model, description: Option, ) -> Self { @@ -126,27 +126,4 @@ impl VulnerabilityHead { cwes: advisory_vulnerability.cwes.clone().unwrap_or_default(), } } - - pub async fn from_vulnerability_entities( - entities: &[vulnerability::Model], - tx: &ConnectionOrTransaction<'_>, - ) -> Result, Error> { - let descriptions = entities - .load_many(vulnerability_description::Entity, tx) - .await? - .into_iter() - .map(|model| { - model - .into_iter() - .filter(|desc| desc.lang == "en") - .map(|desc| desc.description) - .next() - }); - - Ok(entities - .iter() - .zip(descriptions) - .map(|(vuln, desc)| Self::from_vulnerability_entity_and_description(vuln, desc)) - .collect()) - } } From 5804f288ca01927003c38d73ef4e571dee74d0c9 Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Wed, 20 Nov 2024 19:29:40 -0500 Subject: [PATCH 5/8] Update openapi spec Signed-off-by: Jim Crossley --- openapi.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 2a0787f42..f4f3930b3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3521,7 +3521,7 @@ components: SbomStatus: type: object required: - - vulnerability_id + - vulnerability - status - packages properties: @@ -3533,10 +3533,14 @@ components: type: array items: $ref: '#/components/schemas/SbomPackage' + severity: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Severity' status: type: string - vulnerability_id: - type: string + vulnerability: + $ref: '#/components/schemas/VulnerabilityHead' SbomSummary: allOf: - $ref: '#/components/schemas/SbomHead' From b39cf597f26e3dfe775244a3b7ff57b13dad9e38 Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Thu, 21 Nov 2024 11:51:17 -0500 Subject: [PATCH 6/8] Give VulnerabilityHead a Default impl Signed-off-by: Jim Crossley --- .../src/vulnerability/model/mod.rs | 2 +- .../fundamental/tests/advisory/csaf/delete.rs | 10 +------ .../tests/advisory/csaf/reingest.rs | 30 ++----------------- .../tests/advisory/osv/reingest.rs | 20 ++----------- 4 files changed, 7 insertions(+), 55 deletions(-) diff --git a/modules/fundamental/src/vulnerability/model/mod.rs b/modules/fundamental/src/vulnerability/model/mod.rs index bd125d58b..4cc406b97 100644 --- a/modules/fundamental/src/vulnerability/model/mod.rs +++ b/modules/fundamental/src/vulnerability/model/mod.rs @@ -14,7 +14,7 @@ use trustify_common::memo::Memo; use trustify_entity::{advisory_vulnerability, vulnerability, vulnerability_description}; use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Debug, Clone, ToSchema, PartialEq, Eq, SimpleObject)] +#[derive(Default, Serialize, Deserialize, Debug, Clone, ToSchema, PartialEq, Eq, SimpleObject)] pub struct VulnerabilityHead { #[schema(required)] pub normative: bool, diff --git a/modules/fundamental/tests/advisory/csaf/delete.rs b/modules/fundamental/tests/advisory/csaf/delete.rs index 8b1249c24..e79f291fe 100644 --- a/modules/fundamental/tests/advisory/csaf/delete.rs +++ b/modules/fundamental/tests/advisory/csaf/delete.rs @@ -129,15 +129,7 @@ async fn delete_check_vulns(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2023-33201".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "affected".to_string(), context: Some(StatusContext::Cpe( diff --git a/modules/fundamental/tests/advisory/csaf/reingest.rs b/modules/fundamental/tests/advisory/csaf/reingest.rs index fd6d8d795..1cec40d20 100644 --- a/modules/fundamental/tests/advisory/csaf/reingest.rs +++ b/modules/fundamental/tests/advisory/csaf/reingest.rs @@ -141,15 +141,7 @@ async fn change_ps_list_vulns(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2023-33201".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "fixed".to_string(), context: Some(StatusContext::Cpe( @@ -239,15 +231,7 @@ async fn change_ps_list_vulns_all(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2023-33201".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "affected".to_string(), context: Some(StatusContext::Cpe( @@ -261,15 +245,7 @@ async fn change_ps_list_vulns_all(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2023-33201".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "fixed".to_string(), context: Some(StatusContext::Cpe( diff --git a/modules/fundamental/tests/advisory/osv/reingest.rs b/modules/fundamental/tests/advisory/osv/reingest.rs index 821e25245..0610f2f5b 100644 --- a/modules/fundamental/tests/advisory/osv/reingest.rs +++ b/modules/fundamental/tests/advisory/osv/reingest.rs @@ -121,15 +121,7 @@ async fn withdrawn(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2020-5238".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "affected".to_string(), context: None, @@ -141,15 +133,7 @@ async fn withdrawn(ctx: &TrustifyContext) -> anyhow::Result<()> { vulnerability: VulnerabilityHead { normative: true, identifier: "CVE-2020-5238".to_string(), - title: None, - description: None, - reserved: None, - published: None, - modified: None, - withdrawn: None, - discovered: None, - released: None, - cwes: vec![], + ..Default::default() }, status: "affected".to_string(), context: None, From 6ebb7d957534cfab60df56009b17ffa716c75f17 Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Thu, 21 Nov 2024 14:22:32 -0500 Subject: [PATCH 7/8] refactor: compute score/severity in a single place Signed-off-by: Jim Crossley --- cvss/src/cvss3/score.rs | 20 +++++ entity/src/cvss3.rs | 20 ++--- .../model/details/advisory_vulnerability.rs | 26 +----- modules/fundamental/src/sbom/model/details.rs | 88 +++++++++++-------- .../src/vulnerability/model/details/mod.rs | 31 ++----- .../model/details/vulnerability_advisory.rs | 30 +++---- openapi.yaml | 42 +++++---- 7 files changed, 114 insertions(+), 143 deletions(-) diff --git a/cvss/src/cvss3/score.rs b/cvss/src/cvss3/score.rs index 167fd57af..25250e6b7 100644 --- a/cvss/src/cvss3/score.rs +++ b/cvss/src/cvss3/score.rs @@ -1,5 +1,7 @@ use crate::cvss3::severity::Severity; +use super::Cvss3Base; + /// CVSS V3.1 scores. /// /// Formula described in CVSS v3.1 Specification: Section 5: @@ -66,3 +68,21 @@ impl From for Severity { score.severity() } } + +impl FromIterator for Score { + fn from_iter>(iter: I) -> Self { + let mut count: usize = 0; + let mut sum = 0.0; + + for v in iter { + sum += v.score().value(); + count += 1; + } + + if count > 0 { + Self::new(sum / (count as f64)) + } else { + Self::default() + } + } +} diff --git a/entity/src/cvss3.rs b/entity/src/cvss3.rs index 69e63aefb..aa4bec463 100644 --- a/entity/src/cvss3.rs +++ b/entity/src/cvss3.rs @@ -28,8 +28,8 @@ pub struct Model { pub severity: Severity, } -impl From for cvss3::Cvss3Base { - fn from(value: Model) -> Self { +impl From<&Model> for cvss3::Cvss3Base { + fn from(value: &Model) -> Self { Self { minor_version: value.minor_version as u8, av: value.av.into(), @@ -44,19 +44,9 @@ impl From for cvss3::Cvss3Base { } } -impl From<&Model> for cvss3::Cvss3Base { - fn from(value: &Model) -> Self { - Self { - minor_version: value.minor_version as u8, - av: value.av.into(), - ac: value.ac.into(), - pr: value.pr.into(), - ui: value.ui.into(), - s: value.s.into(), - c: value.c.into(), - i: value.i.into(), - a: value.a.into(), - } +impl From for cvss3::Cvss3Base { + fn from(value: Model) -> Self { + Self::from(&value) } } diff --git a/modules/fundamental/src/advisory/model/details/advisory_vulnerability.rs b/modules/fundamental/src/advisory/model/details/advisory_vulnerability.rs index 4ae191345..a22ba465a 100644 --- a/modules/fundamental/src/advisory/model/details/advisory_vulnerability.rs +++ b/modules/fundamental/src/advisory/model/details/advisory_vulnerability.rs @@ -41,18 +41,7 @@ impl AdvisoryVulnerabilityHead { .all(tx) .await?; - let score = if let Some(average) = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e) - { - Score::new(average / cvss3.len() as f64) - } else { - Score::new(0.0) - }; + let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from)); let advisory_vuln = advisory_vulnerability::Entity::find() .filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id)) @@ -94,18 +83,7 @@ impl AdvisoryVulnerabilityHead { let mut heads = Vec::new(); for (vuln, cvss3) in vulnerabilities.iter().zip(cvss3s.iter()) { - let score = if let Some(average) = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e) - { - Score::new(average / cvss3.len() as f64) - } else { - Score::new(0.0) - }; + let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from)); let advisory_vuln = advisory_vulnerability::Entity::find() .filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id)) diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs index feb43d1a2..909fb701c 100644 --- a/modules/fundamental/src/sbom/model/details.rs +++ b/modules/fundamental/src/sbom/model/details.rs @@ -6,7 +6,7 @@ use crate::{ model::SbomPackage, service::{sbom::QueryCatcher, SbomService}, }, - vulnerability::model::{VulnerabilityDetails, VulnerabilityHead}, + vulnerability::model::VulnerabilityHead, Error, }; use cpe::uri::OwnedUri; @@ -25,12 +25,11 @@ use trustify_common::{ }, memo::Memo, }; -use trustify_cvss::cvss3::severity::Severity; +use trustify_cvss::cvss3::{score::Score, severity::Severity, Cvss3Base}; use trustify_entity::{ - advisory, base_purl, product, product_status, product_version, purl_status, - qualified_purl::{self}, - sbom::{self}, - sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, versioned_purl, + advisory, base_purl, cvss3, product, product_status, product_version, purl_status, + qualified_purl, sbom, sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, + versioned_purl, vulnerability, }; use utoipa::ToSchema; @@ -218,22 +217,14 @@ impl SbomAdvisory { }) { status } else { - let (score, _) = - VulnerabilityDetails::average_score(&each.vulnerability, tx).await?; - let status = SbomStatus { - vulnerability: VulnerabilityHead::from_vulnerability_entity( - &each.vulnerability, - Memo::NotProvided, - tx, - ) - .await?, - severity: score.map(|v| v.severity()), - status: each.status.slug.clone(), - context: status_cpe - .as_ref() - .map(|e| StatusContext::Cpe(e.to_string())), - packages: vec![], - }; + let status = SbomStatus::new( + &each.vulnerability, + each.status.slug.clone(), + status_cpe, + vec![], + tx, + ) + .await?; advisory.status.push(status); if let Some(status) = advisory.status.last_mut() { status @@ -271,22 +262,14 @@ impl SbomAdvisory { packages.push(package); } - let (score, _) = - VulnerabilityDetails::average_score(&product.vulnerability, tx).await?; - let status = SbomStatus { - vulnerability: VulnerabilityHead::from_vulnerability_entity( - &product.vulnerability, - Memo::NotProvided, - tx, - ) - .await?, - severity: score.map(|v| v.severity()), - status: product.status.slug.clone(), - context: advisory_cpe - .as_ref() - .map(|e| StatusContext::Cpe(e.to_string())), + let status = SbomStatus::new( + &product.vulnerability, + product.status.slug.clone(), + advisory_cpe, packages, // TODO find purls based on package names - }; + tx, + ) + .await?; match advisories.entry(product.advisory.id) { Entry::Occupied(entry) => entry.into_mut().status.push(status.clone()), @@ -307,14 +290,41 @@ impl SbomAdvisory { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct SbomStatus { + #[serde(flatten)] pub vulnerability: VulnerabilityHead, - pub severity: Option, + pub average_severity: Severity, pub status: String, pub context: Option, pub packages: Vec, } -impl SbomStatus {} +impl SbomStatus { + pub async fn new( + vulnerability: &vulnerability::Model, + status: String, + cpe: Option, + packages: Vec, + tx: &ConnectionOrTransaction<'_>, + ) -> Result { + let cvss3 = vulnerability.find_related(cvss3::Entity).all(tx).await?; + let average_severity = Score::from_iter(cvss3.iter().map(Cvss3Base::from)).severity(); + Ok(Self { + vulnerability: VulnerabilityHead::from_vulnerability_entity( + vulnerability, + Memo::NotProvided, + tx, + ) + .await?, + context: cpe.as_ref().map(|e| StatusContext::Cpe(e.to_string())), + average_severity, + status, + packages, + }) + } + pub fn identifier(&self) -> &str { + &self.vulnerability.identifier + } +} #[derive(Debug)] #[allow(dead_code)] //TODO sbom field is not used at the moment, but we will probably need it for graph search diff --git a/modules/fundamental/src/vulnerability/model/details/mod.rs b/modules/fundamental/src/vulnerability/model/details/mod.rs index d476845d9..c08720484 100644 --- a/modules/fundamental/src/vulnerability/model/details/mod.rs +++ b/modules/fundamental/src/vulnerability/model/details/mod.rs @@ -3,7 +3,7 @@ mod vulnerability_advisory; pub use vulnerability_advisory::*; use crate::{vulnerability::model::VulnerabilityHead, Error}; -use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter}; +use sea_orm::ModelTrait; use serde::{Deserialize, Serialize}; use trustify_common::{db::ConnectionOrTransaction, memo::Memo}; use trustify_cvss::cvss3::{score::Score, severity::Severity, Cvss3Base}; @@ -40,7 +40,12 @@ impl VulnerabilityDetails { .all(tx) .await?; - let (score, cvss3) = Self::average_score(vulnerability, tx).await?; + let cvss3 = vulnerability.find_related(cvss3::Entity).all(tx).await?; + let score = if cvss3.is_empty() { + None + } else { + Some(Score::from_iter(cvss3.iter().map(Cvss3Base::from))) + }; let advisories = VulnerabilityAdvisorySummary::from_entities( vulnerability, @@ -62,26 +67,4 @@ impl VulnerabilityDetails { advisories, }) } - - pub async fn average_score( - vulnerability: &vulnerability::Model, - tx: &ConnectionOrTransaction<'_>, - ) -> Result<(Option, Vec), Error> { - let cvss3 = cvss3::Entity::find() - .filter(cvss3::Column::VulnerabilityId.eq(&vulnerability.id)) - .all(tx) - .await?; - - let total = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let average = total.map(|t| Score::new(t / cvss3.len() as f64)); - - Ok((average, cvss3)) - } } diff --git a/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs b/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs index 64670039a..8500bf74e 100644 --- a/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs +++ b/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs @@ -54,15 +54,11 @@ impl VulnerabilityAdvisoryHead { .all(tx) .await?; - let total_score = cvss3 - .iter() - .map(|e| { - let base = Cvss3Base::from(e.clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let score = total_score.map(|score| Score::new(score / cvss3.len() as f64)); + let score = if cvss3.is_empty() { + None + } else { + Some(Score::from_iter(cvss3.iter().map(Cvss3Base::from))) + }; if let Some(advisory) = &advisory_vulnerability .find_related(advisory::Entity) @@ -90,20 +86,16 @@ impl VulnerabilityAdvisoryHead { for (advisory, issuer) in vuln_advisories.iter().zip(organizations.into_iter()) { // filter all vulnerability cvss3 to those that pertain to only this advisory. - let advisory_cvss3s = vuln_cvss3s + let cvss3 = vuln_cvss3s .iter() .filter(|e| e.vulnerability_id == vulnerability.id) .collect::>(); - let total_score = advisory_cvss3s - .iter() - .map(|e| { - let base = Cvss3Base::from((*e).clone()); - base.score().value() - }) - .reduce(|accum, e| accum + e); - - let score = total_score.map(|score| Score::new(score / advisory_cvss3s.len() as f64)); + let score = if cvss3.is_empty() { + None + } else { + Some(Score::from_iter(cvss3.into_iter().map(Cvss3Base::from))) + }; heads.push(VulnerabilityAdvisoryHead { head: AdvisoryHead::from_advisory(advisory, Memo::Provided(issuer), tx).await?, diff --git a/openapi.yaml b/openapi.yaml index f4f3930b3..13a08398f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3519,28 +3519,26 @@ components: relationship: $ref: '#/components/schemas/Relationship' SbomStatus: - type: object - required: - - vulnerability - - status - - packages - properties: - context: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/StatusContext' - packages: - type: array - items: - $ref: '#/components/schemas/SbomPackage' - severity: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Severity' - status: - type: string - vulnerability: - $ref: '#/components/schemas/VulnerabilityHead' + allOf: + - $ref: '#/components/schemas/VulnerabilityHead' + - type: object + required: + - average_severity + - status + - packages + properties: + average_severity: + $ref: '#/components/schemas/Severity' + context: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/StatusContext' + packages: + type: array + items: + $ref: '#/components/schemas/SbomPackage' + status: + type: string SbomSummary: allOf: - $ref: '#/components/schemas/SbomHead' From 39ea8e2878b36d08c1f66e4fc27ff41db66ea2b1 Mon Sep 17 00:00:00 2001 From: Jim Crossley Date: Thu, 21 Nov 2024 18:01:39 -0500 Subject: [PATCH 8/8] Add a unit test for /api/v1/sbom/{id}/advisory Signed-off-by: Jim Crossley --- cvss/src/cvss3/score.rs | 18 ++++------- .../fundamental/src/sbom/endpoints/test.rs | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/cvss/src/cvss3/score.rs b/cvss/src/cvss3/score.rs index 25250e6b7..2b20ce8a6 100644 --- a/cvss/src/cvss3/score.rs +++ b/cvss/src/cvss3/score.rs @@ -37,16 +37,12 @@ impl Score { /// Convert the numeric score into a `Severity` pub fn severity(self) -> Severity { - if self.0 < 0.1 { - Severity::None - } else if self.0 < 4.0 { - Severity::Low - } else if self.0 < 7.0 { - Severity::Medium - } else if self.0 < 9.0 { - Severity::High - } else { - Severity::Critical + match self.0 { + x if x < 0.1 => Severity::None, + x if x < 4.0 => Severity::Low, + x if x < 7.0 => Severity::Medium, + x if x < 9.0 => Severity::High, + _ => Severity::Critical, } } } @@ -73,12 +69,10 @@ impl FromIterator for Score { fn from_iter>(iter: I) -> Self { let mut count: usize = 0; let mut sum = 0.0; - for v in iter { sum += v.score().value(); count += 1; } - if count > 0 { Self::new(sum / (count as f64)) } else { diff --git a/modules/fundamental/src/sbom/endpoints/test.rs b/modules/fundamental/src/sbom/endpoints/test.rs index 7442d276c..405d7b98d 100644 --- a/modules/fundamental/src/sbom/endpoints/test.rs +++ b/modules/fundamental/src/sbom/endpoints/test.rs @@ -202,3 +202,33 @@ async fn download_sbom(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { Ok(()) } + +#[test_context(TrustifyContext)] +#[test(actix_web::test)] +async fn get_advisories(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { + let id = ctx + .ingest_documents([ + "quarkus-bom-2.13.8.Final-redhat-00004.json", + "csaf/cve-2023-0044.json", + ]) + .await?[0] + .id + .to_string(); + + let app = caller(ctx).await?; + let v: Value = app + .call_and_read_body_json( + TestRequest::get() + .uri(&format!("/api/v1/sbom/{id}/advisory")) + .to_request(), + ) + .await; + + log::debug!("{v:#?}"); + + // assert expected fields + assert_eq!(v[0]["identifier"], "https://www.redhat.com/#CVE-2023-0044"); + assert_eq!(v[0]["status"][0]["average_severity"], "high"); + + Ok(()) +}