This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new a8c062f781 Generate SBOMs for providers (#35667)
a8c062f781 is described below

commit a8c062f781db3c6000f8be76389aa774b2a45c3f
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Nov 16 02:51:21 2023 +0800

    Generate SBOMs for providers (#35667)
    
    * Working POC
    
    * Small adjustements and cleaning
---
 .../src/airflow_breeze/commands/sbom_commands.py   | 178 ++++++++++++----
 .../commands/sbom_commands_config.py               |   1 +
 dev/breeze/src/airflow_breeze/utils/cdxgen.py      | 231 +++++++++++++++------
 dev/breeze/src/airflow_breeze/utils/path_utils.py  |   1 +
 .../breeze/output_sbom_update-sbom-information.svg |  52 +++--
 .../breeze/output_sbom_update-sbom-information.txt |   2 +-
 6 files changed, 338 insertions(+), 127 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/commands/sbom_commands.py 
b/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
index 8fc30faf72..1e01c9b7a9 100644
--- a/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/sbom_commands.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import json
+import os
 import sys
 from pathlib import Path
 
@@ -29,10 +30,14 @@ from airflow_breeze.global_constants import (
     PROVIDER_DEPENDENCIES,
 )
 from airflow_breeze.utils.cdxgen import (
+    PROVIDER_REQUIREMENTS_DIR_PATH,
     SbomApplicationJob,
+    SbomCoreJob,
+    SbomProviderJob,
     build_all_airflow_versions_base_image,
     get_cdxgen_port_mapping,
     get_requirements_for_provider,
+    list_providers_from_providers_requirements,
 )
 from airflow_breeze.utils.ci_group import ci_group
 from airflow_breeze.utils.click_utils import BreezeGroup
@@ -57,7 +62,7 @@ from airflow_breeze.utils.parallel import (
     check_async_run_results,
     run_with_pool,
 )
-from airflow_breeze.utils.path_utils import AIRFLOW_TMP_DIR_PATH, 
PROVIDER_METADATA_JSON_FILE_PATH
+from airflow_breeze.utils.path_utils import FILES_SBOM_DIR, 
PROVIDER_METADATA_JSON_FILE_PATH
 from airflow_breeze.utils.shared_options import get_dry_run
 
 
@@ -71,10 +76,11 @@ def sbom():
 
 
 SBOM_INDEX_TEMPLATE = """
+{% set project_name = " " + provider_id + " " if provider_id else " " -%}
 <html>
-<head><title>CycloneDX SBOMs for Apache Airflow {{ version }}</title></head>
+<head><title>CycloneDX SBOMs for Apache Airflow{{project_name}}{{ version 
}}</title></head>
 <body>
-    <h1>CycloneDX SBOMs for Apache Airflow {{ version }}</h1>
+    <h1>CycloneDX SBOMs for Apache Airflow{{project_name}}{{ version }}</h1>
     <ul>
     {% for sbom_file in sbom_files %}
         <li><a href="{{ sbom_file.name }}">{{ sbom_file.name }}</a></li>
@@ -119,6 +125,14 @@ SBOM_INDEX_TEMPLATE = """
 @option_verbose
 @option_dry_run
 @option_answer
[email protected](
+    "--package-filter",
+    help="List of packages to consider. You can use `apache-airflow` for core "
+    "or `apache-airflow-providers` to consider all the providers.",
+    type=BetterChoice(["apache-airflow-providers", "apache-airflow"]),
+    required=False,
+    default="apache-airflow",
+)
 def update_sbom_information(
     airflow_site_directory: Path,
     airflow_version: str | None,
@@ -130,6 +144,7 @@ def update_sbom_information(
     include_success_outputs: bool,
     skip_cleanup: bool,
     force: bool,
+    package_filter: tuple[str, ...],
 ):
     import jinja2
     from jinja2 import StrictUndefined
@@ -148,55 +163,114 @@ def update_sbom_information(
         python_versions = ALL_HISTORICAL_PYTHON_VERSIONS
     else:
         python_versions = [python]
-    application_root_path = AIRFLOW_TMP_DIR_PATH
+    application_root_path = FILES_SBOM_DIR
     start_cdxgen_server(application_root_path, run_in_parallel, parallelism)
 
     jobs_to_run: list[SbomApplicationJob] = []
 
-    apache_airflow_directory = airflow_site_directory / "docs-archive" / 
"apache-airflow"
+    airflow_site_archive_directory = airflow_site_directory / "docs-archive"
 
-    for airflow_v in airflow_versions:
-        airflow_version_dir = apache_airflow_directory / airflow_v
-        if not airflow_version_dir.exists():
-            get_console().print(f"[warning]The {airflow_version_dir} does not 
exist. Skipping")
-            continue
-        destination_dir = airflow_version_dir / "sbom"
-        if destination_dir.exists():
+    def _dir_exists_warn_and_should_skip(dir: Path, force: bool) -> bool:
+        if dir.exists():
             if not force:
-                get_console().print(f"[warning]The {destination_dir} already 
exists. Skipping")
-                continue
+                get_console().print(f"[warning]The {dir} already exists. 
Skipping")
+                return True
             else:
-                get_console().print(f"[warning]The {destination_dir} already 
exists. Forcing update")
+                get_console().print(f"[warning]The {dir} already exists. 
Forcing update")
+                return False
+        return False
 
-        destination_dir.mkdir(parents=True, exist_ok=True)
+    if package_filter == "apache-airflow":
+        # Create core jobs
+        apache_airflow_documentation_directory = 
airflow_site_archive_directory / "apache-airflow"
 
-        get_console().print(f"[info]Attempting to update sbom for 
{airflow_v}.")
-        get_console().print(f"[success]The {destination_dir} exists. 
Proceeding.")
-        for python_version in python_versions:
-            target_sbom_file_name = 
f"apache-airflow-sbom-{airflow_v}-python{python_version}.json"
-            target_sbom_path = destination_dir / target_sbom_file_name
-            if target_sbom_path.exists():
-                if not force:
-                    get_console().print(f"[warning]The {target_sbom_path} 
already exists. Skipping")
+        for airflow_v in airflow_versions:
+            airflow_version_dir = apache_airflow_documentation_directory / 
airflow_v
+            if not airflow_version_dir.exists():
+                get_console().print(f"[warning]The {airflow_version_dir} does 
not exist. Skipping")
+                continue
+            destination_dir = airflow_version_dir / "sbom"
+
+            if _dir_exists_warn_and_should_skip(destination_dir, force):
+                continue
+
+            destination_dir.mkdir(parents=True, exist_ok=True)
+
+            get_console().print(f"[info]Attempting to update sbom for 
{airflow_v}.")
+            for python_version in python_versions:
+                target_sbom_file_name = 
f"apache-airflow-sbom-{airflow_v}-python{python_version}.json"
+                target_sbom_path = destination_dir / target_sbom_file_name
+
+                if _dir_exists_warn_and_should_skip(target_sbom_path, force):
                     continue
-                else:
-                    get_console().print(f"[warning]The {target_sbom_path} 
already exists. Forcing update")
-            jobs_to_run.append(
-                SbomApplicationJob(
-                    airflow_version=airflow_v,
-                    python_version=python_version,
-                    application_root_path=application_root_path,
-                    
include_provider_dependencies=include_provider_dependencies,
-                    target_path=target_sbom_path,
+
+                jobs_to_run.append(
+                    SbomCoreJob(
+                        airflow_version=airflow_v,
+                        python_version=python_version,
+                        application_root_path=application_root_path,
+                        
include_provider_dependencies=include_provider_dependencies,
+                        target_path=target_sbom_path,
+                    )
                 )
+    elif package_filter == "apache-airflow-providers":
+        # Create providers jobs
+        user_confirm(
+            "You are about to update sbom information for providers, did you 
refresh the "
+            "providers requirements with the command `breeze sbom 
generate-providers-requirements`?",
+            quit_allowed=False,
+            default_answer=Answer.YES,
+        )
+        for (
+            node_name,
+            provider_id,
+            provider_version,
+            provider_version_documentation_directory,
+        ) in 
list_providers_from_providers_requirements(airflow_site_archive_directory):
+            destination_dir = provider_version_documentation_directory / "sbom"
+
+            if _dir_exists_warn_and_should_skip(destination_dir, force):
+                continue
+
+            destination_dir.mkdir(parents=True, exist_ok=True)
+
+            get_console().print(
+                f"[info]Attempting to update sbom for {provider_id} version 
{provider_version}."
+            )
+
+            python_versions = set(
+                dir_name.replace("python", "")
+                for dir_name in os.listdir(PROVIDER_REQUIREMENTS_DIR_PATH / 
node_name)
             )
+
+            for python_version in python_versions:
+                target_sbom_file_name = (
+                    
f"apache-airflow-sbom-{provider_id}-{provider_version}-python{python_version}.json"
+                )
+                target_sbom_path = destination_dir / target_sbom_file_name
+
+                if _dir_exists_warn_and_should_skip(target_sbom_path, force):
+                    continue
+
+                jobs_to_run.append(
+                    SbomProviderJob(
+                        provider_id=provider_id,
+                        provider_version=provider_version,
+                        python_version=python_version,
+                        target_path=target_sbom_path,
+                        folder_name=node_name,
+                    )
+                )
+
+    if len(jobs_to_run) == 0:
+        get_console().print("[info]Nothing to do, there is no job to process")
+        return
+
     if run_in_parallel:
         parallelism = min(parallelism, len(jobs_to_run))
         get_console().print(f"[info]Running {len(jobs_to_run)} jobs in 
parallel")
-        with ci_group(f"Generating SBoMs for 
{airflow_versions}:{python_versions}"):
-            all_params = [
-                f"Generate SBoMs for 
{job.airflow_version}:{job.python_version}" for job in jobs_to_run
-            ]
+        with ci_group(f"Generating SBOMs for {jobs_to_run}"):
+            all_params = [f"Generate SBOMs for {job.get_job_name()}" for job 
in jobs_to_run]
             with run_with_pool(
                 parallelism=parallelism,
                 all_params=all_params,
@@ -217,7 +291,7 @@ def update_sbom_information(
                 ]
         check_async_run_results(
             results=results,
-            success="All SBoMs were generated successfully",
+            success="All SBOMs were generated successfully",
             outputs=outputs,
             include_success_outputs=include_success_outputs,
             skip_cleanup=skip_cleanup,
@@ -226,20 +300,36 @@ def update_sbom_information(
         for job in jobs_to_run:
             produce_sbom_for_application_via_cdxgen_server(job, output=None)
 
-    for airflow_v in airflow_versions:
-        airflow_version_dir = apache_airflow_directory / airflow_v
-        destination_dir = airflow_version_dir / "sbom"
+    html_template = SBOM_INDEX_TEMPLATE
+
+    def _generate_index(destination_dir: Path, provider_id: str | None, 
version: str) -> None:
         destination_index_path = destination_dir / "index.html"
         get_console().print(f"[info]Generating index for {destination_dir}")
         sbom_files = sorted(destination_dir.glob("apache-airflow-sbom-*"))
-        html_template = SBOM_INDEX_TEMPLATE
         if not get_dry_run():
             destination_index_path.write_text(
                 jinja2.Template(html_template, autoescape=True, 
undefined=StrictUndefined).render(
-                    version=airflow_v, sbom_files=sbom_files
+                    provider_id=provider_id,
+                    version=version,
+                    sbom_files=sbom_files,
                 )
             )
 
+    if package_filter == "apache-airflow":
+        for airflow_v in airflow_versions:
+            airflow_version_dir = apache_airflow_documentation_directory / 
airflow_v
+            destination_dir = airflow_version_dir / "sbom"
+            _generate_index(destination_dir, None, airflow_v)
+    elif package_filter == "apache-airflow-providers":
+        for (
+            node_name,
+            provider_id,
+            provider_version,
+            provider_version_documentation_directory,
+        ) in 
list_providers_from_providers_requirements(airflow_site_archive_directory):
+            destination_dir = provider_version_documentation_directory / "sbom"
+            _generate_index(destination_dir, provider_id, provider_version)
+
 
 @sbom.command(name="build-all-airflow-images", help="Generate images with 
airflow versions pre-installed")
 @option_historical_python_version
@@ -283,7 +373,6 @@ def build_all_airflow_images(
                         build_all_airflow_versions_base_image,
                         kwds={
                             "python_version": python_version,
-                            "confirm": False,
                             "output": outputs[index],
                         },
                     )
@@ -300,7 +389,6 @@ def build_all_airflow_images(
         for python_version in python_versions:
             build_all_airflow_versions_base_image(
                 python_version=python_version,
-                confirm=False,
                 output=None,
             )
 
diff --git a/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
index ce17e4a258..9fffaaaaca 100644
--- a/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/sbom_commands_config.py
@@ -34,6 +34,7 @@ SBOM_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] 
= {
                 "--airflow-version",
                 "--python",
                 "--include-provider-dependencies",
+                "--package-filter",
                 "--force",
             ],
         },
diff --git a/dev/breeze/src/airflow_breeze/utils/cdxgen.py 
b/dev/breeze/src/airflow_breeze/utils/cdxgen.py
index ee43715f2f..3bcb4cd21f 100644
--- a/dev/breeze/src/airflow_breeze/utils/cdxgen.py
+++ b/dev/breeze/src/airflow_breeze/utils/cdxgen.py
@@ -23,9 +23,11 @@ import os
 import signal
 import sys
 import time
+from abc import abstractmethod
 from dataclasses import dataclass
 from multiprocessing.pool import Pool
 from pathlib import Path
+from typing import Generator
 
 import yaml
 
@@ -38,7 +40,7 @@ from airflow_breeze.utils.github import (
     download_constraints_file,
     download_file_from_github,
 )
-from airflow_breeze.utils.path_utils import AIRFLOW_SOURCES_ROOT, FILES_DIR
+from airflow_breeze.utils.path_utils import AIRFLOW_SOURCES_ROOT, 
FILES_SBOM_DIR
 from airflow_breeze.utils.run_utils import run_command
 from airflow_breeze.utils.shared_options import get_dry_run
 
@@ -129,8 +131,34 @@ def get_all_airflow_versions_image_name(python_version: 
str) -> str:
     return 
f"ghcr.io/apache/airflow/airflow-dev/all-airflow/python{python_version}"
 
 
+def list_providers_from_providers_requirements(
+    airflow_site_archive_directory: Path,
+) -> Generator[tuple[str, str, str, Path], None, None]:
+    for node_name in os.listdir(PROVIDER_REQUIREMENTS_DIR_PATH):
+        if not node_name.startswith("provider"):
+            continue
+
+        provider_id, provider_version = node_name.rsplit("-", 1)
+
+        provider_documentation_directory = (
+            airflow_site_archive_directory
+            / f"apache-airflow-providers-{provider_id.replace('provider-', 
'').replace('.', '-')}"
+        )
+        provider_version_documentation_directory = 
provider_documentation_directory / provider_version
+
+        if not provider_version_documentation_directory.exists():
+            get_console().print(
+                f"[warning]The {provider_version_documentation_directory} does 
not exist. Skipping"
+            )
+            continue
+
+        yield (node_name, provider_id, provider_version, 
provider_version_documentation_directory)
+
+
 TARGET_DIR_NAME = "provider_requirements"
-DOCKER_FILE_PREFIX = f"/files/{TARGET_DIR_NAME}/"
+
+PROVIDER_REQUIREMENTS_DIR_PATH = FILES_SBOM_DIR / TARGET_DIR_NAME
+DOCKER_FILE_PREFIX = f"/files/sbom/{TARGET_DIR_NAME}/"
 
 
 def get_requirements_for_provider(
@@ -148,19 +176,23 @@ def get_requirements_for_provider(
         ) / "provider.yaml"
         provider_version = 
yaml.safe_load(provider_file.read_text())["versions"][0]
 
-    target_dir = FILES_DIR / TARGET_DIR_NAME
     airflow_core_file_name = 
f"airflow-{airflow_version}-python{python_version}-requirements.txt"
-    airflow_core_path = target_dir / airflow_core_file_name
-
-    provider_with_core_file_name = 
f"python{python_version}-with-core-requirements.txt"
-    provider_without_core_file_name = 
f"python{python_version}-without-core-requirements.txt"
+    airflow_core_path = PROVIDER_REQUIREMENTS_DIR_PATH / airflow_core_file_name
 
     provider_folder_name = f"provider-{provider_id}-{provider_version}"
-    provider_folder_path = target_dir / provider_folder_name
-    provider_with_core_path = provider_folder_path / 
provider_with_core_file_name
-    provider_without_core_file = provider_folder_path / 
provider_without_core_file_name
+    provider_folder_path = PROVIDER_REQUIREMENTS_DIR_PATH / 
provider_folder_name
+
+    provider_with_core_folder_path = provider_folder_path / 
f"python{python_version}" / "with-core"
+    provider_with_core_folder_path.mkdir(exist_ok=True, parents=True)
+    provider_with_core_path = provider_with_core_folder_path / 
"requirements.txt"
 
-    docker_file_provider_folder_prefix = 
f"{DOCKER_FILE_PREFIX}/{provider_folder_name}/"
+    provider_without_core_folder_path = provider_folder_path / 
f"python{python_version}" / "without-core"
+    provider_without_core_folder_path.mkdir(exist_ok=True, parents=True)
+    provider_without_core_file = provider_without_core_folder_path / 
"requirements.txt"
+
+    docker_file_provider_with_core_folder_prefix = (
+        
f"{DOCKER_FILE_PREFIX}{provider_folder_name}/python{python_version}/with-core/"
+    )
 
     if (
         os.path.exists(provider_with_core_path)
@@ -185,8 +217,8 @@ mkdir -pv {DOCKER_FILE_PREFIX}
 /opt/airflow/airflow-{airflow_version}/bin/pip install 
apache-airflow=={airflow_version} \
     apache-airflow-providers-{provider_id}=={provider_version}
 /opt/airflow/airflow-{airflow_version}/bin/pip freeze | sort > \
-    {docker_file_provider_folder_prefix}{provider_with_core_file_name}
-chown --recursive {os.getuid()}:{os.getgid()} 
{DOCKER_FILE_PREFIX}{provider_with_core_file_name}
+    {docker_file_provider_with_core_folder_prefix}requirements.txt
+chown --recursive {os.getuid()}:{os.getgid()} 
{DOCKER_FILE_PREFIX}{provider_folder_name}
 """
     provider_command_result = run_command(
         [
@@ -223,20 +255,19 @@ chown --recursive {os.getuid()}:{os.getgid()} 
{DOCKER_FILE_PREFIX}{provider_with
     get_console(output=output).print(provider_packages)
     provider_without_core_file.write_text("".join(f"{p}\n" for p in 
provider_packages))
     get_console(output=output).print(
-        f"[success]Generated {provider_id}:{provider_version}:{python_version} 
requirements in "
+        f"[success]Generated 
{provider_id}:{provider_version}:python{python_version} requirements in "
         f"{provider_without_core_file}"
     )
 
     return (
         provider_command_result.returncode,
-        f"Provider requirements generated for 
{provider_id}:{provider_version}:{python_version}",
+        f"Provider requirements generated for 
{provider_id}:{provider_version}:python{python_version}",
     )
 
 
 def build_all_airflow_versions_base_image(
     python_version: str,
     output: Output | None,
-    confirm: bool = True,
 ) -> tuple[int, str]:
     """
     Build an image with all airflow versions pre-installed in separate 
virtualenvs.
@@ -288,11 +319,131 @@ 
constraints-{airflow_version}/constraints-{python_version}.txt
 
 @dataclass
 class SbomApplicationJob:
-    airflow_version: str
     python_version: str
+    target_path: Path
+
+    @abstractmethod
+    def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+        pass
+
+    @abstractmethod
+    def get_job_name(self) -> str:
+        pass
+
+
+@dataclass
+class SbomCoreJob(SbomApplicationJob):
+    airflow_version: str
     application_root_path: Path
     include_provider_dependencies: bool
-    target_path: Path
+
+    def get_job_name(self) -> str:
+        return f"{self.airflow_version}:python{self.python_version}"
+
+    def download_dependency_files(self, output: Output | None) -> bool:
+        source_dir = self.application_root_path / self.airflow_version / 
f"python{self.python_version}"
+        source_dir.mkdir(parents=True, exist_ok=True)
+        lock_file_relative_path = "airflow/www/yarn.lock"
+        download_file_from_github(
+            tag=self.airflow_version, path=lock_file_relative_path, 
output_file=source_dir / "yarn.lock"
+        )
+        if not download_constraints_file(
+            airflow_version=self.airflow_version,
+            python_version=self.python_version,
+            include_provider_dependencies=self.include_provider_dependencies,
+            output_file=source_dir / "requirements.txt",
+        ):
+            get_console(output=output).print(
+                f"[warning]Failed to download constraints file for "
+                f"{self.airflow_version} and {self.python_version}. Skipping"
+            )
+            return False
+        return True
+
+    def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+        import requests
+
+        get_console(output=output).print(
+            f"[info]Updating sbom for Airflow {self.airflow_version} and 
python {self.python_version}"
+        )
+        if not self.download_dependency_files(output):
+            return 0, f"SBOM Generate 
{self.airflow_version}:{self.python_version}"
+
+        get_console(output=output).print(
+            f"[info]Generating sbom for Airflow {self.airflow_version} and 
python {self.python_version} with cdxgen"
+        )
+        url = (
+            
f"http://127.0.0.1:{port}/sbom?path=/app/{self.airflow_version}/python{self.python_version}&";
+            
f"project-name=apache-airflow&project-version={self.airflow_version}&multiProject=true"
+        )
+
+        get_console(output=output).print(
+            f"[info]Triggering sbom generation in {self.airflow_version} via 
{url}"
+        )
+        if not get_dry_run():
+            response = requests.get(url)
+            if response.status_code != 200:
+                get_console(output=output).print(
+                    f"[error]Generation for Airflow 
{self.airflow_version}:python{self.python_version} "
+                    f"failed. Status code {response.status_code}"
+                )
+                return (
+                    response.status_code,
+                    f"SBOM Generate 
{self.airflow_version}:python{self.python_version}",
+                )
+            self.target_path.write_bytes(response.content)
+            get_console(output=output).print(
+                f"[success]Generated SBOM for 
{self.airflow_version}:python{self.python_version}"
+            )
+
+        return 0, f"SBOM Generate 
{self.airflow_version}:python{self.python_version}"
+
+
+@dataclass
+class SbomProviderJob(SbomApplicationJob):
+    provider_id: str
+    provider_version: str
+    folder_name: str
+
+    def get_job_name(self) -> str:
+        return 
f"{self.provider_id}:{self.provider_version}:python{self.python_version}"
+
+    def produce(self, output: Output | None, port: int) -> tuple[int, str]:
+        import requests
+
+        get_console(output=output).print(
+            f"[info]Updating sbom for provider {self.provider_id} version 
{self.provider_version} and python "
+            f"{self.python_version}"
+        )
+        get_console(output=output).print(
+            f"[info]Generating sbom for provider {self.provider_id} version 
{self.provider_version} and "
+            f"python {self.python_version}"
+        )
+        url = (
+            
f"http://127.0.0.1:{port}/sbom?path=/app/{TARGET_DIR_NAME}/{self.folder_name}/python{self.python_version}/without-core&";
+            
f"project-name={self.provider_version}&project-version={self.provider_version}&multiProject=true"
+        )
+
+        get_console(output=output).print(f"[info]Triggering sbom generation 
via {url}")
+
+        if not get_dry_run():
+            response = requests.get(url)
+            if response.status_code != 200:
+                get_console(output=output).print(
+                    f"[error]Generation for Airflow 
{self.provider_id}:{self.provider_version}:"
+                    f"{self.python_version} failed. Status code 
{response.status_code}"
+                )
+                return (
+                    response.status_code,
+                    f"SBOM Generate 
{self.provider_id}:{self.provider_version}:{self.python_version}",
+                )
+            self.target_path.write_bytes(response.content)
+            get_console(output=output).print(
+                f"[success]Generated SBOM for 
{self.provider_id}:{self.provider_version}:"
+                f"{self.python_version}"
+            )
+
+        return 0, f"SBOM Generate 
{self.provider_id}:{self.provider_version}:{self.python_version}"
 
 
 def produce_sbom_for_application_via_cdxgen_server(
@@ -306,52 +457,10 @@ def produce_sbom_for_application_via_cdxgen_server(
          in case parallel processing is used
     :return: tuple with exit code and output
     """
-    import requests
 
     if port_map is None:
         port = 9090
     else:
         port = port_map[multiprocessing.current_process().name]
         get_console(output=output).print(f"[info]Using port {port}")
-    get_console(output=output).print(
-        f"[info]Updating sbom for Airflow {job.airflow_version} and python 
{job.python_version}"
-    )
-    source_dir = job.application_root_path / job.airflow_version / 
job.python_version
-    source_dir.mkdir(parents=True, exist_ok=True)
-    lock_file_relative_path = "airflow/www/yarn.lock"
-    download_file_from_github(
-        tag=job.airflow_version, path=lock_file_relative_path, 
output_file=source_dir / "yarn.lock"
-    )
-    if not download_constraints_file(
-        airflow_version=job.airflow_version,
-        python_version=job.python_version,
-        include_provider_dependencies=job.include_provider_dependencies,
-        output_file=source_dir / "requirements.txt",
-    ):
-        get_console(output=output).print(
-            f"[warning]Failed to download constraints file for "
-            f"{job.airflow_version} and {job.python_version}. Skipping"
-        )
-        return 0, f"SBOM Generate {job.airflow_version}:{job.python_version}"
-    get_console(output=output).print(
-        f"[info]Generating sbom for Airflow {job.airflow_version} and python 
{job.python_version} with cdxgen"
-    )
-    url = (
-        
f"http://127.0.0.1:{port}/sbom?path=/app/{job.airflow_version}/{job.python_version}&";
-        
f"project-name=apache-airflow&project-version={job.airflow_version}&multiProject=true"
-    )
-    get_console(output=output).print(f"[info]Triggering sbom generation in 
{job.airflow_version} via {url}")
-    if not get_dry_run():
-        response = requests.get(url)
-        if response.status_code != 200:
-            get_console(output=output).print(
-                f"[error]Generation for Airflow 
{job.airflow_version}:{job.python_version} failed. "
-                f"Status code {response.status_code}"
-            )
-            return response.status_code, f"SBOM Generate 
{job.airflow_version}:{job.python_version}"
-        job.target_path.write_bytes(response.content)
-        get_console(output=output).print(
-            f"[success]Generated SBOM for 
{job.airflow_version}:{job.python_version}"
-        )
-
-    return 0, f"SBOM Generate {job.airflow_version}:{job.python_version}"
+    return job.produce(output, port)
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py 
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index d67868888c..cb48c5a104 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -279,6 +279,7 @@ WWW_ASSET_OUT_FILE = WWW_CACHE_DIR / "asset_compile.out"
 WWW_ASSET_OUT_DEV_MODE_FILE = WWW_CACHE_DIR / "asset_compile_dev_mode.out"
 DAGS_DIR = AIRFLOW_SOURCES_ROOT / "dags"
 FILES_DIR = AIRFLOW_SOURCES_ROOT / "files"
+FILES_SBOM_DIR = FILES_DIR / "sbom"
 HOOKS_DIR = AIRFLOW_SOURCES_ROOT / "hooks"
 KUBE_DIR = AIRFLOW_SOURCES_ROOT / ".kube"
 LOGS_DIR = AIRFLOW_SOURCES_ROOT / "logs"
diff --git a/images/breeze/output_sbom_update-sbom-information.svg 
b/images/breeze/output_sbom_update-sbom-information.svg
index d15f3de36f..802927e439 100644
--- a/images/breeze/output_sbom_update-sbom-information.svg
+++ b/images/breeze/output_sbom_update-sbom-information.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 806.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 879.5999999999999" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -45,7 +45,7 @@
 
     <defs>
     <clipPath id="breeze-sbom-update-sbom-information-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="755.4" />
+      <rect x="0" y="0" width="1463.0" height="828.5999999999999" />
     </clipPath>
     <clipPath id="breeze-sbom-update-sbom-information-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -137,9 +137,18 @@
 <clipPath id="breeze-sbom-update-sbom-information-line-29">
     <rect x="0" y="709.1" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-30">
+    <rect x="0" y="733.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-31">
+    <rect x="0" y="757.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-sbom-update-sbom-information-line-32">
+    <rect x="0" y="782.3" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="804.4" rx="8"/><text 
class="breeze-sbom-update-sbom-information-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;sbom&#160;update-sbom-information</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="877.6" rx="8"/><text 
class="breeze-sbom-update-sbom-information-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;sbom&#160;update-sbom-information</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -163,23 +172,26 @@
 </text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="288.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-11)">│</text><text 
class="breeze-sbom-update-sbom-information-r1" x="488" y="288.4" 
textLength="951.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-11)">versions)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160
 [...]
 </text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="312.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-12)">│</text><text 
class="breeze-sbom-update-sbom-information-r7" x="488" y="312.8" 
textLength="951.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-12)">(3.6&#160;|&#160;3.7&#160;|&#160;3.8&#160;|&#160;3.9&#160;|&#160;3.10&#160;|&#160;3.11)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160
 [...]
 </text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="337.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="61" y="337.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="337.2" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-13)">-include</text><te
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="61" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="361.6" 
textLength="73.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-force</text><text
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="386" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="386" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-15)">
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="410.4" 
textLength="24.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">╭─</text><text 
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="410.4" 
textLength="219.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">&#160;Parallel&#160;running&#160;</text><text
 class="breeze-sbom-update-sbom-information-r5" x="244" y="410.4" 
textLength="1195.6" clip-path="url(#breeze-sbom-update-sbom-inf [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="434.8" 
textLength="48.8" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-run</text><text 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="459.2" 
textLength="146.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">-parallelism</t 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="483.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">│</text><text 
class="breeze-sbom-update-sbom-information-r7" x="378.2" y="483.6" 
textLength="915" 
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">(INTEGER&#160;RANGE)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="508" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">│</text><text 
class="breeze-sbom-update-sbom-information-r5" x="378.2" y="508" 
textLength="915" 
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">[default:&#160;4;&#160;1&lt;=x&lt;=8]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="532.4" 
textLength="61" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-skip</text><text 
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="556.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="556.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="556.8" 
textLength="73.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">-debug</text><te 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="581.2" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">-include</text>< 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="605.6" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="605.6" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-24)">
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="630" 
textLength="24.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">╭─</text><text 
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="630" 
textLength="195.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">&#160;Common&#160;options&#160;</text><text
 class="breeze-sbom-update-sbom-information-r5" x="219.6" y="630" 
textLength="1220" clip-path="url(#breeze-sbom-update-sbom-information [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="654.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="654.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="654.4" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-verbose</text>< 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="678.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="678.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="678.8" 
textLength="48.8" 
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">-dry</text><text 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="703.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="703.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="703.2" 
textLength="85.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">-answer</text><t 
[...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="727.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="727.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="727.6" 
textLength="61" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-help</text><text 
 [...]
-</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="752" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="752" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-30)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="61" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="361.6" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-14)">-package</text><te
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">│</text><text 
class="breeze-sbom-update-sbom-information-r1" x="488" y="386" 
textLength="951.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-15)">`apache-airflow-providers`&#160;to&#160;consider&#160;all&#160;the&#160;providers.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="410.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">│</text><text 
class="breeze-sbom-update-sbom-information-r7" x="488" y="410.4" 
textLength="951.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-16)">(apache-airflow-providers&#160;|&#160;apache-airflow)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#1
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="61" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="73.2" y="434.8" 
textLength="73.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-17)">-force</text><text
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="459.2" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-18)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="459.2" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-18)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="483.6" 
textLength="24.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">╭─</text><text 
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="483.6" 
textLength="219.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-19)">&#160;Parallel&#160;running&#160;</text><text
 class="breeze-sbom-update-sbom-information-r5" x="244" y="483.6" 
textLength="1195.6" clip-path="url(#breeze-sbom-update-sbom-inf [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="508" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="508" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="508" 
textLength="48.8" 
clip-path="url(#breeze-sbom-update-sbom-information-line-20)">-run</text><text 
class [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="532.4" 
textLength="146.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-21)">-parallelism</t 
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="556.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">│</text><text 
class="breeze-sbom-update-sbom-information-r7" x="378.2" y="556.8" 
textLength="915" 
clip-path="url(#breeze-sbom-update-sbom-information-line-22)">(INTEGER&#160;RANGE)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">│</text><text 
class="breeze-sbom-update-sbom-information-r5" x="378.2" y="581.2" 
textLength="915" 
clip-path="url(#breeze-sbom-update-sbom-information-line-23)">[default:&#160;4;&#160;1&lt;=x&lt;=8]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="605.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="605.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="605.6" 
textLength="61" 
clip-path="url(#breeze-sbom-update-sbom-information-line-24)">-skip</text><text 
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="630" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="630" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="630" 
textLength="73.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-25)">-debug</text><text
 cla [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="654.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="654.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="654.4" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-26)">-include</text>< 
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="678.8" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-27)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="678.8" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-27)">
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="703.2" 
textLength="24.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">╭─</text><text 
class="breeze-sbom-update-sbom-information-r5" x="24.4" y="703.2" 
textLength="195.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-28)">&#160;Common&#160;options&#160;</text><text
 class="breeze-sbom-update-sbom-information-r5" x="219.6" y="703.2" 
textLength="1220" clip-path="url(#breeze-sbom-update-sbom-infor [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="727.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="727.6" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="727.6" 
textLength="97.6" 
clip-path="url(#breeze-sbom-update-sbom-information-line-29)">-verbose</text>< 
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="752" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="752" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="752" 
textLength="48.8" 
clip-path="url(#breeze-sbom-update-sbom-information-line-30)">-dry</text><text 
class [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="776.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="776.4" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="776.4" 
textLength="85.4" 
clip-path="url(#breeze-sbom-update-sbom-information-line-31)">-answer</text><t 
[...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="800.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">│</text><text 
class="breeze-sbom-update-sbom-information-r4" x="24.4" y="800.8" 
textLength="12.2" 
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">-</text><text 
class="breeze-sbom-update-sbom-information-r4" x="36.6" y="800.8" 
textLength="61" 
clip-path="url(#breeze-sbom-update-sbom-information-line-32)">-help</text><text 
 [...]
+</text><text class="breeze-sbom-update-sbom-information-r5" x="0" y="825.2" 
textLength="1464" 
clip-path="url(#breeze-sbom-update-sbom-information-line-33)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-sbom-update-sbom-information-r1" x="1464" y="825.2" 
textLength="12.2" clip-path="url(#breeze-sbom-update-sbom-information-line-33)">
 </text>
     </g>
     </g>
diff --git a/images/breeze/output_sbom_update-sbom-information.txt 
b/images/breeze/output_sbom_update-sbom-information.txt
index 0418b1c425..e31da63f35 100644
--- a/images/breeze/output_sbom_update-sbom-information.txt
+++ b/images/breeze/output_sbom_update-sbom-information.txt
@@ -1 +1 @@
-653be48be70b4b7ff5172d491aadc694
+e49cf33ae18ab7d16af7ef3b73035a10

Reply via email to