From 6c4539b3b3f1038b693d8496504b13b147912fe7 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Tue, 5 Nov 2024 12:41:02 +0100 Subject: [PATCH] feat: add the "reserved" field for vulnerabilities Closes #964 --- entity/src/advisory_vulnerability.rs | 1 + entity/src/vulnerability.rs | 1 + migration/src/lib.rs | 2 + .../src/m0000700_advisory_add_reserved.rs | 71 +++++++++++++++++++ .../src/purl/model/details/purl.rs | 1 + .../src/vulnerability/endpoints/test.rs | 2 + .../src/vulnerability/model/details/mod.rs | 1 + .../src/vulnerability/model/mod.rs | 7 ++ .../fundamental/tests/advisory/csaf/delete.rs | 1 + .../tests/advisory/csaf/reingest.rs | 3 + .../tests/advisory/osv/reingest.rs | 2 + modules/graphql/src/vulnerability.rs | 2 + modules/ingestor/src/graph/advisory/mod.rs | 2 + .../ingestor/src/graph/vulnerability/mod.rs | 5 ++ .../src/service/advisory/csaf/loader.rs | 1 + .../src/service/advisory/cve/loader.rs | 14 +++- .../src/service/advisory/osv/loader.rs | 1 + modules/ingestor/tests/reingest/cve.rs | 10 ++- openapi.yaml | 7 ++ 19 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 migration/src/m0000700_advisory_add_reserved.rs diff --git a/entity/src/advisory_vulnerability.rs b/entity/src/advisory_vulnerability.rs index 9bb03fd4c..26ee1b272 100644 --- a/entity/src/advisory_vulnerability.rs +++ b/entity/src/advisory_vulnerability.rs @@ -13,6 +13,7 @@ pub struct Model { pub title: Option, pub summary: Option, pub description: Option, + pub reserved_date: Option, pub discovery_date: Option, pub release_date: Option, pub cwes: Option>, diff --git a/entity/src/vulnerability.rs b/entity/src/vulnerability.rs index a4a2a4521..2f0427956 100644 --- a/entity/src/vulnerability.rs +++ b/entity/src/vulnerability.rs @@ -10,6 +10,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: String, pub title: Option, + pub reserved: Option, pub published: Option, pub modified: Option, pub withdrawn: Option, diff --git a/migration/src/lib.rs b/migration/src/lib.rs index c925deb42..e4358bdb7 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -86,6 +86,7 @@ mod m0000660_purl_id_indexes; mod m0000670_version_cmp; mod m0000680_fix_update_deprecated_advisory; mod m0000690_alter_sbom_details; +mod m0000700_advisory_add_reserved; pub struct Migrator; @@ -179,6 +180,7 @@ impl MigratorTrait for Migrator { Box::new(m0000670_version_cmp::Migration), Box::new(m0000680_fix_update_deprecated_advisory::Migration), Box::new(m0000690_alter_sbom_details::Migration), + Box::new(m0000700_advisory_add_reserved::Migration), ] } } diff --git a/migration/src/m0000700_advisory_add_reserved.rs b/migration/src/m0000700_advisory_add_reserved.rs new file mode 100644 index 000000000..afe878604 --- /dev/null +++ b/migration/src/m0000700_advisory_add_reserved.rs @@ -0,0 +1,71 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Vulnerability::Table) + .add_column( + ColumnDef::new(Vulnerability::Reserved) + .timestamp_with_time_zone() + .to_owned(), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(AdvisoryVulnerability::Table) + .add_column( + ColumnDef::new(AdvisoryVulnerability::ReservedDate) + .timestamp_with_time_zone() + .to_owned(), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(AdvisoryVulnerability::Table) + .drop_column(AdvisoryVulnerability::ReservedDate) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(Vulnerability::Table) + .drop_column(Vulnerability::Reserved) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Vulnerability { + Table, + Reserved, +} + +#[derive(DeriveIden)] +enum AdvisoryVulnerability { + Table, + ReservedDate, +} diff --git a/modules/fundamental/src/purl/model/details/purl.rs b/modules/fundamental/src/purl/model/details/purl.rs index 14d073039..81a61fab6 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -137,6 +137,7 @@ impl PurlAdvisory { let vulnerability = vuln.unwrap_or(vulnerability::Model { id: status.vulnerability_id.clone(), title: None, + reserved: None, published: None, modified: None, withdrawn: None, diff --git a/modules/fundamental/src/vulnerability/endpoints/test.rs b/modules/fundamental/src/vulnerability/endpoints/test.rs index 9df59bda1..6871ab13b 100644 --- a/modules/fundamental/src/vulnerability/endpoints/test.rs +++ b/modules/fundamental/src/vulnerability/endpoints/test.rs @@ -172,6 +172,7 @@ async fn one_vulnerability(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { VulnerabilityInformation { title: Some("Something wicked this way comes".to_string()), published: Some(OffsetDateTime::now_utc()), + reserved: None, modified: None, withdrawn: None, cwes: None, @@ -268,6 +269,7 @@ async fn delete_vulnerability(ctx: &TrustifyContext) -> Result<(), anyhow::Error "CVE-123", VulnerabilityInformation { title: Some("Something wicked this way comes".to_string()), + reserved: None, published: Some(OffsetDateTime::now_utc()), modified: None, withdrawn: None, diff --git a/modules/fundamental/src/vulnerability/model/details/mod.rs b/modules/fundamental/src/vulnerability/model/details/mod.rs index 7cb6677db..d2f628cc8 100644 --- a/modules/fundamental/src/vulnerability/model/details/mod.rs +++ b/modules/fundamental/src/vulnerability/model/details/mod.rs @@ -64,6 +64,7 @@ impl VulnerabilityDetails { identifier: vulnerability.id.clone(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, diff --git a/modules/fundamental/src/vulnerability/model/mod.rs b/modules/fundamental/src/vulnerability/model/mod.rs index 5a083c726..b566a3858 100644 --- a/modules/fundamental/src/vulnerability/model/mod.rs +++ b/modules/fundamental/src/vulnerability/model/mod.rs @@ -31,6 +31,11 @@ pub struct VulnerabilityHead { #[schema(required)] pub description: Option, + /// The date (in RFC3339 format) of when the vulnerability identifier was reserved, if any. + #[schema(required)] + #[serde(with = "time::serde::rfc3339::option")] + pub reserved: Option, + /// The date (in RFC3339 format) of when the vulnerability was published, if any. #[schema(required)] #[serde(with = "time::serde::rfc3339::option")] @@ -92,6 +97,7 @@ impl VulnerabilityHead { identifier: entity.id.clone(), title: entity.title.clone(), description, + reserved: entity.reserved, published: entity.published, modified: entity.modified, withdrawn: entity.withdrawn, @@ -110,6 +116,7 @@ impl VulnerabilityHead { identifier: vuln.id.clone(), title: advisory_vulnerability.title.clone(), description: advisory_vulnerability.description.clone(), + reserved: advisory_vulnerability.reserved_date, published: None, modified: None, withdrawn: None, diff --git a/modules/fundamental/tests/advisory/csaf/delete.rs b/modules/fundamental/tests/advisory/csaf/delete.rs index f506d26d8..8b1249c24 100644 --- a/modules/fundamental/tests/advisory/csaf/delete.rs +++ b/modules/fundamental/tests/advisory/csaf/delete.rs @@ -131,6 +131,7 @@ async fn delete_check_vulns(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2023-33201".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, diff --git a/modules/fundamental/tests/advisory/csaf/reingest.rs b/modules/fundamental/tests/advisory/csaf/reingest.rs index b0c68460b..fd6d8d795 100644 --- a/modules/fundamental/tests/advisory/csaf/reingest.rs +++ b/modules/fundamental/tests/advisory/csaf/reingest.rs @@ -143,6 +143,7 @@ async fn change_ps_list_vulns(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2023-33201".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, @@ -240,6 +241,7 @@ async fn change_ps_list_vulns_all(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2023-33201".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, @@ -261,6 +263,7 @@ async fn change_ps_list_vulns_all(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2023-33201".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, diff --git a/modules/fundamental/tests/advisory/osv/reingest.rs b/modules/fundamental/tests/advisory/osv/reingest.rs index b4f0db257..821e25245 100644 --- a/modules/fundamental/tests/advisory/osv/reingest.rs +++ b/modules/fundamental/tests/advisory/osv/reingest.rs @@ -123,6 +123,7 @@ async fn withdrawn(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2020-5238".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, @@ -142,6 +143,7 @@ async fn withdrawn(ctx: &TrustifyContext) -> anyhow::Result<()> { identifier: "CVE-2020-5238".to_string(), title: None, description: None, + reserved: None, published: None, modified: None, withdrawn: None, diff --git a/modules/graphql/src/vulnerability.rs b/modules/graphql/src/vulnerability.rs index a0d45bd11..e7fe1830b 100644 --- a/modules/graphql/src/vulnerability.rs +++ b/modules/graphql/src/vulnerability.rs @@ -24,6 +24,7 @@ impl VulnerabilityQuery { Ok(Some(vulnerability)) => Ok(Vulnerability { id: vulnerability.vulnerability.id, title: vulnerability.vulnerability.title, + reserved: vulnerability.vulnerability.reserved, published: vulnerability.vulnerability.published, modified: vulnerability.vulnerability.modified, withdrawn: vulnerability.vulnerability.withdrawn, @@ -47,6 +48,7 @@ impl VulnerabilityQuery { Ok(Vulnerability { id: vulnerability.vulnerability.id, title: vulnerability.vulnerability.title, + reserved: vulnerability.vulnerability.reserved, published: vulnerability.vulnerability.published, modified: vulnerability.vulnerability.modified, withdrawn: vulnerability.vulnerability.withdrawn, diff --git a/modules/ingestor/src/graph/advisory/mod.rs b/modules/ingestor/src/graph/advisory/mod.rs index 352dd9d46..80fc6442e 100644 --- a/modules/ingestor/src/graph/advisory/mod.rs +++ b/modules/ingestor/src/graph/advisory/mod.rs @@ -37,6 +37,7 @@ pub struct AdvisoryVulnerabilityInformation { pub title: Option, pub summary: Option, pub description: Option, + pub reserved_date: Option, pub discovery_date: Option, pub release_date: Option, pub cwes: Option>, @@ -275,6 +276,7 @@ impl<'g> AdvisoryContext<'g> { description: Set(information .as_ref() .and_then(|info| info.description.clone())), + reserved_date: Set(information.as_ref().and_then(|info| info.reserved_date)), discovery_date: Set(information.as_ref().and_then(|info| info.discovery_date)), release_date: Set(information.as_ref().and_then(|info| info.release_date)), cwes: Set(information.as_ref().and_then(|info| info.cwes.clone())), diff --git a/modules/ingestor/src/graph/vulnerability/mod.rs b/modules/ingestor/src/graph/vulnerability/mod.rs index b54b0e08a..f08a2cfd7 100644 --- a/modules/ingestor/src/graph/vulnerability/mod.rs +++ b/modules/ingestor/src/graph/vulnerability/mod.rs @@ -16,6 +16,7 @@ use uuid::Uuid; #[derive(Clone, Default, Debug)] pub struct VulnerabilityInformation { pub title: Option, + pub reserved: Option, pub published: Option, pub modified: Option, pub withdrawn: Option, @@ -25,6 +26,7 @@ pub struct VulnerabilityInformation { impl VulnerabilityInformation { pub fn has_data(&self) -> bool { self.title.is_some() + || self.reserved.is_some() || self.published.is_some() || self.modified.is_some() || self.withdrawn.is_some() @@ -36,6 +38,7 @@ impl From<()> for VulnerabilityInformation { fn from(_: ()) -> Self { Self { title: None, + reserved: None, published: None, modified: None, withdrawn: None, @@ -59,6 +62,7 @@ impl Graph { let on_conflict = match information.has_data() { true => on_conflict.update_columns([ vulnerability::Column::Title, + vulnerability::Column::Reserved, vulnerability::Column::Published, vulnerability::Column::Modified, vulnerability::Column::Withdrawn, @@ -75,6 +79,7 @@ impl Graph { let entity = vulnerability::ActiveModel { id: Set(identifier.to_string()), title: Set(information.title), + reserved: Set(information.reserved), published: Set(information.published), modified: Set(information.modified), withdrawn: Set(information.withdrawn), diff --git a/modules/ingestor/src/service/advisory/csaf/loader.rs b/modules/ingestor/src/service/advisory/csaf/loader.rs index 509919a93..70a135d5e 100644 --- a/modules/ingestor/src/service/advisory/csaf/loader.rs +++ b/modules/ingestor/src/service/advisory/csaf/loader.rs @@ -138,6 +138,7 @@ impl<'g> CsafLoader<'g> { title: vulnerability.title.clone(), summary: None, description: None, + reserved_date: None, discovery_date: vulnerability.discovery_date.and_then(|date| { OffsetDateTime::from_unix_timestamp(date.timestamp()).ok() }), diff --git a/modules/ingestor/src/service/advisory/cve/loader.rs b/modules/ingestor/src/service/advisory/cve/loader.rs index fd35165b5..b75a40334 100644 --- a/modules/ingestor/src/service/advisory/cve/loader.rs +++ b/modules/ingestor/src/service/advisory/cve/loader.rs @@ -61,6 +61,7 @@ impl<'g> CveLoader<'g> { let cwes = information.cwes.clone(); let release_date = information.published; + let reserved_date = information.reserved; let title = information.title.clone(); let advisory_info = AdvisoryInformation { title: information.title.clone(), @@ -92,6 +93,7 @@ impl<'g> CveLoader<'g> { title, summary: None, description: english_description.map(ToString::to_string), + reserved_date, discovery_date: assigned, release_date, cwes, @@ -199,6 +201,10 @@ impl<'g> CveLoader<'g> { } fn extract_vuln_info(cve: &Cve) -> VulnerabilityDetails { + let reserved = cve + .common_metadata() + .date_reserved + .map(Timestamp::assume_utc); let published = cve .common_metadata() .date_published @@ -276,6 +282,7 @@ impl<'g> CveLoader<'g> { affected, information: VulnerabilityInformation { title: title.clone(), + reserved, published, modified, withdrawn, @@ -325,6 +332,7 @@ mod test { use std::str::FromStr; use test_context::test_context; use test_log::test; + use time::macros::datetime; use trustify_common::db::Transactional; use trustify_common::purl::Purl; use trustify_test_context::{document, TrustifyContext}; @@ -351,13 +359,17 @@ mod test { let loaded_vulnerability = graph.get_vulnerability("CVE-2024-28111", ()).await?; assert!(loaded_vulnerability.is_some()); + let loaded_vulnerability = loaded_vulnerability.unwrap(); + assert_eq!( + loaded_vulnerability.vulnerability.reserved, + Some(datetime!(2024-03-04 14:19:14.059 UTC)) + ); let loaded_advisory = graph .get_advisory_by_digest(&digests.sha256.encode_hex::(), Transactional::None) .await?; assert!(loaded_advisory.is_some()); - let loaded_vulnerability = loaded_vulnerability.unwrap(); let descriptions = loaded_vulnerability.descriptions("en", ()).await?; assert_eq!(1, descriptions.len()); assert!(descriptions[0] diff --git a/modules/ingestor/src/service/advisory/osv/loader.rs b/modules/ingestor/src/service/advisory/osv/loader.rs index b51298115..635cf0731 100644 --- a/modules/ingestor/src/service/advisory/osv/loader.rs +++ b/modules/ingestor/src/service/advisory/osv/loader.rs @@ -88,6 +88,7 @@ impl<'g> OsvLoader<'g> { title: osv.summary.clone(), summary: osv.summary.clone(), description: osv.details.clone(), + reserved_date: None, discovery_date: None, release_date: None, cwes: None, diff --git a/modules/ingestor/tests/reingest/cve.rs b/modules/ingestor/tests/reingest/cve.rs index 40f5cf095..9c99854e2 100644 --- a/modules/ingestor/tests/reingest/cve.rs +++ b/modules/ingestor/tests/reingest/cve.rs @@ -1,8 +1,10 @@ #![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] use anyhow::bail; use test_context::test_context; use test_log::test; +use time::macros::datetime; use trustify_common::id::Id; use trustify_module_ingestor::model::IngestResult; use trustify_test_context::TrustifyContext; @@ -20,7 +22,13 @@ async fn reingest(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .await? .expect("must be found"); - assert_eq!(adv.vulnerabilities(()).await?.len(), 1); + let mut adv_vulns = adv.vulnerabilities(()).await?; + assert_eq!(adv_vulns.len(), 1); + let adv_vuln = adv_vulns.pop().unwrap(); + assert_eq!( + adv_vuln.advisory_vulnerability.reserved_date, + Some(datetime!(2021-05-12 0:00:00 UTC)) + ); let vulns = ctx.graph.get_vulnerabilities(()).await?; assert_eq!(vulns.len(), 1); diff --git a/openapi.yaml b/openapi.yaml index fc8c499e9..c9f46c563 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3630,6 +3630,7 @@ components: - identifier - title - description + - reserved - published - modified - withdrawn @@ -3679,6 +3680,12 @@ components: - 'null' format: date-time description: The date (in RFC3339 format) of when software containing the vulnerability first released, if known. + reserved: + type: + - string + - 'null' + format: date-time + description: The date (in RFC3339 format) of when the vulnerability identifier was reserved, if any. title: type: - string