Surfaced by the CLI's ADR-0041 adoption (bomly-cli#423) after v0.9.0 stopped blanket-lowercasing versions. Needs a dependency decision, so filing rather than shipping.
What happens
pkg:pypi/requests-toolbelt@1.0.0RC1 and pkg:pypi/requests-toolbelt@1.0.0rc1 mint two identities, so one Python release can appear as two components — two matching results, duplicate vulnerabilities. That is the duplicate-identity problem ADR-0041 exists to remove.
PyPI treats the two as one release: PEP 440 normalizes version case, and both spellings name the same distribution, which is why you cannot upload both.
Why v0.9.0 changed it
The old normVersion lowercased every version containing a letter. That folded this pair by accident and corrupted 1.0-SNAPSHOT into 1.0-snapshot — a different version, since Maven versions are case sensitive. Dropping the blanket rule was correct; it cost the PyPI fold.
Delegating to packageurl-go does not recover it. Its typeAdjustVersion case-folds exactly one type:
func typeAdjustVersion(purlType, version string) string {
switch purlType {
case TypeHuggingface:
return strings.ToLower(version)
}
return version
}
The purl spec normalizes the pypi name (lowercase, _ → -), which we get for free, and says nothing about the version. So this is a rule the identity layer has to apply on top of the library, for pypi coordinates only.
What needs deciding
A dependency. github.com/aquasecurity/go-pep440-version does exactly this and is already an indirect dependency of the CLI (via the grype/syft tree), but the SDK is a contract module every consumer imports and its own guidance requires discussion before adding one. Probed at v0.0.1:
"1.0.0RC1" -> String()="1.0.0rc1"
"1.0.0-rc1" -> String()="1.0.0rc1"
"1.0.0.RC.1" -> String()="1.0.0rc1"
"1.0.0+LOCAL" -> String()="1.0.0+local"
"v1.0.0" -> String()="1.0.0"
"1.0-SNAPSHOT" -> parse error: malformed version
The Maven refusal is worth noting: the normalizer cannot corrupt a non-Python version, because it rejects what it does not understand. A parse failure must leave the version as written rather than dropping the package.
Hand-writing PEP 440 is not an option worth taking — epoch, release, pre/post/dev and local segments are a real grammar, and a transcribed one goes stale silently.
What still needs checking
- Whether normalization applies to every type resolving to the pypi ecosystem or only to
pkg:pypi itself.
- Whether the normalized version should replace the stated one on the coordinates or only key the identity. Everything else in v0.9.0 projects coordinates from the identity verbatim, so replacing is the consistent answer, but it means a node reports a version the manifest did not spell.
- Whether other ecosystems have the same gap (RubyGems, NuGet, and Go all have their own version rules).
- A differential test against the library, so a normalization change upstream fails a test here rather than moving identities silently.
The CLI pins the current behavior in TestPythonVersionCaseIsNotFoldedYet (internal/engine/consolidation/consolidation_test.go), which fails when this lands.
Surfaced by the CLI's ADR-0041 adoption (bomly-cli#423) after v0.9.0 stopped blanket-lowercasing versions. Needs a dependency decision, so filing rather than shipping.
What happens
pkg:pypi/requests-toolbelt@1.0.0RC1andpkg:pypi/requests-toolbelt@1.0.0rc1mint two identities, so one Python release can appear as two components — two matching results, duplicate vulnerabilities. That is the duplicate-identity problem ADR-0041 exists to remove.PyPI treats the two as one release: PEP 440 normalizes version case, and both spellings name the same distribution, which is why you cannot upload both.
Why v0.9.0 changed it
The old
normVersionlowercased every version containing a letter. That folded this pair by accident and corrupted1.0-SNAPSHOTinto1.0-snapshot— a different version, since Maven versions are case sensitive. Dropping the blanket rule was correct; it cost the PyPI fold.Delegating to packageurl-go does not recover it. Its
typeAdjustVersioncase-folds exactly one type:The purl spec normalizes the pypi name (lowercase,
_→-), which we get for free, and says nothing about the version. So this is a rule the identity layer has to apply on top of the library, for pypi coordinates only.What needs deciding
A dependency.
github.com/aquasecurity/go-pep440-versiondoes exactly this and is already an indirect dependency of the CLI (via the grype/syft tree), but the SDK is a contract module every consumer imports and its own guidance requires discussion before adding one. Probed at v0.0.1:The Maven refusal is worth noting: the normalizer cannot corrupt a non-Python version, because it rejects what it does not understand. A parse failure must leave the version as written rather than dropping the package.
Hand-writing PEP 440 is not an option worth taking — epoch, release, pre/post/dev and local segments are a real grammar, and a transcribed one goes stale silently.
What still needs checking
pkg:pypiitself.The CLI pins the current behavior in
TestPythonVersionCaseIsNotFoldedYet(internal/engine/consolidation/consolidation_test.go), which fails when this lands.