Repository navigation
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
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
- Expected: the product purl names the release that is actually built and published, as for the name in VEX product purl detected from pyproject.toml keeps the raw project name (pkg:pypi/My_Service) instead of the normalized pypi name #1547: "SBOMs, and the pypi components in the same document, use the PEP 503 name" (Fix VEX pyproject product purl name (#1547) #1577). The pypi subcomponent purls in the same document already carry lock / dist-info versions, which are normalized. CLI_CONTRACT.md documents no exception for the product version.
- Actual: the raw
[project].versionstring goes into the purl.
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).
Activity