Skip to content

Commit a9d24a5

Browse files
committed
use github purl instead of generic for bundled rpm deps where appropriate
1 parent b1abb59 commit a9d24a5

7 files changed

Lines changed: 167 additions & 48 deletions

File tree

docs/sbom.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -659,28 +659,41 @@ Use a typed purl when the provide name indicates the ecosystem:
659659
| Language prefix in provide | purl type | Example provide | Example purl |
660660
|----------------------------|-----------|-----------------|--------------|
661661
| *(none)* / generic bundled | `generic` | `bundled(libvterm)` | `pkg:generic/libvterm` |
662+
| generic bundled + GitHub URL | `github` | `bundled(libvterm)` | `pkg:github/neovim/libvterm@0.3.3` |
662663
| `golang(...)` | `golang` | `golang(github.com/foo/bar)` | `pkg:golang/github.com/foo/bar@1.2.3` |
663664
| `bundled(python(...))` | `pypi` | `bundled(python(requests))` | `pkg:pypi/requests@2.31.0` |
664665
| `bundled(nodejs(...))` | `npm` | `bundled(nodejs(lodash))` | `pkg:npm/lodash@4.17.21` |
665666
| `bundled(ruby(...))` | `gem` | `bundled(ruby(rake))` | `pkg:gem/rake@13.0.6` |
666667
| `bundled(crate(...))` | `cargo` | `bundled(crate(serde))` | `pkg:cargo/serde@1.0.0` |
667668
| `bundled(mvn(...))` | `maven` | `bundled(mvn(org/foo))` | `pkg:maven/org/foo@1.0.0` |
668669

670+
When upstream provenance is known (for example from the embedded copy in the build tree),
671+
use the [`github` purl type](https://github.com/package-url/purl-spec/blob/main/types-doc/github-definition.md)
672+
if `vcs_url` or `download_url` is a `github.com` URL. Parse `owner` and `repo` from the URL
673+
and emit `pkg:github/<owner>/<repo>@<version>`. When a non-GitHub upstream URL is also known,
674+
add a second purl on the same package: `pkg:generic/<bundled-name>@<version>?download_url=...`
675+
(or `vcs_url=...`). Do not attach non-GitHub URLs as qualifiers on the `github` purl.
676+
669677
=== "SPDX 2.3"
670678

671679
```json
672680
{
673-
"SPDXID": "SPDXRef-Bundled-11cdd6f19dc1",
674-
"name": "libvterm (generic)",
675-
"versionInfo": "NOASSERTION",
676-
"downloadLocation": "NOASSERTION",
681+
"SPDXID": "SPDXRef-Bundled-b33d2447bd15",
682+
"name": "neovim/libvterm (github) 0.3.3",
683+
"versionInfo": "0.3.3",
684+
"downloadLocation": "git+https://github.com/neovim/libvterm",
677685
"filesAnalyzed": false,
678686
"primaryPackagePurpose": "LIBRARY",
679687
"externalRefs": [
680688
{
681689
"referenceCategory": "PACKAGE-MANAGER",
682690
"referenceType": "purl",
683-
"referenceLocator": "pkg:generic/libvterm"
691+
"referenceLocator": "pkg:github/neovim/libvterm@0.3.3"
692+
},
693+
{
694+
"referenceCategory": "PACKAGE-MANAGER",
695+
"referenceType": "purl",
696+
"referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm"
684697
}
685698
]
686699
}
@@ -690,7 +703,7 @@ Use a typed purl when the provide name indicates the ecosystem:
690703

691704
```json
692705
{
693-
"spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1",
706+
"spdxElementId": "SPDXRef-Bundled-b33d2447bd15",
694707
"relationshipType": "DEPENDENCY_OF",
695708
"relatedSpdxElement": "SPDXRef-SRPM"
696709
}

sbom/examples/rpm/build/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ Bundled dependencies
3131

3232
Build-time SBOMs may include packages for `bundled()` / `golang()` Provides
3333
from the RPM spec (see [Understanding SBOMs — Bundled dependencies](../../../../docs/sbom.md#bundled-dependencies)).
34-
The `vim-9.1.083-5.el10` example includes `bundled(libvterm)` linked to the
35-
SRPM with `DEPENDENCY_OF`.
34+
The `vim-9.1.083-5.el10` example includes `bundled(libvterm)` enriched to
35+
`pkg:github/neovim/libvterm@0.3.3`, linked to the SRPM with `DEPENDENCY_OF`.
3636

3737
When a container image is built including RPMs, the SBOM for the container
3838
image should refer to the RPMs using external references both with and without

sbom/examples/rpm/build/bundled_provides.py

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
from __future__ import annotations
88

99
import hashlib
10+
import re
1011
from dataclasses import dataclass
1112
from typing import Any
13+
from urllib.parse import quote
1214

1315
LANG_TO_PURL_TYPE: dict[str, str] = {
1416
"golang": "golang",
@@ -20,6 +22,11 @@
2022
"generic": "generic",
2123
}
2224

25+
_GITHUB_OWNER_REPO_RE = re.compile(
26+
r"(?:git\+)?https?://(?:www\.)?github\.com/([^/\s)>\"']+)/([^/\s)>\"'#]+)",
27+
re.IGNORECASE,
28+
)
29+
2330

2431
@dataclass
2532
class RpmDep:
@@ -37,12 +44,101 @@ class BundledDep:
3744
path: str
3845
version: str
3946
lang: str # "generic", "golang", "python", ...
47+
vcs_url: str = ""
48+
download_url: str = ""
4049

4150

42-
def _bundled_purl(dep: BundledDep) -> str:
43-
purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
51+
def _github_owner_repo(url: str) -> tuple[str, str] | None:
52+
"""Parse ``(owner, repo)`` from a github.com URL, or return ``None``."""
53+
if not url:
54+
return None
55+
match = _GITHUB_OWNER_REPO_RE.search(url)
56+
if not match:
57+
return None
58+
owner = match.group(1).lower().rstrip(".")
59+
repo = match.group(2).lower().rstrip(".")
60+
if repo.endswith(".git"):
61+
repo = repo[:-4]
62+
return owner, repo
63+
64+
65+
def _bundled_purls(dep: BundledDep) -> list[str]:
66+
"""Return one or more purls for a bundled dependency."""
4467
ver = f"@{dep.version}" if dep.version else ""
45-
return f"pkg:{purl_type}/{dep.path}{ver}"
68+
purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
69+
70+
if purl_type != "generic":
71+
base = f"pkg:{purl_type}/{dep.path}{ver}"
72+
qualifiers: list[str] = []
73+
if dep.vcs_url:
74+
qualifiers.append(f"vcs_url={quote(dep.vcs_url, safe='')}")
75+
if dep.download_url:
76+
qualifiers.append(f"download_url={quote(dep.download_url, safe='')}")
77+
if qualifiers:
78+
return [f"{base}?{'&'.join(qualifiers)}"]
79+
return [base]
80+
81+
github_coords = _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url)
82+
non_github_vcs = dep.vcs_url if dep.vcs_url and not _github_owner_repo(dep.vcs_url) else ""
83+
non_github_download = (
84+
dep.download_url if dep.download_url and not _github_owner_repo(dep.download_url) else ""
85+
)
86+
87+
if github_coords:
88+
owner, repo = github_coords
89+
purls = [f"pkg:github/{owner}/{repo}{ver}"]
90+
generic_quals: list[str] = []
91+
if non_github_vcs:
92+
generic_quals.append(f"vcs_url={quote(non_github_vcs, safe='')}")
93+
if non_github_download:
94+
generic_quals.append(f"download_url={quote(non_github_download, safe='')}")
95+
if generic_quals:
96+
purls.append(f"pkg:generic/{dep.path}{ver}?{'&'.join(generic_quals)}")
97+
return purls
98+
99+
base = f"pkg:generic/{dep.path}{ver}"
100+
qualifiers: list[str] = []
101+
if dep.vcs_url:
102+
qualifiers.append(f"vcs_url={quote(dep.vcs_url, safe='')}")
103+
if dep.download_url:
104+
qualifiers.append(f"download_url={quote(dep.download_url, safe='')}")
105+
if qualifiers:
106+
return [f"{base}?{'&'.join(qualifiers)}"]
107+
return [base]
108+
109+
110+
def _bundled_purl(dep: BundledDep) -> str:
111+
"""Primary purl for a bundled dependency (first entry from ``_bundled_purls``)."""
112+
return _bundled_purls(dep)[0]
113+
114+
115+
def _bundled_display_lang(dep: BundledDep) -> str:
116+
if dep.lang != "generic":
117+
return dep.lang
118+
if _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url):
119+
return "github"
120+
return dep.lang
121+
122+
123+
def _bundled_display_name(dep: BundledDep) -> str:
124+
if _bundled_display_lang(dep) == "github":
125+
coords = _github_owner_repo(dep.vcs_url) or _github_owner_repo(dep.download_url)
126+
label = f"{coords[0]}/{coords[1]}" if coords else dep.path
127+
else:
128+
label = dep.path
129+
lang = _bundled_display_lang(dep)
130+
name = f"{label} ({lang})"
131+
if dep.version:
132+
name = f"{name} {dep.version}"
133+
return name
134+
135+
136+
def _download_location(dep: BundledDep) -> str:
137+
if dep.vcs_url:
138+
return dep.vcs_url
139+
if dep.download_url:
140+
return dep.download_url
141+
return "NOASSERTION"
46142

47143

48144
def _dep_lang_from_inner(name_inner: str) -> tuple[str, str]:
@@ -138,25 +234,26 @@ def bundled_provides_to_spdx_fragments(
138234
packages: list[dict[str, Any]] = []
139235
rels: list[dict[str, Any]] = []
140236
for b in bundled:
141-
pid = _ref_id(f"{b.path}\0{b.version}\0{b.lang}")
142-
name = f"{b.path} ({b.lang})"
143-
if b.version:
144-
name = f"{name} {b.version}"
237+
purls = _bundled_purls(b)
238+
purl = purls[0]
239+
pid = _ref_id(purl)
240+
external_refs = [
241+
{
242+
"referenceCategory": "PACKAGE-MANAGER",
243+
"referenceType": "purl",
244+
"referenceLocator": locator,
245+
}
246+
for locator in purls
247+
]
145248
packages.append(
146249
{
147250
"SPDXID": pid,
148-
"name": name,
251+
"name": _bundled_display_name(b),
149252
"versionInfo": b.version or "NOASSERTION",
150-
"downloadLocation": "NOASSERTION",
253+
"downloadLocation": _download_location(b),
151254
"filesAnalyzed": False,
152255
"primaryPackagePurpose": "LIBRARY",
153-
"externalRefs": [
154-
{
155-
"referenceCategory": "PACKAGE-MANAGER",
156-
"referenceType": "purl",
157-
"referenceLocator": _bundled_purl(b),
158-
}
159-
],
256+
"externalRefs": external_refs,
160257
}
161258
)
162259
rels.append(
@@ -174,14 +271,11 @@ def bundled_provides_to_cdx_components(bundled: list[BundledDep]) -> list[dict[s
174271
components: list[dict[str, Any]] = []
175272
for b in bundled:
176273
purl = _bundled_purl(b)
177-
name = f"{b.path} ({b.lang})"
178-
if b.version:
179-
name = f"{name} {b.version}"
180274
components.append(
181275
{
182276
"bom-ref": purl,
183277
"type": "library",
184-
"name": name,
278+
"name": _bundled_display_name(b),
185279
"version": b.version or None,
186280
"purl": purl,
187281
}

sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@
4444
},
4545
"components": [
4646
{
47-
"bom-ref": "pkg:generic/libvterm",
47+
"bom-ref": "pkg:github/neovim/libvterm@0.3.3",
4848
"type": "library",
49-
"name": "libvterm (generic)",
50-
"purl": "pkg:generic/libvterm"
49+
"name": "neovim/libvterm (github) 0.3.3",
50+
"version": "0.3.3",
51+
"purl": "pkg:github/neovim/libvterm@0.3.3"
5152
},
5253
{
5354
"bom-ref": "pkg:rpm/redhat/vim-X11-debuginfo@9.1.083-5.el10?arch=aarch64&epoch=2",
@@ -1200,7 +1201,7 @@
12001201
"pkg:rpm/redhat/xxd@9.1.083-5.el10?arch=x86_64&epoch=2"
12011202
],
12021203
"dependsOn": [
1203-
"pkg:generic/libvterm"
1204+
"pkg:github/neovim/libvterm@0.3.3"
12041205
]
12051206
}
12061207
]

sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,22 @@
6969
]
7070
},
7171
{
72-
"SPDXID": "SPDXRef-Bundled-11cdd6f19dc1",
73-
"name": "libvterm (generic)",
74-
"versionInfo": "NOASSERTION",
75-
"downloadLocation": "NOASSERTION",
72+
"SPDXID": "SPDXRef-Bundled-b33d2447bd15",
73+
"name": "neovim/libvterm (github) 0.3.3",
74+
"versionInfo": "0.3.3",
75+
"downloadLocation": "git+https://github.com/neovim/libvterm",
7676
"filesAnalyzed": false,
7777
"primaryPackagePurpose": "LIBRARY",
7878
"externalRefs": [
7979
{
8080
"referenceCategory": "PACKAGE-MANAGER",
8181
"referenceType": "purl",
82-
"referenceLocator": "pkg:generic/libvterm"
82+
"referenceLocator": "pkg:github/neovim/libvterm@0.3.3"
83+
},
84+
{
85+
"referenceCategory": "PACKAGE-MANAGER",
86+
"referenceType": "purl",
87+
"referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm"
8388
}
8489
]
8590
},
@@ -1752,7 +1757,7 @@
17521757
"relatedSpdxElement": "SPDXRef-Source0"
17531758
},
17541759
{
1755-
"spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1",
1760+
"spdxElementId": "SPDXRef-Bundled-b33d2447bd15",
17561761
"relationshipType": "DEPENDENCY_OF",
17571762
"relatedSpdxElement": "SPDXRef-SRPM"
17581763
},

sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1942,17 +1942,18 @@
19421942
}
19431943
},
19441944
{
1945-
"bom-ref": "pkg:generic/libvterm",
1945+
"bom-ref": "pkg:github/neovim/libvterm@0.3.3",
19461946
"type": "library",
1947-
"name": "libvterm (generic)",
1948-
"purl": "pkg:generic/libvterm"
1947+
"name": "neovim/libvterm (github) 0.3.3",
1948+
"version": "0.3.3",
1949+
"purl": "pkg:github/neovim/libvterm@0.3.3"
19491950
}
19501951
],
19511952
"dependencies": [
19521953
{
19531954
"ref": "pkg:rpm/redhat/vim@9.1.083-5.el10?arch=src&epoch=2",
19541955
"dependsOn": [
1955-
"pkg:generic/libvterm"
1956+
"pkg:github/neovim/libvterm@0.3.3"
19561957
],
19571958
"provides": [
19581959
"pkg:rpm/redhat/vim-X11-debuginfo@9.1.083-5.el10?arch=aarch64&epoch=2",

sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,22 @@
139139
]
140140
},
141141
{
142-
"SPDXID": "SPDXRef-Bundled-11cdd6f19dc1",
143-
"name": "libvterm (generic)",
144-
"versionInfo": "NOASSERTION",
145-
"downloadLocation": "NOASSERTION",
142+
"SPDXID": "SPDXRef-Bundled-b33d2447bd15",
143+
"name": "neovim/libvterm (github) 0.3.3",
144+
"versionInfo": "0.3.3",
145+
"downloadLocation": "git+https://github.com/neovim/libvterm",
146146
"filesAnalyzed": false,
147147
"primaryPackagePurpose": "LIBRARY",
148148
"externalRefs": [
149149
{
150150
"referenceCategory": "PACKAGE-MANAGER",
151151
"referenceType": "purl",
152-
"referenceLocator": "pkg:generic/libvterm"
152+
"referenceLocator": "pkg:github/neovim/libvterm@0.3.3"
153+
},
154+
{
155+
"referenceCategory": "PACKAGE-MANAGER",
156+
"referenceType": "purl",
157+
"referenceLocator": "pkg:generic/libvterm@0.3.3?download_url=http%3A%2F%2Fwww.leonerd.org.uk%2Fcode%2Flibvterm"
153158
}
154159
]
155160
},
@@ -2282,7 +2287,7 @@
22822287
"relatedSpdxElement": "SPDXRef-Source0"
22832288
},
22842289
{
2285-
"spdxElementId": "SPDXRef-Bundled-11cdd6f19dc1",
2290+
"spdxElementId": "SPDXRef-Bundled-b33d2447bd15",
22862291
"relationshipType": "DEPENDENCY_OF",
22872292
"relatedSpdxElement": "SPDXRef-SRPM"
22882293
},

0 commit comments

Comments
 (0)