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 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: aesir-oss/pixi-sbom-action@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: 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.