Skip to content

feat(cve-json): emit affected[].packageURL for generated CVE records - #1197

Merged
potiuk merged 3 commits into
apache:mainfrom
potiuk:feat/cve-json-packageurl
Sep 10, 2026
Merged

feat(cve-json): emit affected[].packageURL for generated CVE records#1197
potiuk merged 3 commits into
apache:mainfrom
potiuk:feat/cve-json-packageurl

Conversation

@potiuk

@potiuk potiuk commented Sep 10, 2026

Copy link
Copy Markdown
Member

The CVE Record Format defines an optional packageURL on each affected product entry — a Package URL identifying the package on its host. generate-cve-json already emitted collectionURL and packageName but never a purl, so consumers had no machine-matchable package identifier.

Two new optional config keys under [product], both off by default — an adopter who sets neither gets byte-identical records to today:

  • purl_type — the Package URL type (pypi, npm, maven, …). When set, every affected[] entry gains pkg:<type>/<name>.
  • purl_namespace — the namespace for types that require one: a Maven groupId, a Composer vendor, a Go module prefix.

Per the schema the purl never carries a version — the entry's versions[] holds the range.

Per-type rules come from the spec's own type definitions, not from memory. Reading them caught three things this PR would otherwise have got wrong:

  • npm names are case-sensitive. Only new packages must be lowercase; pre-existing mixed-case ones were grandfathered in, so folding case points at a different package.
  • nuget is case-preserving, so it must not be lowercased either.
  • A namespace can contain slashes as separators — Go's github.com/gorilla is two segments, not one with an encoded /.

Supported today: pypi (lowercase, _-), npm (scope becomes the namespace, @ percent-encoded, case preserved), maven, golang, composer (namespace-required), and cargo/gem/nuget (plain pass-through). Any other type passes its name through unchanged rather than applying a guessed normalisation.

Where it emits nothing: a type that requires a namespace and has none, and a name needing namespace semantics the type's rule doesn't model. A guessed Maven groupId is a valid-looking identifier pointing at another organisation's artifact — worse than an absent optional field.

One naming hazard worth flagging for review: the pre-existing compute_package_url() is not a purl — it returns a human-readable project-page URL for the attachment table. The new function is compute_purl(), and both docstrings now say so. Renaming the old one may be worth a follow-up.

24 new tests; 306 pass; ruff/mypy clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv

…ured

The CVE Record Format defines an optional packageURL on each affected
product entry — a Package URL identifying the package on its host. The
generator carried collectionURL and packageName but never emitted one.

Adds an optional product.purl_type config key. When set, every
affected[] entry gains pkg:<type>/<packageName>; when unset the field is
omitted, so existing adopters' records are unchanged. The purl never
carries a version, per the schema — the entry's versions[] holds the
range. A name needing namespace semantics (an npm scope, a Maven group
ID) emits no purl rather than a guessed one.

The new compute_purl() sits beside the pre-existing
compute_package_url(), which despite its name returns a human-readable
project-page URL for the attachment table, not a purl; both docstrings
now say so.

Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv
Checked pypi and npm against the package-url spec's type definitions.
pypi was right. npm was wrong twice: scoped names returned no purl at
all, and names were lowercased.

A scope is a namespace, not a guess — @angular/animation is
pkg:npm/%40angular/animation. And npm names are case-sensitive: only new
packages must be lowercase, pre-existing mixed-case ones were
grandfathered in, so folding case points at a different package.

Also drops golang, composer, hex, github and bitbucket from the
lowercase set. Those were added from memory without reading their rules;
a guessed normalisation corrupts an identifier silently, so unverified
types now pass their name through untouched.

Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv
Maven, Go and Composer purls need a namespace the package name does not
carry: a groupId is separate coordinate metadata, and Go's module/package
split cannot be derived without consulting the module proxy. An optional
product.purl_namespace supplies it, and without it those types still
emit nothing rather than a guess pointing at another organisation's
artifact.

Types the spec gives no namespace (pypi, cargo, gem, nuget) ignore the
key, and an npm scope in the package name wins over it. A namespace may
itself be a path — Go's github.com/gorilla — so its slashes are kept as
segment separators rather than percent-encoded.

Also pins cargo, gem and nuget with tests. They already worked through
the pass-through path; nuget in particular must keep its case, which the
first cut of this feature would have folded.

Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01M7zttXzAM1wbTdBAjJCECv
@potiuk
potiuk merged commit 9dcf58b into apache:main Sep 10, 2026
9 checks passed
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.

1 participant