diff --git a/docs/sbom.md b/docs/sbom.md index 73b00cb..945b02f 100644 --- a/docs/sbom.md +++ b/docs/sbom.md @@ -659,6 +659,7 @@ Use a typed purl when the provide name indicates the ecosystem: | Language prefix in provide | purl type | Example provide | Example purl | |----------------------------|-----------|-----------------|--------------| | *(none)* / generic bundled | `generic` | `bundled(libvterm)` | `pkg:generic/libvterm` | +| generic bundled + GitHub URL | `github` | `bundled(libvterm)` | `pkg:github/neovim/libvterm@0.3.3` | | `golang(...)` | `golang` | `golang(github.com/foo/bar)` | `pkg:golang/github.com/foo/bar@1.2.3` | | `bundled(python(...))` | `pypi` | `bundled(python(requests))` | `pkg:pypi/requests@2.31.0` | | `bundled(nodejs(...))` | `npm` | `bundled(nodejs(lodash))` | `pkg:npm/lodash@4.17.21` | @@ -666,21 +667,33 @@ Use a typed purl when the provide name indicates the ecosystem: | `bundled(crate(...))` | `cargo` | `bundled(crate(serde))` | `pkg:cargo/serde@1.0.0` | | `bundled(mvn(...))` | `maven` | `bundled(mvn(org/foo))` | `pkg:maven/org/foo@1.0.0` | +When upstream provenance is known (for example from the embedded copy in the build tree), +use the [`github` purl type](https://github.com/package-url/purl-spec/blob/main/types-doc/github-definition.md) +if `vcs_url` or `download_url` is a `github.com` URL. Parse `owner` and `repo` from the URL +and emit `pkg:github//@`. When a non-GitHub upstream URL is also known, +add a second purl on the same package: `pkg:generic/@?download_url=...` +(or `vcs_url=...`). Do not attach non-GitHub URLs as qualifiers on the `github` purl. + === "SPDX 2.3" ```json { - "SPDXID": "SPDXRef-Bundled-11cdd6f19dc1", - "name": "libvterm (generic)", - "versionInfo": "NOASSERTION", - "downloadLocation": "NOASSERTION", + "SPDXID": "SPDXRef-Bundled-b33d2447bd15", + "name": "neovim/libvterm (github) 0.3.3", + "versionInfo": "0.3.3", + "downloadLocation": "git+https://github.com/neovim/libvterm", "filesAnalyzed": false, "primaryPackagePurpose": "LIBRARY", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:generic/libvterm" + "referenceLocator": "pkg:github/neovim/libvterm@0.3.3" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm" } ] } @@ -690,7 +703,7 @@ Use a typed purl when the provide name indicates the ecosystem: ```json { - "spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1", + "spdxElementId": "SPDXRef-Bundled-b33d2447bd15", "relationshipType": "DEPENDENCY_OF", "relatedSpdxElement": "SPDXRef-SRPM" } diff --git a/sbom/examples/rpm/build/README.md b/sbom/examples/rpm/build/README.md index 0ce3b6c..fd610ed 100644 --- a/sbom/examples/rpm/build/README.md +++ b/sbom/examples/rpm/build/README.md @@ -31,8 +31,8 @@ Bundled dependencies Build-time SBOMs may include packages for `bundled()` / `golang()` Provides from the RPM spec (see [Understanding SBOMs — Bundled dependencies](../../../../docs/sbom.md#bundled-dependencies)). -The `vim-9.1.083-5.el10` example includes `bundled(libvterm)` linked to the -SRPM with `DEPENDENCY_OF`. +The `vim-9.1.083-5.el10` example includes `bundled(libvterm)` enriched to +`pkg:github/neovim/libvterm@0.3.3`, linked to the SRPM with `DEPENDENCY_OF`. When a container image is built including RPMs, the SBOM for the container image should refer to the RPMs using external references both with and without diff --git a/sbom/examples/rpm/build/bundled_provides.py b/sbom/examples/rpm/build/bundled_provides.py index a100dec..11b633c 100644 --- a/sbom/examples/rpm/build/bundled_provides.py +++ b/sbom/examples/rpm/build/bundled_provides.py @@ -7,8 +7,10 @@ from __future__ import annotations import hashlib +import re from dataclasses import dataclass from typing import Any +from urllib.parse import quote LANG_TO_PURL_TYPE: dict[str, str] = { "golang": "golang", @@ -20,6 +22,11 @@ "generic": "generic", } +_GITHUB_OWNER_REPO_RE = re.compile( + r"(?:git\+)?https?://(?:www\.)?github\.com/([^/\s)>\"']+)/([^/\s)>\"'#]+)", + re.IGNORECASE, +) + @dataclass class RpmDep: @@ -37,12 +44,101 @@ class BundledDep: path: str version: str lang: str # "generic", "golang", "python", ... + vcs_url: str = "" + download_url: str = "" -def _bundled_purl(dep: BundledDep) -> str: - purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic") +def _github_owner_repo(url: str) -> tuple[str, str] | None: + """Parse ``(owner, repo)`` from a github.com URL, or return ``None``.""" + if not url: + return None + match = _GITHUB_OWNER_REPO_RE.search(url) + if not match: + return None + owner = match.group(1).lower().rstrip(".") + repo = match.group(2).lower().rstrip(".") + if repo.endswith(".git"): + repo = repo[:-4] + return owner, repo + + +def _bundled_purls(dep: BundledDep) -> list[str]: + """Return one or more purls for a bundled dependency.""" ver = f"@{dep.version}" if dep.version else "" - return f"pkg:{purl_type}/{dep.path}{ver}" + purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic") + + if purl_type != "generic": + base = f"pkg:{purl_type}/{dep.path}{ver}" + qualifiers: list[str] = [] + if dep.vcs_url: + qualifiers.append(f"vcs_url={quote(dep.vcs_url, safe='')}") + if dep.download_url: + qualifiers.append(f"download_url={quote(dep.download_url, safe='')}") + if qualifiers: + return [f"{base}?{'&'.join(qualifiers)}"] + return [base] + + github_coords = _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url) + non_github_vcs = dep.vcs_url if dep.vcs_url and not _github_owner_repo(dep.vcs_url) else "" + non_github_download = ( + dep.download_url if dep.download_url and not _github_owner_repo(dep.download_url) else "" + ) + + if github_coords: + owner, repo = github_coords + purls = [f"pkg:github/{owner}/{repo}{ver}"] + generic_quals: list[str] = [] + if non_github_vcs: + generic_quals.append(f"vcs_url={quote(non_github_vcs, safe='')}") + if non_github_download: + generic_quals.append(f"download_url={quote(non_github_download, safe='')}") + if generic_quals: + purls.append(f"pkg:generic/{dep.path}{ver}?{'&'.join(generic_quals)}") + return purls + + base = f"pkg:generic/{dep.path}{ver}" + qualifiers: list[str] = [] + if dep.vcs_url: + qualifiers.append(f"vcs_url={quote(dep.vcs_url, safe='')}") + if dep.download_url: + qualifiers.append(f"download_url={quote(dep.download_url, safe='')}") + if qualifiers: + return [f"{base}?{'&'.join(qualifiers)}"] + return [base] + + +def _bundled_purl(dep: BundledDep) -> str: + """Primary purl for a bundled dependency (first entry from ``_bundled_purls``).""" + return _bundled_purls(dep)[0] + + +def _bundled_display_lang(dep: BundledDep) -> str: + if dep.lang != "generic": + return dep.lang + if _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url): + return "github" + return dep.lang + + +def _bundled_display_name(dep: BundledDep) -> str: + if _bundled_display_lang(dep) == "github": + coords = _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url) + label = f"{coords[0]}/{coords[1]}" if coords else dep.path + else: + label = dep.path + lang = _bundled_display_lang(dep) + name = f"{label} ({lang})" + if dep.version: + name = f"{name} {dep.version}" + return name + + +def _download_location(dep: BundledDep) -> str: + if dep.vcs_url: + return dep.vcs_url + if dep.download_url: + return dep.download_url + return "NOASSERTION" def _dep_lang_from_inner(name_inner: str) -> tuple[str, str]: @@ -138,25 +234,26 @@ def bundled_provides_to_spdx_fragments( packages: list[dict[str, Any]] = [] rels: list[dict[str, Any]] = [] for b in bundled: - pid = _ref_id(f"{b.path}\0{b.version}\0{b.lang}") - name = f"{b.path} ({b.lang})" - if b.version: - name = f"{name} {b.version}" + purls = _bundled_purls(b) + purl = purls[0] + pid = _ref_id(purl) + external_refs = [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": locator, + } + for locator in purls + ] packages.append( { "SPDXID": pid, - "name": name, + "name": _bundled_display_name(b), "versionInfo": b.version or "NOASSERTION", - "downloadLocation": "NOASSERTION", + "downloadLocation": _download_location(b), "filesAnalyzed": False, "primaryPackagePurpose": "LIBRARY", - "externalRefs": [ - { - "referenceCategory": "PACKAGE-MANAGER", - "referenceType": "purl", - "referenceLocator": _bundled_purl(b), - } - ], + "externalRefs": external_refs, } ) rels.append( @@ -174,14 +271,11 @@ def bundled_provides_to_cdx_components(bundled: list[BundledDep]) -> list[dict[s components: list[dict[str, Any]] = [] for b in bundled: purl = _bundled_purl(b) - name = f"{b.path} ({b.lang})" - if b.version: - name = f"{name} {b.version}" components.append( { "bom-ref": purl, "type": "library", - "name": name, + "name": _bundled_display_name(b), "version": b.version or None, "purl": purl, } diff --git a/sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json b/sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json index 52f5f99..970cb31 100644 --- a/sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json +++ b/sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json @@ -44,10 +44,11 @@ }, "components": [ { - "bom-ref": "pkg:generic/libvterm", + "bom-ref": "pkg:github/neovim/libvterm@0.3.3", "type": "library", - "name": "libvterm (generic)", - "purl": "pkg:generic/libvterm" + "name": "neovim/libvterm (github) 0.3.3", + "version": "0.3.3", + "purl": "pkg:github/neovim/libvterm@0.3.3" }, { "bom-ref": "pkg:rpm/redhat/vim-X11-debuginfo@9.1.083-5.el10?arch=aarch64&epoch=2", @@ -1200,7 +1201,7 @@ "pkg:rpm/redhat/xxd@9.1.083-5.el10?arch=x86_64&epoch=2" ], "dependsOn": [ - "pkg:generic/libvterm" + "pkg:github/neovim/libvterm@0.3.3" ] } ] diff --git a/sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json b/sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json index 986f447..df2f0e0 100644 --- a/sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json +++ b/sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json @@ -69,17 +69,22 @@ ] }, { - "SPDXID": "SPDXRef-Bundled-11cdd6f19dc1", - "name": "libvterm (generic)", - "versionInfo": "NOASSERTION", - "downloadLocation": "NOASSERTION", + "SPDXID": "SPDXRef-Bundled-b33d2447bd15", + "name": "neovim/libvterm (github) 0.3.3", + "versionInfo": "0.3.3", + "downloadLocation": "git+https://github.com/neovim/libvterm", "filesAnalyzed": false, "primaryPackagePurpose": "LIBRARY", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:generic/libvterm" + "referenceLocator": "pkg:github/neovim/libvterm@0.3.3" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm" } ] }, @@ -1752,7 +1757,7 @@ "relatedSpdxElement": "SPDXRef-Source0" }, { - "spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1", + "spdxElementId": "SPDXRef-Bundled-b33d2447bd15", "relationshipType": "DEPENDENCY_OF", "relatedSpdxElement": "SPDXRef-SRPM" }, diff --git a/sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json b/sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json index 34bcefa..48c6769 100644 --- a/sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json +++ b/sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json @@ -1942,17 +1942,18 @@ } }, { - "bom-ref": "pkg:generic/libvterm", + "bom-ref": "pkg:github/neovim/libvterm@0.3.3", "type": "library", - "name": "libvterm (generic)", - "purl": "pkg:generic/libvterm" + "name": "neovim/libvterm (github) 0.3.3", + "version": "0.3.3", + "purl": "pkg:github/neovim/libvterm@0.3.3" } ], "dependencies": [ { "ref": "pkg:rpm/redhat/vim@9.1.083-5.el10?arch=src&epoch=2", "dependsOn": [ - "pkg:generic/libvterm" + "pkg:github/neovim/libvterm@0.3.3" ], "provides": [ "pkg:rpm/redhat/vim-X11-debuginfo@9.1.083-5.el10?arch=aarch64&epoch=2", diff --git a/sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json b/sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json index b17a9f4..76690b8 100644 --- a/sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json +++ b/sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json @@ -139,17 +139,22 @@ ] }, { - "SPDXID": "SPDXRef-Bundled-11cdd6f19dc1", - "name": "libvterm (generic)", - "versionInfo": "NOASSERTION", - "downloadLocation": "NOASSERTION", + "SPDXID": "SPDXRef-Bundled-b33d2447bd15", + "name": "neovim/libvterm (github) 0.3.3", + "versionInfo": "0.3.3", + "downloadLocation": "git+https://github.com/neovim/libvterm", "filesAnalyzed": false, "primaryPackagePurpose": "LIBRARY", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:generic/libvterm" + "referenceLocator": "pkg:github/neovim/libvterm@0.3.3" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm" } ] }, @@ -2282,7 +2287,7 @@ "relatedSpdxElement": "SPDXRef-Source0" }, { - "spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1", + "spdxElementId": "SPDXRef-Bundled-b33d2447bd15", "relationshipType": "DEPENDENCY_OF", "relatedSpdxElement": "SPDXRef-SRPM" },