Skip to content

feat(purl): build package urls from the ecosystem they belong to - #1398

Open
alex-powell wants to merge 2 commits into
OWASP:mainfrom
alex-powell:feat/ecosystem-purls
Open

alex-powell wants to merge 2 commits into
OWASP:mainfrom
alex-powell:feat/ecosystem-purls

Conversation

@alex-powell

@alex-powell alex-powell commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1402
Refs #672

Depends on #1397, review the last commit.

What changed

buildPurl(name, version) still builds an npm package URL, including the %40 encoding of a scoped name. buildEcosystemPurl(ecosystem, name, version) is new. For pypi it lowercases the name and replaces _ with -, and it leaves dots in place, so Django_REST becomes pkg:pypi/django-rest@... and pdfminer.six stays pkg:pypi/pdfminer.six@.... An ecosystem with no rule returns null.

SPDX and CycloneDX call purlForSbom. That returns the npm URL, and null for every other ecosystem. A PyPI URL can be built. It is not written into either document. Existing tests are unchanged. The new cases are in tests/utils/ecosystem-purl.test.ts. No runtime dependency was added.

Done when

  • SPDX and CycloneDX output for an npm project is byte-identical before and after. The CLI on this repo (385 package URLs) and on examples/vscode (1374 package URLs, 172 CycloneDX vulnerabilities) matched the parent commit after removing timestamps, the SPDX document namespace, and the CycloneDX serial number. The npm encoder also matched on 400, 1604, and 1788 lockfile entries, with 0 differences.
  • Tests cover a PyPI name with mixed case and underscores, a scoped npm name, and an unknown ecosystem producing no purl. tests/utils/ecosystem-purl.test.ts expects pkg:pypi/django-rest@3.14.0, pkg:npm/%40babel/core@7.0.0, and null for Maven. The same file expects zope.interface and pdfminer.six to keep their dots, and it expects PyPI and Maven packages to be absent from both SBOM documents.
  • No new runtime dependency. package.json is unchanged.

packageurl-python 0.17.6 and packageurl-js 2.0.1 parsed the PyPI examples above and returned the same type, name, and version. normalize_name in packageurl-python agreed with the emitted PyPI names.

Verified against a production requirements.txt

I took the 20 exact pins from the pinned requirements.txt of the production Python backend I mentioned in #672 and ran them through buildEcosystemPurl("PyPI", ...) and purlForSbom from this commit (2904c36).

check result
pins 20
packageurl-python parse and re-serialise unchanged 20 of 20
OSV query by purl returns the same advisories as by ecosystem and name 20 of 20
purlForSbom returns null (kept out of SPDX and CycloneDX) 20 of 20

The one name the rule changes is pdfminer.six, which is emitted as pkg:pypi/pdfminer.six@20260107 with its dot kept, and OSV resolves that purl to the same result as the ecosystem query.

compareVersions, looksLikeVersion, isPreReleaseVersion, and
normalizeAdvisoryVersion keep their names. Passing a PyPI ecosystem
selects PEP 440. Other callers stay on semver, including sort and
filter callbacks that pass an index.

A PyPI advisory whose only fix is 2026.3.post1 is reported.
scripts/check-pep440.ts judges a pinned requirements file against OSV
and compares a PyPI sample with packaging.
@cursor
cursor Bot force-pushed the feat/ecosystem-purls branch from 7b7a78e to c4823e9 Compare October 25, 2026 03:23
An npm URL is still buildPurl. buildEcosystemPurl adds PyPI, where the
name is lowercased and underscores become hyphens, and returns null when
the ecosystem has no rule. SPDX and CycloneDX publish only npm.
@cursor
cursor Bot force-pushed the feat/ecosystem-purls branch from c4823e9 to 2904c36 Compare October 25, 2026 03:26

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sbom): build package URLs per ecosystem instead of always pkg:npm

1 participant