Skip to content

Commit

Permalink
Add package URL (purl) and CPE to SPDX SBOM files
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aristotelos committed Aug 28, 2024
1 parent 4063a94 commit 483b6c1
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 5 deletions.
8 changes: 8 additions & 0 deletions include/vcpkg/base/contractual-constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ namespace vcpkg
inline constexpr StringLiteral SpdxDocumentNamespace = "documentNamespace";
inline constexpr StringLiteral SpdxDownloadLocation = "downloadLocation";
inline constexpr StringLiteral SpdxElementId = "spdxElementId";
inline constexpr StringLiteral SpdxExternalRefs = "externalRefs";
inline constexpr StringLiteral SpdxExternalReferenceCategory = "referenceCategory";
inline constexpr StringLiteral SpdxExternalReferenceCategoryPackageManager = "PACKAGE_MANAGER";
inline constexpr StringLiteral SpdxExternalReferenceCategorySecurity = "SECURITY";
inline constexpr StringLiteral SpdxExternalReferenceLocator = "referenceLocator";
inline constexpr StringLiteral SpdxExternalReferenceType = "referenceType";
inline constexpr StringLiteral SpdxExternalReferenceTypePurl = "purl";
inline constexpr StringLiteral SpdxExternalReferenceTypeCpe23 = "cpe23Type";
inline constexpr StringLiteral SpdxFileName = "fileName";
inline constexpr StringLiteral SpdxGeneratedFrom = "GENERATED_FROM";
inline constexpr StringLiteral SpdxGenerates = "GENERATES";
Expand Down
189 changes: 184 additions & 5 deletions src/vcpkg-test/spdx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ TEST_CASE ("spdx maximum serialization", "[spdx]")
cpgh.name = "zlib";
cpgh.summary = {"summary"};
cpgh.description = {"description"};
cpgh.homepage = "homepage";
cpgh.homepage = "https://www.zlib.net/";
cpgh.license = "MIT";
cpgh.version_scheme = VersionScheme::Relaxed;
cpgh.version = Version{"1.0", 5};
Expand Down Expand Up @@ -100,13 +100,25 @@ TEST_CASE ("spdx maximum serialization", "[spdx]")
"SPDXID": "SPDXRef-port",
"versionInfo": "1.0#5",
"downloadLocation": "git://some-vcs-url",
"homepage": "homepage",
"homepage": "https://www.zlib.net/",
"licenseConcluded": "MIT",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"summary": "summary",
"description": "description",
"comment": "This is the port (recipe) consumed by vcpkg."
"comment": "This is the port (recipe) consumed by vcpkg.",
"externalRefs": [
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:vcpkg/zlib@1.0",
"referenceType": "purl"
},
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
"referenceType": "cpe23Type"
}
]
},
{
"name": "zlib:arm-uwp",
Expand Down Expand Up @@ -247,7 +259,19 @@ TEST_CASE ("spdx minimum serialization", "[spdx]")
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"comment": "This is the port (recipe) consumed by vcpkg."
"comment": "This is the port (recipe) consumed by vcpkg.",
"externalRefs": [
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:vcpkg/zlib@1.0",
"referenceType": "purl"
},
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
"referenceType": "cpe23Type"
}
]
},
{
"name": "zlib:arm-uwp",
Expand Down Expand Up @@ -366,7 +390,19 @@ TEST_CASE ("spdx concat resources", "[spdx]")
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"comment": "This is the port (recipe) consumed by vcpkg."
"comment": "This is the port (recipe) consumed by vcpkg.",
"externalRefs": [
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:vcpkg/zlib@1.0",
"referenceType": "purl"
},
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:a:zlib:zlib:1.0",
"referenceType": "cpe23Type"
}
]
},
{
"name": "zlib:arm-uwp",
Expand Down Expand Up @@ -396,3 +432,146 @@ TEST_CASE ("spdx concat resources", "[spdx]")
auto doc = Json::parse(sbom, "test").value(VCPKG_LINE_INFO);
Test::check_json_eq(expected.value, doc.value);
}

TEST_CASE ("spdx github source", "[spdx]")
{
PackageSpec spec{"glew", Test::ARM_UWP};
SourceControlFileAndLocation scfl;
auto& scf = *(scfl.source_control_file = std::make_unique<SourceControlFile>());
auto& cpgh = *(scf.core_paragraph = std::make_unique<SourceParagraph>());
cpgh.name = "glew";
cpgh.homepage = "https://github.com/nigels-com/glew";
cpgh.version_scheme = VersionScheme::String;
cpgh.version = Version{"2.2.0", 3};

InstallPlanAction ipa(
spec, scfl, "test_packages_root", RequestType::USER_REQUESTED, UseHeadVersion::No, Editable::No, {}, {}, {});
auto& abi = *(ipa.abi_info = AbiInfo{}).get();
abi.package_abi = "deadbeef";

const auto sbom = create_spdx_sbom(ipa,
std::vector<Path>{"vcpkg.json", "portfile.cmake"},
std::vector<std::string>{"hash-vcpkg.json", "hash-portfile.cmake"},
"now+1",
"https://test-document-namespace-2",
{});

auto expected = Json::parse(R"json(
{
"$schema": "https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.1/schemas/spdx-schema.json",
"spdxVersion": "SPDX-2.2",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"documentNamespace": "https://test-document-namespace-2",
"name": "glew:arm-uwp@2.2.0#3 deadbeef",
"creationInfo": {
"creators": [
"Tool: vcpkg-2999-12-31-unknownhash"
],
"created": "now+1"
},
"relationships": [
{
"spdxElementId": "SPDXRef-port",
"relationshipType": "GENERATES",
"relatedSpdxElement": "SPDXRef-binary"
},
{
"spdxElementId": "SPDXRef-port",
"relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-file-0"
},
{
"spdxElementId": "SPDXRef-port",
"relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-file-1"
},
{
"spdxElementId": "SPDXRef-binary",
"relationshipType": "GENERATED_FROM",
"relatedSpdxElement": "SPDXRef-port"
},
{
"spdxElementId": "SPDXRef-file-0",
"relationshipType": "CONTAINED_BY",
"relatedSpdxElement": "SPDXRef-port"
},
{
"spdxElementId": "SPDXRef-file-0",
"relationshipType": "DEPENDENCY_MANIFEST_OF",
"relatedSpdxElement": "SPDXRef-port"
},
{
"spdxElementId": "SPDXRef-file-1",
"relationshipType": "CONTAINED_BY",
"relatedSpdxElement": "SPDXRef-port"
}
],
"packages": [
{
"name": "glew",
"SPDXID": "SPDXRef-port",
"versionInfo": "2.2.0#3",
"downloadLocation": "NOASSERTION",
"homepage": "https://github.com/nigels-com/glew",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"comment": "This is the port (recipe) consumed by vcpkg.",
"externalRefs": [
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:vcpkg/glew@2.2.0",
"referenceType": "purl"
},
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:a:glew:glew:2.2.0",
"referenceType": "cpe23Type"
}
]
},
{
"name": "glew:arm-uwp",
"SPDXID": "SPDXRef-binary",
"versionInfo": "deadbeef",
"downloadLocation": "NONE",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"comment": "This is a binary package built by vcpkg."
}
],
"files": [
{
"fileName": "./vcpkg.json",
"SPDXID": "SPDXRef-file-0",
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "hash-vcpkg.json"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": "NOASSERTION"
},
{
"fileName": "./portfile.cmake",
"SPDXID": "SPDXRef-file-1",
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "hash-portfile.cmake"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": "NOASSERTION"
}
]
})json",
"test")
.value(VCPKG_LINE_INFO);

auto doc = Json::parse(sbom, "test").value(VCPKG_LINE_INFO);
Test::check_json_eq(expected.value, doc.value);
}
45 changes: 45 additions & 0 deletions src/vcpkg/spdx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <vcpkg/dependencies.h>
#include <vcpkg/spdx.h>

#include <regex>

using namespace vcpkg;

static std::string fix_ref_version(StringView ref, StringView version)
Expand Down Expand Up @@ -82,6 +84,30 @@ static Json::Object make_resource(
return obj;
}

static std::string get_vendor(const PackageSpec& spec, const SourceParagraph& cpgh)
{
// Unfortunately, the vendor of the upstream library is not known here, so we use the port name,
// or use the homepage domain name without the top level domain and prefixes
if (!cpgh.homepage.empty())
{
StringView homepage = cpgh.homepage;
const std::regex homepage_regex{R"###(\w+://.*\.(\w+)\.\w+)###"};
std::cmatch match_vendor;
const bool has_homepage_match =
std::regex_search(homepage.begin(), homepage.end(), match_vendor, homepage_regex);
if (has_homepage_match)
{
auto vendor = match_vendor[1].str();
if (vendor != "github" && vendor != "bitbucket" && vendor != "gitlab" && vendor != "sourceforge")
{
return vendor;
}
}
}

return spec.name();
}

Json::Value vcpkg::run_resource_heuristics(StringView contents, StringView version_text)
{
// These are a sequence of heuristics to enable proof-of-concept extraction of remote resources for SPDX SBOM
Expand Down Expand Up @@ -195,6 +221,25 @@ std::string vcpkg::create_spdx_sbom(const InstallPlanAction& action,
rel.insert(SpdxRelationshipType, SpdxContains);
rel.insert(SpdxRelatedSpdxElement, fmt::format("SPDXRef-file-{}", i));
}

auto& external_refs = obj.insert(SpdxExternalRefs, Json::Array());

// Insert Package URL (purl)
auto& purl = external_refs.push_back(Json::Object());
purl.insert(SpdxExternalReferenceCategory, SpdxExternalReferenceCategoryPackageManager);
purl.insert(SpdxExternalReferenceType, SpdxExternalReferenceTypePurl);
purl.insert(SpdxExternalReferenceLocator,
Strings::concat("pkg:vcpkg/", action.spec.name(), '@', cpgh.version.text));

// Insert CPE
// See https://nvd.nist.gov/products/cpe/search?namingFormat=2.3&orderBy=CPEURI&keyword=zlib&status=FINAL
// for example entries for "zlib" in the NIST national vulnerability database
auto& cpe = external_refs.push_back(Json::Object());
cpe.insert(SpdxExternalReferenceCategory, SpdxExternalReferenceCategorySecurity);
cpe.insert(SpdxExternalReferenceType, SpdxExternalReferenceTypeCpe23);
auto vendor = get_vendor(action.spec, cpgh);
cpe.insert(SpdxExternalReferenceLocator,
Strings::concat("cpe:2.3:a:", vendor, ':', action.spec.name(), ':', cpgh.version.text));
}
{
auto& obj = packages.push_back(Json::Object());
Expand Down

0 comments on commit 483b6c1

Please sign in to comment.