diff --git a/entity/src/product_status.rs b/entity/src/product_status.rs index 5dc08b1fb..e613867a6 100644 --- a/entity/src/product_status.rs +++ b/entity/src/product_status.rs @@ -1,5 +1,7 @@ use sea_orm::entity::prelude::*; +use super::status::Status; + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "product_status")] pub struct Model { @@ -7,7 +9,7 @@ pub struct Model { pub id: Uuid, pub advisory_id: Uuid, pub vulnerability_id: String, - pub status_id: Uuid, + pub status: Status, pub package: Option, pub product_version_range_id: Uuid, pub context_cpe_id: Option, @@ -34,12 +36,6 @@ pub enum Relation { )] Advisory, - #[sea_orm(belongs_to = "super::status::Entity", - from = "Column::StatusId" - to = "super::status::Column::Id" - )] - Status, - #[sea_orm(belongs_to = "super::advisory_vulnerability::Entity", from = "(Column::AdvisoryId, Column::VulnerabilityId)" to = "(super::advisory_vulnerability::Column::AdvisoryId, super::advisory_vulnerability::Column::VulnerabilityId)" @@ -77,12 +73,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Status.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::AdvisoryVulnerability.def() diff --git a/entity/src/purl_status.rs b/entity/src/purl_status.rs index 3c4509e96..24e3dec9c 100644 --- a/entity/src/purl_status.rs +++ b/entity/src/purl_status.rs @@ -1,6 +1,8 @@ use sea_orm::LinkDef; use sea_orm::entity::prelude::*; +use super::status::Status; + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "purl_status")] pub struct Model { @@ -8,7 +10,7 @@ pub struct Model { pub id: Uuid, pub advisory_id: Uuid, pub vulnerability_id: String, - pub status_id: Uuid, + pub status: Status, pub base_purl_id: Uuid, pub version_range_id: Uuid, pub context_cpe_id: Option, @@ -43,12 +45,6 @@ pub enum Relation { )] Advisory, - #[sea_orm(belongs_to = "super::status::Entity", - from = "Column::StatusId" - to = "super::status::Column::Id" - )] - Status, - #[sea_orm(belongs_to = "super::advisory_vulnerability::Entity", from = "(Column::AdvisoryId, Column::VulnerabilityId)" to = "(super::advisory_vulnerability::Column::AdvisoryId, super::advisory_vulnerability::Column::VulnerabilityId)" @@ -106,12 +102,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Status.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::AdvisoryVulnerability.def() diff --git a/entity/src/status.rs b/entity/src/status.rs index 00743299b..53d003937 100644 --- a/entity/src/status.rs +++ b/entity/src/status.rs @@ -1,31 +1,6 @@ -use crate::purl_status; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "status")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub slug: String, - pub name: String, - pub description: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::purl_status::Entity")] - PackageStatus, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::PackageStatus.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - #[derive( Copy, Clone, @@ -37,12 +12,20 @@ impl ActiveModelBehavior for ActiveModel {} strum::Display, Serialize, Deserialize, + DeriveActiveEnum, + EnumIter, )] #[strum(serialize_all = "snake_case")] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "status")] pub enum Status { + #[sea_orm(string_value = "affected")] Affected, + #[sea_orm(string_value = "fixed")] Fixed, + #[sea_orm(string_value = "not_affected")] NotAffected, + #[sea_orm(string_value = "under_investigation")] UnderInvestigation, + #[sea_orm(string_value = "recommended")] Recommended, } diff --git a/migration/src/lib.rs b/migration/src/lib.rs index f40c951a8..e87b85cf2 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -58,6 +58,7 @@ mod m0002160_fix_ref_fk; mod m0002170_drop_cvss_tables; mod m0002180_advisory_fk_indexes; mod m0002190_vulnerability_base_score_advisory; +mod m0002200_status_enum; pub trait MigratorExt: Send { fn build_migrations() -> Migrations; @@ -132,6 +133,7 @@ impl MigratorExt for Migrator { .normal(m0002170_drop_cvss_tables::Migration) .normal(m0002180_advisory_fk_indexes::Migration) .normal(m0002190_vulnerability_base_score_advisory::Migration) + .normal(m0002200_status_enum::Migration) } } diff --git a/migration/src/m0002200_status_enum.rs b/migration/src/m0002200_status_enum.rs new file mode 100644 index 000000000..eb1ca2d17 --- /dev/null +++ b/migration/src/m0002200_status_enum.rs @@ -0,0 +1,171 @@ +use sea_orm_migration::prelude::*; +use sea_query::extension::postgres::Type; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // 1. Snapshot status slugs into a temp table, then drop old FK columns and status table. + // The table name "status" also reserves the composite type name in PostgreSQL, + // so it must be dropped before we can create the enum type with the same name. + db.execute_unprepared( + r#"CREATE TEMP TABLE _status_snapshot AS + SELECT ps.id AS row_id, 'purl' AS kind, s.slug + FROM purl_status ps JOIN status s ON ps.status_id = s.id + UNION ALL + SELECT ps.id AS row_id, 'product' AS kind, s.slug + FROM product_status ps JOIN status s ON ps.status_id = s.id"#, + ) + .await?; + + db.execute_unprepared(r#"ALTER TABLE "purl_status" DROP COLUMN "status_id""#) + .await?; + db.execute_unprepared(r#"ALTER TABLE "product_status" DROP COLUMN "status_id""#) + .await?; + db.execute_unprepared(r#"DROP TABLE "status""#).await?; + + // 2. Create PostgreSQL enum type + let builder = db.get_database_backend(); + let stmt = builder + .build(Type::create().as_enum(StatusEnum::Table).values([ + StatusEnum::Affected, + StatusEnum::Fixed, + StatusEnum::NotAffected, + StatusEnum::UnderInvestigation, + StatusEnum::Recommended, + ])) + .to_string(); + db.execute_unprepared(&stmt).await?; + + // 3. Add nullable enum columns + db.execute_unprepared(r#"ALTER TABLE "purl_status" ADD COLUMN "status" "status" NULL"#) + .await?; + db.execute_unprepared(r#"ALTER TABLE "product_status" ADD COLUMN "status" "status" NULL"#) + .await?; + + // 4. Populate from snapshot + db.execute_unprepared( + r#"UPDATE "purl_status" ps + SET "status" = snap."slug"::status + FROM _status_snapshot snap + WHERE snap.row_id = ps.id AND snap.kind = 'purl'"#, + ) + .await?; + db.execute_unprepared( + r#"UPDATE "product_status" ps + SET "status" = snap."slug"::status + FROM _status_snapshot snap + WHERE snap.row_id = ps.id AND snap.kind = 'product'"#, + ) + .await?; + + db.execute_unprepared(r#"DROP TABLE _status_snapshot"#) + .await?; + + // 5. Make NOT NULL + db.execute_unprepared(r#"ALTER TABLE "purl_status" ALTER COLUMN "status" SET NOT NULL"#) + .await?; + db.execute_unprepared(r#"ALTER TABLE "product_status" ALTER COLUMN "status" SET NOT NULL"#) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // 1. Recreate the status table + db.execute_unprepared( + r#"CREATE TABLE "status" ( + "id" uuid PRIMARY KEY, + "slug" text NOT NULL, + "name" text NOT NULL, + "description" text + )"#, + ) + .await?; + + // 2. Seed status rows + db.execute_unprepared( + r#"INSERT INTO "status" ("id", "slug", "name") VALUES + ('7cc29b44-e708-11ed-a05b-0242ac120003', 'affected', 'Affected'), + ('7cc29e00-e708-11ed-a05b-0242ac120003', 'not_affected', 'Not affected'), + ('7cc29f04-e708-11ed-a05b-0242ac120003', 'fixed', 'Fixed'), + ('7cc2a01c-e708-11ed-a05b-0242ac120003', 'under_investigation', 'Under investigation'), + ('7cc2a0ee-e708-11ed-a05b-0242ac120003', 'recommended', 'Recommended')"#, + ) + .await?; + + // 3. Add status_id columns back + db.execute_unprepared(r#"ALTER TABLE "purl_status" ADD COLUMN "status_id" uuid NULL"#) + .await?; + db.execute_unprepared(r#"ALTER TABLE "product_status" ADD COLUMN "status_id" uuid NULL"#) + .await?; + + // 4. Populate from enum via JOIN + db.execute_unprepared( + r#"UPDATE "purl_status" ps + SET "status_id" = s."id" + FROM "status" s + WHERE ps."status"::text = s."slug""#, + ) + .await?; + db.execute_unprepared( + r#"UPDATE "product_status" ps + SET "status_id" = s."id" + FROM "status" s + WHERE ps."status"::text = s."slug""#, + ) + .await?; + + // 5. Make NOT NULL and add FK + db.execute_unprepared(r#"ALTER TABLE "purl_status" ALTER COLUMN "status_id" SET NOT NULL"#) + .await?; + db.execute_unprepared( + r#"ALTER TABLE "product_status" ALTER COLUMN "status_id" SET NOT NULL"#, + ) + .await?; + db.execute_unprepared( + r#"ALTER TABLE "purl_status" ADD CONSTRAINT "fk_purl_status_status" + FOREIGN KEY ("status_id") REFERENCES "status"("id")"#, + ) + .await?; + db.execute_unprepared( + r#"ALTER TABLE "product_status" ADD CONSTRAINT "fk_product_status_status" + FOREIGN KEY ("status_id") REFERENCES "status"("id")"#, + ) + .await?; + + // 6. Drop enum columns and drop enum type + db.execute_unprepared(r#"ALTER TABLE "purl_status" DROP COLUMN "status""#) + .await?; + db.execute_unprepared(r#"ALTER TABLE "product_status" DROP COLUMN "status""#) + .await?; + + manager + .drop_type(Type::drop().if_exists().name(StatusEnum::Table).to_owned()) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +pub enum StatusEnum { + #[sea_orm(iden = "status")] + Table, + #[sea_orm(iden = "affected")] + Affected, + #[sea_orm(iden = "fixed")] + Fixed, + #[sea_orm(iden = "not_affected")] + NotAffected, + #[sea_orm(iden = "under_investigation")] + UnderInvestigation, + #[sea_orm(iden = "recommended")] + Recommended, +} diff --git a/modules/fundamental/src/advisory/service/test.rs b/modules/fundamental/src/advisory/service/test.rs index 560ffeaeb..5d7b90ed5 100644 --- a/modules/fundamental/src/advisory/service/test.rs +++ b/modules/fundamental/src/advisory/service/test.rs @@ -13,6 +13,7 @@ use trustify_common::{ use trustify_entity::{ advisory_vulnerability_score::{ScoreType, Severity}, labels::Labels, + status::Status, version_scheme::VersionScheme, }; use trustify_module_ingestor::graph::{ @@ -117,7 +118,7 @@ async fn single_advisory(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -130,7 +131,7 @@ async fn single_advisory(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -204,7 +205,7 @@ async fn delete_advisory(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -217,7 +218,7 @@ async fn delete_advisory(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -262,7 +263,7 @@ async fn set_advisory_label(ctx: &TrustifyContext) -> Result<(), anyhow::Error> .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -275,7 +276,7 @@ async fn set_advisory_label(ctx: &TrustifyContext) -> Result<(), anyhow::Error> .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -338,7 +339,7 @@ async fn update_advisory_label(ctx: &TrustifyContext) -> Result<(), anyhow::Erro .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), @@ -351,7 +352,7 @@ async fn update_advisory_label(ctx: &TrustifyContext) -> Result<(), anyhow::Erro .ingest_package_status( None, &Purl::from_str("pkg:maven/org.apache/log4j")?, - "fixed", + Status::Fixed, VersionInfo { scheme: VersionScheme::Maven, spec: VersionSpec::Exact("1.2.3".to_string()), diff --git a/modules/fundamental/src/purl/endpoints/test.rs b/modules/fundamental/src/purl/endpoints/test.rs index 28c9cbb05..68da0d800 100644 --- a/modules/fundamental/src/purl/endpoints/test.rs +++ b/modules/fundamental/src/purl/endpoints/test.rs @@ -440,14 +440,14 @@ async fn get_recommendations_dedup(ctx: &TrustifyContext) -> Result<(), anyhow:: Ok(()) } -/// Verifies that a custom vulnerability status is reflected in the recommendation response. +/// Verifies that a non-default vulnerability status is reflected in the recommendation response. #[test_context(TrustifyContext)] #[test(actix_web::test)] async fn get_recommendations_other_status(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; - use trustify_entity::{purl_status, status}; + use trustify_entity::{purl_status, status::Status}; - // Given a package with a vulnerability whose status is overridden to a custom value + // Given a package with a vulnerability whose status is overridden to under_investigation ctx.graph .ingest_qualified_package( &Purl::from_str("pkg:cargo/hyper@0.14.1-redhat-00001")?, @@ -457,15 +457,6 @@ async fn get_recommendations_other_status(ctx: &TrustifyContext) -> Result<(), a ctx.ingest_documents(["osv/RUSTSEC-2021-0079.json"]).await?; - let custom_status_id = Uuid::new_v4(); - let custom_status = status::ActiveModel { - id: Set(custom_status_id), - slug: Set("custom_status".to_string()), - name: Set("Custom Status".to_string()), - description: Set(Some("A custom status for testing".to_string())), - }; - status::Entity::insert(custom_status).exec(&ctx.db).await?; - let purl_statuses = purl_status::Entity::find() .filter(purl_status::Column::VulnerabilityId.eq("CVE-2021-32714")) .all(&ctx.db) @@ -475,7 +466,7 @@ async fn get_recommendations_other_status(ctx: &TrustifyContext) -> Result<(), a for ps in purl_statuses { let mut active: purl_status::ActiveModel = ps.into(); - active.status_id = Set(custom_status_id); + active.status = Set(Status::UnderInvestigation); active.update(&ctx.db).await?; } @@ -485,7 +476,7 @@ async fn get_recommendations_other_status(ctx: &TrustifyContext) -> Result<(), a log::info!("{recommendations:#?}"); - // Then the vulnerability status reflects the custom status + // Then the vulnerability status reflects the updated status let entry = &recommendations["recommendations"].as_object().unwrap()["pkg:cargo/hyper@0.14.1"][0]; let vulns = entry["vulnerabilities"].as_array().unwrap(); @@ -494,7 +485,7 @@ async fn get_recommendations_other_status(ctx: &TrustifyContext) -> Result<(), a .find(|v| v["id"].as_str().unwrap() == "CVE-2021-32714") .unwrap(); - assert_eq!(vuln["status"], "custom_status"); + assert_eq!(vuln["status"], "under_investigation"); Ok(()) } @@ -635,7 +626,7 @@ async fn get_recommendations_fallback_package_str( #[test(actix_web::test)] async fn get_recommendations_fixed_status(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; - use trustify_entity::{purl_status, status}; + use trustify_entity::{purl_status, status::Status}; // Given a package with a vulnerability whose status is set to "fixed" ctx.graph @@ -647,25 +638,6 @@ async fn get_recommendations_fixed_status(ctx: &TrustifyContext) -> Result<(), a ctx.ingest_documents(["osv/RUSTSEC-2021-0079.json"]).await?; - let fixed_status = status::Entity::find() - .filter(status::Column::Slug.eq("fixed")) - .one(&ctx.db) - .await?; - - let status_id = if let Some(s) = fixed_status { - s.id - } else { - let id = Uuid::new_v4(); - let new_status = status::ActiveModel { - id: Set(id), - slug: Set("fixed".to_string()), - name: Set("Fixed".to_string()), - description: Set(Some("Vulnerability has been fixed".to_string())), - }; - status::Entity::insert(new_status).exec(&ctx.db).await?; - id - }; - let purl_statuses = purl_status::Entity::find() .filter(purl_status::Column::VulnerabilityId.eq("CVE-2021-32714")) .all(&ctx.db) @@ -673,7 +645,7 @@ async fn get_recommendations_fixed_status(ctx: &TrustifyContext) -> Result<(), a for ps in purl_statuses { let mut active: purl_status::ActiveModel = ps.into(); - active.status_id = Set(status_id); + active.status = Set(Status::Fixed); active.update(&ctx.db).await?; } diff --git a/modules/fundamental/src/purl/model/details/purl.rs b/modules/fundamental/src/purl/model/details/purl.rs index 60e5d8fb8..51de40e7d 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -28,8 +28,8 @@ use trustify_common::{ use trustify_entity::{ advisory, advisory_vulnerability_score, base_purl, cpe, license, organization, product, product_status, product_version, product_version_range, purl_status, qualified_purl, sbom, - sbom_license_expanded, sbom_node, sbom_node_purl_ref, sbom_package_license, status, - version_range, versioned_purl, vulnerability, + sbom_license_expanded, sbom_node, sbom_node_purl_ref, sbom_package_license, version_range, + versioned_purl, vulnerability, }; use trustify_module_ingestor::common::{Deprecation, DeprecationForExt}; use utoipa::ToSchema; @@ -181,7 +181,6 @@ async fn get_product_statuses_for_purl( ) .join(JoinType::Join, cpe::Relation::Product.def()) .join(JoinType::LeftJoin, product::Relation::ProductVersion.def()) - .join(JoinType::Join, product_status::Relation::Status.def()) .join(JoinType::Join, product_status::Relation::Advisory.def()) .join( JoinType::Join, @@ -195,7 +194,7 @@ async fn get_product_statuses_for_purl( )) .distinct_on([ (product_status::Entity, product_status::Column::ContextCpeId), - (product_status::Entity, product_status::Column::StatusId), + (product_status::Entity, product_status::Column::Status), (product_status::Entity, product_status::Column::Package), ( product_status::Entity, @@ -203,7 +202,7 @@ async fn get_product_statuses_for_purl( ), ]) .order_by_asc(product_status::Column::ContextCpeId) - .order_by_asc(product_status::Column::StatusId) + .order_by_asc(product_status::Column::Status) .order_by_asc(product_status::Column::Package) .order_by_asc(product_status::Column::VulnerabilityId); @@ -280,7 +279,7 @@ impl PurlAdvisory { let purl_status = PurlStatus::new( &product_status.vulnerability, &product_status.advisory, - product_status.status.slug.clone(), + product_status.status.to_string(), Some(VersionRange::from_entity(product_status.version_range)?), Some(product_status.cpe.to_string()), tx, @@ -397,11 +396,7 @@ impl PurlStatus { package_status: &purl_status::Model, tx: &C, ) -> Result { - let status = status::Entity::find_by_id(package_status.status_id) - .one(tx) - .await? - .map(|e| e.slug) - .unwrap_or("unknown".into()); + let status = package_status.status.to_string(); let cpe = match package_status.context_cpe_id { Some(context_cpe) => { let cpe = cpe::Entity::find_by_id(context_cpe).one(tx).await?; @@ -485,17 +480,19 @@ pub struct ProductStatusCatcher { advisory: advisory::Model, vulnerability: vulnerability::Model, cpe: trustify_entity::cpe::Model, - status: status::Model, + status: trustify_entity::status::Status, version_range: version_range::Model, } impl FromQueryResult for ProductStatusCatcher { fn from_query_result(res: &QueryResult, _pre: &str) -> Result { + let ps: product_status::Model = + Self::from_query_result_multi_model(res, "", product_status::Entity)?; Ok(Self { advisory: Self::from_query_result_multi_model(res, "", advisory::Entity)?, vulnerability: Self::from_query_result_multi_model(res, "", vulnerability::Entity)?, cpe: Self::from_query_result_multi_model(res, "", trustify_entity::cpe::Entity)?, - status: Self::from_query_result_multi_model(res, "", status::Entity)?, + status: ps.status, version_range: Self::from_query_result_multi_model(res, "", version_range::Entity)?, }) } @@ -507,7 +504,7 @@ impl FromQueryResultMultiModel for ProductStatusCatcher { .try_model_columns(advisory::Entity)? .try_model_columns(vulnerability::Entity)? .try_model_columns(trustify_entity::cpe::Entity)? - .try_model_columns(status::Entity)? + .try_model_columns(product_status::Entity)? .try_model_columns(version_range::Entity) } } diff --git a/modules/fundamental/src/purl/model/details/versioned_purl.rs b/modules/fundamental/src/purl/model/details/versioned_purl.rs index d2ab97113..2ef8de353 100644 --- a/modules/fundamental/src/purl/model/details/versioned_purl.rs +++ b/modules/fundamental/src/purl/model/details/versioned_purl.rs @@ -16,7 +16,7 @@ use std::collections::HashMap; use trustify_common::{db::VersionMatches, memo::Memo}; use trustify_entity::{ advisory, base_purl, organization, purl_status, qualified_purl, remediation, - remediation_purl_status, status, version_range, versioned_purl, vulnerability, + remediation_purl_status, version_range, versioned_purl, vulnerability, }; use utoipa::ToSchema; @@ -115,16 +115,6 @@ impl VersionedPurlAdvisory { .map(|(advisory, org)| (advisory.id, org.clone())) .collect(); - // Batch load status entities to avoid more queries - let status_models = statuses.load_one(status::Entity, tx).await?; - - // Create a HashMap for fast status lookup by status ID - let status_map: HashMap> = statuses - .iter() - .zip(status_models.iter()) - .map(|(purl_status, status)| (purl_status.status_id, status.clone())) - .collect(); - let remediations = statuses .load_many_to_many(remediation::Entity, remediation_purl_status::Entity, tx) .await?; @@ -138,10 +128,8 @@ impl VersionedPurlAdvisory { .zip(remediations.iter()) { if let (Some(vulnerability), Some(advisory)) = (vuln, advisory) { - let status_model = status_map.get(&purl_status.status_id).cloned().flatten(); - let qualified_package_status = - VersionedPurlStatus::from_entity(vulnerability, status_model, remediations, tx) + VersionedPurlStatus::from_entity(vulnerability, purl_status, remediations, tx) .await?; if let Some(entry) = results.iter_mut().find(|e| e.head.uuid == advisory.id) { @@ -176,13 +164,11 @@ pub struct VersionedPurlStatus { impl VersionedPurlStatus { pub async fn from_entity( vuln: &vulnerability::Model, - status_model: Option, + purl_status: &purl_status::Model, remediations: &[remediation::Model], tx: &C, ) -> Result { - let status = status_model - .map(|e| e.slug) - .unwrap_or("unknown".to_string()); + let status = purl_status.status.to_string(); Ok(Self { vulnerability: VulnerabilityHead::from_vulnerability_entity( diff --git a/modules/fundamental/src/purl/service/mod.rs b/modules/fundamental/src/purl/service/mod.rs index 351d3fd7a..76917a82b 100644 --- a/modules/fundamental/src/purl/service/mod.rs +++ b/modules/fundamental/src/purl/service/mod.rs @@ -36,7 +36,7 @@ use trustify_entity::{ advisory, base_purl, license, purl_status, qualified_purl::{self, CanonicalPurl}, remediation, remediation_purl_status, sbom_license_expanded, sbom_node, sbom_node_purl_ref, - sbom_package_license, status, version_range, versioned_purl, vulnerability, + sbom_package_license, version_range, versioned_purl, vulnerability, }; use trustify_module_ingestor::common::Deprecation; @@ -612,15 +612,6 @@ impl PurlService { .load_one(advisory::Entity, connection) .instrument(info_span!("loading advisories")) .await?; - let status_models = all_statuses - .load_one(status::Entity, connection) - .instrument(info_span!("loading statuses")) - .await?; - let status_slug_map: HashMap<_, _> = status_models - .into_iter() - .flatten() - .map(|s| (s.id, s.slug)) - .collect(); let remediations = all_statuses .load_many_to_many( remediation::Entity, @@ -637,10 +628,7 @@ impl PurlService { .zip(remediations) { if let (Some(v), Some(advisory)) = (vuln, advisory) { - let slug = status_slug_map - .get(&ps.status_id) - .cloned() - .unwrap_or_else(|| "unknown".to_string()); + let slug = ps.status.to_string(); statuses_by_base .entry(ps.base_purl_id) .or_default() diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs index b108f1be2..6081d9324 100644 --- a/modules/fundamental/src/sbom/model/details.rs +++ b/modules/fundamental/src/sbom/model/details.rs @@ -15,7 +15,7 @@ use sea_orm::{ ConnectionTrait, DbBackend, DbErr, EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QueryResult, QuerySelect, RelationTrait, Statement, }; -use sea_query::{Asterisk, Expr, Func, PgFunc, SimpleExpr}; +use sea_query::{Alias, Asterisk, Expr, ExprTrait, Func, PgFunc, SimpleExpr}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, BTreeSet}, @@ -26,7 +26,7 @@ use trustify_common::{db::VersionMatches, memo::Memo}; use trustify_entity::{ advisory, advisory_vulnerability, advisory_vulnerability_score, base_purl, cpe, organization, purl_status, qualified_purl, sbom, sbom_node, sbom_node_purl_ref, sbom_package, - source_document, status, version_range, versioned_purl, vulnerability, + source_document, version_range, versioned_purl, vulnerability, }; use utoipa::ToSchema; use uuid::Uuid; @@ -42,7 +42,7 @@ struct IdSet { advisory_vulnerability_vulnerability_id: String, vulnerability_id: String, context_cpe_id: Option, - status_id: Uuid, + status: String, organization_id: Option, } @@ -57,7 +57,7 @@ impl FromQueryResult for IdSet { advisory_vulnerability_vulnerability_id: res.try_get("", "av_vulnerability_id")?, vulnerability_id: res.try_get("", "vulnerability_id")?, context_cpe_id: res.try_get("", "cpe_id").ok(), - status_id: res.try_get("", "status_id")?, + status: res.try_get("", "status_value")?, organization_id: res.try_get("", "organization_id").ok(), }) } @@ -100,7 +100,11 @@ impl SbomDetails { .column_as(qualified_purl::Column::Id, "qualified_purl_id") .column_as(sbom_package::Column::SbomId, "sbom_id") .column_as(sbom_package::Column::NodeId, "node_id") - .column_as(status::Column::Id, "status_id") + .column_as( + Expr::col((purl_status::Entity, purl_status::Column::Status)) + .cast_as(Alias::new("text")), + "status_value", + ) .column_as(cpe::Column::Id, "cpe_id") .column_as(organization::Column::Id, "organization_id") .join(JoinType::Join, sbom_package::Relation::Node.def()) @@ -111,12 +115,14 @@ impl SbomDetails { qualified_purl::Relation::VersionedPurl.def(), ) .join(JoinType::LeftJoin, versioned_purl::Relation::BasePurl.def()) - .join(JoinType::Join, base_purl::Relation::PurlStatus.def()) - .join(JoinType::Join, purl_status::Relation::Status.def()); + .join(JoinType::Join, base_purl::Relation::PurlStatus.def()); if !statuses.is_empty() { - query = query - .filter(Expr::col((status::Entity, status::Column::Slug)).is_in(statuses.clone())); + query = query.filter( + Expr::col((purl_status::Entity, purl_status::Column::Status)) + .cast_as(Alias::new("text")) + .is_in(statuses.clone()), + ); } query = query.filter(Expr::cust_with_values( @@ -193,7 +199,6 @@ impl SbomDetails { let mut advisory_vulnerability_ids_set: BTreeSet<(Uuid, String)> = BTreeSet::new(); let mut vulnerability_ids_set: BTreeSet = BTreeSet::new(); let mut cpe_ids_set: BTreeSet = BTreeSet::new(); - let mut status_ids_set: BTreeSet = BTreeSet::new(); let mut organization_ids_set: BTreeSet = BTreeSet::new(); for id_set in &id_sets { @@ -208,7 +213,6 @@ impl SbomDetails { if let Some(cpe_id) = id_set.context_cpe_id { cpe_ids_set.insert(cpe_id); } - status_ids_set.insert(id_set.status_id); if let Some(org_id) = id_set.organization_id { organization_ids_set.insert(org_id); } @@ -221,7 +225,6 @@ impl SbomDetails { advisory_vulnerability_ids_set.into_iter().collect(); let vulnerability_ids: Vec = vulnerability_ids_set.into_iter().collect(); let cpe_ids: Vec = cpe_ids_set.into_iter().collect(); - let status_ids: Vec = status_ids_set.into_iter().collect(); let organization_ids: Vec = organization_ids_set.into_iter().collect(); // Pre-fetch all entities in bulk and build lookup maps with Arc @@ -314,16 +317,6 @@ impl SbomDetails { .collect(); log::debug!("Pre-fetched {} cpes", cpes_map.len()); - let statuses_map: BTreeMap> = status::Entity::find() - .filter(Expr::col(status::Column::Id).eq(PgFunc::any(status_ids))) - .all(tx) - .instrument(info_span!("fetch status")) - .await? - .into_iter() - .map(|s| (s.id, Arc::new(s))) - .collect(); - log::debug!("Pre-fetched {} statuses", statuses_map.len()); - let organizations_map: BTreeMap> = organization::Entity::find() .filter(Expr::col(organization::Column::Id).eq(PgFunc::any(organization_ids))) @@ -414,9 +407,6 @@ impl SbomDetails { let context_cpe = id_set .context_cpe_id .and_then(|id| cpes_map.get(&id).cloned()); - let status = statuses_map.get(&id_set.status_id).ok_or_else(|| { - Error::NotFound(format!("Status {} not found in lookup", id_set.status_id)) - })?; let organization = id_set .organization_id .and_then(|id| organizations_map.get(&id).cloned()); @@ -429,7 +419,7 @@ impl SbomDetails { advisory_vulnerability: Arc::clone(advisory_vulnerability), vulnerability: Arc::clone(vulnerability), context_cpe, - status: Arc::clone(status), + status: id_set.status.clone(), organization, }); } @@ -488,7 +478,7 @@ impl SbomAdvisory { }; let sbom_status = if let Some(status) = advisory.status.iter_mut().find(|status| { - status.status == each.status.slug + status.status == each.status && status.vulnerability.identifier == each.vulnerability.id }) { status @@ -496,7 +486,7 @@ impl SbomAdvisory { let status = SbomStatus::new( &each.advisory_vulnerability, &each.vulnerability, - each.status.slug.clone(), + each.status.clone(), status_cpe, vec![], // Look up pre-fetched scores from the map diff --git a/modules/fundamental/src/sbom/model/raw_sql.rs b/modules/fundamental/src/sbom/model/raw_sql.rs index 31005b7d5..78ece9b28 100644 --- a/modules/fundamental/src/sbom/model/raw_sql.rs +++ b/modules/fundamental/src/sbom/model/raw_sql.rs @@ -110,7 +110,7 @@ pub fn product_advisory_info_sql() -> String { ps.id as product_status_id, ps.advisory_id, ps.vulnerability_id, - ps.status_id, + ps.status, ps.context_cpe_id, sp.qualified_purl_id, sp.sbom_id, @@ -128,7 +128,7 @@ pub fn product_advisory_info_sql() -> String { ps.id as product_status_id, ps.advisory_id, ps.vulnerability_id, - ps.status_id, + ps.status, ps.context_cpe_id, sp.qualified_purl_id, sp.sbom_id, @@ -157,20 +157,19 @@ pub fn product_advisory_info_sql() -> String { m.qualified_purl_id AS "qualified_purl_id", m.sbom_id AS "sbom_id", m.node_id AS "node_id", - "status"."id" AS "status_id", + m.status::text AS "status_value", "cpe"."id" AS "cpe_id", "organization"."id" AS "organization_id" FROM all_matches m JOIN sbom_package ON sbom_package.sbom_id = m.sbom_id AND sbom_package.node_id = m.node_id JOIN sbom_node ON sbom_node.sbom_id = m.sbom_id AND sbom_node.node_id = m.node_id - JOIN "status" ON m.status_id = "status"."id" JOIN "advisory" ON m.advisory_id = "advisory"."id" LEFT JOIN "organization" ON "advisory"."issuer_id" = "organization"."id" JOIN "advisory_vulnerability" ON m.advisory_id = "advisory_vulnerability"."advisory_id" AND m.vulnerability_id = "advisory_vulnerability"."vulnerability_id" JOIN "vulnerability" ON "advisory_vulnerability"."vulnerability_id" = "vulnerability"."id" LEFT JOIN "cpe" ON m.context_cpe_id = "cpe"."id" - WHERE ($2::text[] = ARRAY[]::text[] OR "status"."slug" = ANY($2::text[])) + WHERE ($2::text[] = ARRAY[]::text[] OR m.status::text = ANY($2::text[])) AND "advisory"."deprecated" = false "# .to_string() diff --git a/modules/fundamental/src/sbom/service/sbom.rs b/modules/fundamental/src/sbom/service/sbom.rs index 3d85d47e7..d6b23f65b 100644 --- a/modules/fundamental/src/sbom/service/sbom.rs +++ b/modules/fundamental/src/sbom/service/sbom.rs @@ -39,8 +39,8 @@ use trustify_entity::{ license, organization, package_relates_to_package, qualified_purl, relationship::Relationship, sbom, sbom_ai, sbom_group_assignment, sbom_license_expanded, sbom_node, sbom_node_cpe_ref, - sbom_node_purl_ref, sbom_package, sbom_package_license, source_document, status, - versioned_purl, vulnerability, + sbom_node_purl_ref, sbom_package, sbom_package_license, source_document, versioned_purl, + vulnerability, }; #[derive(Clone, Debug, Default)] @@ -1051,7 +1051,7 @@ pub struct QueryCatcher { pub advisory_vulnerability: Arc, pub vulnerability: Arc, pub context_cpe: Option>, - pub status: Arc, + pub status: String, pub organization: Option>, } @@ -1090,11 +1090,7 @@ impl FromQueryResult for QueryCatcher { )?), context_cpe: Self::from_query_result_multi_model_optional(res, "", cpe::Entity)? .map(Arc::new), - status: Arc::new(Self::from_query_result_multi_model( - res, - "", - status::Entity, - )?), + status: res.try_get("", "status_value")?, organization: Self::from_query_result_multi_model_optional( res, "", @@ -1116,7 +1112,6 @@ impl FromQueryResultMultiModel for QueryCatcher { .try_model_columns(qualified_purl::Entity)? .try_model_columns(sbom_package::Entity)? .try_model_columns(sbom_node::Entity)? - .try_model_columns(status::Entity)? .try_model_columns(cpe::Entity)? .try_model_columns(organization::Entity) } diff --git a/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs b/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs index 9798c6583..acc811366 100644 --- a/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs +++ b/modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs @@ -11,7 +11,7 @@ use sea_orm::{ ModelTrait, PaginatorTrait, QueryFilter, QueryResult, QuerySelect, RelationTrait, Select, Statement, }; -use sea_query::{Asterisk, Expr, Func, JoinType, SimpleExpr}; +use sea_query::{Alias, Asterisk, Expr, Func, JoinType, SimpleExpr}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use tracing::{info_span, instrument}; @@ -27,8 +27,7 @@ use trustify_common::{ use trustify_entity::{ advisory, advisory_vulnerability, advisory_vulnerability_score, base_purl, cpe, organization, package_relates_to_package, purl_status, qualified_purl, relationship::Relationship, sbom, - sbom_node, sbom_node_purl_ref, sbom_package, status, version_range, versioned_purl, - vulnerability, + sbom_node, sbom_node_purl_ref, sbom_package, version_range, versioned_purl, vulnerability, }; use utoipa::ToSchema; use uuid::Uuid; @@ -131,7 +130,6 @@ impl VulnerabilityAdvisorySummary { tx: &C, ) -> Result, Error> { let purl_status_query = purl_status::Entity::find() - .left_join(status::Entity) .filter(purl_status::Column::VulnerabilityId.eq(&vulnerability.id)) .left_join(base_purl::Entity) .left_join(version_range::Entity) @@ -145,7 +143,6 @@ impl VulnerabilityAdvisorySummary { ) .join(JoinType::Join, versioned_purl::Relation::BasePurl.def()) .join(JoinType::Join, base_purl::Relation::PurlStatus.def()) - .join(JoinType::Join, purl_status::Relation::Status.def()) .join(JoinType::Join, purl_status::Relation::VersionRange.def()) .join(JoinType::Join, sbom_node_purl_ref::Relation::Sbom.def()) .join(JoinType::Join, sbom::Relation::SbomNode.def()) @@ -155,7 +152,7 @@ impl VulnerabilityAdvisorySummary { package_relates_to_package::Relation::RightPackage.def(), ) .filter(purl_status::Column::VulnerabilityId.eq(&vulnerability.id)) - .filter(status::Column::Slug.ne("not_affected")) + .filter(purl_status::Column::Status.ne(trustify_entity::status::Status::NotAffected)) .filter(SimpleExpr::FunctionCall( Func::cust(VersionMatches) .arg(Expr::col(( @@ -165,7 +162,12 @@ impl VulnerabilityAdvisorySummary { .arg(Expr::col((version_range::Entity, Asterisk))), )) .select_only() - .column(purl_status::Column::AdvisoryId); + .column(purl_status::Column::AdvisoryId) + .column_as( + Expr::col((purl_status::Entity, purl_status::Column::Status)) + .cast_as(Alias::new("text")), + "status_value", + ); let mut vuln_sbom_statuses = sbom_status_query .try_into_multi_model::()? @@ -202,17 +204,13 @@ impl VulnerabilityAdvisorySummary { "sbom_node"."sbom_id" AS "sbom_node$sbom_id", "sbom_node"."node_id" AS "sbom_node$node_id", "sbom_node"."name" AS "sbom_node$name", - "status"."id" AS "status$id", - "status"."slug" AS "status$slug", - "status"."name" AS "status$name", - "status"."description" AS "status$description", + "product_status"."status"::text AS "status_value", "qualified_purl"."id" AS "qualified_purl$id", "qualified_purl"."versioned_purl_id" AS "qualified_purl$versioned_purl_id", "qualified_purl"."qualifiers" AS "qualified_purl$qualifiers", "qualified_purl"."purl" AS "qualified_purl$purl" FROM "product_status" JOIN "cpe" ON "product_status"."context_cpe_id" = "cpe"."id" - JOIN "status" ON "product_status"."status_id" = "status"."id" -- find all related products and versions JOIN "product" ON "cpe"."product" = "product"."cpe_key" @@ -235,7 +233,7 @@ impl VulnerabilityAdvisorySummary { JOIN sbom_package on sbom_package.sbom_id = package_relates_to_package.sbom_id AND sbom_package.node_id = package_relates_to_package.right_node_id WHERE - "product_status"."vulnerability_id" = $1 AND "product_status"."package" IS NOT NULL and status.slug != 'not_affected' + "product_status"."vulnerability_id" = $1 AND "product_status"."package" IS NOT NULL AND "product_status"."status"::text != 'not_affected' "#; let result: Vec = tx @@ -327,7 +325,6 @@ impl VulnerabilityAdvisorySummary { #[derive(Debug)] struct PurlStatusCatcher { - status: status::Model, purl_status: purl_status::Model, version_range: version_range::Model, cpe: Option, @@ -338,7 +335,6 @@ struct PurlStatusCatcher { impl FromQueryResult for PurlStatusCatcher { fn from_query_result(res: &QueryResult, _pre: &str) -> Result { Ok(Self { - status: Self::from_query_result_multi_model(res, "", status::Entity)?, purl_status: Self::from_query_result_multi_model(res, "", purl_status::Entity)?, version_range: Self::from_query_result_multi_model(res, "", version_range::Entity)?, cpe: Self::from_query_result_multi_model_optional(res, "", cpe::Entity)?, @@ -350,7 +346,6 @@ impl FromQueryResult for PurlStatusCatcher { impl FromQueryResultMultiModel for PurlStatusCatcher { fn try_into_multi_model(select: Select) -> Result, DbErr> { select - .try_model_columns(status::Entity)? .try_model_columns(purl_status::Entity)? .try_model_columns(version_range::Entity)? .try_model_columns(cpe::Entity)? @@ -442,7 +437,9 @@ impl VulnerabilityAdvisoryStatus { cpe.ok().map(|cpe| StatusContext::Cpe(cpe.to_string())) }); - let status_entry = statuses.entry(each.status.slug.clone()).or_insert(vec![]); + let status_entry = statuses + .entry(each.purl_status.status.to_string()) + .or_insert(vec![]); status_entry.push(VulnerabilityAdvisoryStatus { base_purl: BasePurlHead { uuid: each.base_purl.id, @@ -463,7 +460,7 @@ struct SbomStatusCatcher { sbom: sbom::Model, sbom_package: sbom_package::Model, sbom_node: sbom_node::Model, - status: status::Model, + status: String, qualified_purl: qualified_purl::Model, } @@ -474,7 +471,7 @@ impl FromQueryResult for SbomStatusCatcher { sbom: Self::from_query_result_multi_model(res, "", sbom::Entity)?, sbom_package: Self::from_query_result_multi_model(res, "", sbom_package::Entity)?, sbom_node: Self::from_query_result_multi_model(res, "", sbom_node::Entity)?, - status: Self::from_query_result_multi_model(res, "", status::Entity)?, + status: res.try_get("", "status_value")?, qualified_purl: Self::from_query_result_multi_model(res, "", qualified_purl::Entity)?, }) } @@ -486,7 +483,6 @@ impl FromQueryResultMultiModel for SbomStatusCatcher { .try_model_columns(sbom::Entity)? .try_model_columns(sbom_package::Entity)? .try_model_columns(sbom_node::Entity)? - .try_model_columns(status::Entity)? .try_model_columns(base_purl::Entity)? .try_model_columns(versioned_purl::Entity)? .try_model_columns(qualified_purl::Entity) @@ -527,7 +523,7 @@ impl VulnerabilitySbomStatus { let purl_status = sbom_status .purl_statuses - .entry(status.status.slug.clone()) + .entry(status.status.clone()) .or_insert(Default::default()); purl_status.insert(PurlSummary::from_entity(&status.qualified_purl)); diff --git a/modules/fundamental/src/vulnerability/service/mod.rs b/modules/fundamental/src/vulnerability/service/mod.rs index 5ab6acb01..8cfb7034c 100644 --- a/modules/fundamental/src/vulnerability/service/mod.rs +++ b/modules/fundamental/src/vulnerability/service/mod.rs @@ -467,6 +467,7 @@ impl VulnerabilityService { vulnerabilities_tables: &str, conditions: &str, remediations_data_expression: &str, + status_table: &str, ) -> String { format!( r#" @@ -482,7 +483,7 @@ SELECT vulnerability.id_sort_key, jsonb_agg( DISTINCT jsonb_build_object( - 'status', status.slug, + 'status', {status_table}.status::text, {advisory_columns}, 'version_range', jsonb_build_object( 'version_scheme_id', version_range.version_scheme_id, @@ -507,7 +508,7 @@ SELECT ) AS advisories FROM {vulnerabilities_tables} WHERE {conditions} - AND status.slug NOT IN ( + AND {status_table}.status::text NOT IN ( 'fixed', 'not_affected', 'recommended' @@ -569,7 +570,6 @@ GROUP BY LEFT JOIN purl_status ON base_purl.id = purl_status.base_purl_id INNER JOIN version_range ON purl_status.version_range_id = version_range.id LEFT JOIN vulnerability ON purl_status.vulnerability_id = vulnerability.id - INNER JOIN status ON purl_status.status_id = status.id "#, format!(r#" {ns_condition} AND base_purl.name = $2 @@ -577,6 +577,7 @@ GROUP BY AND version_matches($4, version_range.*) = TRUE "#).as_str(), "r.data", + "purl_status", ); let purl_status_query = Statement::from_sql_and_values( @@ -621,7 +622,6 @@ GROUP BY WHERE rps.product_status_id = product_status.id "#, r#" product_status - JOIN status ON product_status.status_id = status.id JOIN vulnerability ON product_status.vulnerability_id = vulnerability.id JOIN product_version_range ON product_status.product_version_range_id = product_version_range.id JOIN version_range ON product_version_range.version_range_id = version_range.id @@ -643,6 +643,7 @@ GROUP BY ) ), '[]'::jsonb) ) "#, + "product_status", ); let product_status_query = Statement::from_sql_and_values( connection.get_database_backend(), diff --git a/modules/ingestor/src/graph/advisory/advisory_vulnerability.rs b/modules/ingestor/src/graph/advisory/advisory_vulnerability.rs index 095f01449..9478e4f90 100644 --- a/modules/ingestor/src/graph/advisory/advisory_vulnerability.rs +++ b/modules/ingestor/src/graph/advisory/advisory_vulnerability.rs @@ -5,9 +5,8 @@ use sea_orm::{ use sea_query::IntoCondition; use tracing::instrument; use trustify_common::{cpe::Cpe, purl::Purl}; -use trustify_entity::{self as entity, purl_status, status, version_range, vulnerability}; +use trustify_entity::{self as entity, purl_status, status::Status, version_range, vulnerability}; -// Add these use statements use crate::graph::advisory::version::VersionInfo; #[derive(Clone, Debug)] @@ -42,27 +41,22 @@ impl AdvisoryVulnerabilityContext<'_> { ) } + /// Ingest a package status entry linking a PURL to this advisory+vulnerability #[instrument(skip(self, connection), err)] pub async fn ingest_package_status( &self, cpe_context: Option, purl: &Purl, - status: &str, + status: Status, info: VersionInfo, connection: &C, ) -> Result<(), Error> { - let status = status::Entity::find() - .filter(status::Column::Slug.eq(status)) - .one(connection) - .await? - .ok_or(Error::InvalidStatus(status.to_string()))?; - let package = self.advisory.graph.ingest_package(purl, connection).await?; let package_status = purl_status::Entity::find() .filter(purl_status::Column::BasePurlId.eq(package.base_purl.id)) .filter(purl_status::Column::AdvisoryId.eq(self.advisory.advisory.id)) - .filter(purl_status::Column::StatusId.eq(status.id)) + .filter(purl_status::Column::Status.eq(status)) .left_join(version_range::Entity) .filter(info.clone().into_condition()) .one(connection) @@ -83,7 +77,7 @@ impl AdvisoryVulnerabilityContext<'_> { id: Default::default(), advisory_id: Set(self.advisory_vulnerability.advisory_id), vulnerability_id: Set(self.advisory_vulnerability.vulnerability_id.clone()), - status_id: Set(status.id), + status: Set(status), base_purl_id: Set(package.base_purl.id), version_range_id: Set(info.clone().uuid()), context_cpe_id: NotSet, @@ -102,6 +96,7 @@ mod test { use test_context::test_context; use test_log::test; use trustify_common::hashing::Digests; + use trustify_entity::status::Status; use trustify_entity::version_scheme::VersionScheme; use trustify_test_context::TrustifyContext; @@ -130,7 +125,7 @@ mod test { .ingest_package_status( None, &"pkg:maven/io.quarkus/quarkus-core".try_into()?, - "affected", + Status::Affected, VersionInfo { scheme: VersionScheme::Semver, spec: VersionSpec::Range( @@ -146,7 +141,7 @@ mod test { .ingest_package_status( None, &"pkg:maven/io.quarkus/quarkus-core".try_into()?, - "not_affected", + Status::NotAffected, VersionInfo { scheme: VersionScheme::Semver, spec: VersionSpec::Exact("1.1.9".to_string()), @@ -155,15 +150,6 @@ mod test { ) .await?; - /* - let affected = advisory_vulnerability - .affected_assertions(Transactional::None) - .await?; - - assert_eq!(1, affected.assertions.len()); - - */ - Ok(()) } @@ -192,7 +178,7 @@ mod test { .ingest_package_status( None, &"pkg:maven/io.quarkus/quarkus-core".try_into()?, - "affected", + Status::Affected, VersionInfo { scheme: VersionScheme::Semver, spec: VersionSpec::Range( @@ -208,7 +194,7 @@ mod test { .ingest_package_status( None, &"pkg:maven/io.quarkus/quarkus-core".try_into()?, - "not_affected", + Status::NotAffected, VersionInfo { scheme: VersionScheme::Semver, spec: VersionSpec::Exact("1.1.9".to_string()), diff --git a/modules/ingestor/src/graph/advisory/product_status.rs b/modules/ingestor/src/graph/advisory/product_status.rs index 04125f506..1acd3d837 100644 --- a/modules/ingestor/src/graph/advisory/product_status.rs +++ b/modules/ingestor/src/graph/advisory/product_status.rs @@ -1,6 +1,6 @@ use crate::graph::advisory::version::VersionInfo; use trustify_common::cpe::Cpe; -use trustify_entity::{product_status, product_version_range, version_range}; +use trustify_entity::{product_status, product_version_range, status::Status, version_range}; use uuid::Uuid; use sea_orm::Set; @@ -56,7 +56,7 @@ impl ProductVersionRange { pub struct ProductStatus { pub cpe: Option, pub package: Option, - pub status: Uuid, + pub status: Status, pub product_version_range_id: Uuid, pub csaf_product_ids: Option>, } @@ -71,7 +71,7 @@ impl ProductStatus { id: Set(self.uuid(advisory_id, vulnerability_id.clone())), advisory_id: Set(advisory_id), vulnerability_id: Set(vulnerability_id), - status_id: Set(self.status), + status: Set(self.status), package: Set(self.package), context_cpe_id: Set(self.cpe.as_ref().map(Cpe::uuid)), product_version_range_id: Set(self.product_version_range_id), @@ -79,8 +79,9 @@ impl ProductStatus { } } + /// Compute a deterministic UUID for deduplication pub fn uuid(&self, advisory_id: Uuid, vulnerability_id: String) -> Uuid { - let mut result = Uuid::new_v5(&NAMESPACE, self.status.as_bytes()); + let mut result = Uuid::new_v5(&NAMESPACE, self.status.to_string().as_bytes()); result = Uuid::new_v5(&result, self.product_version_range_id.as_bytes()); result = Uuid::new_v5(&result, advisory_id.as_bytes()); result = Uuid::new_v5(&result, vulnerability_id.as_bytes()); diff --git a/modules/ingestor/src/graph/advisory/purl_status.rs b/modules/ingestor/src/graph/advisory/purl_status.rs index cccf22a9e..9a3782f2f 100644 --- a/modules/ingestor/src/graph/advisory/purl_status.rs +++ b/modules/ingestor/src/graph/advisory/purl_status.rs @@ -1,6 +1,6 @@ use crate::graph::advisory::version::VersionInfo; use trustify_common::{cpe::Cpe, purl::Purl}; -use trustify_entity::{purl_status, version_range}; +use trustify_entity::{purl_status, status::Status, version_range}; use uuid::Uuid; use sea_orm::Set; @@ -13,12 +13,12 @@ const NAMESPACE: Uuid = Uuid::from_bytes([ pub struct PurlStatus { pub cpe: Option, pub purl: Purl, - pub status: Uuid, + pub status: Status, pub info: VersionInfo, } impl PurlStatus { - pub fn new(cpe: Option, purl: Purl, status: Uuid, info: VersionInfo) -> Self { + pub fn new(cpe: Option, purl: Purl, status: Status, info: VersionInfo) -> Self { Self { cpe, purl, @@ -41,7 +41,7 @@ impl PurlStatus { id: Set(self.uuid(advisory_id, vulnerability_id.clone())), advisory_id: Set(advisory_id), vulnerability_id: Set(vulnerability_id), - status_id: Set(self.status), + status: Set(self.status), base_purl_id: Set(package_id), context_cpe_id: Set(cpe_id), version_range_id: version_range.clone().id, @@ -50,8 +50,9 @@ impl PurlStatus { (version_range, package_status) } + /// Compute a deterministic UUID for deduplication pub fn uuid(&self, advisory_id: Uuid, vulnerability_id: String) -> Uuid { - let mut result = Uuid::new_v5(&NAMESPACE, self.status.as_bytes()); + let mut result = Uuid::new_v5(&NAMESPACE, self.status.to_string().as_bytes()); result = Uuid::new_v5(&result, self.purl.package_uuid().as_bytes()); result = Uuid::new_v5(&result, self.info.uuid().as_bytes()); result = Uuid::new_v5(&result, advisory_id.as_bytes()); diff --git a/modules/ingestor/src/graph/db_context.rs b/modules/ingestor/src/graph/db_context.rs deleted file mode 100644 index e2ae3b60c..000000000 --- a/modules/ingestor/src/graph/db_context.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::collections::HashMap; - -use crate::graph::error::Error; -use sea_orm::ConnectionTrait; -use sea_orm::EntityTrait; -use trustify_entity::status; -use uuid::Uuid; - -#[derive(Debug, Clone)] -pub struct DbContext { - pub status_cache: HashMap, -} - -impl DbContext { - pub fn new() -> Self { - Self { - status_cache: HashMap::new(), - } - } - - pub async fn load_statuses(&mut self, connection: &impl ConnectionTrait) -> Result<(), Error> { - self.status_cache.clear(); - let statuses = status::Entity::find().all(connection).await?; - statuses - .iter() - .map(|s| self.status_cache.insert(s.slug.clone(), s.id)) - .for_each(drop); - - Ok(()) - } - - pub async fn get_status_id( - &mut self, - status: &str, - connection: &impl ConnectionTrait, - ) -> Result { - if let Some(s) = self.status_cache.get(status) { - return Ok(*s); - } - - // If not found, reload the cache and check again - self.load_statuses(connection).await?; - - self.status_cache - .get(status) - .cloned() - .ok_or_else(|| crate::graph::error::Error::InvalidStatus(status.to_string())) - } -} - -impl Default for DbContext { - fn default() -> Self { - Self::new() - } -} diff --git a/modules/ingestor/src/graph/error.rs b/modules/ingestor/src/graph/error.rs index d360c46a1..aa9fea247 100644 --- a/modules/ingestor/src/graph/error.rs +++ b/modules/ingestor/src/graph/error.rs @@ -19,9 +19,6 @@ pub enum Error { #[error(transparent)] Any(#[from] anyhow::Error), - #[error("Invalid status {0}")] - InvalidStatus(String), - #[error(transparent)] Label(#[from] labels::Error), diff --git a/modules/ingestor/src/graph/mod.rs b/modules/ingestor/src/graph/mod.rs index 2a14f1606..1942ccda2 100644 --- a/modules/ingestor/src/graph/mod.rs +++ b/modules/ingestor/src/graph/mod.rs @@ -1,7 +1,6 @@ pub mod advisory; pub mod cpe; pub mod cvss; -pub mod db_context; pub mod error; pub mod organization; pub mod product; @@ -9,7 +8,6 @@ pub mod purl; pub mod sbom; pub mod vulnerability; -use db_context::DbContext; use hex::ToHex; use sea_orm::{ ActiveValue::Set, ConnectionTrait, DbErr, EntityTrait, TransactionError, TransactionTrait, @@ -17,19 +15,15 @@ use sea_orm::{ use std::{ fmt::Debug, ops::{Deref, DerefMut}, - sync::Arc, }; use time::OffsetDateTime; -use tokio::sync::Mutex; use tracing::instrument; use trustify_common::hashing::Digests; use trustify_entity::source_document; use uuid::Uuid; #[derive(Debug, Clone)] -pub struct Graph { - pub(crate) db_context: Arc>, -} +pub struct Graph; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -41,9 +35,7 @@ pub enum Error { impl Graph { pub fn new(_db: trustify_common::db::Database) -> Self { - Self { - db_context: Arc::new(Mutex::new(DbContext::new())), - } + Self } /// Create a new source document, or return an existing sha256 digest if a document with that diff --git a/modules/ingestor/src/graph/purl/status_creator.rs b/modules/ingestor/src/graph/purl/status_creator.rs index adfa60acd..44f0585cd 100644 --- a/modules/ingestor/src/graph/purl/status_creator.rs +++ b/modules/ingestor/src/graph/purl/status_creator.rs @@ -2,12 +2,12 @@ use crate::graph::{ advisory::{purl_status::PurlStatus, version::VersionInfo}, error::Error, }; -use sea_orm::{ActiveValue::Set, ConnectionTrait, EntityTrait, QueryFilter}; -use sea_query::{Expr, OnConflict, PgFunc}; -use std::collections::{BTreeMap, BTreeSet}; +use sea_orm::{ActiveValue::Set, ConnectionTrait, EntityTrait}; +use sea_query::OnConflict; +use std::collections::BTreeMap; use tracing::instrument; use trustify_common::{cpe::Cpe, db::chunk::EntityChunkedIter, purl::Purl}; -use trustify_entity::{purl_status, status, version_range}; +use trustify_entity::{purl_status, status::Status, version_range}; use uuid::Uuid; /// Input data for creating a PURL status entry @@ -16,7 +16,7 @@ pub struct PurlStatusEntry { pub advisory_id: Uuid, pub vulnerability_id: String, pub purl: Purl, - pub status: String, + pub status: Status, pub version_info: VersionInfo, pub context_cpe: Option, } @@ -51,40 +51,15 @@ impl PurlStatusCreator { return Ok(()); } - // 1. Batch lookup all unique status slugs - let unique_statuses: Vec = self - .entries - .iter() - .map(|e| e.status.clone()) - .collect::>() - .into_iter() - .collect(); - - let status_models = status::Entity::find() - .filter(Expr::col(status::Column::Slug).eq(PgFunc::any(unique_statuses))) - .all(connection) - .await?; - - let status_map: BTreeMap = status_models - .into_iter() - .map(|s| (s.slug.clone(), s.id)) - .collect(); - - // 2. Deduplicate and build ActiveModels + // Deduplicate and build ActiveModels let mut version_ranges = BTreeMap::new(); let mut purl_statuses = BTreeMap::new(); for entry in self.entries { - // Validate status exists - let status_id = *status_map - .get(&entry.status) - .ok_or_else(|| Error::InvalidStatus(entry.status.clone()))?; - - // Create PurlStatus and use its uuid() method let purl_status = PurlStatus { cpe: entry.context_cpe.clone(), purl: entry.purl.clone(), - status: status_id, + status: entry.status, info: entry.version_info.clone(), }; @@ -93,19 +68,17 @@ impl PurlStatusCreator { let version_range_id = entry.version_info.uuid(); let context_cpe_id = entry.context_cpe.as_ref().map(|cpe| cpe.uuid()); - // Deduplicate version ranges version_ranges .entry(version_range_id) .or_insert_with(|| entry.version_info.clone().into_active_model()); - // Deduplicate purl_statuses by UUID purl_statuses .entry(uuid) .or_insert_with(|| purl_status::ActiveModel { id: Set(uuid), advisory_id: Set(entry.advisory_id), vulnerability_id: Set(entry.vulnerability_id.clone()), - status_id: Set(status_id), + status: Set(entry.status), base_purl_id: Set(base_purl_id), version_range_id: Set(version_range_id), context_cpe_id: Set(context_cpe_id), diff --git a/modules/ingestor/src/service/advisory/csaf/creator.rs b/modules/ingestor/src/service/advisory/csaf/creator.rs index dbb43565a..e0813ca90 100644 --- a/modules/ingestor/src/service/advisory/csaf/creator.rs +++ b/modules/ingestor/src/service/advisory/csaf/creator.rs @@ -19,7 +19,6 @@ use crate::{ use csaf::{Csaf, definitions::ProductIdT, vulnerability::Remediation}; use sea_orm::{ActiveValue::Set, ConnectionTrait, EntityTrait}; use std::collections::{HashMap, HashSet}; -use std::str::FromStr; use tracing::instrument; use trustify_common::{db::chunk::EntityChunkedIter, purl::Purl}; use trustify_entity::{ @@ -63,11 +62,16 @@ impl<'a> StatusCreator<'a> { } } - pub fn add_all(&mut self, ps: &Option>, status: &'static str) { + pub fn add_all(&mut self, ps: &Option>, status: Status) { for r in ps.iter().flatten() { let mut product = ProductStatus { + vendor: None, + product: String::new(), + version: None, + cpe: None, status, - ..Default::default() + purls: Vec::new(), + packages: Vec::new(), }; let mut product_ids = vec![]; match self.cache.get_relationship(&r.0) { @@ -158,13 +162,6 @@ impl<'a> StatusCreator<'a> { } for product in product_statuses { - let status_id = graph - .db_context - .lock() - .await - .get_status_id(product.status, connection) - .await?; - // Organizations have been pre-ingested, just look up from cache let org_id = product .vendor @@ -219,7 +216,7 @@ impl<'a> StatusCreator<'a> { let product_status = crate::graph::advisory::product_status::ProductStatus { cpe: product.cpe.clone(), package, - status: status_id, + status: product.status, product_version_range_id: range.uuid(), csaf_product_ids: csaf_product_ids.clone(), }; @@ -253,13 +250,13 @@ impl<'a> StatusCreator<'a> { Some(version) => VersionSpec::Exact(version.clone()), None => VersionSpec::Range(Version::Unbounded, Version::Unbounded), }; - self.create_purl_status(&product, purl, scheme, spec, status_id); + self.create_purl_status(&product, purl, scheme, spec, product.status); // For "fixed" status and Red Hat CSAF advisories, // insert "affected" status up until this version. // Let's keep this here for now as a special case. If more exceptions arise, // we can refactor and provide support for vendor-specific parsing. - if let Ok(Status::Fixed) = Status::from_str(product.status) + if product.status == Status::Fixed && let Some(cpe_vendor) = product .cpe .as_ref() @@ -269,18 +266,7 @@ impl<'a> StatusCreator<'a> { { let spec = VersionSpec::Range(Version::Unbounded, Version::Exclusive(version.clone())); - self.create_purl_status( - &product, - purl, - scheme, - spec, - graph - .db_context - .lock() - .await - .get_status_id(&Status::Affected.to_string(), connection) - .await?, - ); + self.create_purl_status(&product, purl, scheme, spec, Status::Affected); } } } @@ -378,7 +364,7 @@ impl<'a> StatusCreator<'a> { purl: &Purl, scheme: VersionScheme, spec: VersionSpec, - status: Uuid, + status: Status, ) { let purl_status = PurlStatus { cpe: product.cpe.clone(), diff --git a/modules/ingestor/src/service/advisory/csaf/loader.rs b/modules/ingestor/src/service/advisory/csaf/loader.rs index 1d3479d27..1265ff09e 100644 --- a/modules/ingestor/src/service/advisory/csaf/loader.rs +++ b/modules/ingestor/src/service/advisory/csaf/loader.rs @@ -25,7 +25,7 @@ use std::{fmt::Debug, str::FromStr}; use time::OffsetDateTime; use tracing::instrument; use trustify_common::hashing::Digests; -use trustify_entity::labels::Labels; +use trustify_entity::{labels::Labels, status::Status}; struct Information<'a>(&'a Csaf); @@ -206,9 +206,9 @@ impl<'g> CsafLoader<'g> { .clone(), ); - creator.add_all(&product_status.fixed, "fixed"); - creator.add_all(&product_status.known_not_affected, "not_affected"); - creator.add_all(&product_status.known_affected, "affected"); + creator.add_all(&product_status.fixed, Status::Fixed); + creator.add_all(&product_status.known_not_affected, Status::NotAffected); + creator.add_all(&product_status.known_affected, Status::Affected); let product_id_mapping = creator.create(self.graph, connection).await?; diff --git a/modules/ingestor/src/service/advisory/csaf/product_status.rs b/modules/ingestor/src/service/advisory/csaf/product_status.rs index 9ed46b08a..c789860be 100644 --- a/modules/ingestor/src/service/advisory/csaf/product_status.rs +++ b/modules/ingestor/src/service/advisory/csaf/product_status.rs @@ -3,15 +3,15 @@ use crate::graph::advisory::version::{Version, VersionInfo, VersionSpec}; use cpe::cpe::Cpe; use csaf::definitions::{Branch, BranchCategory, FullProductName}; use trustify_common::purl::Purl; -use trustify_entity::version_scheme::VersionScheme; +use trustify_entity::{status::Status, version_scheme::VersionScheme}; -#[derive(Clone, Default, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct ProductStatus { pub vendor: Option, pub product: String, pub version: Option, pub cpe: Option, - pub status: &'static str, + pub status: Status, pub purls: Vec, pub packages: Vec, } diff --git a/modules/ingestor/src/service/advisory/cve/loader.rs b/modules/ingestor/src/service/advisory/cve/loader.rs index 0ec716072..0f92dbe89 100644 --- a/modules/ingestor/src/service/advisory/cve/loader.rs +++ b/modules/ingestor/src/service/advisory/cve/loader.rs @@ -32,7 +32,9 @@ use time::OffsetDateTime; use tracing::instrument; use trustify_common::hashing::Digests; use trustify_entity::advisory_vulnerability_score::{ScoreType, Severity}; -use trustify_entity::{labels::Labels, version_scheme::VersionScheme, vulnerability}; +use trustify_entity::{ + labels::Labels, status::Status as EntityStatus, version_scheme::VersionScheme, vulnerability, +}; /// Loader capable of parsing a CVE Record JSON file /// and manipulating the Graph to integrate it into @@ -179,9 +181,9 @@ impl<'g> CveLoader<'g> { .clone(), purl: purl.clone(), status: match status { - Status::Affected => "affected".to_string(), - Status::Unaffected => "not_affected".to_string(), - Status::Unknown => "unknown".to_string(), + Status::Affected => EntityStatus::Affected, + Status::Unaffected => EntityStatus::NotAffected, + Status::Unknown => EntityStatus::UnderInvestigation, }, version_info: VersionInfo { scheme: version_type diff --git a/modules/ingestor/src/service/advisory/osv/loader.rs b/modules/ingestor/src/service/advisory/osv/loader.rs index 5a2e8420c..3b81a7832 100644 --- a/modules/ingestor/src/service/advisory/osv/loader.rs +++ b/modules/ingestor/src/service/advisory/osv/loader.rs @@ -27,7 +27,7 @@ use sea_orm::{ConnectionTrait, TransactionTrait}; use std::{collections::HashSet, fmt::Debug, str::FromStr}; use tracing::instrument; use trustify_common::{hashing::Digests, purl::Purl, time::ChronoExt}; -use trustify_entity::{labels::Labels, version_scheme::VersionScheme}; +use trustify_entity::{labels::Labels, status::Status, version_scheme::VersionScheme}; pub struct OsvLoader<'g> { graph: &'g Graph, @@ -148,7 +148,7 @@ impl<'g> OsvLoader<'g> { .vulnerability_id .clone(), purl: purl.clone(), - status: "affected".to_string(), + status: Status::Affected, version_info: VersionInfo { scheme: VersionScheme::Generic, spec: VersionSpec::Exact(version.to_string()), @@ -366,7 +366,7 @@ fn build_package_status( .vulnerability_id .clone(), purl: purl.clone(), - status: "affected".to_string(), + status: Status::Affected, version_info: VersionInfo { scheme: version_scheme, spec, @@ -383,7 +383,7 @@ fn build_package_status( .vulnerability_id .clone(), purl: purl.clone(), - status: "fixed".to_string(), + status: Status::Fixed, version_info: VersionInfo { scheme: version_scheme, spec: VersionSpec::Exact(fixed.clone()), @@ -417,14 +417,13 @@ fn build_package_status_versions<'a>( entries.extend(build_range_from( advisory_vuln, purl, - "affected", + Status::Affected, start, Some(version), &versions, )); } - // Add "fixed" status entries.push(PurlStatusEntry { advisory_id: advisory_vuln.advisory.advisory.id, vulnerability_id: advisory_vuln @@ -432,7 +431,7 @@ fn build_package_status_versions<'a>( .vulnerability_id .clone(), purl: purl.clone(), - status: "fixed".to_string(), + status: Status::Fixed, version_info: VersionInfo { scheme: VersionScheme::Generic, spec: VersionSpec::Exact(version.to_string()), @@ -450,7 +449,7 @@ fn build_package_status_versions<'a>( entries.extend(build_range_from( advisory_vuln, purl, - "affected", + Status::Affected, start, None, &versions, @@ -464,7 +463,7 @@ fn build_package_status_versions<'a>( fn build_range_from( advisory_vuln: &AdvisoryVulnerabilityContext<'_>, purl: &Purl, - status: &str, + status: Status, start: &str, // exclusive end end: Option<&str>, @@ -481,7 +480,7 @@ fn build_range_from( .vulnerability_id .clone(), purl: purl.clone(), - status: status.to_string(), + status, version_info: VersionInfo { scheme: VersionScheme::Generic, spec: VersionSpec::Exact(version.to_string()),