diff --git a/include/vcpkg/base/contractual-constants.h b/include/vcpkg/base/contractual-constants.h index e637a800db..c4fcd21fd1 100644 --- a/include/vcpkg/base/contractual-constants.h +++ b/include/vcpkg/base/contractual-constants.h @@ -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"; diff --git a/src/vcpkg-test/spdx.cpp b/src/vcpkg-test/spdx.cpp index 743e0207f2..7809fd9e37 100644 --- a/src/vcpkg-test/spdx.cpp +++ b/src/vcpkg-test/spdx.cpp @@ -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}; @@ -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", @@ -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", @@ -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", @@ -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()); + auto& cpgh = *(scf.core_paragraph = std::make_unique()); + 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{"vcpkg.json", "portfile.cmake"}, + std::vector{"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); +} \ No newline at end of file diff --git a/src/vcpkg/spdx.cpp b/src/vcpkg/spdx.cpp index bcaba964b6..1c2819892e 100644 --- a/src/vcpkg/spdx.cpp +++ b/src/vcpkg/spdx.cpp @@ -7,6 +7,8 @@ #include #include +#include + using namespace vcpkg; static std::string fix_ref_version(StringView ref, StringView version) @@ -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 @@ -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());