Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions docs/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,28 +659,41 @@ 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` |
| `bundled(ruby(...))` | `gem` | `bundled(ruby(rake))` | `pkg:gem/rake@13.0.6` |
| `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/<owner>/<repo>@<version>`. When a non-GitHub upstream URL is also known,
add a second purl on the same package: `pkg:generic/<bundled-name>@<version>?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"
}
]
}
Expand All @@ -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"
}
Expand Down
4 changes: 2 additions & 2 deletions sbom/examples/rpm/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 114 additions & 20 deletions sbom/examples/rpm/build/bundled_provides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -20,6 +22,11 @@
"generic": "generic",
}

_GITHUB_OWNER_REPO_RE = re.compile(
r"(?:git\+)?https?://(?:www\.)?github\.com/([^/\s)>\"']+)/([^/\s)>\"'#]+)",
re.IGNORECASE,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)


@dataclass
class RpmDep:
Expand All @@ -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]:
Expand Down Expand Up @@ -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(
Expand All @@ -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,
}
Expand Down
9 changes: 5 additions & 4 deletions sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
]
}
]
Expand Down
17 changes: 11 additions & 6 deletions sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down Expand Up @@ -1752,7 +1757,7 @@
"relatedSpdxElement": "SPDXRef-Source0"
},
{
"spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1",
"spdxElementId": "SPDXRef-Bundled-b33d2447bd15",
"relationshipType": "DEPENDENCY_OF",
"relatedSpdxElement": "SPDXRef-SRPM"
},
Expand Down
9 changes: 5 additions & 4 deletions sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 11 additions & 6 deletions sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down Expand Up @@ -2282,7 +2287,7 @@
"relatedSpdxElement": "SPDXRef-Source0"
},
{
"spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1",
"spdxElementId": "SPDXRef-Bundled-b33d2447bd15",
"relationshipType": "DEPENDENCY_OF",
"relatedSpdxElement": "SPDXRef-SRPM"
},
Expand Down
Loading