Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5513268
🔧 build: add project tooling and policies
gaborbernat Nov 7, 2026
5c0a18b
✨ feat: add the PEP 770 SBOM build hook
gaborbernat Nov 7, 2026
041efed
👷 ci: add check, release and security workflows
gaborbernat Nov 7, 2026
a692292
📝 docs: pin the first release in the example
gaborbernat Nov 7, 2026
b57cafb
♻️ refactor: split into private modules, add embed
gaborbernat Nov 7, 2026
6b77aea
📝 docs: add the Read the Docs site
gaborbernat Nov 7, 2026
6092cc2
🔧 build: adopt the bump-deps-index tooling
gaborbernat Nov 7, 2026
522b147
🚚 refactor: rename the project to python-sbom
gaborbernat Nov 7, 2026
993f1a7
🐛 fix: hash absolute build scripts on Windows
gaborbernat Nov 7, 2026
1120300
👷 ci: trim the check matrix to 13 jobs
gaborbernat Nov 7, 2026
2c0a899
✅ test: build the bundled seed wheel at test time
gaborbernat Nov 7, 2026
f91c1c9
♻️ refactor: split the core from the hatch backend
gaborbernat Nov 7, 2026
e9efe48
🔒 build: relock for the hatch extra and docs deps
gaborbernat Nov 7, 2026
9f55e4a
♻️ refactor: rebuild around one SBOM pipeline
gaborbernat Nov 7, 2026
c4bc75c
📦 build: embed the project's own SBOM
gaborbernat Nov 7, 2026
2891f8a
📝 docs: document providers and the pipeline
gaborbernat Nov 7, 2026
26b526e
🔧 chore: align repository files with conventions
gaborbernat Nov 7, 2026
c45b6ef
♻️ refactor: break the CLI options import cycle
gaborbernat Nov 7, 2026
af0e85f
🐛 fix: import TypeIs only when type checking
gaborbernat Nov 7, 2026
5238152
♻️ refactor: shrink the API to the provider seam
gaborbernat Nov 7, 2026
ab7d9f0
📝 docs: document providers and the archive command
gaborbernat Nov 7, 2026
d4ebc94
🔧 chore: follow bump-deps-index tooling
gaborbernat Nov 7, 2026
86def47
📝 docs: match the virtualenv example to its patch
gaborbernat Nov 7, 2026
6e4335e
🚧 wip: adopt libraries, cap validation, bench
gaborbernat Nov 7, 2026
764f69a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2026
3bdd22e
♻️ refactor: parse dist-info files with installer
gaborbernat Nov 7, 2026
2d87c0d
🔒 build: lock dep-logic
gaborbernat Nov 7, 2026
0c4aba6
✅ test: pin embed output byte for byte
gaborbernat Nov 7, 2026
95af3d1
⏪ revert: evaluate markers without dep-logic
gaborbernat Nov 7, 2026
ed0130b
⚡ perf: read build-env RECORDs directly
gaborbernat Nov 7, 2026
ff8e0fc
⚡ perf: copy wheel members without recompressing
gaborbernat Nov 7, 2026
d89b69a
📝 docs: say why the CycloneDX model is our own
gaborbernat Nov 7, 2026
f986ea0
♻️ refactor: parse core metadata with packaging
gaborbernat Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .git_archival.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git_archival.txt export-subst
tests/_generate/pinned/** -text
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @gaborbernat
18 changes: 18 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contributing

Add a `docs/changelog.d/+short-name.<type>.rst` fragment for a change that affects package users. Choose a unique short
name; the release workflow links the fragment to its merged squash pull request and credits the human author. A pull
request number is not needed in the filename.

Use `feature` for Added, `change` for Changes, `bugfix` for Fixed, and `doc` for Documentation. Use `breaking`,
`security`, `performance`, `deprecation`, `removal`, or `packaging` only when the change affects users in that way. A
supported Python version added or removed is a `feature` and results in a minor release.

Use `skip news` for internal maintenance without a user-facing change. The label may be added after opening the pull
request. Dependabot and maintenance bot pull requests without a fragment receive it automatically; a bot pull request
with a fragment still needs the news check to pass. For a change written by more than one person, put all human authors
in a `by` credit. Do not list bots or reviewers.

The documentation preview shows fragments under Unreleased. The versioned `docs/changelog.rst` changes only during
release preparation. The release workflow chooses a version from the fragment types, runs `prek` on the assembled
changelog, and publishes GitHub notes from the tagged changelog after PyPI succeeds.
2 changes: 2 additions & 0 deletions .github/FUNDING.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: gaborbernat
thanks_dev: u/gh/gaborbernat
46 changes: 46 additions & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Security policy

python-sbom writes a CycloneDX SBOM into wheels, from a hatchling build hook or from a command that rewrites a wheel
another build backend built, and renders or validates that SBOM. This policy says how to report a security problem and
how we decide whether a report is one.

## Reporting a vulnerability

**Do not open a public issue for a security problem.** Use
[GitHub's private vulnerability reporting](https://github.com/tox-dev/python-sbom/security/advisories/new). If neither
works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com).

Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform
you tested. Write the report yourself; unreviewed tool or model output takes longer to triage.

## What to expect

Volunteers maintain python-sbom, with no on-call rotation. We aim to acknowledge a report within **5 working days** and
assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, ship in a
new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default embargo is
**90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under the name
they choose, or not at all. There is no bug bounty.

## Supported versions

Only the latest release receives fixes. Confirm the problem reproduces on it before reporting.

## Vulnerability or bug

A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists
as untrusted breaks a guarantee python-sbom makes. A report that first needs control of something the threat model
trusts, such as the project source, its `pyproject.toml` or the build environment, is a bug: we fix it in the open and
publish no advisory. Classify by who supplies the dangerous input, not by how bad the outcome sounds.

**In scope**

- A bundled wheel, or an SBOM given to `python-sbom spdx` or `validate`, that makes python-sbom write outside its output
file or the wheel it rewrites, or run code.
- An SBOM that misstates what a wheel holds in a way its inputs cannot explain, such as a wrong hash.

**Out of scope**

- Anything hatchling's own build trusts.
- Vulnerabilities in the packages an SBOM lists; report those to their projects.

If you cannot tell which side a finding falls on, report it privately and we will decide.
78 changes: 78 additions & 0 deletions .github/THREAT_MODEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Threat model

This model says what python-sbom trusts, where untrusted input enters, and which guarantees hold at each boundary, so a
report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the two disagree,
SECURITY.md wins and we fix this file.

## How python-sbom works

The build hook runs inside a hatchling wheel build. It reads the wheel's core metadata, the project's license files, the
wheels the project bundles and the installed distributions of the build environment, and writes a CycloneDX document
that it writes into the built wheel. `python-sbom embed` does the same for a wheel another backend built,
`python-sbom archive` describes another zip archive such as a zipapp, and `python-sbom spdx` and `validate` read
CycloneDX documents and write or check them.

```mermaid
flowchart LR
project[("Project source, pyproject.toml<br/>(trusted)")] --> hook["python-sbom"]
env[("Build environment<br/>(trusted)")] --> hook
bundled[("Bundled wheels<br/>(third-party content)")] -->|"metadata, RECORD, vendor list"| hook
sbom[("SBOM files given to spdx, validate")] -->|"untrusted JSON"| hook
hook --> wheel[("Wheel")]
classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a;
classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00;
class project,env,hook,wheel trusted;
class bundled,sbom boundary;
```

## Trusted

An attacker who controls any of these already has what python-sbom could give them:

- The project source, its `pyproject.toml`, the build hook script, and the module the `content-provider` option or
`--content-provider` names: python-sbom imports that module from the project directory, so `embed` and `archive` run
code from `--root`.
- The git checkout that tracks the project, whose `HEAD` python-sbom reads. A repository that merely contains the
project directory is not trusted: git is used only when it tracks the project, and runs with `core.fsmonitor` off.
- The build environment and the build backend.
- The wheel handed to `python-sbom embed` and the archive handed to `python-sbom archive`, which the project's own build
produced.

## Untrusted inputs

| Input | Comes from | What python-sbom guarantees |
| --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Metadata and `RECORD` of bundled wheels | Upstream projects | Read as text with capped sizes, copied as JSON strings; every member is checked against its `RECORD` row, duplicate members are refused, nothing is executed. |
| `_vendor/vendor.txt` of bundled wheels | Upstream projects | Each line must pin one exact version, or the build fails. |
| SBOM files given to `spdx`, `validate` | Whoever produced the file | Parsed as JSON only, with capped sizes and nesting; problems are reported, not raised; `spdx` writes the one file named on the command line. |

## Threats and controls

| STRIDE | Threat | Control |
| ---------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- |
| Tampering | A bundled wheel's metadata injects JSON into the document. | The document is serialized with `json.dumps`. |
| Tampering | A bundled wheel changes and the SBOM still lists it. | Each bundled wheel is hashed from its bytes at build time. |
| Elevation of privilege | A bundled wheel runs code during the build. | Wheels are read as zip archives; nothing in them is imported. |

## Release chain

Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release
App, not a person, pushes the release commit and tag; the build installs its tools from a hash-checked `pylock.toml`
resolved with a seven-day upload cooldown by default; PyPI receives the files through
[trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable.
Every workflow pins third-party actions to a commit SHA.

## Severity

- **High:** a bundled wheel or SBOM file that makes python-sbom run code or write outside its output.
- **Medium:** an SBOM that misstates a hash, a license or the contents of a wheel.
- **Low:** a crash on a malformed bundled wheel or SBOM file instead of a clean error.

## Accepted risks

- The SBOM reports what the build environment holds; it does not check that environment against a lock.

## Keeping this current

Revisit this model when python-sbom gains a new input, a new way to run code, or a new place it writes to, and whenever
an advisory is published.
30 changes: 30 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: 2
multi-ecosystem-groups:
dependencies:
schedule:
interval: weekly
day: tuesday
labels:
- dependencies
- skip news
updates:
- package-ecosystem: github-actions
directory: /
patterns:
- '*'
multi-ecosystem-group: dependencies
labels:
- dependencies
- skip news
cooldown:
default-days: 7
- package-ecosystem: pre-commit
directory: /
patterns:
- '*'
multi-ecosystem-group: dependencies
labels:
- dependencies
- skip news
cooldown:
default-days: 7
5 changes: 5 additions & 0 deletions .github/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
changelog:
exclude:
authors:
- dependabot[bot]
- pre-commit-ci[bot]
62 changes: 62 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: 🧪 check
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: "27 8 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
name: 🧪 test
if: github.event_name != 'schedule' || github.repository_owner == 'gaborbernat'
uses: tox-dev/github-workflows/.github/workflows/check.yaml@e1361e2efa1e9dd9705bb9be719d923cd9154707 # 6.7.1
permissions:
contents: read
with:
include: |
[
{"env": "3.15", "python": "3.15"},
{"env": "3.14", "python": "3.14"},
{"env": "3.13", "python": "3.13"},
{"env": "3.12", "python": "3.12"},
{"env": "3.11", "python": "3.11"},
{"env": "3.15", "python": "3.15", "os": "windows-2025"},
{"env": "3.11", "python": "3.11", "os": "windows-2025"},
{"env": "3.15", "python": "3.15", "os": "macos-26"},
{"env": "dev"},
{"env": "docs"},
{"env": "fix"},
{"env": "type"},
{"env": "pkg_meta"}
]
release:
name: 🏷️ Release dry run
uses: $/.github/workflows/prepare-release.yaml
permissions:
contents: read
pull-requests: read
with:
dry-run: true
hooks:
name: 🧹 Hooks under the cooldown
uses: tox-dev/github-workflows/.github/workflows/cooled-hooks.yaml@0c4cffd63033d38196319524d101caae1eef925b # 6.7.2
permissions:
contents: read
gate:
name: 🚦 ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Non-PR checks pass' }}
if: ${{ !cancelled() }} # codespell:ignore cancelled
needs: [test, release, hooks]
runs-on: ubuntu-26.04
permissions: {}
steps:
- name: 🚦 Require all needed jobs to pass
uses: tox-dev/github-workflows/required-checks@e1361e2efa1e9dd9705bb9be719d923cd9154707 # 6.7.1
with:
needs: ${{ toJSON(needs) }}
22 changes: 22 additions & 0 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: 🛡️ CodeQL
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
schedule:
- cron: "35 8 * * 2"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
analyze:
name: 🛡️ Analyze
uses: tox-dev/github-workflows/.github/workflows/codeql.yaml@e1361e2efa1e9dd9705bb9be719d923cd9154707 # 6.7.1
permissions:
contents: read
security-events: write
with:
languages: '["actions","python"]'
paths-ignore: '["tests"]'
30 changes: 30 additions & 0 deletions .github/workflows/news.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Changelog
on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
jobs:
changelog:
name: 📝 News fragment
runs-on: ubuntu-26.04
steps:
- name: 📥 Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: 🐍 Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: 📦 Install locked release tools
uses: tox-dev/github-workflows/install-locked-tools@e1361e2efa1e9dd9705bb9be719d923cd9154707 # 6.7.1
with:
groups: release-news
venv: ${{ runner.temp }}/release-tools
- name: 🔍 Validate changelog fragments
uses: tox-dev/github-workflows/validate-news@e1361e2efa1e9dd9705bb9be719d923cd9154707 # 6.7.1
34 changes: 34 additions & 0 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
bump:
description: Version bump strategy
required: true
type: choice
options:
- auto
- major
- minor
- patch
default: auto
workflow_call:
inputs:
dry-run:
description: Assemble the release without pushing, for the pull request gate
type: boolean
required: true
permissions: {}
jobs:
prepare:
name: 🏷️ Prepare Towncrier release
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@0c4cffd63033d38196319524d101caae1eef925b # 6.7.2
permissions:
contents: read
pull-requests: read
with:
bump: ${{ inputs.bump || 'auto' }}
dry-run: ${{ inputs.dry-run == true }}
release-auth-deployment: false
secrets:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
Loading
Loading