Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ WORKDIR /tmp
# Define tool versions
ENV BOMCTL_VERSION=0.4.3 \
TRIVY_VERSION=0.67.2 \
SYFT_VERSION=1.39.0
SYFT_VERSION=1.39.0 \
CARGO_CYCLONEDX_VERSION=0.5.7

RUN apt-get update && \
apt-get install -y curl unzip
Expand Down Expand Up @@ -64,6 +65,29 @@ WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

# cargo-cyclonedx builder stage
# Downloads pre-built binary for amd64, compiles from source for arm64
FROM rust:1-slim AS rust-builder

ARG TARGETARCH
ARG CARGO_CYCLONEDX_VERSION=0.5.7

RUN apt-get update && apt-get install -y curl xz-utils && \
if [ "${TARGETARCH}" = "amd64" ]; then \
curl -sL \
-o cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz \
"https://github.com/CycloneDX/cyclonedx-rust-cargo/releases/download/cargo-cyclonedx-${CARGO_CYCLONEDX_VERSION}/cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz" && \
curl -sL \
-o cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz.sha256 \
"https://github.com/CycloneDX/cyclonedx-rust-cargo/releases/download/cargo-cyclonedx-${CARGO_CYCLONEDX_VERSION}/cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz.sha256" && \
sha256sum -c cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz.sha256 && \
tar xvf cargo-cyclonedx-x86_64-unknown-linux-gnu.tar.xz && \
mv cargo-cyclonedx-x86_64-unknown-linux-gnu/cargo-cyclonedx /usr/local/cargo/bin/ && \
chmod +x /usr/local/cargo/bin/cargo-cyclonedx; \
else \
cargo install cargo-cyclonedx@${CARGO_CYCLONEDX_VERSION}; \
fi

# Python builder stage
FROM python:3.13-slim-trixie AS builder

Expand Down Expand Up @@ -127,6 +151,8 @@ LABEL com.sbomify.maintainer="sbomify <hello@sbomify.com>" \
COPY --from=fetcher /usr/local/bin/trivy /usr/local/bin/
COPY --from=fetcher /usr/local/bin/bomctl /usr/local/bin/
COPY --from=fetcher /usr/local/bin/syft /usr/local/bin/
# cargo-cyclonedx: pre-built for amd64, compiled for arm64
COPY --from=rust-builder /usr/local/cargo/bin/cargo-cyclonedx /usr/local/bin/
COPY --from=node-fetcher /usr/local/bin/bun /usr/local/bin/
COPY --from=node-fetcher /app/node_modules /app/node_modules
COPY --from=builder /opt/venv /opt/venv
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,17 +571,19 @@ Generators are tried in priority order. Native tools (optimized for specific eco
| 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** | Python, JavaScript, Java/Gradle, Go, Rust, Ruby, C++, PHP, .NET, Docker images | 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 |

### How It Works

1. **Python lockfiles** → cyclonedx-py (native, most accurate for Python)
2. **Java lockfiles** (pom.xml, build.gradle, gradle.lockfile) → cdxgen (best Java support)
3. **Dart lockfiles** (pubspec.lock) → cdxgen or Syft (Trivy doesn't support Dart)
4. **Other lockfiles** (Cargo.lock, package-lock.json, go.mod, etc.) → cdxgen (then Trivy, then Syft as fallbacks)
5. **Docker images** → cdxgen (then Trivy, then Syft as fallbacks)
2. **Rust lockfiles** (Cargo.lock) → cargo-cyclonedx (native, most accurate for Rust)
3. **Java lockfiles** (pom.xml, build.gradle, gradle.lockfile) → cdxgen (best Java support)
4. **Dart lockfiles** (pubspec.lock) → cdxgen or Syft (Trivy doesn't support Dart)
5. **Other lockfiles** (package-lock.json, go.mod, etc.) → cdxgen (then Trivy, then Syft as fallbacks)
6. **Docker images** → cdxgen (then Trivy, then Syft as fallbacks)

If the primary generator fails or doesn't support the input, the next one in priority order is tried automatically.

Expand Down
5 changes: 5 additions & 0 deletions sbomify_action/_generation/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .generators import (
CdxgenFsGenerator,
CdxgenImageGenerator,
CycloneDXCargoGenerator,
CycloneDXPyGenerator,
SyftFsGenerator,
SyftImageGenerator,
Expand All @@ -28,6 +29,9 @@ def create_default_registry() -> GeneratorRegistry:
- CycloneDXPyGenerator: Native Python CycloneDX generator
- Input: Python lock files only (requirements.txt, poetry.lock, Pipfile.lock, pyproject.toml)
- Output: CycloneDX 1.0-1.7
- CycloneDXCargoGenerator: Native Rust/Cargo CycloneDX generator
- Input: Rust lock files only (Cargo.lock)
- Output: CycloneDX 1.4-1.6

Priority 20 - Comprehensive Multi-Ecosystem (cdxgen):
- CdxgenFsGenerator: Filesystem/lock file scanning
Expand Down Expand Up @@ -66,6 +70,7 @@ def create_default_registry() -> GeneratorRegistry:

# Priority 10: Native generators
registry.register(CycloneDXPyGenerator())
registry.register(CycloneDXCargoGenerator())

# Priority 20: cdxgen generators (comprehensive multi-ecosystem)
registry.register(CdxgenFsGenerator())
Expand Down
3 changes: 3 additions & 0 deletions sbomify_action/_generation/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This module contains all generator plugins:
- 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)
- TrivyFsGenerator: Trivy filesystem scanner (priority 30)
Expand All @@ -11,12 +12,14 @@
"""

from .cdxgen import CdxgenFsGenerator, CdxgenImageGenerator
from .cyclonedx_cargo import CycloneDXCargoGenerator
from .cyclonedx_py import CycloneDXPyGenerator
from .syft import SyftFsGenerator, SyftImageGenerator
from .trivy import TrivyFsGenerator, TrivyImageGenerator

__all__ = [
"CycloneDXPyGenerator",
"CycloneDXCargoGenerator",
"CdxgenFsGenerator",
"CdxgenImageGenerator",
"TrivyFsGenerator",
Expand Down
157 changes: 157 additions & 0 deletions sbomify_action/_generation/generators/cyclonedx_cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""CycloneDX Cargo generator plugin for Rust projects.

This is the native/authoritative generator for Rust/Cargo packages.
Priority: 10 (native)

Supported inputs:
- Cargo.lock

Supported outputs:
- CycloneDX 1.4-1.6 (via --spec-version)
"""

from pathlib import Path

from sbomify_action.exceptions import SBOMGenerationError
from sbomify_action.logging_config import logger

from ..protocol import (
CARGO_CYCLONEDX_DEFAULT,
CARGO_CYCLONEDX_VERSIONS,
FormatVersion,
GenerationInput,
)
from ..result import GenerationResult
from ..utils import log_command_error, run_command


class CycloneDXCargoGenerator:
"""
Native CycloneDX generator for Rust Cargo projects.

Uses cargo-cyclonedx to generate CycloneDX SBOMs from Cargo.lock
files. This is the authoritative generator for Rust packages
and should be preferred over generic tools.

Verified capabilities (cargo-cyclonedx 0.5.7):
- CycloneDX versions: 1.4, 1.5, 1.6
- Default version: 1.6
- Version selection: --spec-version flag
"""

@property
def name(self) -> str:
return "cyclonedx-cargo"

@property
def priority(self) -> int:
# Native/authoritative for Rust
return 10

@property
def supported_formats(self) -> list[FormatVersion]:
return [
FormatVersion(
format="cyclonedx",
versions=CARGO_CYCLONEDX_VERSIONS,
default_version=CARGO_CYCLONEDX_DEFAULT,
)
]

def supports(self, input: GenerationInput) -> bool:
"""
Check if this generator supports the given input.

Supports Cargo.lock files when requesting CycloneDX format.
Does not support Docker images or SPDX format.
"""
# Only supports lock files, not Docker images
if not input.is_lock_file:
return False

# Only supports CycloneDX format
if input.output_format != "cyclonedx":
return False

# Only supports Cargo.lock
if input.lock_file_name != "Cargo.lock":
return False

# Check version if specified
if input.spec_version:
if input.spec_version not in CARGO_CYCLONEDX_VERSIONS:
return False

return True

def generate(self, input: GenerationInput) -> GenerationResult:
"""Generate a CycloneDX SBOM using cargo-cyclonedx."""
spec_version = input.spec_version or CARGO_CYCLONEDX_DEFAULT

# Validate version
if spec_version not in CARGO_CYCLONEDX_VERSIONS:
return GenerationResult.failure_result(
error_message=f"Unsupported CycloneDX version: {spec_version}. "
f"Supported: {', '.join(CARGO_CYCLONEDX_VERSIONS)}",
sbom_format="cyclonedx",
spec_version=spec_version,
generator_name=self.name,
)

try:
return self._generate(input, spec_version)
except SBOMGenerationError as e:
return GenerationResult.failure_result(
error_message=str(e),
sbom_format="cyclonedx",
spec_version=spec_version,
generator_name=self.name,
)

def _generate(self, input: GenerationInput, spec_version: str) -> GenerationResult:
"""Generate SBOM for Cargo.lock."""
# cargo-cyclonedx needs to run from the project directory containing Cargo.lock
lock_file_path = Path(input.lock_file)
project_dir = lock_file_path.parent.resolve()

# Convert output file to absolute path since we're changing cwd
output_file_abs = str(Path(input.output_file).resolve())

cmd = [
"cargo-cyclonedx",
"cyclonedx",
"--spec-version",
spec_version,
"--format",
"json",
"--output-file",
output_file_abs,
]

logger.info(f"Running cargo-cyclonedx for {input.lock_file_name} (cyclonedx {spec_version})")
result = run_command(cmd, "cargo-cyclonedx", timeout=300, cwd=str(project_dir))

if result.returncode == 0:
# Verify output file was created
if not Path(output_file_abs).exists():
return GenerationResult.failure_result(
error_message="cargo-cyclonedx completed but output file not created",
sbom_format="cyclonedx",
spec_version=spec_version,
generator_name=self.name,
)

return GenerationResult.success_result(
output_file=output_file_abs,
sbom_format="cyclonedx",
spec_version=spec_version,
generator_name=self.name,
)
else:
log_command_error("cargo-cyclonedx", result.stderr)
return GenerationResult.failure_result(
error_message=f"cargo-cyclonedx failed with return code {result.returncode}",
sbom_format="cyclonedx",
spec_version=spec_version,
generator_name=self.name,
)
4 changes: 4 additions & 0 deletions sbomify_action/_generation/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
CDXGEN_CYCLONEDX_VERSIONS = ("1.4", "1.5", "1.6", "1.7")
CDXGEN_CYCLONEDX_DEFAULT = "1.6"

# cargo-cyclonedx (native Rust generator) - CycloneDX only
CARGO_CYCLONEDX_VERSIONS = ("1.4", "1.5", "1.6")
CARGO_CYCLONEDX_DEFAULT = "1.6"


@dataclass
class FormatVersion:
Expand Down
8 changes: 8 additions & 0 deletions scripts/generate_additional_packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fi
TRIVY_VERSION=$(extract_version "TRIVY" "$DOCKERFILE")
BOMCTL_VERSION=$(extract_version "BOMCTL" "$DOCKERFILE")
SYFT_VERSION=$(extract_version "SYFT" "$DOCKERFILE")
CARGO_CYCLONEDX_VERSION=$(extract_version "CARGO_CYCLONEDX" "$DOCKERFILE")

if [ -z "$TRIVY_VERSION" ]; then
echo "ERROR: Could not extract TRIVY_VERSION from Dockerfile" >&2
Expand All @@ -41,14 +42,21 @@ if [ -z "$SYFT_VERSION" ]; then
exit 1
fi

if [ -z "$CARGO_CYCLONEDX_VERSION" ]; then
echo "ERROR: Could not extract CARGO_CYCLONEDX_VERSION from Dockerfile" >&2
exit 1
fi

# Export for sourcing
export TRIVY_VERSION
export BOMCTL_VERSION
export SYFT_VERSION
export CARGO_CYCLONEDX_VERSION

# When executed directly (not sourced), output PURLs
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "pkg:golang/github.com/aquasecurity/trivy@v${TRIVY_VERSION}"
echo "pkg:golang/github.com/bomctl/bomctl@v${BOMCTL_VERSION}"
echo "pkg:golang/github.com/anchore/syft@v${SYFT_VERSION}"
echo "pkg:cargo/cargo-cyclonedx@${CARGO_CYCLONEDX_VERSION}"
fi
Loading