GitHub Action¶
Listed on the GitHub Marketplace; the source is action.yml at
the root of the repository.
This repository doubles as a GitHub Action. It 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: millsks/pixi-sbom@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"
@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.0.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: millsks/pixi-sbom@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: millsks/pixi-sbom@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, an explicit spec file). 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 |
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) |
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 | |
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).
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: millsks/pixi-sbom@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.
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 throughactions/attest-sbom, single- document mode only; - without it, the documents themselves get a build provenance attestation through
actions/attest-build-provenance(every*.jsonin the output directory in batch mode).
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: millsks/pixi-sbom@v1
with:
attest: "true"
Anyone can then verify a downloaded document against the repository:
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: millsks/pixi-sbom@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.