SBOM Generation
What is an SBOM?
A Software Bill of Materials (SBOM) is a complete, machine-readable inventory of all components in a software artifact — direct dependencies, transitive dependencies, and their metadata. Think of it as an ingredient list for your software.
boring.tools generates SBOMs in the following industry-standard formats:
| Format | Versions | Output |
|---|---|---|
| CycloneDX | 1.5, 1.6 | JSON, XML |
| SPDX | 2.3 | JSON |
All outputs are compliant with NTIA minimum elements and EU CRA requirements.
Supported package ecosystems
boring.tools detects the package manager automatically from your project’s lockfile:
| Lockfile | Package manager |
|---|---|
bun.lock | Bun |
package-lock.json | npm |
pnpm-lock.yaml | pnpm |
go.sum | Go modules |
poetry.lock / requirements.txt | Python (PyPI) |
Cargo.lock | Rust |
pom.xml | Java (Maven) |
Ways to generate an SBOM
1. Automatic — via Git integration (recommended)
Connect a Git repository to a project and boring.tools generates an SBOM automatically on every new commit. Scans run approximately every 30 minutes by polling for new commits. You can also trigger a manual scan at any time.
See Git Integration for setup.
2. CI/CD upload — no Git integration required
If you already generate SBOMs in your pipeline, you can upload them to boring.tools instead of connecting a Git provider. This is useful when your repositories are not reachable by boring.tools, when you already have a standardized CI process, or when you want full control over how SBOMs are generated.
Use the public boring-tools/upload-sbom-action GitHub Action to upload an existing CycloneDX JSON SBOM. It does not generate the SBOM itself, so you can pair it with Syft, Trivy, CycloneDX CLI, cdxgen, or any other generator. For a product-focused walkthrough, see Upload SBOMs from GitHub Actions.
Example with Syft:
name: SBOM
on:
push:
branches: [main]
workflow_dispatch:
jobs:
sbom:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Generate CycloneDX SBOM
run: syft dir:. -o cyclonedx-json=sbom.cdx.json
- name: Upload SBOM to boring.tools
uses: boring-tools/upload-sbom-action@v1
with:
api-key: ${{ secrets.BORING_TOOLS_API_KEY }}
project-id: ${{ vars.BORING_TOOLS_PROJECT_ID }}
sbom-file: sbom.cdx.json
Store the API key as a GitHub Actions secret and the project ID as a repository variable. The uploaded SBOM is stored, parsed, and scanned the same way as SBOMs generated by the Git integration.
The action is distributed from the public boring-tools/upload-sbom-action repository. Pin to @v1 for the stable major version, or to a full version tag such as @v1.0.0 when you want an immutable reference. If you want the security background for action pinning, read GitHub Action tags are part of your software supply chain.
For CI/CD pipelines, create a project-scoped API key whenever possible:
- Open the target project and copy its project ID.
- Go to Organization settings → API Keys.
- Create a new key and set Project scope to that project.
- Store the key as
BORING_TOOLS_API_KEYin your CI secret store. - Store the project ID as
BORING_TOOLS_PROJECT_IDin your CI variables.
Project-scoped keys can only upload SBOMs to the selected project. Organization-scoped keys still work, but they should be reserved for trusted automation that needs access to multiple projects.
Generic upload with curl:
export BORING_TOOLS_API_URL="https://api.boring.tools"
export BORING_TOOLS_API_KEY="bt_..."
export BORING_TOOLS_PROJECT_ID="..."
curl --fail-with-body \
-X POST "$BORING_TOOLS_API_URL/v1/sboms?projectId=$BORING_TOOLS_PROJECT_ID" \
-H "Content-Type: application/json" \
-H "x-api-key: $BORING_TOOLS_API_KEY" \
--data-binary @sbom.cdx.json
GitLab CI example:
sbom:
image: alpine:3.21
stage: test
variables:
BORING_TOOLS_API_URL: https://api.boring.tools
before_script:
- apk add --no-cache curl
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
script:
- syft dir:. -o cyclonedx-json=sbom.cdx.json
- |
curl --fail-with-body \
-X POST "$BORING_TOOLS_API_URL/v1/sboms?projectId=$BORING_TOOLS_PROJECT_ID" \
-H "Content-Type: application/json" \
-H "x-api-key: $BORING_TOOLS_API_KEY" \
--data-binary @sbom.cdx.json
Store BORING_TOOLS_API_KEY as a masked CI/CD variable and BORING_TOOLS_PROJECT_ID as a normal project or group variable.
Forgejo Actions example:
name: Upload SBOM
on:
push:
branches: [main]
jobs:
sbom:
runs-on: docker
steps:
- uses: actions/checkout@v4
- name: Install Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b "$PWD/bin"
echo "$PWD/bin" >> "$GITHUB_PATH"
- name: Generate CycloneDX SBOM
run: syft dir:. -o cyclonedx-json=sbom.cdx.json
- name: Upload SBOM to boring.tools
env:
BORING_TOOLS_API_URL: https://api.boring.tools
BORING_TOOLS_API_KEY: ${{ secrets.BORING_TOOLS_API_KEY }}
BORING_TOOLS_PROJECT_ID: ${{ vars.BORING_TOOLS_PROJECT_ID }}
run: |
curl --fail-with-body \
-X POST "$BORING_TOOLS_API_URL/v1/sboms?projectId=$BORING_TOOLS_PROJECT_ID" \
-H "Content-Type: application/json" \
-H "x-api-key: $BORING_TOOLS_API_KEY" \
--data-binary @sbom.cdx.json
Troubleshooting CI/CD uploads
| Status | Meaning | Fix |
|---|---|---|
400 | Missing projectId, mismatched body/query projectId, or invalid CycloneDX JSON | Make sure BORING_TOOLS_PROJECT_ID is set and points to the same project used in the upload payload. |
401 | API key is missing or invalid | Store the one-time API key value as a masked CI secret and pass it as x-api-key. |
403 | API key is valid but not allowed to upload to this project | Create a new key with SBOM upload access and select the target project in Project scope. |
404 | Project was not found in the API key’s organization | Check that the project ID belongs to the same organization as the API key. |
3. Manual upload
Upload an existing SBOM file directly in the UI:
- Open your project
- Click Upload SBOM
- Select a CycloneDX (JSON or XML) or SPDX (JSON) file
- Click Upload
SBOM detail view
Click any SBOM in the project’s SBOM list to open its detail page. The page has three main sections:
Header
Shows the SBOM name, version, format (e.g. CycloneDX 1.6), scan status, upload date, and the time of the last scan. A Re-scan button lets you re-run vulnerability matching at any time.
While a scan is in progress, a live progress bar shows how many packages have been checked (X / Y).
Vulnerability summary
Six stat tiles (Critical, High, Medium, Low, Unknown, Total) show the current vulnerability counts for this specific SBOM.
Vulnerabilities table
The full list of detected CVEs with the following columns:
| Column | Description |
|---|---|
| ID | CVE ID or OSV ID, links to the CVE detail page |
| Severity | Color-coded badge: Critical / High / Medium / Low |
| Component | The affected package name and version |
| CVSS | Numeric CVSS score (sortable) |
| Fixed In | Version where the vulnerability is patched, if known |
| Latest | Latest available version in the package registry (links to registry page) |
| Recommended Version | Suggested upgrade target that closes known CVEs for this package |
| Ecosystem | Package ecosystem (npm, Go, PyPI, etc.) |
| Analysis | Your triage status for this CVE (see below) |
| Published | Date the CVE was published (sortable) |
| Links | Direct links to OSV.dev and NVD/NIST |
You can search across all columns using the search box (fuzzy matching by CVE ID, component name, severity, etc.) and sort by Severity, CVSS, or Published date.
If you have suppressed vulnerabilities, a Show suppressed (N) button appears to toggle their visibility.
Recommended Version (new)
For vulnerable packages, boring.tools now shows a Recommended Version directly in the SBOM vulnerability table and in the vulnerability side panel.
What you get:
- A concrete target version to upgrade to
- Upgrade type (
patch,minor, ormajor) - A copy-ready install command (for npm packages)
- A warning when the recommendation is likely a breaking major upgrade
If the field is empty, run a new Re-scan first. Recommendations are calculated during scan runs, not at page load time.
How the recommendation is calculated
At scan time, boring.tools evaluates the package version in your SBOM against known affected and fixed ranges from vulnerability advisories.
In plain terms, the engine:
- Starts from your currently installed version
- Looks for the smallest fixed version that is not affected anymore
- If no direct fixed marker is usable, checks published npm versions and picks the smallest safe candidate
- Marks whether the upgrade is likely breaking (
major)
The goal is to provide the smallest practical upgrade that closes known vulnerabilities while minimizing change risk.
Current scope and limitations
- In V1, recommendations are available for npm packages
- Recommendations are based on known advisories (they do not cover unknown/zero-day issues)
- If no safe version can be derived from current advisory data, the field remains empty
Components table
A searchable, sortable list of every package in the SBOM:
| Column | Description |
|---|---|
| Name | Package name |
| Version | Installed version |
| Type | Component type (library, framework, etc.) |
| Supplier | Vendor or maintainer if available |
| PURL | Package URL — the unique identifier for the package |
Search by name, version, supplier, or PURL.
Scan history
A paginated table of every vulnerability scan run for this SBOM:
| Column | Description |
|---|---|
| Trigger | How the scan was started (Upload, Manual, Scheduled) |
| User | Who triggered it (for manual scans) |
| Started | Timestamp when the scan started |
| Completed | Timestamp when it finished |
| Status | completed / failed (with error message on hover for failures) |
| Found | Total vulnerabilities found |
| New | Newly introduced CVEs compared to the previous scan |
| Resolved | CVEs no longer present compared to the previous scan |
Vulnerability analysis (triage)
For each vulnerability in an SBOM, you can record a triage decision. Click the + Add link in the Analysis column (or click an existing analysis badge) to open the analysis panel.
Analysis state
| State | Meaning |
|---|---|
| Not set | No decision recorded yet |
| In triage | Under investigation |
| Exploitable | Confirmed as exploitable in your environment |
| Not affected | The vulnerability does not affect your usage |
| False positive | Incorrectly identified |
| Fixed | A fix has been applied |
Justification (for “Not affected”)
When marking a vulnerability as Not affected, you must provide a justification:
- Code not reachable
- Code not present
- Inline mitigations
- Protected at perimeter
- Protected by compiler
- Requires configuration
- Requires dependency
- Requires environment
- Requires other component
- Not in affected subset
Responses
Select one or more response actions that have been taken:
- Can’t fix
- Rollback
- Update
- Will not fix
- Workaround available
Detail / Notes
A free-text field to explain the analysis decision. Supports any notes you want to keep alongside the triage record.
Suppressing a vulnerability
Toggle Suppress vulnerability to hide it from the default view. Suppressed vulnerabilities are excluded from counts and hidden in the table unless you explicitly click Show suppressed. Use this for vulnerabilities you’ve assessed as not actionable in your context.
Click Save Analysis to persist your triage. Click Reset to clear the analysis entirely.