← Back to DevBytes

SBOM Generation: Complete Configuration Guide

Introduction to SBOM Generation

A Software Bill of Materials (SBOM) is a formal, machine-readable inventory of software components and dependencies, their versions, and their supply chain relationships. Think of it as a nutritional label for your software — it tells you exactly what ingredients went into building your application. SBOMs have become a cornerstone of modern software security practices, driven by regulatory requirements like the U.S. Executive Order 14028 and frameworks such as SLSA and NIST SSDF.

In this guide, we'll walk through everything you need to know about generating SBOMs: what they are, why they matter, the leading formats and tools, and how to configure them across different ecosystems. By the end, you'll have a working pipeline that produces SBOMs automatically as part of your build process.

What Is an SBOM?

An SBOM is a structured document that enumerates every component in a piece of software. This includes direct dependencies, transitive dependencies, build tools, and metadata about each component such as:

SBOMs are typically generated at build time and stored alongside artifacts. They can be produced at different stages of the software lifecycle: design, build, and post-build analysis. The most valuable SBOMs are generated automatically from the build environment, ensuring accuracy and completeness.

Why SBOMs Matter

The software supply chain has become a primary attack vector. Incidents like the SolarWinds breach, Log4Shell, and the npm color-theft incident demonstrated that organizations often don't know what's inside their applications. SBOMs address this in several concrete ways:

SBOM Formats

Two formats dominate the SBOM landscape, and both are recognized by NTIA and NIST:

CycloneDX

Created by the OWASP Foundation, CycloneDX is a lightweight specification designed for application security and supply chain analysis. It supports JSON and XML serialization and is particularly strong in build-time integration scenarios.

SPDX

The Software Package Data Exchange (SPDX) is a Linux Foundation project. It's widely used for license compliance and has strong adoption in enterprise and open-source governance contexts. SPDX supports JSON, YAML, RDF, and tag-value formats.

For most development teams, CycloneDX is the easier starting point because of its tooling integration, but both formats are valid. Many tools can convert between them.

Choosing an SBOM Generation Tool

Several mature tools can generate SBOMs. The right choice depends on your ecosystem and workflow:

In this guide, we'll focus on Syft and CycloneDX native plugins because they cover the widest range of use cases.

Installing Syft

Syft is the fastest way to get started with SBOM generation because it works across ecosystems without per-project configuration. Install it on macOS, Linux, or Windows:

# macOS
brew install syft

# Linux (via the install script)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Verify installation
syft version

Once installed, Syft can analyze a directory, a container image, or an archive. Let's generate our first SBOM from a local project directory:

syft dir:. -o cyclonedx-json --file sbom.json

This command scans the current directory, infers the package ecosystems present, and writes a CycloneDX JSON file named sbom.json. The output includes every dependency Syft can detect from lockfiles, manifests, and installed packages.

Generating an SBOM from a Container Image

Container images are one of the most common SBOM targets because they represent the actual deployable artifact. Syft makes this straightforward:

# Generate SBOM from a local Docker image
syft myapp:latest -o cyclonedx-json --file sbom-image.json

# Generate SBOM from an image in a remote registry
syft registry:ghcr.io/myorg/myapp:1.2.3 -o spdx-json --file sbom-spdx.json

The resulting SBOM captures the OS packages, language-level dependencies, and any other detectable components inside the image. This is invaluable for runtime vulnerability analysis.

Configuring Syft with a Configuration File

For reproducible SBOM generation across teams, use a configuration file. Syft supports YAML configuration that can be checked into your repository:

# syft.yaml
output:
  - format: cyclonedx-json
    file: sbom.json

# Scope of analysis
scope: squashed

# Package types to include (empty = all)
package:
  - type: npm
  - type: python
  - type: deb
  - type: apk

# Exclude paths from analysis
exclude:
  paths:
    - "**/node_modules/**"
    - "**/.git/**"
    - "**/vendor/**"

# Include files in the SBOM
file:
  metadata:
    - digests:
        - sha256

Run Syft with the configuration file:

syft dir:. --config syft.yaml

Generating SBOMs in a Node.js Project

For Node.js projects, the official CycloneDX plugin integrates directly into your build. Install it as a development dependency:

npm install --save-dev @cyclonedx/cyclonedx-npm

Add an npm script to generate the SBOM:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "scripts": {
    "sbom": "cyclonedx-npm --output-format json --output-file sbom.json"
  },
  "devDependencies": {
    "@cyclonedx/cyclonedx-npm": "^1.0.0"
  }
}

Generate the SBOM by running:

npm run sbom

This approach reads from your package-lock.json, ensuring the SBOM reflects the exact resolved dependency tree rather than what's declared in package.json alone.

Generating SBOMs in a Java/Maven Project

The cyclonedx-maven-plugin is the standard way to produce SBOMs in Maven builds. Add it to your pom.xml:

<build>
  <plugins>
    <plugin>
      <groupId>org.cyclonedx</groupId>
      <artifactId>cyclonedx-maven-plugin</artifactId>
      <version>2.7.11</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>makeAggregateBom</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <projectType>application</projectType>
        <schemaVersion>1.5</schemaVersion>
        <outputFormat>json</outputFormat>
        <outputName>sbom</outputName>
        <includeCompileScope>true</includeCompileScope>
        <includeProvidedScope>true</includeProvidedScope>
        <includeRuntimeScope>true</includeRuntimeScope>
        <includeTestScope>false</includeTestScope>
      </configuration>
    </plugin>
  </plugins>
</build>

Run the build to generate the SBOM:

mvn package
# The SBOM will be at target/sbom.json

The makeAggregateBom goal produces a single SBOM that includes dependencies from all modules in a multi-module project, which is essential for accurate enterprise reporting.

Generating SBOMs in a Python Project

For Python projects, cyclonedx-bom works with requirements.txt, poetry.lock, or Pipfile.lock. Install it in your environment:

pip install cyclonedx-bom

Generate an SBOM from a Poetry lockfile:

cyclonedx-py environment --output-format json --output-file sbom.json

Or from a requirements file:

cyclonedx-py requirements --input-file requirements.txt --output-format json --output-file sbom.json

For projects using pip-compile or pinned lockfiles, the SBOM will accurately reflect the resolved versions. For projects without lockfiles, consider generating one first with pip freeze > requirements.lock to ensure reproducibility.

Generating SBOMs in a Go Project

Go has native SBOM support through go version -m metadata embedded in binaries, but for structured SBOM output, use syft or the cyclonedx-gomod tool:

go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest

# Generate SBOM from the current module
cyclonedx-gomod app -json -output sbom.json

For Go binaries, Syft can extract module information directly:

syft myapp-binary -o cyclonedx-json --file sbom.json

Integrating SBOM Generation into CI/CD

Generating SBOMs locally is useful, but the real value comes from automating it in CI/CD. Here's a complete GitHub Actions workflow that builds an application, generates an SBOM, and uploads it as a build artifact:

name: Build and Generate SBOM

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Install Syft
        uses: anchore/sbom-action/download-syft@v0
        with:
          syft-version: "v1.0.0"

      - name: Generate SBOM from source
        run: syft dir:. -o cyclonedx-json --file sbom-source.json

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Generate SBOM from image
        run: syft myapp:${{ github.sha }} -o cyclonedx-json --file sbom-image.json

      - name: Upload SBOM artifacts
        uses: actions/upload-artifact@v4
        with:
          name: sboms
          path: |
            sbom-source.json
            sbom-image.json
          retention-days: 90

      - name: Scan SBOM for vulnerabilities
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
          grype sbom:sbom-image.json --fail-on high

This workflow produces two SBOMs: one from the source tree and one from the built container image. The image SBOM is typically more complete because it includes OS-level packages. The final step scans the SBOM with Grype and fails the build if high-severity vulnerabilities are found.

GitLab CI Configuration

For teams using GitLab, here's an equivalent pipeline configuration:

stages:
  - build
  - sbom
  - scan

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

generate-sbom:
  stage: sbom
  image: anchore/syft:latest
  script:
    - syft dir:. -o cyclonedx-json --file sbom.json
  artifacts:
    reports:
      cyclonedx: sbom.json
    paths:
      - sbom.json
    expire_in: 90 days

vulnerability-scan:
  stage: scan
  image: anchore/grype:latest
  script:
    - grype sbom:sbom.json --fail-on high
  needs:
    - generate-sbom

GitLab natively understands CycloneDX reports, so the reports: cyclonedx key makes the SBOM visible in the merge request UI and feeds into GitLab's dependency scanning features.

Validating SBOMs

An SBOM is only useful if it's accurate and well-formed. Always validate generated SBOMs against the official schema. The CycloneDX CLI provides built-in validation:

# Install CycloneDX CLI
curl -sSfL https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-x64 -o /usr/local/bin/cyclonedx
chmod +x /usr/local/bin/cyclonedx

# Validate the SBOM
cyclonedx validate --input-file sbom.json --input-format json --fail-on-errors

For SPDX, use the official SPDX tools:

pip install spdx-tools
python -c "from spdx_tools.spdx.parser.parse_anything import parse_file; parse_file('sbom-spdx.json')"

Validation should be a mandatory step in your CI pipeline. A malformed SBOM can break downstream tools and give false confidence about your supply chain visibility.

Enriching SBOMs with VEX Information

A Vulnerability Exploitability eXchange (VEX) document accompanies an SBOM and states whether a known vulnerability in a component is actually exploitable in your specific context. CycloneDX supports VEX natively. Here's an example of adding VEX statements to a CycloneDX SBOM:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "metadata": {
    "component": {
      "type": "application",
      "name": "myapp",
      "version": "1.0.0"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
    }
  ],
  "vulnerabilities": [
    {
      "id": "CVE-2021-44228",
      "source": {
        "name": "NVD",
        "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228"
      },
      "ratings": [
        {
          "severity": "critical",
          "method": "CVSSv3"
        }
      ],
      "affects": [
        {
          "ref": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
        }
      ],
      "analysis": {
        "state": "not_affected",
        "justification": "requires_environment_configuration",
        "response": ["will_not_fix"],
        "detail": "JNDI lookups are disabled in our deployment configuration."
      }
    }
  ]
}

VEX statements transform an SBOM from a static inventory into an actionable security document. They prevent alert fatigue by clearly documenting why a vulnerability doesn't apply to your use case.

Storing and Distributing SBOMs

Once generated, SBOMs need a storage strategy. Common approaches include:

Here's an example of publishing an SBOM to a GitHub Release:

- name: Upload SBOM to Release
  if: startsWith(github.ref, 'refs/tags/')
  uses: softprops/action-gh-release@v2
  with:
    files: sbom-image.json

Best Practices for SBOM Generation

Generate SBOMs at Build Time

Always generate SBOMs during the build process, not after the fact. Build-time generation captures the exact dependency versions that went into the artifact, including transitive dependencies resolved during the build.

Generate SBOMs from the Final Artifact

Source-level SBOMs are useful, but the most authoritative SBOM comes from the actual deployed artifact — typically a container image or compiled binary. This captures OS packages, build tools, and any dependencies introduced during the build.

Use Lockfiles When Available

Lockfiles (package-lock.json, yarn.lock, poetry.lock, Gemfile.lock) pin exact versions and ensure your SBOM reflects reality rather than declared ranges. Always commit lockfiles to version control.

Automate Everything

Manual SBOM generation is error-prone and quickly falls out of date. Make SBOM generation a non-negotiable step in your CI/CD pipeline, and fail builds if the SBOM can't be produced or validated.

Version Your SBOMs

Each SBOM should be associated with a specific build or release. Include build metadata in the SBOM (commit hash, build timestamp, build tool versions) so you can trace any SBOM back to its exact source state.

Keep SBOMs Accessible

SBOMs are only valuable if they can be found when needed. Publish them alongside releases, make them available to customers on request, and integrate them with your vulnerability management tooling.

Regularly Regenerate SBOMs

Dependencies change over time. Even if your application code hasn't changed, transitive dependencies may have been updated. Regenerate SBOMs on a schedule (e.g., weekly) and whenever dependencies are updated.

Combine SBOMs with Vulnerability Scanning

An SBOM alone doesn't tell you about vulnerabilities — it's the input to a scanning tool. Pair SBOM generation with tools like Grype, Trivy, or OWASP Dependency-Check to continuously monitor for new CVEs affecting your components.

Common Pitfalls to Avoid

Conclusion

SBOM generation is no longer optional for serious software development teams. Regulatory pressure, customer demands, and the escalating threat landscape have made supply chain transparency a baseline requirement. The good news is that the tooling has matured significantly — with Syft, CycloneDX plugins, and CI/CD integration, you can establish automated SBOM generation in a matter of hours. Start by generating an SBOM for your most critical application today, validate it, integrate it into your CI pipeline, and pair it with vulnerability scanning. From there, expand coverage to all your projects and establish the practices outlined in this guide. The investment pays off the moment the next Log4Shell-scale vulnerability is disclosed and you can answer the question "are we affected?" in minutes instead of days.

— Ad —

Google AdSense will appear here after approval

← Back to all articles