From e39e0a8e3b75d2206a6e64e54d582d7c4b619656 Mon Sep 17 00:00:00 2001 From: Jens Reimann Date: Tue, 19 May 2026 13:33:53 +0200 Subject: [PATCH] feat: replace status lookup table with PostgreSQL enum Replace the `status` lookup table with a native PostgreSQL enum type to eliminate JOIN overhead on purl_status and product_status queries. The migration snapshots existing status values, drops the old table and FK columns, creates the enum type, and repopulates from the snapshot. The entity layer now uses SeaORM's DeriveActiveEnum with strum for Display/EnumString support. All ingestor write paths, fundamental read paths, raw SQL queries, and tests are updated to use the Status enum directly. Co-Authored-By: Claude Opus 4.6 --- entity/src/product_status.rs | 16 +- entity/src/purl_status.rs | 16 +- entity/src/status.rs | 33 +--- migration/src/lib.rs | 2 + migration/src/m0002200_status_enum.rs | 171 ++++++++++++++++++ .../fundamental/src/advisory/service/test.rs | 17 +- .../fundamental/src/purl/endpoints/test.rs | 44 +---- .../src/purl/model/details/purl.rs | 25 ++- .../src/purl/model/details/versioned_purl.rs | 22 +-- modules/fundamental/src/purl/service/mod.rs | 16 +- modules/fundamental/src/sbom/model/details.rs | 46 ++--- modules/fundamental/src/sbom/model/raw_sql.rs | 9 +- modules/fundamental/src/sbom/service/sbom.rs | 13 +- .../model/details/vulnerability_advisory.rs | 38 ++-- .../src/vulnerability/service/mod.rs | 9 +- .../graph/advisory/advisory_vulnerability.rs | 34 +--- .../src/graph/advisory/product_status.rs | 9 +- .../src/graph/advisory/purl_status.rs | 11 +- modules/ingestor/src/graph/db_context.rs | 55 ------ modules/ingestor/src/graph/error.rs | 3 - modules/ingestor/src/graph/mod.rs | 12 +- .../ingestor/src/graph/purl/status_creator.rs | 43 +---- .../src/service/advisory/csaf/creator.rs | 38 ++-- .../src/service/advisory/csaf/loader.rs | 8 +- .../service/advisory/csaf/product_status.rs | 6 +- .../src/service/advisory/cve/loader.rs | 10 +- .../src/service/advisory/osv/loader.rs | 19 +- 27 files changed, 334 insertions(+), 391 deletions(-) create mode 100644 migration/src/m0002200_status_enum.rs delete mode 100644 modules/ingestor/src/graph/db_context.rs 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()),