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:
- Component name and version
- Supplier or author information
- Unique identifiers (PURL, CPE, SWID)
- License information
- Cryptographic hashes for integrity verification
- Relationships between components (dependencies, contains, describes)
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:
- Vulnerability management: When a new CVE is disclosed, you can quickly query your SBOM to determine whether you're affected and where the vulnerable component is used.
- License compliance: SBOMs surface license obligations before they become legal liabilities.
- Supply chain transparency: Customers and regulators increasingly demand visibility into third-party components.
- Incident response: During a security incident, SBOMs accelerate root-cause analysis and remediation.
- End-of-life tracking: Identify outdated or abandoned dependencies before they become risks.
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:
- Syft: A CLI tool from Anchore that generates SBOMs from container images, filesystems, and source repositories. Supports both CycloneDX and SPDX output.
- CycloneDX CLI: The official OWASP toolchain with plugins for Maven, Gradle, npm, pip, and more.
- Trivy: A vulnerability scanner that also produces SBOMs, useful if you want scanning and SBOM generation in one step.
- cdxgen: A Node.js-based generator that supports many languages and produces CycloneDX output.
- Native plugins: Most ecosystems now have first-party plugins (e.g.,
cyclonedx-maven-plugin,@cyclonedx/webpack-plugin).
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:
- Artifact repositories: Store SBOMs alongside build artifacts in Nexus, Artifactory, or GitHub Releases.
- Dedicated SBOM platforms: Tools like Dependency-Track, Anchore Enterprise, or OWASP DepHub ingest and analyze SBOMs continuously.
- Git repositories: For small projects, committing SBOMs to a dedicated branch or directory works well.
- Object storage: S3 or equivalent with lifecycle policies for long-term retention.
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
- Ignoring transitive dependencies: Some tools only capture direct dependencies. Verify that your SBOM includes the full dependency tree by checking component counts against your lockfile.
- Missing license information: License data is often incomplete in automatically generated SBOMs. Consider enriching SBOMs with license data from external sources.
- Not handling multi-module projects: Use aggregate SBOM generation for monorepos and multi-module builds to avoid fragmented visibility.
- Storing SBOMs without retention policies: SBOMs accumulate quickly. Define retention policies that balance historical analysis needs with storage costs.
- Assuming one SBOM is enough: Different stakeholders need different views. A source SBOM serves developers; an image SBOM serves security teams; a runtime SBOM serves incident responders.
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.