From aef092fcacd7d629d031fa6c15927a9036b2a1bb Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Thu, 16 Jul 2026 12:41:20 +0000 Subject: [PATCH 1/4] fix: prefer Syft over cdxgen for Docker image SBOMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdxgen image SBOMs lack the operating-system component and distro metadata, so vulnerability scanners can't detect OS packages: Trivy bails with 'Unsupported os family=none' and scans nothing, which also breaks Dependency-Track's Trivy analyzer. Syft image SBOMs carry the OS component and distro PURL qualifiers, and survive the full augment/enrich pipeline intact — Trivy and Grype both scan them correctly. Swap the image generator priorities (lockfile generators unchanged): - syft-image: 35 -> 25 (preferred) - cdxgen-image: 20 -> 40 (fallback when Syft is unavailable, and the only generator for CycloneDX 1.7 image requests, which Syft does not support) Verified against gcr.io/distroless/cc-debian13: Trivy now detects 'debian 13' and scans all 13 packages; Grype reports the same 12 CVEs as against a raw syft SBOM. Package coverage is equal or better — cdxgen's extra entries were source-package duplicates and pkg:generic noise (shared-object filenames, license IDs). Fixes #264 Co-Authored-By: Claude Fable 5 --- README.md | 18 +++++----- sbomify_action/_generation/generator.py | 36 +++++++++++++------ .../_generation/generators/cdxgen.py | 22 ++++++++---- sbomify_action/_generation/generators/syft.py | 15 ++++++-- tests/test_generation_plugin.py | 12 ++++--- 5 files changed, 70 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 15f305bd..d2376e54 100644 --- a/README.md +++ b/README.md @@ -1042,13 +1042,15 @@ sbomify uses a plugin architecture for SBOM generation, automatically selecting Generators are tried in priority order. Native tools (optimized for specific ecosystems) are preferred over generic scanners. Each tool supports different ecosystems: -| Priority | Generator | Supported Ecosystems | Output Formats | -| -------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------- | -| 10 | **cyclonedx-py** | Python only | CycloneDX 1.0–1.7 | -| 10 | **cargo-cyclonedx** | Rust only | CycloneDX 1.4–1.6 | -| 20 | **cdxgen** | Python, JavaScript, **Java/Gradle**, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Scala, Docker images | CycloneDX 1.4–1.7 | -| ~~30~~ | ~~**Trivy**~~ | ~~Temporarily disabled due to security vulnerabilities~~ | ~~CycloneDX 1.6, SPDX 2.3~~ | -| 35 | **Syft** | Python, JavaScript, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Terraform, Docker images | CycloneDX 1.2–1.6, SPDX 2.2–2.3 | +| Priority | Generator | Supported Ecosystems | Output Formats | +| -------- | --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------- | +| 10 | **cyclonedx-py** | Python only | CycloneDX 1.0–1.7 | +| 10 | **cargo-cyclonedx** | Rust only | CycloneDX 1.4–1.6 | +| 20 | **cdxgen** (lockfiles) | Python, JavaScript, **Java/Gradle**, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Scala | CycloneDX 1.4–1.7 | +| 25 | **Syft** (images) | Docker images | CycloneDX 1.2–1.6, SPDX 2.2–2.3 | +| ~~30~~ | ~~**Trivy**~~ | ~~Temporarily disabled due to security vulnerabilities~~ | ~~CycloneDX 1.6, SPDX 2.3~~ | +| 35 | **Syft** (lockfiles) | Python, JavaScript, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Terraform | CycloneDX 1.2–1.6, SPDX 2.2–2.3 | +| 40 | **cdxgen** (images) | Docker images (fallback: lacks the OS metadata vulnerability scanners need) | CycloneDX 1.4–1.7 | #### How It Works @@ -1057,7 +1059,7 @@ Generators are tried in priority order. Native tools (optimized for specific eco 3. **Java lockfiles** (pom.xml, build.gradle, gradle.lockfile) → cdxgen (best Java support) 4. **Dart lockfiles** (pubspec.lock) → cdxgen or Syft 5. **Other lockfiles** (package-lock.json, go.mod, etc.) → cdxgen (then Syft as fallback) -6. **Docker images** → cdxgen (then Syft as fallback) +6. **Docker images** → Syft (then cdxgen as fallback). Syft SBOMs include the `operating-system` component and distro PURL qualifiers that vulnerability scanners (Trivy, Grype, Dependency-Track) need to detect OS packages. If the primary generator fails or doesn't support the input, the next one in priority order is tried automatically. diff --git a/sbomify_action/_generation/generator.py b/sbomify_action/_generation/generator.py index e28c7924..5f076b0d 100644 --- a/sbomify_action/_generation/generator.py +++ b/sbomify_action/_generation/generator.py @@ -31,14 +31,19 @@ def create_default_registry() -> GeneratorRegistry: - Input: Rust lock files only (Cargo.lock) - Output: CycloneDX 1.4-1.6 - Priority 20 - Comprehensive Multi-Ecosystem (cdxgen): + Priority 20 - Comprehensive Multi-Ecosystem (cdxgen, lock files): - CdxgenFsGenerator: Filesystem/lock file scanning - Input: Python, JavaScript, Java/Gradle, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Scala - Output: CycloneDX 1.4-1.7 (no SPDX support) - - CdxgenImageGenerator: Docker image scanning + + Priority 25 - Preferred Image Scanner (Syft): + - SyftImageGenerator: Docker image scanning - Input: Container images - - Output: CycloneDX 1.4-1.7 (no SPDX support) + - Output: CycloneDX 1.2-1.6, SPDX 2.2-2.3 + - Emits the operating-system component and distro PURL qualifiers + that vulnerability scanners (Trivy, Dependency-Track, Grype) + need to detect OS packages (see issue #264) Priority 30 - Generic Multi-Ecosystem (Trivy) [TEMPORARILY DISABLED]: - TrivyFsGenerator: Filesystem/lock file scanning @@ -50,14 +55,18 @@ def create_default_registry() -> GeneratorRegistry: - Output: CycloneDX 1.6, SPDX 2.3 NOTE: Trivy is temporarily disabled due to recurring security vulnerabilities. - Priority 35 - Generic Multi-Ecosystem (Syft): + Priority 35 - Generic Multi-Ecosystem (Syft, lock files): - SyftFsGenerator: Filesystem/lock file scanning - Input: Python, JavaScript, Go, Rust, Ruby, Dart, C++, PHP, .NET, Swift, Elixir, Terraform (NOT Java/Gradle lock files) - Output: CycloneDX 1.2-1.6, SPDX 2.2-2.3 - - SyftImageGenerator: Docker image scanning + + Priority 40 - Fallback Image Scanner (cdxgen): + - CdxgenImageGenerator: Docker image scanning - Input: Container images - - Output: CycloneDX 1.2-1.6, SPDX 2.2-2.3 + - Output: CycloneDX 1.4-1.7 (no SPDX support) + - Fallback only: its image SBOMs lack the operating-system + component, so Trivy/Dependency-Track can't scan them Generators are queried sequentially in priority order. The first generator that supports the input and requested format/version is used. @@ -71,9 +80,13 @@ def create_default_registry() -> GeneratorRegistry: registry.register(CycloneDXPyGenerator()) registry.register(CycloneDXCargoGenerator()) - # Priority 20: cdxgen generators (comprehensive multi-ecosystem) + # Priority 20: cdxgen lock file generator (comprehensive multi-ecosystem) registry.register(CdxgenFsGenerator()) - registry.register(CdxgenImageGenerator()) + + # Priority 25: Syft image generator — preferred for Docker images because + # its SBOMs carry the operating-system component that downstream + # vulnerability scanners (Trivy, Dependency-Track) require (issue #264) + registry.register(SyftImageGenerator()) # Priority 30: Trivy generators - TEMPORARILY DISABLED due to recurring # security vulnerabilities in Trivy. To re-enable, reintroduce the @@ -83,9 +96,12 @@ def create_default_registry() -> GeneratorRegistry: # registry.register(TrivyFsGenerator()) # registry.register(TrivyImageGenerator()) - # Priority 35: Syft generators (version selection, wide ecosystem support) + # Priority 35: Syft lock file generator (version selection, wide ecosystem support) registry.register(SyftFsGenerator()) - registry.register(SyftImageGenerator()) + + # Priority 40: cdxgen image generator — fallback behind Syft; its image + # SBOMs lack the operating-system component (issue #264) + registry.register(CdxgenImageGenerator()) return registry diff --git a/sbomify_action/_generation/generators/cdxgen.py b/sbomify_action/_generation/generators/cdxgen.py index d14978af..401b99a3 100644 --- a/sbomify_action/_generation/generators/cdxgen.py +++ b/sbomify_action/_generation/generators/cdxgen.py @@ -1,6 +1,7 @@ """cdxgen generator plugins for filesystem and Docker image scanning. -Priority: 20 (comprehensive multi-ecosystem) +Priority: 20 for lock files (comprehensive multi-ecosystem), +40 for Docker images (fallback behind Syft — see CdxgenImageGenerator). cdxgen is a comprehensive SBOM generator that supports many ecosystems and programming languages with excellent coverage. @@ -228,6 +229,11 @@ class CdxgenImageGenerator: Uses cdxgen to scan Docker images and generate SBOMs. + Fallback behind Syft for Docker images: cdxgen image SBOMs lack the + `operating-system` component, so vulnerability scanners like Trivy + (including Dependency-Track's Trivy analyzer) can't detect OS + packages and scan nothing (see issue #264). + Verified capabilities (cdxgen 12.0.0): - CycloneDX versions: 1.4, 1.5, 1.6, 1.7 (default: 1.6) - SPDX: Not supported @@ -245,8 +251,9 @@ def command(self) -> str: @property def priority(self) -> int: - # Comprehensive multi-ecosystem, higher priority than Trivy/Syft - return 20 + # Fallback behind Syft (25): cdxgen image SBOMs omit the + # operating-system component that Trivy/Dependency-Track need + return 40 @property def supported_formats(self) -> list[FormatVersion]: @@ -306,10 +313,11 @@ def generate(self, input: GenerationInput) -> GenerationResult: try: # run_command raises SBOMGenerationError on failure (uses check=True). - # log_errors=False: cdxgen image scanning is a priority-20 fallback - # ahead of syft; a benign failure here shouldn't spam ERROR when syft - # can still succeed. (Docker-image-not-found is handled separately and - # still logs at WARNING.) + # log_errors=False: cdxgen image scanning is a fallback behind syft; + # a benign failure here shouldn't spam ERROR — the orchestrator + # surfaces a real ERROR only if every generator fails. + # (Docker-image-not-found is handled separately and still logs at + # WARNING.) run_command(cmd, "cdxgen", timeout=DEFAULT_TIMEOUT, docker_image=input.docker_image, log_errors=False) # Verify output file was created diff --git a/sbomify_action/_generation/generators/syft.py b/sbomify_action/_generation/generators/syft.py index f8d7a083..93a3dc45 100644 --- a/sbomify_action/_generation/generators/syft.py +++ b/sbomify_action/_generation/generators/syft.py @@ -1,6 +1,7 @@ """Syft generator plugins for filesystem and Docker image scanning. -Priority: 35 (generic multi-ecosystem, lower than Trivy) +Priority: 35 for lock files (generic multi-ecosystem, lower than Trivy), +25 for Docker images (preferred over cdxgen — see SyftImageGenerator). Syft is a comprehensive SBOM generator that supports version selection. It supports more versions than Trivy but is slightly lower priority. @@ -176,6 +177,13 @@ class SyftImageGenerator: Uses Syft to scan Docker images and generate SBOMs. + Preferred over cdxgen for Docker images: Syft emits the + `operating-system` component and distro PURL qualifiers that + vulnerability scanners (Trivy, Dependency-Track's Trivy analyzer, + Grype) need to detect OS packages. cdxgen image SBOMs lack the OS + component, so Trivy reports "Unsupported os" and scans nothing + (see issue #264). + Verified capabilities (Syft 1.38.2): - CycloneDX versions: 1.2, 1.3, 1.4, 1.5, 1.6 (default: 1.6) - SPDX versions: 2.2, 2.3 (default: 2.3) @@ -192,8 +200,9 @@ def command(self) -> str: @property def priority(self) -> int: - # Generic multi-ecosystem, slightly lower priority than Trivy - return 35 + # Preferred image scanner: unlike cdxgen, Syft's image SBOMs are + # consumable by downstream vulnerability scanners (Trivy/Grype) + return 25 @property def supported_formats(self) -> list[FormatVersion]: diff --git a/tests/test_generation_plugin.py b/tests/test_generation_plugin.py index 0c1387f6..a9796485 100644 --- a/tests/test_generation_plugin.py +++ b/tests/test_generation_plugin.py @@ -235,11 +235,13 @@ def test_get_generators_for_docker_image(self): input = GenerationInput(docker_image="alpine:3.18", output_format="cyclonedx") generators = self.registry.get_generators_for(input) - # cyclonedx-py doesn't support Docker images + # cyclonedx-py doesn't support Docker images. + # Syft is preferred for images: its SBOMs carry the operating-system + # component that Trivy/Dependency-Track need (issue #264). self.assertEqual(len(generators), 3) - self.assertEqual(generators[0].name, "cdxgen-image") # Priority 20 + self.assertEqual(generators[0].name, "syft-image") # Priority 25 self.assertEqual(generators[1].name, "trivy-image") # Priority 30 - self.assertEqual(generators[2].name, "syft-image") # Priority 35 + self.assertEqual(generators[2].name, "cdxgen-image") # Priority 40 @patch("sbomify_action._generation.generators.cyclonedx_py._CYCLONEDX_PY_AVAILABLE", True) @@ -642,7 +644,7 @@ def setUp(self): def test_name_and_priority(self): """Test generator name and priority.""" self.assertEqual(self.generator.name, "cdxgen-image") - self.assertEqual(self.generator.priority, 20) + self.assertEqual(self.generator.priority, 40) def test_supports_docker_images(self): """Test support for Docker images.""" @@ -862,7 +864,7 @@ def setUp(self): def test_name_and_priority(self): """Test generator name and priority.""" self.assertEqual(self.generator.name, "syft-image") - self.assertEqual(self.generator.priority, 35) + self.assertEqual(self.generator.priority, 25) def test_supports_docker_images(self): """Test support for Docker images.""" From ff6a3b4d958e5b9268f8c8bb93e2ae08e6afb9a8 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Thu, 16 Jul 2026 12:54:26 +0000 Subject: [PATCH 2/4] Address Copilot's round-1 review on the image generator priorities - Fix the syft.py module docstring: the "slightly lower priority than Trivy" comparison only holds for lock files now; for Docker images Syft ranks higher than both Trivy and cdxgen. Co-Authored-By: Claude Fable 5 --- sbomify_action/_generation/generators/syft.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sbomify_action/_generation/generators/syft.py b/sbomify_action/_generation/generators/syft.py index 93a3dc45..dc7825d3 100644 --- a/sbomify_action/_generation/generators/syft.py +++ b/sbomify_action/_generation/generators/syft.py @@ -4,7 +4,10 @@ 25 for Docker images (preferred over cdxgen — see SyftImageGenerator). Syft is a comprehensive SBOM generator that supports version selection. -It supports more versions than Trivy but is slightly lower priority. +For lock files it supports more spec versions than Trivy but ranks +slightly lower; for Docker images it ranks higher than both Trivy and +cdxgen because its image SBOMs carry the OS metadata downstream +vulnerability scanners need. Verified capabilities (Syft 1.38.2): - CycloneDX versions: 1.2, 1.3, 1.4, 1.5, 1.6 (default: 1.6) From 2b97439f6abeaa02ec1e7e43ca900c9d54311220 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Thu, 16 Jul 2026 13:01:37 +0000 Subject: [PATCH 3/4] Address Copilot's round-2 review on the image generator priorities - Fix the log_errors comment in CdxgenImageGenerator: the registry logs generator failures at INFO; what actually surfaces an all-generators failure is the SBOMGenerationError raised by the pipeline, not an ERROR log from the orchestrator. Co-Authored-By: Claude Fable 5 --- sbomify_action/_generation/generators/cdxgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbomify_action/_generation/generators/cdxgen.py b/sbomify_action/_generation/generators/cdxgen.py index 401b99a3..c873980e 100644 --- a/sbomify_action/_generation/generators/cdxgen.py +++ b/sbomify_action/_generation/generators/cdxgen.py @@ -314,8 +314,8 @@ def generate(self, input: GenerationInput) -> GenerationResult: try: # run_command raises SBOMGenerationError on failure (uses check=True). # log_errors=False: cdxgen image scanning is a fallback behind syft; - # a benign failure here shouldn't spam ERROR — the orchestrator - # surfaces a real ERROR only if every generator fails. + # a benign failure here shouldn't spam ERROR — if every generator + # fails, the pipeline raises SBOMGenerationError and the run fails. # (Docker-image-not-found is handled separately and still logs at # WARNING.) run_command(cmd, "cdxgen", timeout=DEFAULT_TIMEOUT, docker_image=input.docker_image, log_errors=False) From e6b6f87f8fcfa291cc85792ede5c4b20039feff9 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Thu, 16 Jul 2026 13:08:04 +0000 Subject: [PATCH 4/4] Address Copilot's round-3 review on the image generator priorities - Update the generators/__init__.py docstring to the new image generator priorities (syft-image 25, cdxgen-image 40), reordered by priority. Co-Authored-By: Claude Fable 5 --- sbomify_action/_generation/generators/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbomify_action/_generation/generators/__init__.py b/sbomify_action/_generation/generators/__init__.py index c627da29..65cc00c9 100644 --- a/sbomify_action/_generation/generators/__init__.py +++ b/sbomify_action/_generation/generators/__init__.py @@ -4,11 +4,11 @@ - CycloneDXPyGenerator: Native Python CycloneDX generator (priority 10) - CycloneDXCargoGenerator: Native Rust/Cargo CycloneDX generator (priority 10) - CdxgenFsGenerator: cdxgen filesystem scanner (priority 20) -- CdxgenImageGenerator: cdxgen Docker image scanner (priority 20) +- SyftImageGenerator: Syft Docker image scanner (priority 25) - TrivyFsGenerator: Trivy filesystem scanner (priority 30) - TrivyImageGenerator: Trivy Docker image scanner (priority 30) - SyftFsGenerator: Syft filesystem scanner (priority 35) -- SyftImageGenerator: Syft Docker image scanner (priority 35) +- CdxgenImageGenerator: cdxgen Docker image scanner (priority 40) """ from .cdxgen import CdxgenFsGenerator, CdxgenImageGenerator