Skip to content

GitHub Action

Published as aesir-oss/pixi-sbom-action, from actions/pixi-sbom in this repository. uses: millsks/pixi-sbom@v1, the action's original home, keeps working.

The action downloads the pinned release binary for the runner (verifying the checksum), runs it, and uploads the documents as a workflow artifact; pixi itself is not needed, and neither is pixi install, since the lockfile is the only input:

- uses: actions/checkout@v4
- uses: aesir-oss/pixi-sbom-action@v1
  with:
    all-environments: "true"
    fetch-licenses: "true"
    pypi-mapping: prefix
    primary-purl: pypi
    deny-license: "GPL-3.0-only AGPL-3.0-only"
    require-license: "true"

primary-purl: pypi is what lets a scanner match conda-installed Python packages; see Scanning with Grype.

@v1 follows the newest 1.x.y release: each release moves the tag once its binaries are published, and the action installs the binary of the release the tag points at. For a build that never changes underneath you, pin an exact release (@v1.12.0) or a commit sha and let Dependabot bump it.

Without pixi: uv, Poetry, PDM, pylock and venvs

Nothing about the action needs pixi, so a project that does not use it needs nothing else on the runner. lockfile, scan, prefix and from-sbom say what to describe; give at most one (two at once fail the step).

- uses: actions/checkout@v4
# A uv project: the lockfile is found by the upward search, or named.
- uses: aesir-oss/pixi-sbom-action@v1
  with:
    lockfile: uv.lock
    fetch-licenses: "true"
    vulnerabilities: osv
# What is actually installed: the venv this job built, checked against the lock.
- run: uv sync --frozen
- uses: aesir-oss/pixi-sbom-action@v1
  with:
    prefix: .venv
    output: sbom-installed
    artifact-name: sbom-installed
    diff-against: uv.lock
    fail-on-diff: "true"

The second step documents the environment the job built and fails if it drifted from uv.lock (see Does this venv still match its lock?). CI runs both shapes, on a uv project and a venv, on a runner with no pixi installed.

Input Default Meaning
version the action's own tag (@v1: the newest 1.x.y), else the latest release pixi-sbom version to run
lockfile upward search The lockfile to describe: any kind pixi-sbom reads (pixi.lock, uv.lock, pylock.toml, poetry.lock, pdm.lock, conda-lock.yml, package-lock.json, pnpm-lock.yaml, yarn.lock, an explicit spec file). A JavaScript lockfile beside a conda or Python one joins its document; extra-args: --skip-input javascript leaves it out. The action downloads its own binary, so pixi is not needed on the runner
format, spec-version, environment, platform, all-environments, all-platforms as the CLI Selection and format, see the options above
scan Describe every project under this directory, one document per directory with a lockfile of any supported kind, under output at the same relative path
prefix Describe an installed environment instead: a conda or pixi environment, a venv, or a Python installation
from-sbom Read an existing CycloneDX or SPDX document instead, and write it again with the other inputs applied
config Configuration file; the pyproject.toml table or pixi-sbom.toml next to the lockfile are read by default, none reads nothing. format is always passed and wins over the file; the other inputs are passed only when set
output sboms Output file (*.json, or - for the log) or directory; a single document lands in the directory as sbom.cdx.json / sbom.spdx.json / sbom.github.json
fetch-licenses, license-texts, embedded-sboms, pypi-mapping, primary-purl as the CLI Enrichment
allow-license, deny-license, require-license License policy; whitespace-separated lists
ignore-license Packages the policy does not apply to, one per line: PACKAGE or PACKAGE:justification
scorecard, scorecard-min, fail-on-scorecard false, 5, With fetch-licenses: record each repository's OpenSSF Scorecard, and fail the step (exit code 9) below the given score
fail-on-yanked false With fetch-licenses, fail the step on a yanked release (exit code 7)
fail-on-policy true Fail the step on a policy violation; with false it becomes a warning and the policy-violated output is true
vulnerabilities, kev osv looks findings up and records them; kev: "true" marks the known-exploited ones
fail-on-severity, fail-on-kev The vulnerability gate (exit code 4)
ignore-vuln Accepted findings, one per line: ID, ID:text or ID:state[:justification][:response,...][:text], as for --ignore-vuln
fail-on-vulnerabilities true Fail the step when the gate trips; with false it becomes a warning and the vulnerabilities-found output is true
vex, vex-open Also write a standalone CycloneDX VEX to that path, and the state it gives findings nobody assessed (in-triage, exploitable). Needs format: cyclonedx.
upload-sarif, sarif-category false, pixi-sbom Write the findings as SARIF and upload them to GitHub code scanning (see below)
dependency-submission false Also write a dependency submission snapshot for each document and submit it to the dependency graph, for Dependabot alerts (needs contents: write; see below)
diff-against Also compare the environment with the document at this path and put the comparison in the job summary; the file has to be there already (the action fetches nothing)
fail-on-diff With diff-against: true fails the step (exit code 6) on any change, or name the sections โ€” added removed version license
attest, attest-subject false, Sign the documents with a GitHub artifact attestation (see below)
extra-args Any other CLI arguments, split on whitespace: the flags without an input of their own, such as --epss, --fail-on-epss, --vex-in, --min-quality and --merge (see below)
upload-artifact, artifact-name true, sboms Artifact upload

Outputs: version, output, document (the file, in single-document mode), policy-violated, vulnerabilities-found, diff-changed, sarif, attestation-url. The action runs on Linux (x64, arm64), macOS (Intel, Apple Silicon) and Windows runners, and describes any platform in the lockfile regardless of the runner (platform: linux-64 on a macOS runner is fine).

Flags without an input

Newer flags reach the action through extra-args. The gates behave as they do on the command line: a tripped --fail-on-epss is the vulnerability gate (exit code 4, so fail-on-vulnerabilities: "false" turns it into a warning like the others), and --min-quality fails the step with exit code 10. Paths given this way must not contain spaces, because the value is split on whitespace.

- uses: aesir-oss/pixi-sbom-action@v1
  with:
    from-sbom: vendor/sbom.cdx.json
    vulnerabilities: osv
    fail-on-severity: high
    extra-args: >-
      --min-quality 70
      --vex-in vendor/vex.openvex.json
      --epss --fail-on-epss 0.1

The from-sbom input takes one file; to merge several documents into one, give the first there and the rest as --from-sbom <FILE> in extra-args.

Without the action

The binary has no runtime dependencies, so any job can download it from the releases page and run it, or install it with pixi:

- uses: prefix-dev/setup-pixi@v0.8.1
  with:
    run-install: false
- name: Generate SBOMs
  run: |
    pixi global install pixi-sbom
    pixi sbom --all-environments -p linux-64 --output sboms/
- uses: actions/upload-artifact@v4
  with:
    name: sboms
    path: sboms/

Vulnerabilities in the Security tab

With vulnerabilities: osv the document carries the findings; upload-sarif: "true" additionally renders them as SARIF (--report vulnerabilities --report-format sarif, one result per finding and affected package, located at the lockfile, security-severity from the CVSS score or 10 for known-exploited findings, accepted findings as suppressions) and uploads the file with github/codeql-action/upload-sarif, so they appear under Security โ†’ Code scanning with the sarif-category you choose. The job needs security-events: write:

permissions:
  contents: read
  security-events: write
steps:
  - uses: actions/checkout@v4
  - uses: aesir-oss/pixi-sbom-action@v1
    with:
      pypi-mapping: prefix
      vulnerabilities: osv
      kev: "true"
      fail-on-severity: high
      fail-on-kev: "true"
      ignore-vuln: |
        GHSA-2xpw-w6gg-jr37:streaming API is not used
        CVE-2023-43804:false_positive:only reachable through a removed code path
      upload-sarif: "true"

Each document is a SARIF run with its own automation id, so code scanning files the alerts under pixi-sbom/<environment> (or pixi-sbom/<environment>/<platform> in batch mode) rather than under sarif-category. The SARIF report reuses the lookup's cache, so the second run costs no network requests. Outside the action, the same file comes from pixi sbom --vulnerabilities osv --report vulnerabilities --report-format sarif > findings.sarif.

Dependabot alerts

dependency-submission: "true" writes a GitHub dependency submission snapshot for each document the step describes and submits it, so the environment's packages appear in the repository's dependency graph (Insights โ†’ Dependency graph) and Dependabot raises alerts for them. Conda packages with a PyPI identity are submitted by it, so pypi-mapping: prefix gives more of them one. The submission needs contents: write:

permissions:
  contents: write
steps:
  - uses: actions/checkout@v4
  - uses: aesir-oss/pixi-sbom-action@v1
    with:
      pypi-mapping: prefix
      dependency-submission: "true"

Each environment and platform is submitted under its own correlator, so a run with all-environments keeps every environment in the graph. This repository submits its own environments this way, on every push to main that changes the lockfile or the action: dependency-graph.yml. The submission is a second run of the same inputs with --format github; the document, gates and artifact of the first are unchanged.

Signed SBOMs

An SBOM published from CI is only as trustworthy as its provenance. attest: "true" signs the documents with a GitHub artifact attestation (Sigstore, bound to the workflow run, listed on the repository's Attestations page) and needs id-token: write and attestations: write:

  • with attest-subject, the path of the artifact the SBOM describes (a wheel, a container image tarball, an archive), the SBOM is attached to that artifact as an SBOM attestation through actions/attest-sbom, single- document mode only;
  • without it, the documents themselves get a build provenance attestation through actions/attest-build-provenance (every *.json in the output directory in batch mode).
permissions:
  contents: read
  id-token: write
  attestations: write
steps:
  - uses: actions/checkout@v4
  - uses: aesir-oss/pixi-sbom-action@v1
    with:
      attest: "true"

Anyone can then verify a downloaded document against the repository:

gh attestation verify sbom.cdx.json --repo <owner>/<repo>

Pull requests from forks have no id-token, so attest on pushes only (attest: ${{ github.event_name == 'push' && 'true' || 'false' }}), as this repository's own workflow does.

A worked example

This repository dogfoods its own action in .github/workflows/ci.yml: every environment of its lockfile is described with licenses fetched, PyPI identities from the conda-forge mapping and embedded SBOMs attached, a deny list that is known to trip (Python-2.0) proves the policy-violated output with fail-on-policy: "false", and a second step with a satisfiable policy proves exit 0:

- uses: aesir-oss/pixi-sbom-action@v1
  id: sbom
  with:
    all-environments: "true"
    fetch-licenses: "true"
    pypi-mapping: prefix
    embedded-sboms: "true"
    deny-license: "Python-2.0"
    require-license: "true"
    fail-on-policy: "false"
- run: test "${{ steps.sbom.outputs.policy-violated }}" = "true"

Every input maps to a command-line option of the same name; the action adds nothing the binary cannot do.