Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide scores in sbom details response #1005

Merged
merged 8 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions cvss/src/cvss3/score.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::cvss3::severity::Severity;

use super::Cvss3Base;

/// CVSS V3.1 scores.
///
/// Formula described in CVSS v3.1 Specification: Section 5:
Expand Down Expand Up @@ -35,16 +37,12 @@ impl Score {

/// Convert the numeric score into a `Severity`
pub fn severity(self) -> Severity {
if self.0 < 0.1 {
Severity::None
} else if self.0 < 4.0 {
Severity::Low
} else if self.0 < 7.0 {
Severity::Medium
} else if self.0 < 9.0 {
Severity::High
} else {
Severity::Critical
match self.0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

x if x < 0.1 => Severity::None,
x if x < 4.0 => Severity::Low,
x if x < 7.0 => Severity::Medium,
x if x < 9.0 => Severity::High,
_ => Severity::Critical,
}
}
}
Expand All @@ -66,3 +64,19 @@ impl From<Score> for Severity {
score.severity()
}
}

impl FromIterator<Cvss3Base> for Score {
fn from_iter<I: IntoIterator<Item = Cvss3Base>>(iter: I) -> Self {
let mut count: usize = 0;
let mut sum = 0.0;
for v in iter {
sum += v.score().value();
count += 1;
}
if count > 0 {
Self::new(sum / (count as f64))
} else {
Self::default()
}
}
}
20 changes: 5 additions & 15 deletions entity/src/cvss3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub struct Model {
pub severity: Severity,
}

impl From<Model> for cvss3::Cvss3Base {
fn from(value: Model) -> Self {
impl From<&Model> for cvss3::Cvss3Base {
fn from(value: &Model) -> Self {
Self {
minor_version: value.minor_version as u8,
av: value.av.into(),
Expand All @@ -44,19 +44,9 @@ impl From<Model> for cvss3::Cvss3Base {
}
}

impl From<&Model> for cvss3::Cvss3Base {
fn from(value: &Model) -> Self {
Self {
minor_version: value.minor_version as u8,
av: value.av.into(),
ac: value.ac.into(),
pr: value.pr.into(),
ui: value.ui.into(),
s: value.s.into(),
c: value.c.into(),
i: value.i.into(),
a: value.a.into(),
}
impl From<Model> for cvss3::Cvss3Base {
fn from(value: Model) -> Self {
Self::from(&value)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,7 @@ impl AdvisoryVulnerabilityHead {
.all(tx)
.await?;

let score = if let Some(average) = cvss3
.iter()
.map(|e| {
let base = Cvss3Base::from(e.clone());
base.score().value()
})
.reduce(|accum, e| accum + e)
{
Score::new(average / cvss3.len() as f64)
} else {
Score::new(0.0)
};
let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from));

let advisory_vuln = advisory_vulnerability::Entity::find()
.filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id))
Expand Down Expand Up @@ -94,18 +83,7 @@ impl AdvisoryVulnerabilityHead {
let mut heads = Vec::new();

for (vuln, cvss3) in vulnerabilities.iter().zip(cvss3s.iter()) {
let score = if let Some(average) = cvss3
.iter()
.map(|e| {
let base = Cvss3Base::from(e.clone());
base.score().value()
})
.reduce(|accum, e| accum + e)
{
Score::new(average / cvss3.len() as f64)
} else {
Score::new(0.0)
};
let score = Score::from_iter(cvss3.iter().map(Cvss3Base::from));

let advisory_vuln = advisory_vulnerability::Entity::find()
.filter(advisory_vulnerability::Column::AdvisoryId.eq(advisory.id))
Expand Down
4 changes: 2 additions & 2 deletions modules/fundamental/src/ai/service/tools/sbom_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ The tool provides a list of advisories/CVEs affecting the SBOM.
.status
.iter()
.map(|v| Vulnerability {
identifier: v.vulnerability_id.clone(),
identifier: v.vulnerability.identifier.clone(),
link: format!(
"http://localhost:3000/vulnerability/{}",
v.vulnerability_id
v.vulnerability.identifier
),
})
.collect(),
Expand Down
30 changes: 30 additions & 0 deletions modules/fundamental/src/sbom/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,33 @@ async fn download_sbom(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {

Ok(())
}

#[test_context(TrustifyContext)]
#[test(actix_web::test)]
async fn get_advisories(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
let id = ctx
.ingest_documents([
"quarkus-bom-2.13.8.Final-redhat-00004.json",
"csaf/cve-2023-0044.json",
])
.await?[0]
.id
.to_string();

let app = caller(ctx).await?;
let v: Value = app
.call_and_read_body_json(
TestRequest::get()
.uri(&format!("/api/v1/sbom/{id}/advisory"))
.to_request(),
)
.await;

log::debug!("{v:#?}");

// assert expected fields
assert_eq!(v[0]["identifier"], "https://www.redhat.com/#CVE-2023-0044");
assert_eq!(v[0]["status"][0]["average_severity"], "high");

Ok(())
}
96 changes: 68 additions & 28 deletions modules/fundamental/src/sbom/model/details.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use super::SbomSummary;
use crate::{
advisory::model::AdvisoryHead,
purl::{model::details::purl::StatusContext, model::summary::purl::PurlSummary},
sbom::{model::SbomPackage, service::sbom::QueryCatcher, service::SbomService},
purl::model::{details::purl::StatusContext, summary::purl::PurlSummary},
sbom::{
model::SbomPackage,
service::{sbom::QueryCatcher, SbomService},
},
vulnerability::model::VulnerabilityHead,
Error,
};
use async_graphql::SimpleObject;
use cpe::uri::OwnedUri;
use sea_orm::{
DbErr, EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QueryOrder,
Expand All @@ -22,11 +25,11 @@ use trustify_common::{
},
memo::Memo,
};
use trustify_cvss::cvss3::{score::Score, severity::Severity, Cvss3Base};
use trustify_entity::{
advisory, base_purl, product, product_status, product_version, purl_status,
qualified_purl::{self},
sbom::{self},
sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, versioned_purl,
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,
};
use utoipa::ToSchema;

Expand Down Expand Up @@ -90,6 +93,10 @@ impl SbomDetails {
)
.join(JoinType::Join, product_status::Relation::Status.def())
.join(JoinType::Join, product_status::Relation::Advisory.def())
.join(
JoinType::Join,
product_status::Relation::Vulnerability.def(),
)
.distinct_on([
(product_status::Entity, product_status::Column::ContextCpeId),
(product_status::Entity, product_status::Column::StatusId),
Expand Down Expand Up @@ -195,7 +202,7 @@ impl SbomAdvisory {

let sbom_status = if let Some(status) = advisory.status.iter_mut().find(|status| {
if status.status == each.status.slug
&& status.vulnerability_id == each.vulnerability.id
&& status.vulnerability.identifier == each.vulnerability.id
{
match (&status.context, &status_cpe) {
(Some(StatusContext::Cpe(context_cpe)), Some(status_cpe)) => {
Expand All @@ -210,14 +217,14 @@ impl SbomAdvisory {
}) {
status
} else {
let status = SbomStatus {
vulnerability_id: each.vulnerability.id.clone(),
status: each.status.slug.clone(),
context: status_cpe
.as_ref()
.map(|e| StatusContext::Cpe(e.to_string())),
packages: vec![],
};
let status = SbomStatus::new(
&each.vulnerability,
each.status.slug.clone(),
status_cpe,
vec![],
tx,
)
.await?;
advisory.status.push(status);
if let Some(status) = advisory.status.last_mut() {
status
Expand Down Expand Up @@ -255,14 +262,14 @@ impl SbomAdvisory {
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())),
let status = SbomStatus::new(
&product.vulnerability,
product.status.slug.clone(),
advisory_cpe,
packages, // TODO find purls based on package names
};
tx,
)
.await?;

match advisories.entry(product.advisory.id) {
Entry::Occupied(entry) => entry.into_mut().status.push(status.clone()),
Expand All @@ -281,22 +288,49 @@ impl SbomAdvisory {
}
}

#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, SimpleObject)]
#[graphql(concrete(name = "SbomStatus", params()))]
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct SbomStatus {
pub vulnerability_id: String,
#[serde(flatten)]
pub vulnerability: VulnerabilityHead,
pub average_severity: Severity,
pub status: String,
#[graphql(skip)]
pub context: Option<StatusContext>,
pub packages: Vec<SbomPackage>,
}

impl SbomStatus {}
impl SbomStatus {
pub async fn new(
vulnerability: &vulnerability::Model,
status: String,
cpe: Option<OwnedUri>,
packages: Vec<SbomPackage>,
tx: &ConnectionOrTransaction<'_>,
) -> Result<Self, Error> {
let cvss3 = vulnerability.find_related(cvss3::Entity).all(tx).await?;
let average_severity = Score::from_iter(cvss3.iter().map(Cvss3Base::from)).severity();
Ok(Self {
vulnerability: VulnerabilityHead::from_vulnerability_entity(
vulnerability,
Memo::NotProvided,
tx,
)
.await?,
context: cpe.as_ref().map(|e| StatusContext::Cpe(e.to_string())),
average_severity,
status,
packages,
})
}
pub fn identifier(&self) -> &str {
&self.vulnerability.identifier
}
}

#[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,
vulnerability: trustify_entity::vulnerability::Model,
product_status: product_status::Model,
cpe: trustify_entity::cpe::Model,
status: status::Model,
Expand All @@ -307,6 +341,11 @@ 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)?,
vulnerability: Self::from_query_result_multi_model(
res,
"",
trustify_entity::vulnerability::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)?,
Expand All @@ -319,6 +358,7 @@ 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(trustify_entity::vulnerability::Entity)?
.try_model_columns(product_status::Entity)?
.try_model_columns(trustify_entity::cpe::Entity)?
.try_model_columns(status::Entity)?
Expand Down
Loading