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:

FormatVersionsOutput
CycloneDX1.5, 1.6JSON, XML
SPDX2.3JSON

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:

LockfilePackage manager
bun.lockBun
package-lock.jsonnpm
pnpm-lock.yamlpnpm
go.sumGo modules
poetry.lock / requirements.txtPython (PyPI)
Cargo.lockRust
pom.xmlJava (Maven)

Ways to generate an SBOM

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:

  1. Open the target project and copy its project ID.
  2. Go to Organization settings → API Keys.
  3. Create a new key and set Project scope to that project.
  4. Store the key as BORING_TOOLS_API_KEY in your CI secret store.
  5. Store the project ID as BORING_TOOLS_PROJECT_ID in 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

StatusMeaningFix
400Missing projectId, mismatched body/query projectId, or invalid CycloneDX JSONMake sure BORING_TOOLS_PROJECT_ID is set and points to the same project used in the upload payload.
401API key is missing or invalidStore the one-time API key value as a masked CI secret and pass it as x-api-key.
403API key is valid but not allowed to upload to this projectCreate a new key with SBOM upload access and select the target project in Project scope.
404Project was not found in the API key’s organizationCheck 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:

  1. Open your project
  2. Click Upload SBOM
  3. Select a CycloneDX (JSON or XML) or SPDX (JSON) file
  4. 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:

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:

ColumnDescription
IDCVE ID or OSV ID, links to the CVE detail page
SeverityColor-coded badge: Critical / High / Medium / Low
ComponentThe affected package name and version
CVSSNumeric CVSS score (sortable)
Fixed InVersion where the vulnerability is patched, if known
LatestLatest available version in the package registry (links to registry page)
Recommended VersionSuggested upgrade target that closes known CVEs for this package
EcosystemPackage ecosystem (npm, Go, PyPI, etc.)
AnalysisYour triage status for this CVE (see below)
PublishedDate the CVE was published (sortable)
LinksDirect 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.

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, or major)
  • 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:

  1. Starts from your currently installed version
  2. Looks for the smallest fixed version that is not affected anymore
  3. If no direct fixed marker is usable, checks published npm versions and picks the smallest safe candidate
  4. 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:

ColumnDescription
NamePackage name
VersionInstalled version
TypeComponent type (library, framework, etc.)
SupplierVendor or maintainer if available
PURLPackage 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:

ColumnDescription
TriggerHow the scan was started (Upload, Manual, Scheduled)
UserWho triggered it (for manual scans)
StartedTimestamp when the scan started
CompletedTimestamp when it finished
Statuscompleted / failed (with error message on hover for failures)
FoundTotal vulnerabilities found
NewNewly introduced CVEs compared to the previous scan
ResolvedCVEs 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

StateMeaning
Not setNo decision recorded yet
In triageUnder investigation
ExploitableConfirmed as exploitable in your environment
Not affectedThe vulnerability does not affect your usage
False positiveIncorrectly identified
FixedA 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.

Last updated: July 2, 2026