SBOM: say where a package was fetched from, and qualify the purl only when it is not the public registry - #212
Conversation
… when it is not the public registry A consumer reading the document cannot tell a package restored from the public registry from one that only the organisation's own feed serves. For a vulnerability consumer that is the difference between a match it can trust and a name collision, and it is the question dependency-confusion review starts from. An analyzer now has a place to put that fact: the ecosystem-neutral element attribute 'package_source', the same convention package_name/version/ecosystem already follow. The converter publishes it twice. softagram:packageSource carries it whenever it is known, the public registry included. purl cannot say "unknown" -- an absent repository_url already means "the type's default registry" -- so without the property a package confirmed to come from the public registry and one whose source nobody recorded would read the same. The repository_url qualifier carries it only when it is NOT the type's default registry. Stating the default would not be false, but the spec already implies it, and it would change the purl string of nearly every public component: a consumer comparing purls as strings would stop matching the rows whose identity is least in doubt. The defaults are the purl type definitions' own, plus the other spellings of the same registry that the definitions or the clients use (registry.yarnpkg.com, api.nuget.org, repo1.maven.org, ...). Every purl type the module emits is either in that table or explicitly listed as having no default, and a test derives the emitted types from the purl-building code itself, so a type added later cannot silently qualify its own public registry. The value is percent-encoded as ECMA-427 clause 5.4 requires of a qualifier value -- everything but the alphanumerics, '.-_~' and ':', so '/' becomes %2F, as in every repository_url example the standard gives. Only scheme, host, port and path leave. User info and the query are where registry credentials travel, and an SBOM leaves the organisation, so they are dropped at the last point before it does. A token embedded in the path is not detected -- nothing distinguishes it from a feed name -- and the docs say so. A source that is not an http(s) URL, such as a local NuGet feed directory, is treated as unknown rather than published as a path on the analysis host, and so is one containing a backslash, which parsers disagree about the host of. A purl that already contains '?' or '#' gets no qualifier: versions are spliced in unencoded, so a git-shaped version like 'github:user/repo#abc123' already opens a subpath, and a qualifier after it would be read as part of it. The property still states the source. Identity does not change. bom-ref stays unqualified, because dedup_key folds on it and every dependency edge resolves through it: one package restored from two feeds is still one component. When the occurrences folded into one row disagree about their source, or some of them state none, both the property and the qualifier are dropped rather than one checkout's answer being chosen for all -- the same retraction the name-repair provenance already makes. Dormant today: no analyzer stamps 'package_source' yet, so no existing document changes until one does.
Softagram Impact Report for pull/212 (head commit: 8328e57)TL;DR Arch. Impact: 📈 +12 | Changed code files: 2 | Directly impacted code files: 5⭐ Change Overview
⭐ Details of Dependency Changes (diagram)
🤖 AGENTS - machine-readable impact data (2 files changed, 5 impacted, +64/-0 deps)Change overviewHead Added dependencies (61, showing 50)
11 more omitted. Complete data: https://opensource.softagram.com/cdn/impact/d20d45f2-ea4e-4ae4-a9ca-0f350bd8fac5_sgraph_212_impact_change_graph_b20KVHz6Ft8OPwPmxWeh9Ct2dL01KU.png_change_info.json Removed dependencies (0)None. Impacted files (5)Unchanged files that directly depend on files changed in this PR - check them for behavioral impact. Grouped by changed file; dependent paths starting with ./ are relative to the changed file's directory:
Complete data
[] 📄 Full report
Impact Report explained. Give feedback on this report to support@softagram.com |


What
The CycloneDX generator can now say where a 3rd-party package was fetched from. An analyzer stamps the ecosystem-neutral element attribute
package_source(same convention aspackage_name/version/ecosystem), and the component publishes it as:purlsoftagram:packageSourcepkg:npm/x@1.0.0?repository_url=https:%2F%2Fnpm.internal.example%2Frepopkg:npm/x@1.0.0(unqualified)pkg:npm/x@1.0.0(unqualified)Why this shape
bom-refis never qualified.dedup_keyfolds on it and every dependency edge resolves through it, so one package restored from two feeds is still one component..-_~and:is percent-encoded, so/becomes%2F, as in the standard'srepository_urlexamples. Round-tripped throughpackageurl-python.Status
Dormant. No analyzer stamps
package_sourceyet, so no existing document changes until one does.Docs: new section "Where a package was fetched from" in
docs/data-formats.md.Verification
pytest: 630 passed. That's the 587 onmainplus 43 new tests intests/converters/test_package_source_provenance.py.#guard, default port, trailing dot, gem entry) makes at least one test fail.#, backslash host confusion) are fixed in this PR.main(indirectExposurePathsline).Known follow-ups (not in this PR)
?/#). Fixing it changesbom-reffor such versions and deserves its own change.registry.npmjs.organdregistry.yarnpkg.com) count as disagreeing sources on merge. This is conservative but loses information.