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
48 changes: 39 additions & 9 deletions docs/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ Container Images are also linked to one or more upstream sources that were used
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:generic/kernel-model-management@d027509b6861d8a9f923cc99dd3e15d9b209e63e?download_url=https://github.com/rh-ecosystem-edge/kernel-module-management#d027509b6861d8a9f923cc99dd3e15d9b209e63e"
"referenceLocator": "pkg:github/rh-ecosystem-edge/kernel-module-management@d027509b6861d8a9f923cc99dd3e15d9b209e63e"
}
]
},
Expand Down Expand Up @@ -598,8 +598,25 @@ source can be represented by a package object using the following data:
purl identifiers
: In cases where the upstream source is a package in a registry such as PyPI or NPM, the purl identifier will use
the respective package type. For components that are distributed as standalone bundles (such as OpenSSL in the
example above), `generic` purls should be used with an exact download URL from where a specific bundle of source
code was fetched from, including a checksum (which should also be specified in the `checksums` field).
example above), use `generic` purls with an exact download URL from where a specific bundle of source code was
fetched from, including a checksum (which should also be specified in the `checksums` field). When the source
`downloadLocation` is a `github.com` URL, use the [`github` purl type](https://github.com/package-url/purl-spec/blob/main/types-doc/github-definition.md)
and emit `pkg:github/<owner>/<repo>@<version>` only — do not also emit a `generic` purl whose `download_url`
points at the same GitHub location. When a separate non-GitHub fetch URL is also known (for example an upstream
tarball mirror alongside a GitHub repository), add a second purl:
`pkg:generic/<source-name>@<version>?download_url=...` (and `checksum=...` when known).

Example for a GitHub-hosted source archive (`delve` `Source0`):

```json
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:github/go-delve/delve@1.7.2"
}
]
```

To associate a set of source archives with the SRPM that includes them, use:

Expand Down Expand Up @@ -659,28 +676,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 +720,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
171 changes: 151 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,
)


@dataclass
class RpmDep:
Expand All @@ -37,12 +44,138 @@ 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 source_purls(
name: str,
version: str,
download_url: str,
*,
checksum: str = "",
) -> list[str]:
"""Return one or more purls for an upstream source archive (``Source0``, ``Source-origin``, …)."""
ver = f"@{version}" if version else ""
github_coords = _github_owner_repo(download_url)

if github_coords:
owner, repo = github_coords
return [f"pkg:github/{owner}/{repo}{ver}"]

base = f"pkg:generic/{name}{ver}"
qualifiers: list[str] = []
if download_url:
qualifiers.append(f"download_url={quote(download_url, safe='')}")
if checksum:
qualifiers.append(f"checksum={checksum}")
if qualifiers:
return [f"{base}?{'&'.join(qualifiers)}"]
return [base]


def source_purl(
name: str,
version: str,
download_url: str,
*,
checksum: str = "",
) -> str:
"""Primary purl for a source archive (first entry from ``source_purls``)."""
return source_purls(name, version, download_url, checksum=checksum)[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 +271,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 +308,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
Original file line number Diff line number Diff line change
Expand Up @@ -1508,11 +1508,11 @@
"pedigree": {
"ancestors": [
{
"bom-ref": "pkg:generic/delve@1.7.2?download_url=https://github.com/go-delve/delve/archive/v1.7.2.tar.gz#/delve-1.7.2.tar.gz",
"bom-ref": "pkg:github/go-delve/delve@1.7.2",
"type": "library",
"name": "delve",
"version": "1.7.2",
"purl": "pkg:generic/delve@1.7.2?download_url=https://github.com/go-delve/delve/archive/v1.7.2.tar.gz#/delve-1.7.2.tar.gz",
"purl": "pkg:github/go-delve/delve@1.7.2",
"hashes": [
{
"alg": "SHA-256",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:generic/delve@1.7.2?download_url=https://github.com/go-delve/delve/archive/v1.7.2.tar.gz#/delve-1.7.2.tar.gz"
"referenceLocator": "pkg:github/go-delve/delve@1.7.2"
}
]
},
Expand Down
Loading
Loading