Repository: trustification/trustify
Pull request #1564: refactor: move advisory status cpe filtering to the queries
Author: login: dejanb
Labels: name: backport release/0.2.z
description: Backport (0.2.z)
Created: 2025-04-16T10:08:52Z | Merged: 2025-04-24T07:49:02Z

This PR refactors SBOM/advisory correlation logic to correctly filter all SBOM-related CPEs in the query, moving it from the post-query loop.

This helps the performance by properly and early selecting statuses that are only related to the SBOM in question. It should lead to much less rows pulled from the database and filtered later (which is one of the problems with large SBOMs).

It also makes this code a bit more maintainable and suitable for future refactoring.

It still doesn't solve all performance issue with the endpoint. RHOSE-4.14 SBOM doesn't fail for me any more locally, but it still take a few minutes (That's why I didn't include load test yet). I don't expect it to have impact on other endpoints.

This is a first step toward performance of SBOM correlation logic for large SBOMs.


Diff:
diff --git a/modules/fundamental/src/sbom/model/details.rs b/modules/fundamental/src/sbom/model/details.rs
index 21643c0fc..daaa3eb12 100644
--- a/modules/fundamental/src/sbom/model/details.rs
+++ b/modules/fundamental/src/sbom/model/details.rs
@@ -4,32 +4,31 @@ use crate::{
     advisory::model::AdvisoryHead,
     purl::model::{details::purl::StatusContext, summary::purl::PurlSummary},
     sbom::{
-        model::SbomPackage,
+        model::{SbomPackage, raw_sql},
         service::{SbomService, sbom::QueryCatcher},
     },
     vulnerability::model::VulnerabilityHead,
 };
-use cpe::{cpe::Cpe, uri::OwnedUri};
+use cpe::uri::OwnedUri;
 use futures_util::{Stream, pin_mut, stream::StreamExt};
 use sea_orm::{
-    Condition, ConnectionTrait, DbBackend, DbErr, FromQueryResult, JoinType, ModelTrait,
-    QueryFilter, QuerySelect, RelationTrait, Statement, StreamTrait,
+    ConnectionTrait, DbBackend, DbErr, FromQueryResult, JoinType, ModelTrait, QueryFilter,
+    QuerySelect, RelationTrait, Statement, StreamTrait,
 };
-use sea_query::{Asterisk, Expr, Func, Query, SimpleExpr};
+use sea_query::{Asterisk, Expr, Func, SimpleExpr};
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use tracing::{debug_span, instrument};
 use tracing_futures::Instrument;
 use trustify_common::{
-    cpe::CpeCompare,
     db::{VersionMatches, multi_model::SelectIntoMultiModel},
     memo::Memo,
 };
 use trustify_cvss::cvss3::{Cvss3Base, score::Score, severity::Severity};
 use trustify_entity::{
     advisory, advisory_vulnerability, base_purl, cvss3, purl_status, qualified_purl, sbom,
-    sbom_node, sbom_package, sbom_package_cpe_ref, sbom_package_purl_ref, status, version_range,
-    versioned_purl, vulnerability,
+    sbom_node, sbom_package, sbom_package_purl_ref, status, version_range, versioned_purl,
+    vulnerability,
 };
 use utoipa::ToSchema;
 
@@ -79,22 +78,10 @@ impl SbomDetails {
                 .filter(Expr::col((status::Entity, status::Column::Slug)).is_in(statuses.clone()));
         }
 
-        // find all the CPEs associated with this SBOM
-        let subquery = Query::select()
-            .column(sbom_package_cpe_ref::Column::CpeId)
-            .from(sbom_package_cpe_ref::Entity)
-            .and_where(Expr::col(sbom_package_cpe_ref::Column::SbomId).eq(sbom.sbom_id))
-            .distinct()
-            .to_owned();
-
-        query = query.filter(
-            Condition::any()
-                .add(Expr::col((purl_status::Entity, purl_status::Column::ContextCpeId)).is_null())
-                .add(
-                    Expr::col((purl_status::Entity, purl_status::Column::ContextCpeId))
-                        .in_subquery(subquery),
-                ),
-        );
+        query = query.filter(Expr::cust_with_values(
+            raw_sql::CONTEXT_CPE_FILTER_SQL,
+            vec![sbom.sbom_id],
+        ));
 
         let relevant_advisory_info = query
             .join(
@@ -127,101 +114,10 @@ impl SbomDetails {
 
         log::info!("Result: {:?}", relevant_advisory_info.size_hint());
 
-        // 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",
-                "advisory_vulnerability"."advisory_id" AS "advisory_vulnerability$advisory_id",
-                "advisory_vulnerability"."vulnerability_id" AS "advisory_vulnerability$vulnerability_id",
-                "advisory_vulnerability"."title" AS "advisory_vulnerability$title",
-                "advisory_vulnerability"."summary" AS "advisory_vulnerability$summary",
-                "advisory_vulnerability"."description" AS "advisory_vulnerability$description",
-                "advisory_vulnerability"."reserved_date" AS "advisory_vulnerability$reserved_date",
-                "advisory_vulnerability"."discovery_date" AS "advisory_vulnerability$discovery_date",
-                "advisory_vulnerability"."release_date" AS "advisory_vulnerability$release_date",
-                "advisory_vulnerability"."cwes" AS "advisory_vulnerability$cwes",
-                "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",
-                "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",
-                "organization"."id" AS "organization$id",
-                "organization"."name" AS "organization$name",
-                "organization"."cpe_key" AS "organization$cpe_key",
-                "organization"."website" AS "organization$website"
-            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 = base_purl.name OR product_status.package LIKE CONCAT(base_purl.namespace, '/', 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"
-            LEFT JOIN "organization" ON "advisory"."issuer_id" = "organization"."id"
-            JOIN "advisory_vulnerability" ON "product_status"."advisory_id" = "advisory_vulnerability"."advisory_id"
-            AND "product_status"."vulnerability_id" = "advisory_vulnerability"."vulnerability_id"
-            JOIN "vulnerability" ON "advisory_vulnerability"."vulnerability_id" = "vulnerability"."id"
-            WHERE
-            "sbom"."sbom_id" = $1
-            AND ($2::text[] = ARRAY[]::text[] OR "status"."slug" = ANY($2::text[]))
-            "#;
-
         let result = tx
             .stream(Statement::from_sql_and_values(
                 DbBackend::Postgres,
-                product_advisory_info,
+                raw_sql::product_advisory_info_sql(),
                 [sbom.sbom_id.into(), statuses.into()],
             ))
             .await?
@@ -232,8 +128,7 @@ impl SbomDetails {
 
         let relevant_advisory_info = relevant_advisory_info.chain(result);
 
-        let advisories =
-            SbomAdvisory::from_models(&summary.described_by, relevant_advisory_info, tx).await?;
+        let advisories = SbomAdvisory::from_models(relevant_advisory_info, tx).await?;
 
         Ok(Some(SbomDetails {
             summary,
@@ -252,61 +147,20 @@ pub struct SbomAdvisory {
 impl SbomAdvisory {
     #[instrument(skip_all, err(level=tracing::Level::INFO))]
     pub async fn from_models<C: ConnectionTrait>(
-        described_by: &[SbomPackage],
         statuses: impl Stream<Item = Result<QueryCatcher, DbErr>>,
         tx: &C,
     ) -> Result<Vec<Self>, Error> {
-        log::info!(
-            "Packages: {}, Statuses: {:?}",
-            described_by.len(),
-            statuses.size_hint()
-        );
-
         let mut advisories = HashMap::new();
 
-        let sbom_cpes = described_by
-            .iter()
-            .flat_map(|each| each.cpe.iter())
-            .flat_map(|e| {
-                let e = e.replace(":*:", "::");
-                let e = e.replace(":*", "");
-                let result = cpe::uri::Uri::parse(&e);
-                result.ok().map(|wfn| wfn.as_uri().to_owned())
-            })
-            .collect::<Vec<_>>();
-
-        log::info!("CPEs: {}", sbom_cpes.len());
-
         let statuses = statuses.instrument(debug_span!("consuming statuses"));
         pin_mut!(statuses);
 
-        'status: while let Some(each) = statuses.next().await.transpose()? {
-            let status_cpe = if let Some(status_cpe) = &each.context_cpe {
-                let status_cpe: Result<OwnedUri, _> = status_cpe.try_into();
-                if let Ok(status_cpe) = status_cpe {
-                    if sbom_cpes.iter().any(|sbom_cpe| {
-                        let status_version = status_cpe.version().to_string();
-                        let sbom_version = sbom_cpe.version().to_string();
-                        // This is a bit simplified logic, but it is tune with v1 parity.
-                        // We need to investigate this more and apply proper version matching in the future
-                        status_cpe.is_superset(sbom_cpe)
-                            || status_version == "*"
-                            || sbom_version.starts_with(&status_version)
-                    }) {
-                        // status context is applicable, keep truckin'
-                    } else {
-                        // status context excludes this one, skip over
-                        continue 'status;
-                    }
-                    Some(status_cpe)
-                } else {
-                    None
-                }
-            } else {
-                None
-            };
+        while let Some(each) = statuses.next().await.transpose()? {
+            let status_cpe = each
+                .context_cpe
+                .as_ref()
+                .and_then(|cpe| cpe.try_into().ok());
 
-            // if we got here, then there's either no context or the context matches this SBOM
             let advisory = if let Some(advisory) = advisories.get_mut(&each.advisory.id) {
                 advisory
             } else {
@@ -329,19 +183,8 @@ impl SbomAdvisory {
             };
 
             let sbom_status = if let Some(status) = advisory.status.iter_mut().find(|status| {
-                if status.status == each.status.slug
+                status.status == each.status.slug
                     && status.vulnerability.identifier == each.vulnerability.id
-                {
-                    match (&status.context, &status_cpe) {
-                        (Some(StatusContext::Cpe(context_cpe)), Some(status_cpe)) => {
-                            *context_cpe == status_cpe.to_string()
-                        }
-                        (None, None) => true,
-                        _ => false,
-                    }
-                } else {
-                    false
-                }
             }) {
                 status
             } else {
diff --git a/modules/fundamental/src/sbom/model/mod.rs b/modules/fundamental/src/sbom/model/mod.rs
index de8d4f6a1..03cc00dfd 100644
--- a/modules/fundamental/src/sbom/model/mod.rs
+++ b/modules/fundamental/src/sbom/model/mod.rs
@@ -1,4 +1,5 @@
 pub mod details;
+pub mod raw_sql;
 
 use super::service::SbomService;
 use crate::{
diff --git a/modules/fundamental/src/sbom/model/raw_sql.rs b/modules/fundamental/src/sbom/model/raw_sql.rs
new file mode 100644
index 000000000..140a760ec
--- /dev/null
+++ b/modules/fundamental/src/sbom/model/raw_sql.rs
@@ -0,0 +1,126 @@
+/// This constant is a SQL subquery that filters the context_cpe_id
+/// based on the given sbom_id. It checks if the context_cpe_id is null
+/// or if it is in the list of CPEs that are related to the packages
+/// that describes the SBOM. The additional logic allows us to find
+/// superset of generalized CPEs that don't include subfields like edition
+/// and find "stream" releases based on the major version.
+pub const CONTEXT_CPE_FILTER_SQL: &str = r#"
+(
+    context_cpe_id IS NULL OR
+    context_cpe_id IN (
+        WITH related_nodes AS (
+            SELECT DISTINCT right_node_id
+            FROM package_relates_to_package
+            WHERE sbom_id = $1
+              AND relationship = 13
+        ),
+        sbom_cpes AS (
+            SELECT cpe_id, node_id
+            FROM sbom_package_cpe_ref
+            WHERE sbom_id = $1
+              AND node_id IN (SELECT right_node_id FROM related_nodes)
+        ),
+        filtered_cpes AS (
+            SELECT cpe.*
+            FROM sbom_cpes spcr
+            JOIN cpe ON spcr.cpe_id = cpe.id
+        ),
+        generalized_cpes AS (
+            SELECT *
+            FROM cpe
+            WHERE (edition IS NULL OR edition = '*')
+              AND (vendor, product, version) IN (
+                  SELECT vendor, product, split_part(version, '.', 1)
+                  FROM filtered_cpes
+              )
+        )
+        SELECT id FROM filtered_cpes
+        UNION
+        SELECT id FROM generalized_cpes
+    )
+)
+"#;
+
+pub fn product_advisory_info_sql() -> String {
+    format!(
+        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",
+            "advisory_vulnerability"."advisory_id" AS "advisory_vulnerability$advisory_id",
+            "advisory_vulnerability"."vulnerability_id" AS "advisory_vulnerability$vulnerability_id",
+            "advisory_vulnerability"."title" AS "advisory_vulnerability$title",
+            "advisory_vulnerability"."summary" AS "advisory_vulnerability$summary",
+            "advisory_vulnerability"."description" AS "advisory_vulnerability$description",
+            "advisory_vulnerability"."reserved_date" AS "advisory_vulnerability$reserved_date",
+            "advisory_vulnerability"."discovery_date" AS "advisory_vulnerability$discovery_date",
+            "advisory_vulnerability"."release_date" AS "advisory_vulnerability$release_date",
+            "advisory_vulnerability"."cwes" AS "advisory_vulnerability$cwes",
+            "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",
+            "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",
+            "organization"."id" AS "organization$id",
+            "organization"."name" AS "organization$name",
+            "organization"."cpe_key" AS "organization$cpe_key",
+            "organization"."website" AS "organization$website"
+        FROM product_status
+        JOIN cpe ON product_status.context_cpe_id = cpe.id
+
+        -- now find matching purls in these statuses
+        JOIN base_purl ON product_status.package = base_purl.name OR product_status.package LIKE CONCAT(base_purl.namespace, '/', 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 = $1
+        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"
+        LEFT JOIN "organization" ON "advisory"."issuer_id" = "organization"."id"
+        JOIN "advisory_vulnerability" ON "product_status"."advisory_id" = "advisory_vulnerability"."advisory_id"
+        AND "product_status"."vulnerability_id" = "advisory_vulnerability"."vulnerability_id"
+        JOIN "vulnerability" ON "advisory_vulnerability"."vulnerability_id" = "vulnerability"."id"
+        WHERE
+        ($2::text[] = ARRAY[]::text[] OR "status"."slug" = ANY($2::text[]))
+        AND {}
+        "#,
+        CONTEXT_CPE_FILTER_SQL
+    )
+}


Reviews:
user: login: ctron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:54:33Z
user: login: dejanb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:59:20Z
user: login: dejanb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:50:31Z
user: login: ctron
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T07:30:30Z

Review comments:
diff_hunk: @@ -0,0 +1,129 @@
+/// This function returns SQL subquery that filters the context_cpe_id
+/// based on the given sbom_id. It checks if the context_cpe_id is null
+/// or if it is in the list of CPEs that are related to the packages
+/// that describes the SBOM. The additional logic allows us to find
+/// superset of generalized CPEs that don't include subfields like edition
+/// and find "zstream" releases based on the major version.
+pub fn context_cpe_filter_sql() -> &'static str {
path: modules/fundamental/src/sbom/model/raw_sql.rs
user: login: ctron
body: I guess (not sure) you can make those `fn`s `const`. So that they would be run a compile time.
created_at: 2025-04-16T13:54:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,129 @@
+/// This function returns SQL subquery that filters the context_cpe_id
+/// based on the given sbom_id. It checks if the context_cpe_id is null
+/// or if it is in the list of CPEs that are related to the packages
+/// that describes the SBOM. The additional logic allows us to find
+/// superset of generalized CPEs that don't include subfields like edition
+/// and find "zstream" releases based on the major version.
+pub fn context_cpe_filter_sql() -> &'static str {
path: modules/fundamental/src/sbom/model/raw_sql.rs
user: login: dejanb
body: Cool. Let me give it a try
created_at: 2025-04-16T13:59:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,129 @@
+/// This function returns SQL subquery that filters the context_cpe_id
+/// based on the given sbom_id. It checks if the context_cpe_id is null
+/// or if it is in the list of CPEs that are related to the packages
+/// that describes the SBOM. The additional logic allows us to find
+/// superset of generalized CPEs that don't include subfields like edition
+/// and find "zstream" releases based on the major version.
+pub fn context_cpe_filter_sql() -> &'static str {
path: modules/fundamental/src/sbom/model/raw_sql.rs
user: login: dejanb
body: I actually converted it to simple const. No need for function in this case. Thanks for spotting it.
created_at: 2025-04-16T15:50:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: dejanb
created_at: 2025-04-16T10:09:20Z
body: /scale-test
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-16T10:09:28Z
body: 🛠️ Scale test has started! Follow the progress here: [Workflow Run](https://github.com/trustification/trustify/actions/runs/14490175814)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-16T10:56:04Z
body: <details><summary>Goose Report</summary>


# Goose Attack Report


## Plan Overview

| Action | Started | Stopped | Elapsed | Users |
| ------ | ------- | ------- | ------- | ----: |
| Increasing | 25-04-16 10:50:55 | 25-04-16 10:51:00 | 00:00:05 | 0 &rarr; 5 |
| Maintaining | 25-04-16 10:51:00 | 25-04-16 10:56:00 | 00:05:00 | 5 |
| Decreasing | 25-04-16 10:56:00 | 25-04-16 10:56:01 | 00:00:01 | 0 &larr; 5 |

## Request Metrics

| Method | Name | # Requests | # Fails | Average (ms) | Min (ms) | Max (ms) | RPS | Failures/s |
| ------ | ---- | ---------: | ------: | -----------: | -------: | -------: | --: | ---------: |
| GET | get_advisory_by_doc_id | 104 (+7) | 0 | 14.00 (-0.23) | 3 (-1) | 67 (+7) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | get_sbom[sha256:f293eb89…6720f692ec5f3081] | 103 (+4) | 0 | 1174.35 (-29.28) | 483 (-84) | 2237 (-256) | 0.34 (+0.01) | 0.00 (+0.00) |
| GET | list_advisory | 104 (+4) | 0 | 2494.97 (-199.54) | 2004 (+614) | 3131 (-321) | 0.35 (+0.01) | 0.00 (+0.00) |
| GET | list_advisory_paginated | 106 (+7) | 0 | 2429.17 (-131.80) | 1403 (-262) | 2948 (-452) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | list_importer | 103 (+5) | 0 | 4.02 (-0.32) | 1 (0) | 52 (0) | 0.34 (+0.02) | 0.00 (+0.00) |
| GET | list_organizations | 103 (+5) | 0 | 16.13 (-0.16) | 2 (0) | 64 (+7) | 0.34 (+0.02) | 0.00 (+0.00) |
| GET | list_packages | 103 (+5) | 0 | 431.68 (-0.01) | 104 (-5) | 734 (+52) | 0.34 (+0.02) | 0.00 (+0.00) |
| GET | list_packages_paginated | 103 (+5) | 0 | 411.48 (-22.57) | 109 (+4) | 606 (-82) | 0.34 (+0.02) | 0.00 (+0.00) |
| GET | list_products | 104 (+5) | 0 | 11.90 (-3.86) | 3 (0) | 61 (0) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | list_sboms | 104 (+5) | 0 | 1069.20 (-213.45) | 452 (-71) | 1979 (-255) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | list_sboms_paginated | 104 (+5) | 0 | 917.38 (-81.03) | 292 (+6) | 2044 (+53) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | list_vulnerabilities | 104 (+7) | 0 | 1038.49 (-17.85) | 319 (-60) | 1404 (-2) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | list_vulnerabilities_paginated | 104 (+6) | 0 | 829.23 (-31.81) | 222 (-38) | 1115 (-96) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | sbom_by_package[pkg:maven/io.qu…dhat.com%2fga%2f] | 103 (+5) | 0 | 50.46 (-19.72) | 11 (0) | 182 (-19) | 0.34 (+0.02) | 0.00 (+0.00) |
| GET | search_advisory | 104 (+7) | 0 | 1120.64 (-12.73) | 513 (+69) | 1806 (-84) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | search_exact_purl | 104 (+5) | 0 | 9.15 (-2.66) | 2 (-1) | 61 (+3) | 0.35 (+0.02) | 0.00 (+0.00) |
| GET | search_purls | 104 (+5) | 0 | 2438.54 (-44.75) | 1678 (-47) | 13277 (+749) | 0.35 (+0.02) | 0.00 (+0.00) |
|  | Aggregated | 1764 (+92) | 0 | 854.13 (-48.05) | 1 (0) | 13277 (+749) | 5.88 (+0.31) | 0.00 (+0.00) |

## Response Time Metrics

| Method | Name | 50%ile (ms) | 60%ile (ms) | 70%ile (ms) | 80%ile (ms) | 90%ile (ms) | 95%ile (ms) | 99%ile (ms) | 100%ile (ms) |
| ------ | ---- | ----------: | ----------: | ----------: | ----------: | ----------: | ----------: | ----------: | -----------: |
| GET | get_advisory_by_doc_id | 8 (0) | 9 (0) | 11 (+1) | 13 (0) | 52 (-1) | 57 (+1) | 61 (+1) | 67 (+7) |
| GET | get_sbom[sha256:f293eb89…6720f692ec5f3081] | 1,000 (0) | 1,000 (0) | 2,000 (+1,000) | 2,000 (0) | 2,000 (0) | 2,000 (0) | 2,000 (0) | 2,000 (0) |
| GET | list_advisory | 2,004 (-996) | 3,000 (0) | 3,000 (0) | 3,000 (0) | 3,000 (0) | 3,000 (0) | 3,000 (0) | 3,000 (0) |
| GET | list_advisory_paginated | 2,000 (-1,000) | 2,948 (-52) | 2,948 (-52) | 2,948 (-52) | 2,948 (-52) | 2,948 (-52) | 2,948 (-52) | 2,948 (-52) |
| GET | list_importer | 2 (0) | 3 (+1) | 3 (0) | 4 (0) | 5 (0) | 7 (+1) | 51 (0) | 52 (0) |
| GET | list_organizations | 5 (0) | 6 (0) | 10 (0) | 43 (0) | 47 (0) | 48 (-2) | 53 (-4) | 64 (+7) |
| GET | list_packages | 410 (-10) | 470 (+10) | 480 (0) | 490 (0) | 500 (0) | 600 (0) | 700 (+100) | 700 (+18) |
| GET | list_packages_paginated | 400 (-20) | 410 (-50) | 460 (-20) | 480 (-20) | 500 (0) | 600 (0) | 600 (-88) | 600 (-88) |
| GET | list_products | 6 (-2) | 7 (-2) | 8 (-2) | 10 (-2) | 48 (-5) | 56 (+2) | 60 (+3) | 61 (0) |
| GET | list_sboms | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (-1,000) | 1,979 (-21) | 1,979 (-21) | 1,979 (-21) |
| GET | list_sboms_paginated | 800 (-200) | 900 (-100) | 1,000 (0) | 1,000 (0) | 2,000 (+9) | 2,000 (+9) | 2,000 (+9) | 2,000 (+9) |
| GET | list_vulnerabilities | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) |
| GET | list_vulnerabilities_paginated | 800 (-100) | 900 (0) | 900 (0) | 900 (-100) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) |
| GET | sbom_by_package[pkg:maven/io.qu…dhat.com%2fga%2f] | 25 (-35) | 60 (-7) | 66 (-44) | 72 (-48) | 120 (-50) | 170 (-10) | 180 (-10) | 180 (-20) |
| GET | search_advisory | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,000 (0) | 1,806 (-84) | 1,806 (-84) |
| GET | search_exact_purl | 5 (-1) | 6 (0) | 7 (0) | 8 (-1) | 10 (-39) | 54 (+1) | 58 (+2) | 61 (+3) |
| GET | search_purls | 2,000 (0) | 2,000 (0) | 2,000 (0) | 2,000 (0) | 2,000 (0) | 3,000 (0) | 11,000 (+1,000) | 13,000 (+472) |
|  | Aggregated | 600 (-100) | 900 (-100) | 1,000 (0) | 2,000 (0) | 2,000 (0) | 3,000 (0) | 3,000 (0) | 13,000 (+472) |

## Status Code Metrics

| Method | Name | Status Codes |
| ------ | ---- | ------------ |
| GET | get_advisory_by_doc_id | 104 [200] |
| GET | get_sbom[sha256:f293eb89…6720f692ec5f3081] | 103 [200] |
| GET | list_advisory | 104 [200] |
| GET | list_advisory_paginated | 106 [200] |
| GET | list_importer | 103 [200] |
| GET | list_organizations | 103 [200] |
| GET | list_packages | 103 [200] |
| GET | list_packages_paginated | 103 [200] |
| GET | list_products | 104 [200] |
| GET | list_sboms | 104 [200] |
| GET | list_sboms_paginated | 104 [200] |
| GET | list_vulnerabilities | 104 [200] |
| GET | list_vulnerabilities_paginated | 104 [200] |
| GET | sbom_by_package[pkg:maven/io.qu…dhat.com%2fga%2f] | 103 [200] |
| GET | search_advisory | 104 [200] |
| GET | search_exact_purl | 104 [200] |
| GET | search_purls | 104 [200] |
|  | Aggregated | 1,764 [200] |

## Transaction Metrics

| Transaction | # Times Run | # Fails | Average (ms) | Min (ms) | Max (ms) | RPS | Failures/s |
| ----------- | ----------: | ------: | -----------: | -------: | -------: | --: | ---------: |
| **WebsiteUser** |
| 0.0 logon | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.1 website_index | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.2 website_openapi | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.3 website_sboms | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.4 website_packages | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.5 website_advisories | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| 0.6 website_importers | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0) | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| **RestAPIUser** |
| 1.0 logon | 103 (+5) | 0 (0) | 13.88 (+0.20) | 6 (0) | 26 (+1) | 0.34 (+0.02) | 0.00 (+0.00) |
| 1.1 list_organizations | 103 (+5) | 0 (0) | 16.32 (-0.26) | 2 (0) | 64 (+7) | 0.34 (+0.02) | 0.00 (+0.00) |
| 1.2 list_advisory | 104 (+4) | 0 (0) | 2495.10 (-199.48) | 2004 (+614) | 3131 (-321) | 0.35 (+0.01) | 0.00 (+0.00) |
| 1.3 list_advisory_paginated | 106 (+7) | 0 (0) | 2429.24 (-131.85) | 1403 (-262) | 2948 (-452) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.4 get_advisory_by_doc_id | 104 (+7) | 0 (0) | 14.05 (-0.31) | 3 (-1) | 67 (+7) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.5 search_advisory | 104 (+7) | 0 (0) | 1120.75 (-12.76) | 513 (+69) | 1806 (-84) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.6 list_vulnerabilities | 104 (+7) | 0 (0) | 1038.59 (-17.86) | 319 (-60) | 1404 (-2) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.7 list_vulnerabilities_paginated | 104 (+6) | 0 (0) | 829.26 (-31.90) | 222 (-38) | 1115 (-96) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.8 list_importer | 103 (+5) | 0 (0) | 4.05 (-0.34) | 1 (0) | 52 (0) | 0.34 (+0.02) | 0.00 (+0.00) |
| 1.9 list_packages | 103 (+5) | 0 (0) | 431.73 (-0.05) | 104 (-5) | 734 (+52) | 0.34 (+0.02) | 0.00 (+0.00) |
| 1.10 list_packages_paginated | 103 (+5) | 0 (0) | 411.56 (-22.52) | 109 (+4) | 608 (-80) | 0.34 (+0.02) | 0.00 (+0.00) |
| 1.11 search_purls | 104 (+5) | 0 (0) | 2438.65 (-44.77) | 1678 (-47) | 13277 (+748) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.12 search_exact_purl | 104 (+5) | 0 (0) | 9.24 (-2.64) | 2 (-1) | 61 (+3) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.13 list_products | 104 (+5) | 0 (0) | 11.95 (-3.91) | 3 (0) | 61 (0) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.14 list_sboms | 104 (+5) | 0 (0) | 1069.26 (-213.46) | 452 (-71) | 1979 (-255) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.15 list_sboms_paginated | 104 (+5) | 0 (0) | 917.47 (-81.10) | 292 (+6) | 2044 (+53) | 0.35 (+0.02) | 0.00 (+0.00) |
| 1.16 get_sbom[sha256:f293eb89…6720f692ec5f3081] | 103 (+4) | 0 (0) | 1174.43 (-29.29) | 484 (-83) | 2237 (-256) | 0.34 (+0.01) | 0.00 (+0.00) |
| 1.17 sbom_by_package[pkg:maven/io.qu…dhat.com%2fga%2f] | 103 (+5) | 0 (0) | 50.57 (-19.68) | 11 (-1) | 182 (-19) | 0.34 (+0.02) | 0.00 (+0.00) |
|  Aggregated | 1867 (+97) | 0 (0) | 807.01 (-45.22) | 1 (0) | 13277 (+748) | 6.22 (+0.32) | 0.00 (+0.00) |

## Scenario Metrics

| Transaction | # Users | # Times Run | Average (ms) | Min (ms) | Max (ms) | Scenarios/s | Iterations |
| ----------- | ------: | ----------: | -----------: | -------: | -------: | ----------: | ---------: |
| WebsiteUser | 0 (0) | 0 (0) | 0.00 (+0.00) | 0 (0)  | 0 (0) | 0.00 (+0.00) | 0.00 (+0.00) |
| RestAPIUser | 5 (0) | 103 (+5) | 14388.24 (-824.58) | 12100 (-890)  | 18680 (+581) | 0.34 (+0.02) | 20.60 (+1.00) |
| Aggregated | 5 (0) | 103 (+5) | 14388.24 (-824.58) | 12100 (-890)  | 18680 (+581) | 0.34 (+0.02) | 20.60 (+1.00) |

</details>

📄 **[Full Report](https://github.com/trustification/trustify/actions/runs/14490175814)** (Go to "Artifacts" and download **report**)
author_association: NONE