Skip to content

VEX product purl from pyproject.toml keeps the raw version (pkg:pypi/my-service@1.0.0-RC1) instead of the PEP 440-normalized one that uv.lock, the wheel and SBOMs use (1.0.0rc1) #1589

Open

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

This is the version half of #1547. PR #1577 made the VEX product purl from pyproject.toml use the PEP 503 name, but the version still goes into the purl exactly as written (only trimmed). PEP 440 lets a project spell its version in non-normalized ways: 1.0.0-RC1, v1.2, 01.02.0, 1.0.0+Local.1. Every other tool records the normalized form: uv.lock, the built wheel's METADATA, PyPI and uv's own CycloneDX export. The OpenVEX product therefore names a release that no SBOM contains.

Impact

OpenVEX consumers match products by exact purl. That's the reason given in #1547 and #1577. A document for pkg:pypi/my-service@1.0.0-RC1 matches nothing in an SBOM that lists my-service@1.0.0rc1, so every not_affected statement is silently ignored. Pre-releases (-rc1, -beta.2), a leading v and zero-padded segments are common spellings.

Repro (uv project, vendored mode; standalone vex and embedded scan --vex give the same result)

mkdir demo && cd demo
cat > pyproject.toml <<'EOF'
[project]
name = "My_Service"
version = "1.0.0-RC1"
requires-python = ">=3.9"
dependencies = ["six==1.16.0"]
EOF
uv lock && uv sync --locked
socket-patch scan --mode vendored --yes          # any pypi patch; six 1.16.0 here
socket-patch vex --output vex.json
grep -o '"@id": "pkg:pypi/my-service@[^"]*"' vex.json   # pkg:pypi/my-service@1.0.0-RC1
grep -A1 'name = "my-service"' uv.lock                  # version = "1.0.0rc1"
uv build --wheel && unzip -p dist/*.whl '*/METADATA' | grep ^Version   # Version: 1.0.0rc1
uv export --frozen --format cyclonedx1.5 | grep '"version"'             # "1.0.0rc1" (root component)
pyproject version VEX product (main 15cbde4) uv.lock / wheel / uv SBOM
1.0.0-RC1 pkg:pypi/my-service@1.0.0-RC1 1.0.0rc1
v1.2 pkg:pypi/my-service@v1.2 1.2
01.02.0 pkg:pypi/my-service@01.02.0 1.2.0
1.0.0+Local.1 pkg:pypi/my-service@1.0.0+Local.1 1.0.0+local.1
1.0.post0 (already normalized) pkg:pypi/my-service@1.0.post0 1.0.post0 (OK)

Expected vs actual

OS × version

Cell Result
Linux, uv 0.5.31, vendored (vex + scan --vex) reproduces
Linux, uv 0.13.1, vendored (vex + scan --vex) reproduces
Linux, uv 0.13.1, release v5.0.1 reproduces (there the name is also raw: pkg:pypi/My_Service@1.0.0-RC1, which is #1547)
macOS / Windows not run. The code path is OS-independent (pure string handling).

The product detection isn't uv-specific. Poetry ([tool.poetry] version) and PDM ([tool.pdm]) go through the same function.

First bad: this isn't a regression. It has never been normalized (v5.0.1 and main 15cbde4).

Suspect code

crates/socket-patch-core/src/vex/product.rs:172: parse_pyproject passes version.trim() straight to pypi_purl. The PEP 440 canonical form, i.e. what packaging.version.Version(v) prints (lowercase, -/_/. pre/post/dev separators folded, leading v dropped, release segments' leading zeros dropped, local segment lowercased), is needed there. A version that isn't valid PEP 440 could keep the current verbatim behaviour.

No probe runs: macOS / Windows probe branches are paused (see ledger #310).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions