Skip to content

Commit

Permalink
feat: csaf correlation - correlate vulnerability to purls and sboms
Browse files Browse the repository at this point in the history
feat: csaf correlation - correlate sbom to vulnerabilities

chore: add basic test for product status correlation

fix: broken sbom advisory logic

fix: product status should only be valid for the current major stream

fix: improve sbom product_status query

fix: sbom status query to distinct on vulnerabilty id as well

fix: improve the performance of vulnerability to sbom query

feat: add sbom packages, with only names for now
  • Loading branch information
dejanb committed Nov 12, 2024
1 parent 1fd508f commit 4a23380
Show file tree
Hide file tree
Showing 13 changed files with 521 additions and 85 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions entity/src/cpe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ pub enum Relation {
to = "super::sbom_package_cpe_ref::Column::CpeId"
)]
SbomPackage,
#[sea_orm(
belongs_to = "super::product::Entity",
from = "Column::Product",
to = "super::product::Column::CpeKey"
)]
Product,
#[sea_orm(
belongs_to = "super::product_status::Entity",
from = "Column::Id",
to = "super::product_status::Column::ContextCpeId"
)]
ProductStatus,
}

impl Related<super::sbom_package_cpe_ref::Entity> for Entity {
Expand All @@ -36,6 +48,18 @@ impl Related<super::sbom_package_cpe_ref::Entity> for Entity {
}
}

impl Related<super::product::Entity> for Entity {
fn to() -> RelationDef {
Relation::Product.def()
}
}

impl Related<super::product_status::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProductStatus.def()
}
}

impl ActiveModelBehavior for ActiveModel {}

impl Display for Model {
Expand Down
11 changes: 11 additions & 0 deletions entity/src/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ pub enum Relation {
Vendor,
#[sea_orm(has_many = "super::product_version::Entity")]
ProductVersion,
#[sea_orm(
has_many = "super::cpe::Entity"
from = "Column::CpeKey"
to = "super::cpe::Column::Product")]
Cpe,
}

impl Related<organization::Entity> for Entity {
Expand All @@ -35,4 +40,10 @@ impl Related<super::product_version::Entity> for Entity {
}
}

impl Related<super::cpe::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cpe.def()
}
}

impl ActiveModelBehavior for ActiveModel {}
14 changes: 1 addition & 13 deletions entity/src/product_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub struct Model {
pub advisory_id: Uuid,
pub vulnerability_id: String,
pub status_id: Uuid,
pub base_purl_id: Option<Uuid>,
pub component: Option<String>,
pub product_version_range_id: Uuid,
pub context_cpe_id: Option<Uuid>,
}
Expand All @@ -21,12 +21,6 @@ pub enum Relation {
)]
ProductVersionRange,

#[sea_orm(belongs_to = "super::base_purl::Entity",
from = "Column::BasePurlId"
to = "super::base_purl::Column::Id"
)]
BasePurl,

#[sea_orm(belongs_to = "super::vulnerability::Entity",
from = "Column::VulnerabilityId"
to = "super::vulnerability::Column::Id"
Expand Down Expand Up @@ -64,12 +58,6 @@ impl Related<super::product_version_range::Entity> for Entity {
}
}

impl Related<super::base_purl::Entity> for Entity {
fn to() -> RelationDef {
Relation::BasePurl.def()
}
}

impl Related<super::vulnerability::Entity> for Entity {
fn to() -> RelationDef {
Relation::Vulnerability.def()
Expand Down
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ mod m0000625_alter_qualified_purl_purl_column;
mod m0000630_create_product_version_range;
mod m0000631_alter_product_cpe_key;
mod m0000640_create_product_status;
mod m0000641_update_product_status;
mod m0000650_alter_advisory_tracking;
mod m0000660_purl_id_indexes;
mod m0000670_version_cmp;
Expand Down Expand Up @@ -178,6 +179,7 @@ impl MigratorTrait for Migrator {
Box::new(m0000630_create_product_version_range::Migration),
Box::new(m0000631_alter_product_cpe_key::Migration),
Box::new(m0000640_create_product_status::Migration),
Box::new(m0000641_update_product_status::Migration),
Box::new(m0000650_alter_advisory_tracking::Migration),
Box::new(m0000660_purl_id_indexes::Migration),
Box::new(m0000670_version_cmp::Migration),
Expand Down
72 changes: 72 additions & 0 deletions migration/src/m0000641_update_product_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
#[allow(deprecated)]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProductStatus::Table)
.drop_column(ProductStatus::BasePurlId)
.to_owned(),
)
.await?;

manager
.alter_table(
Table::alter()
.table(ProductStatus::Table)
.add_column(ColumnDef::new(ProductStatus::Component).string())
.to_owned(),
)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProductStatus::Table)
.drop_column(ProductStatus::Component)
.to_owned(),
)
.await?;

manager
.alter_table(
Table::alter()
.table(ProductStatus::Table)
.add_column(ColumnDef::new(ProductStatus::BasePurlId).uuid())
.add_foreign_key(
TableForeignKey::new()
.from_tbl(ProductStatus::Table)
.from_col(ProductStatus::BasePurlId)
.to_tbl(BasePurl::Table)
.to_col(BasePurl::Id),
)
.to_owned(),
)
.await?;

Ok(())
}
}

#[derive(DeriveIden)]
enum ProductStatus {
Table,
BasePurlId,
Component,
}

#[derive(DeriveIden)]
enum BasePurl {
Table,
Id,
}
1 change: 1 addition & 0 deletions modules/fundamental/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ base64 = { workspace = true }
cpe = { workspace = true }
futures-util = { workspace = true }
itertools = { workspace = true }
lenient_semver = { workspace = true }
langchain-rust = { workspace = true }
log = { workspace = true }
sea-orm = { workspace = true }
Expand Down
115 changes: 110 additions & 5 deletions modules/fundamental/src/sbom/model/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ use crate::{
};
use async_graphql::SimpleObject;
use cpe::uri::OwnedUri;
use sea_orm::{JoinType, ModelTrait, QueryFilter, QuerySelect, RelationTrait};
use sea_orm::{
DbErr, EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QueryOrder,
QueryResult, QuerySelect, RelationTrait, Select,
};
use sea_query::{Asterisk, Expr, Func, SimpleExpr};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{hash_map::Entry, HashMap};
use trustify_common::{
cpe::CpeCompare,
db::{multi_model::SelectIntoMultiModel, ConnectionOrTransaction, VersionMatches},
db::{
multi_model::{FromQueryResultMultiModel, SelectIntoMultiModel},
ConnectionOrTransaction, VersionMatches,
},
memo::Memo,
};
use trustify_entity::{
base_purl, purl_status,
advisory, base_purl, product, product_status, product_version, purl_status,
qualified_purl::{self},
sbom::{self},
sbom_node, sbom_package, sbom_package_purl_ref, version_range, versioned_purl,
sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, versioned_purl,
};
use utoipa::ToSchema;

Expand Down Expand Up @@ -74,6 +80,35 @@ impl SbomDetails {
.all(tx)
.await?;

let 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())
.distinct_on([
(product_status::Entity, product_status::Column::ContextCpeId),
(product_status::Entity, product_status::Column::StatusId),
(product_status::Entity, product_status::Column::Component),
(
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::Component)
.order_by_asc(product_status::Column::VulnerabilityId);

let product_advisory_statuses = product_advisory_info
.try_into_multi_model::<ProductStatusCatcher>()?
.all(tx)
.await?;

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

Ok(match summary {
Expand All @@ -82,6 +117,7 @@ impl SbomDetails {
advisories: SbomAdvisory::from_models(
&summary.clone().described_by,
&relevant_advisory_info,
&product_advisory_statuses,
tx,
)
.await?,
Expand All @@ -102,6 +138,7 @@ impl SbomAdvisory {
pub async fn from_models(
described_by: &[SbomPackage],
statuses: &[QueryCatcher],
product_statuses: &[ProductStatusCatcher],
tx: &ConnectionOrTransaction<'_>,
) -> Result<Vec<Self>, Error> {
let mut advisories = HashMap::new();
Expand Down Expand Up @@ -206,6 +243,40 @@ impl SbomAdvisory {
});
}

for product in product_statuses {
let advisory_cpe: Option<OwnedUri> = (&product.cpe).try_into().ok();

let mut packages = vec![];
if let Some(component) = &product.product_status.component {
let package = SbomPackage {
name: component.to_string(),
..Default::default()
};
packages.push(package);
}

let status = SbomStatus {
vulnerability_id: product.product_status.vulnerability_id.clone(),
status: product.status.slug.clone(),
context: advisory_cpe
.as_ref()
.map(|e| StatusContext::Cpe(e.to_string())),
packages, // TODO find packages based on component
};

match advisories.entry(product.advisory.id) {
Entry::Occupied(entry) => entry.into_mut().status.push(status.clone()),
Entry::Vacant(entry) => {
let advisory = SbomAdvisory {
head: AdvisoryHead::from_advisory(&product.advisory, Memo::NotProvided, tx)
.await?,
status: vec![status.clone()],
};
entry.insert(advisory.clone());
}
}
}

Ok(advisories.values().cloned().collect::<Vec<_>>())
}
}
Expand All @@ -221,3 +292,37 @@ pub struct SbomStatus {
}

impl SbomStatus {}

#[derive(Debug)]
#[allow(dead_code)] //TODO sbom field is not used at the moment, but we will probably need it for graph search
pub struct ProductStatusCatcher {
advisory: advisory::Model,
product_status: product_status::Model,
cpe: trustify_entity::cpe::Model,
status: status::Model,
sbom: Option<sbom::Model>,
}

impl FromQueryResult for ProductStatusCatcher {
fn from_query_result(res: &QueryResult, _pre: &str) -> Result<Self, DbErr> {
Ok(Self {
advisory: Self::from_query_result_multi_model(res, "", advisory::Entity)?,
product_status: Self::from_query_result_multi_model(res, "", product_status::Entity)?,
cpe: Self::from_query_result_multi_model(res, "", trustify_entity::cpe::Entity)?,
status: Self::from_query_result_multi_model(res, "", status::Entity)?,
sbom: Self::from_query_result_multi_model_optional(res, "", sbom::Entity)?,
})
}
}

impl FromQueryResultMultiModel for ProductStatusCatcher {
fn try_into_multi_model<E: EntityTrait>(select: Select<E>) -> Result<Select<E>, DbErr> {
select
.try_model_columns(advisory::Entity)?
.try_model_columns(product_status::Entity)?
.try_model_columns(trustify_entity::cpe::Entity)?
.try_model_columns(status::Entity)?
.try_model_columns(product_version::Entity)?
.try_model_columns(sbom::Entity)
}
}
2 changes: 1 addition & 1 deletion modules/fundamental/src/sbom/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl SbomSummary {
}
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, SimpleObject)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, SimpleObject, Default)]
#[graphql(concrete(name = "SbomPackage", params()))]
pub struct SbomPackage {
pub id: String,
Expand Down
Loading

0 comments on commit 4a23380

Please sign in to comment.