Skip to content

Commit 483b6c1

Browse files
committed
Add package URL (purl) and CPE to SPDX SBOM files
Add a package URL and CPE to generated SBOM files so that vulnerability databases can start linking CVEs to vcpkg port versions. Fixes microsoft/vcpkg#39254. See also package-url/purl-spec#217 that has not been resolved yet but should be resolved before this commit is merged. See also https://nvd.nist.gov/products/cpe/search?namingFormat=2.3 for a CPE database.
1 parent 4063a94 commit 483b6c1

File tree

3 files changed

+237
-5
lines changed

3 files changed

+237
-5
lines changed

include/vcpkg/base/contractual-constants.h

+8
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ namespace vcpkg
128128
inline constexpr StringLiteral SpdxDocumentNamespace = "documentNamespace";
129129
inline constexpr StringLiteral SpdxDownloadLocation = "downloadLocation";
130130
inline constexpr StringLiteral SpdxElementId = "spdxElementId";
131+
inline constexpr StringLiteral SpdxExternalRefs = "externalRefs";
132+
inline constexpr StringLiteral SpdxExternalReferenceCategory = "referenceCategory";
133+
inline constexpr StringLiteral SpdxExternalReferenceCategoryPackageManager = "PACKAGE_MANAGER";
134+
inline constexpr StringLiteral SpdxExternalReferenceCategorySecurity = "SECURITY";
135+
inline constexpr StringLiteral SpdxExternalReferenceLocator = "referenceLocator";
136+
inline constexpr StringLiteral SpdxExternalReferenceType = "referenceType";
137+
inline constexpr StringLiteral SpdxExternalReferenceTypePurl = "purl";
138+
inline constexpr StringLiteral SpdxExternalReferenceTypeCpe23 = "cpe23Type";
131139
inline constexpr StringLiteral SpdxFileName = "fileName";
132140
inline constexpr StringLiteral SpdxGeneratedFrom = "GENERATED_FROM";
133141
inline constexpr StringLiteral SpdxGenerates = "GENERATES";

src/vcpkg-test/spdx.cpp

+184-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ TEST_CASE ("spdx maximum serialization", "[spdx]")
1515
cpgh.name = "zlib";
1616
cpgh.summary = {"summary"};
1717
cpgh.description = {"description"};
18-
cpgh.homepage = "homepage";
18+
cpgh.homepage = "https://www.zlib.net/";
1919
cpgh.license = "MIT";
2020
cpgh.version_scheme = VersionScheme::Relaxed;
2121
cpgh.version = Version{"1.0", 5};
@@ -100,13 +100,25 @@ TEST_CASE ("spdx maximum serialization", "[spdx]")
100100
"SPDXID": "SPDXRef-port",
101101
"versionInfo": "1.0#5",
102102
"downloadLocation": "git://some-vcs-url",
103-
"homepage": "homepage",
103+
"homepage": "https://www.zlib.net/",
104104
"licenseConcluded": "MIT",
105105
"licenseDeclared": "NOASSERTION",
106106
"copyrightText": "NOASSERTION",
107107
"summary": "summary",
108108
"description": "description",
109-
"comment": "This is the port (recipe) consumed by vcpkg."
109+
"comment": "This is the port (recipe) consumed by vcpkg.",
110+
"externalRefs": [
111+
{
112+
"referenceCategory": "PACKAGE_MANAGER",
113+
"referenceLocator": "pkg:vcpkg/[email protected]",
114+
"referenceType": "purl"
115+
},
116+
{
117+
"referenceCategory": "SECURITY",
118+
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
119+
"referenceType": "cpe23Type"
120+
}
121+
]
110122
},
111123
{
112124
"name": "zlib:arm-uwp",
@@ -247,7 +259,19 @@ TEST_CASE ("spdx minimum serialization", "[spdx]")
247259
"licenseConcluded": "NOASSERTION",
248260
"licenseDeclared": "NOASSERTION",
249261
"copyrightText": "NOASSERTION",
250-
"comment": "This is the port (recipe) consumed by vcpkg."
262+
"comment": "This is the port (recipe) consumed by vcpkg.",
263+
"externalRefs": [
264+
{
265+
"referenceCategory": "PACKAGE_MANAGER",
266+
"referenceLocator": "pkg:vcpkg/[email protected]",
267+
"referenceType": "purl"
268+
},
269+
{
270+
"referenceCategory": "SECURITY",
271+
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
272+
"referenceType": "cpe23Type"
273+
}
274+
]
251275
},
252276
{
253277
"name": "zlib:arm-uwp",
@@ -366,7 +390,19 @@ TEST_CASE ("spdx concat resources", "[spdx]")
366390
"licenseConcluded": "NOASSERTION",
367391
"licenseDeclared": "NOASSERTION",
368392
"copyrightText": "NOASSERTION",
369-
"comment": "This is the port (recipe) consumed by vcpkg."
393+
"comment": "This is the port (recipe) consumed by vcpkg.",
394+
"externalRefs": [
395+
{
396+
"referenceCategory": "PACKAGE_MANAGER",
397+
"referenceLocator": "pkg:vcpkg/[email protected]",
398+
"referenceType": "purl"
399+
},
400+
{
401+
"referenceCategory": "SECURITY",
402+
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
403+
"referenceType": "cpe23Type"
404+
}
405+
]
370406
},
371407
{
372408
"name": "zlib:arm-uwp",
@@ -396,3 +432,146 @@ TEST_CASE ("spdx concat resources", "[spdx]")
396432
auto doc = Json::parse(sbom, "test").value(VCPKG_LINE_INFO);
397433
Test::check_json_eq(expected.value, doc.value);
398434
}
435+
436+
TEST_CASE ("spdx github source", "[spdx]")
437+
{
438+
PackageSpec spec{"glew", Test::ARM_UWP};
439+
SourceControlFileAndLocation scfl;
440+
auto& scf = *(scfl.source_control_file = std::make_unique<SourceControlFile>());
441+
auto& cpgh = *(scf.core_paragraph = std::make_unique<SourceParagraph>());
442+
cpgh.name = "glew";
443+
cpgh.homepage = "https://github.com/nigels-com/glew";
444+
cpgh.version_scheme = VersionScheme::String;
445+
cpgh.version = Version{"2.2.0", 3};
446+
447+
InstallPlanAction ipa(
448+
spec, scfl, "test_packages_root", RequestType::USER_REQUESTED, UseHeadVersion::No, Editable::No, {}, {}, {});
449+
auto& abi = *(ipa.abi_info = AbiInfo{}).get();
450+
abi.package_abi = "deadbeef";
451+
452+
const auto sbom = create_spdx_sbom(ipa,
453+
std::vector<Path>{"vcpkg.json", "portfile.cmake"},
454+
std::vector<std::string>{"hash-vcpkg.json", "hash-portfile.cmake"},
455+
"now+1",
456+
"https://test-document-namespace-2",
457+
{});
458+
459+
auto expected = Json::parse(R"json(
460+
{
461+
"$schema": "https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.1/schemas/spdx-schema.json",
462+
"spdxVersion": "SPDX-2.2",
463+
"dataLicense": "CC0-1.0",
464+
"SPDXID": "SPDXRef-DOCUMENT",
465+
"documentNamespace": "https://test-document-namespace-2",
466+
"name": "glew:[email protected]#3 deadbeef",
467+
"creationInfo": {
468+
"creators": [
469+
"Tool: vcpkg-2999-12-31-unknownhash"
470+
],
471+
"created": "now+1"
472+
},
473+
"relationships": [
474+
{
475+
"spdxElementId": "SPDXRef-port",
476+
"relationshipType": "GENERATES",
477+
"relatedSpdxElement": "SPDXRef-binary"
478+
},
479+
{
480+
"spdxElementId": "SPDXRef-port",
481+
"relationshipType": "CONTAINS",
482+
"relatedSpdxElement": "SPDXRef-file-0"
483+
},
484+
{
485+
"spdxElementId": "SPDXRef-port",
486+
"relationshipType": "CONTAINS",
487+
"relatedSpdxElement": "SPDXRef-file-1"
488+
},
489+
{
490+
"spdxElementId": "SPDXRef-binary",
491+
"relationshipType": "GENERATED_FROM",
492+
"relatedSpdxElement": "SPDXRef-port"
493+
},
494+
{
495+
"spdxElementId": "SPDXRef-file-0",
496+
"relationshipType": "CONTAINED_BY",
497+
"relatedSpdxElement": "SPDXRef-port"
498+
},
499+
{
500+
"spdxElementId": "SPDXRef-file-0",
501+
"relationshipType": "DEPENDENCY_MANIFEST_OF",
502+
"relatedSpdxElement": "SPDXRef-port"
503+
},
504+
{
505+
"spdxElementId": "SPDXRef-file-1",
506+
"relationshipType": "CONTAINED_BY",
507+
"relatedSpdxElement": "SPDXRef-port"
508+
}
509+
],
510+
"packages": [
511+
{
512+
"name": "glew",
513+
"SPDXID": "SPDXRef-port",
514+
"versionInfo": "2.2.0#3",
515+
"downloadLocation": "NOASSERTION",
516+
"homepage": "https://github.com/nigels-com/glew",
517+
"licenseConcluded": "NOASSERTION",
518+
"licenseDeclared": "NOASSERTION",
519+
"copyrightText": "NOASSERTION",
520+
"comment": "This is the port (recipe) consumed by vcpkg.",
521+
"externalRefs": [
522+
{
523+
"referenceCategory": "PACKAGE_MANAGER",
524+
"referenceLocator": "pkg:vcpkg/[email protected]",
525+
"referenceType": "purl"
526+
},
527+
{
528+
"referenceCategory": "SECURITY",
529+
"referenceLocator": "cpe:2.3:a:glew:glew:2.2.0",
530+
"referenceType": "cpe23Type"
531+
}
532+
]
533+
},
534+
{
535+
"name": "glew:arm-uwp",
536+
"SPDXID": "SPDXRef-binary",
537+
"versionInfo": "deadbeef",
538+
"downloadLocation": "NONE",
539+
"licenseConcluded": "NOASSERTION",
540+
"licenseDeclared": "NOASSERTION",
541+
"copyrightText": "NOASSERTION",
542+
"comment": "This is a binary package built by vcpkg."
543+
}
544+
],
545+
"files": [
546+
{
547+
"fileName": "./vcpkg.json",
548+
"SPDXID": "SPDXRef-file-0",
549+
"checksums": [
550+
{
551+
"algorithm": "SHA256",
552+
"checksumValue": "hash-vcpkg.json"
553+
}
554+
],
555+
"licenseConcluded": "NOASSERTION",
556+
"copyrightText": "NOASSERTION"
557+
},
558+
{
559+
"fileName": "./portfile.cmake",
560+
"SPDXID": "SPDXRef-file-1",
561+
"checksums": [
562+
{
563+
"algorithm": "SHA256",
564+
"checksumValue": "hash-portfile.cmake"
565+
}
566+
],
567+
"licenseConcluded": "NOASSERTION",
568+
"copyrightText": "NOASSERTION"
569+
}
570+
]
571+
})json",
572+
"test")
573+
.value(VCPKG_LINE_INFO);
574+
575+
auto doc = Json::parse(sbom, "test").value(VCPKG_LINE_INFO);
576+
Test::check_json_eq(expected.value, doc.value);
577+
}

src/vcpkg/spdx.cpp

+45
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include <vcpkg/dependencies.h>
88
#include <vcpkg/spdx.h>
99

10+
#include <regex>
11+
1012
using namespace vcpkg;
1113

1214
static std::string fix_ref_version(StringView ref, StringView version)
@@ -82,6 +84,30 @@ static Json::Object make_resource(
8284
return obj;
8385
}
8486

87+
static std::string get_vendor(const PackageSpec& spec, const SourceParagraph& cpgh)
88+
{
89+
// Unfortunately, the vendor of the upstream library is not known here, so we use the port name,
90+
// or use the homepage domain name without the top level domain and prefixes
91+
if (!cpgh.homepage.empty())
92+
{
93+
StringView homepage = cpgh.homepage;
94+
const std::regex homepage_regex{R"###(\w+://.*\.(\w+)\.\w+)###"};
95+
std::cmatch match_vendor;
96+
const bool has_homepage_match =
97+
std::regex_search(homepage.begin(), homepage.end(), match_vendor, homepage_regex);
98+
if (has_homepage_match)
99+
{
100+
auto vendor = match_vendor[1].str();
101+
if (vendor != "github" && vendor != "bitbucket" && vendor != "gitlab" && vendor != "sourceforge")
102+
{
103+
return vendor;
104+
}
105+
}
106+
}
107+
108+
return spec.name();
109+
}
110+
85111
Json::Value vcpkg::run_resource_heuristics(StringView contents, StringView version_text)
86112
{
87113
// These are a sequence of heuristics to enable proof-of-concept extraction of remote resources for SPDX SBOM
@@ -195,6 +221,25 @@ std::string vcpkg::create_spdx_sbom(const InstallPlanAction& action,
195221
rel.insert(SpdxRelationshipType, SpdxContains);
196222
rel.insert(SpdxRelatedSpdxElement, fmt::format("SPDXRef-file-{}", i));
197223
}
224+
225+
auto& external_refs = obj.insert(SpdxExternalRefs, Json::Array());
226+
227+
// Insert Package URL (purl)
228+
auto& purl = external_refs.push_back(Json::Object());
229+
purl.insert(SpdxExternalReferenceCategory, SpdxExternalReferenceCategoryPackageManager);
230+
purl.insert(SpdxExternalReferenceType, SpdxExternalReferenceTypePurl);
231+
purl.insert(SpdxExternalReferenceLocator,
232+
Strings::concat("pkg:vcpkg/", action.spec.name(), '@', cpgh.version.text));
233+
234+
// Insert CPE
235+
// See https://nvd.nist.gov/products/cpe/search?namingFormat=2.3&orderBy=CPEURI&keyword=zlib&status=FINAL
236+
// for example entries for "zlib" in the NIST national vulnerability database
237+
auto& cpe = external_refs.push_back(Json::Object());
238+
cpe.insert(SpdxExternalReferenceCategory, SpdxExternalReferenceCategorySecurity);
239+
cpe.insert(SpdxExternalReferenceType, SpdxExternalReferenceTypeCpe23);
240+
auto vendor = get_vendor(action.spec, cpgh);
241+
cpe.insert(SpdxExternalReferenceLocator,
242+
Strings::concat("cpe:2.3:a:", vendor, ':', action.spec.name(), ':', cpgh.version.text));
198243
}
199244
{
200245
auto& obj = packages.push_back(Json::Object());

0 commit comments

Comments
 (0)