Provetry opens in November 2026. The free CRA check, the tools and the guides are available now.

SBOM for Python projects: pip, Poetry and uv under the CRA

7 min read

On this page
  1. Start from a lock file
  2. Generate the SBOM
  3. What the lock file does not show
  4. Pitfalls specific to Python
  5. From SBOM to vulnerabilities
  6. How Provetry helps
  7. Sources

The Cyber Resilience Act asks every manufacturer to keep a software bill of materials “in a commonly used and machine-readable format covering at the very least the top-level dependencies” of the product (Annex I, Part II, point 1). Python makes this easy when the project uses a real lock file, and hard when it does not. It also hides a second layer that lock files never show: the C libraries compiled into the wheels you install.

For the regulation itself, see SBOM for the Cyber Resilience Act.

Start from a lock file

An SBOM must say which version of each component the product contains. A requirements.txt with requests>=2.31 says what you accept, not what you ship. Pin everything, transitive dependencies included:

Tool Lock file Direct dependencies recorded in
uv uv.lock pyproject.toml
Poetry poetry.lock pyproject.toml
Pipenv Pipfile.lock Pipfile
pip-tools requirements.txt from pip-compile the # via comments

A plain requirements.txt written by hand, or by pip freeze, pins versions but cannot tell your direct dependencies from the rest. The CRA asks for “at the very least the top-level dependencies”, so you need to know which they are. pip-compile writes a # via comment under each line, and the other tools above record it.

Generate the SBOM

uv writes CycloneDX directly from its lock file (since version 0.9.11, as a preview feature):

uv export --format cyclonedx1.5 --no-dev --output-file sbom.cdx.json

For Poetry, Pipenv, requirements files or an installed environment, the CycloneDX project’s cyclonedx-py covers each case:

cyclonedx-py poetry --no-dev -o sbom.cdx.json
cyclonedx-py pipenv -o sbom.cdx.json
cyclonedx-py requirements requirements.txt -o sbom.cdx.json
cyclonedx-py environment .venv -o sbom.cdx.json

It writes JSON in CycloneDX 1.6 by default (--sv picks another version). The environment form describes what is installed, which is the most faithful picture of a built image or virtual environment, but only if that environment is the one you ship.

What the lock file does not show

C and Rust code inside wheels. Many popular packages ship compiled wheels that carry native libraries: cryptography links OpenSSL statically, Pillow bundles libjpeg and libpng, NumPy bundles an OpenBLAS build. When OpenSSL publishes a vulnerability, cryptography==44.0.0 in your lock file does not tell a scanner that OpenSSL is inside. The PEP 770 standard, accepted in 2025, gives these libraries a home: a wheel can carry its own SBOM in .dist-info/sboms/, and auditwheel (from version 6.5) and maturin now add one automatically, for the shared libraries they copy into the wheel; libraries linked statically from source, such as OpenSSL in cryptography, may still be missing. Tools that read installed environments are starting to pick them up. Until yours does, list the native components of your largest dependencies by hand, or check which releases of those packages fix vulnerabilities in the libraries they embed.

The interpreter. If you distribute Python itself, inside a container image, an installer, or a PyInstaller or Nuitka bundle, the CPython version is a component of your product and has advisories of its own. It belongs in the SBOM.

Extras and markers. uv.lock and poetry.lock resolve for every platform and every extra. A dependency marked sys_platform == "win32" is not in your Linux build. Export with the extras and groups you actually ship, and keep in mind that a lock-file SBOM may list a little more than the product contains.

Pitfalls specific to Python

  • Import names differ from package names. You install PyYAML and import yaml, install beautifulsoup4 and import bs4. Anything that reasons about which packages your code uses has to account for that.
  • Packages loaded by name. Django loads database backends and apps from strings in settings.py, SQLAlchemy loads drivers from a URL (postgresql+psycopg://), entry points load plugins. A package that is never imported in your code can still run.
  • Build dependencies (setuptools, hatchling, maturin) run while the wheel is built but are not inside it. Keep them in the record as build-time components, not in the product’s list.

From SBOM to vulnerabilities

pip-audit, from the Python Packaging Authority, checks a requirements file or an environment against the PyPA advisory database and OSV. For the CRA, look first at the vulnerabilities exploited in the wild, which can start the reporting clock, and record in a VEX statement why the others do not affect your product when they do not.

How Provetry helps

Provetry reads uv.lock, poetry.lock, Pipfile.lock and pinned requirements.txt files (== lines), from your CI or from the files you upload, takes direct dependencies from pyproject.toml, Pipfile or pip-compile’s comments, and says so when it cannot tell. It writes the SBOM of each release in CycloneDX, and in SPDX on paid plans, and watches every component for new and exploited vulnerabilities, every day on paid plans and every week on Free. Its CI step looks for evidence that a vulnerable package is never imported, counting packages named in settings, URLs and scripts as used. On the Pro and Consultant plans, it goes down to the vulnerable function for PyPI packages. Every such finding is a suggestion with its evidence, which you accept or not. If you ship a container image, its SBOM, uploaded instead of the lock files, also shows the interpreter and the native libraries a scanner can see in it.

Sources