Skip to content

Commit

Permalink
fix: improve sbom to vunerabilities correlation both in performance a…
Browse files Browse the repository at this point in the history
…nd accuracy
  • Loading branch information
dejanb committed Dec 18, 2024
1 parent 88c8632 commit a98d468
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 79 deletions.
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ mod m0000750_alter_advisory_add_document_id;
mod m0000760_product_status_index;
mod m0000780_alter_source_document_time;
mod m0000790_alter_sbom_alter_document_id;
mod m0000800_alter_product_version_range_scheme;

pub struct Migrator;

Expand Down Expand Up @@ -199,6 +200,7 @@ impl MigratorTrait for Migrator {
Box::new(m0000760_product_status_index::Migration),
Box::new(m0000780_alter_source_document_time::Migration),
Box::new(m0000790_alter_sbom_alter_document_id::Migration),
Box::new(m0000800_alter_product_version_range_scheme::Migration),
]
}
}
Expand Down
27 changes: 27 additions & 0 deletions migration/src/m0000800_alter_product_version_range_scheme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// use rpm version range scheme as it covers more usecases
manager
.get_connection()
.execute_unprepared(r#"UPDATE version_range SET version_scheme_id = 'rpm' WHERE id IN (SELECT version_range_id FROM product_version_range)"#)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// return to semver version range scheme
manager
.get_connection()
.execute_unprepared(r#"UPDATE version_range SET version_scheme_id = 'semver' WHERE id IN (SELECT version_range_id FROM product_version_range)"#)
.await?;

Ok(())
}
}
174 changes: 98 additions & 76 deletions modules/fundamental/src/sbom/model/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use crate::{
};
use cpe::{cpe::Cpe, uri::OwnedUri};
use sea_orm::{
ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Iden, JoinType, ModelTrait, QueryFilter,
QueryOrder, QueryResult, QuerySelect, RelationTrait, Select,
ConnectionTrait, DbBackend, DbErr, EntityTrait, FromQueryResult, JoinType, ModelTrait,
QueryFilter, QueryResult, QuerySelect, RelationTrait, Select, Statement,
};
use sea_query::{Asterisk, Expr, Func, SimpleExpr};
use serde::{Deserialize, Serialize};
Expand All @@ -27,9 +27,9 @@ use trustify_common::{
};
use trustify_cvss::cvss3::{score::Score, severity::Severity, Cvss3Base};
use trustify_entity::{
advisory, base_purl, cvss3, product, product_status, product_version, purl_status,
qualified_purl, sbom, sbom_node, sbom_package, sbom_package_purl_ref, status, version_range,
versioned_purl, vulnerability,
advisory, base_purl, cvss3, product_status, product_version, purl_status, qualified_purl, sbom,
sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, versioned_purl,
vulnerability,
};
use utoipa::ToSchema;

Expand Down Expand Up @@ -83,73 +83,101 @@ impl SbomDetails {
.all(tx)
.await?;

let mut product_advisory_info = sbom
.find_related(product_version::Entity)
.join(JoinType::LeftJoin, product_version::Relation::Product.def())
.join(JoinType::LeftJoin, product::Relation::Cpe.def())
.join(
JoinType::Join,
trustify_entity::cpe::Relation::ProductStatus.def(),
)
.join(JoinType::Join, product_status::Relation::Status.def())
.join(JoinType::Join, product_status::Relation::Advisory.def())
.join(
JoinType::Join,
product_status::Relation::Vulnerability.def(),
)
// Joins for purl-related tables
.join(JoinType::Join, sbom::Relation::Node.def())
.join(JoinType::Join, sbom_node::Relation::Package.def())
.join(JoinType::Join, sbom_package::Relation::Purl.def())
.join(JoinType::Join, sbom_package_purl_ref::Relation::Purl.def())
.join(
JoinType::Join,
qualified_purl::Relation::VersionedPurl.def(),
)
.join(JoinType::Join, versioned_purl::Relation::BasePurl.def())
.distinct_on([
(product_status::Entity, product_status::Column::ContextCpeId),
(product_status::Entity, product_status::Column::StatusId),
(product_status::Entity, product_status::Column::Package),
(
product_status::Entity,
product_status::Column::VulnerabilityId,
),
])
.order_by_asc(product_status::Column::ContextCpeId)
.order_by_asc(product_status::Column::StatusId)
.order_by_asc(product_status::Column::Package)
.order_by_asc(product_status::Column::VulnerabilityId)
// Filter for product_status.package
.filter(
Expr::col((product_status::Entity, product_status::Column::Package))
.is_null()
.or(Expr::col((product_status::Entity, product_status::Column::Package)).eq(""))
.or(SimpleExpr::Binary(
Box::new(
Expr::col((product_status::Entity, product_status::Column::Package))
.into(),
),
sea_query::BinOper::Like,
Box::new(SimpleExpr::FunctionCall(
Func::cust(CustomFunc::Concat).args([
Expr::col((base_purl::Entity, base_purl::Column::Namespace)).into(),
Expr::val("/").into(),
Expr::col((base_purl::Entity, base_purl::Column::Name)).into(),
]),
)),
))
.or(
Expr::col((product_status::Entity, product_status::Column::Package))
.eq(Expr::col((base_purl::Entity, base_purl::Column::Name))),
),
)
.select_only()
.try_into_multi_model::<QueryCatcher>()?
.all(tx)
// The query for now is in the raw form for couple of reasons
// First some of the join are not easily (or at all) doable using sea-orm concepts
// Second, it's much easier to iterate over query and work on it in this form
// than using the code
// It might be a good practice to start like this for complex query logic and
// turn it into a code once things stabilize
let product_advisory_info = r#"
SELECT
"advisory"."id" AS "advisory$id",
"advisory"."identifier" AS "advisory$identifier",
"advisory"."version" AS "advisory$version",
"advisory"."document_id" AS "advisory$document_id",
"advisory"."deprecated" AS "advisory$deprecated",
"advisory"."issuer_id" AS "advisory$issuer_id",
"advisory"."published" AS "advisory$published",
"advisory"."modified" AS "advisory$modified",
"advisory"."withdrawn" AS "advisory$withdrawn",
"advisory"."title" AS "advisory$title",
"advisory"."labels" AS "advisory$labels",
"advisory"."source_document_id" AS "advisory$source_document_id",
"vulnerability"."id" AS "vulnerability$id",
"vulnerability"."title" AS "vulnerability$title",
"vulnerability"."reserved" AS "vulnerability$reserved",
"vulnerability"."published" AS "vulnerability$published",
"vulnerability"."modified" AS "vulnerability$modified",
"vulnerability"."withdrawn" AS "vulnerability$withdrawn",
"vulnerability"."cwes" AS "vulnerability$cwes",
"base_purl"."id" AS "base_purl$id",
"base_purl"."type" AS "base_purl$type",
"base_purl"."namespace" AS "base_purl$namespace",
"base_purl"."name" AS "base_purl$name",
"versioned_purl"."id" AS "versioned_purl$id",
"versioned_purl"."base_purl_id" AS "versioned_purl$base_purl_id",
"versioned_purl"."version" AS "versioned_purl$version",
"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",
"sbom_package"."sbom_id" AS "sbom_package$sbom_id",
"sbom_package"."node_id" AS "sbom_package$node_id",
"sbom_package"."version" AS "sbom_package$version",
"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",
"cpe"."id" AS "cpe$id",
"cpe"."part" AS "cpe$part",
"cpe"."vendor" AS "cpe$vendor",
"cpe"."product" AS "cpe$product",
"cpe"."version" AS "cpe$version",
"cpe"."update" AS "cpe$update",
"cpe"."edition" AS "cpe$edition",
"cpe"."language" AS "cpe$language"
FROM "sbom"
-- find statuses that matches SBOMs
JOIN "product_version" ON "product_version"."sbom_id" = "sbom"."sbom_id"
JOIN "product" ON "product_version"."product_id" = "product"."id"
JOIN "cpe" ON "product"."cpe_key" = "cpe"."product"
JOIN "product_status" ON "cpe"."id" = "product_status"."context_cpe_id" AND product_status.package IS NOT NULL
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" AND version_matches("product_version"."version", "version_range".*)
-- now find matching purls in these statuses
JOIN base_purl ON "product_status"."package" LIKE CONCAT("base_purl"."namespace", '/', "base_purl"."name") OR "product_status"."package" = "base_purl"."name"
JOIN "versioned_purl" ON "versioned_purl"."base_purl_id" = "base_purl"."id"
JOIN "qualified_purl" ON "qualified_purl"."versioned_purl_id" = "versioned_purl"."id"
join sbom_package_purl_ref ON sbom_package_purl_ref.qualified_purl_id = qualified_purl.id AND sbom_package_purl_ref.sbom_id = sbom.sbom_id
JOIN sbom_package on sbom_package.sbom_id = sbom_package_purl_ref.sbom_id AND sbom_package.node_id = sbom_package_purl_ref.node_id
JOIN sbom_node on sbom_node.sbom_id = sbom_package_purl_ref.sbom_id AND sbom_node.node_id = sbom_package_purl_ref.node_id
-- get basic status info
JOIN "status" ON "product_status"."status_id" = "status"."id"
JOIN "advisory" ON "product_status"."advisory_id" = "advisory"."id"
JOIN "vulnerability" ON "product_status"."vulnerability_id" = "vulnerability"."id"
WHERE
"sbom"."sbom_id" = $1
"#;

let result: Vec<QueryResult> = tx
.query_all(Statement::from_sql_and_values(
DbBackend::Postgres,
product_advisory_info,
[sbom.sbom_id.into()],
))
.await?;

relevant_advisory_info.append(&mut product_advisory_info);
relevant_advisory_info.extend(
result
.iter()
.map(|row| QueryCatcher::from_query_result(row, ""))
.collect::<Result<Vec<_>, _>>()?,
);

let summary = SbomSummary::from_entity((sbom, node), service, tx).await?;

Expand All @@ -168,12 +196,6 @@ impl SbomDetails {
}
}

#[derive(Iden)]
enum CustomFunc {
#[iden = "CONCAT"]
Concat,
}

#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SbomAdvisory {
#[serde(flatten)]
Expand Down
3 changes: 1 addition & 2 deletions modules/fundamental/src/vulnerability/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,9 @@ async fn product_statuses(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
assert!(!quarkus_sbom.advisories.is_empty());
let quarkus_adv = &quarkus_sbom.advisories[0].status[0];

assert_eq!(quarkus_adv.status, "fixed");
assert_eq!(quarkus_adv.status, "affected");
assert_eq!(quarkus_adv.vulnerability.identifier, "CVE-2023-0044");

let quarkus_adv = &quarkus_sbom.advisories[0].status[1];
assert_eq!(quarkus_adv.packages.len(), 1);
assert_eq!(quarkus_adv.packages[0].purl.len(), 1);
assert_eq!(quarkus_adv.packages[0].purl[0].head.purl, Purl::from_str("pkg:maven/io.quarkus/[email protected]?repository_url=https://maven.repository.redhat.com/ga/&type=jar").unwrap());
Expand Down
4 changes: 3 additions & 1 deletion modules/ingestor/src/service/advisory/csaf/product_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ impl ProductStatus {
// let upper = semver.clone().set_major(semver.major + 1).build();
let mut upper = semver.clone();
upper.major += 1;
upper.minor = 0;
upper.patch = 0;
VersionInfo {
spec: VersionSpec::Range(
Version::Inclusive(semver.to_string()),
Version::Exclusive(upper.to_string()),
),
scheme: VersionScheme::Semver,
scheme: VersionScheme::Rpm,
}
}
Err(_) => VersionInfo {
Expand Down

0 comments on commit a98d468

Please sign in to comment.