blob: b4add759eff1df49197579854fb4d86077ccda98 [file] [edit]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmSbomBuilder.h"
#include <algorithm>
#include <array>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <cm/optional>
#include <cmext/algorithm>
#include <cmext/string_view>
#include "cmArgumentParserTypes.h"
#include "cmDiagnostics.h"
#include "cmExportFileGenerator.h"
#include "cmExportSet.h"
#include "cmFindPackageStack.h"
#include "cmGeneratorExpression.h"
#include "cmGeneratorTarget.h"
#include "cmList.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmSbomArguments.h"
#include "cmSbomObject.h"
#include "cmSpdx.h"
#include "cmSpdxSerializer.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmTargetExport.h"
#include "cmValue.h"
cmSpdxPackage::PurposeId GetPurpose(cmStateEnums::TargetType type)
{
switch (type) {
case cmStateEnums::TargetType::EXECUTABLE:
return cmSpdxPackage::PurposeId::APPLICATION;
case cmStateEnums::TargetType::STATIC_LIBRARY:
case cmStateEnums::TargetType::SHARED_LIBRARY:
case cmStateEnums::TargetType::MODULE_LIBRARY:
case cmStateEnums::TargetType::OBJECT_LIBRARY:
case cmStateEnums::TargetType::INTERFACE_LIBRARY:
return cmSpdxPackage::PurposeId::LIBRARY;
case cmStateEnums::TargetType::UTILITY:
return cmSpdxPackage::PurposeId::SOURCE;
case cmStateEnums::TargetType::GLOBAL_TARGET:
case cmStateEnums::TargetType::UNKNOWN_LIBRARY:
default:
return cmSpdxPackage::PurposeId::ARCHIVE;
}
}
cmSbomBuilder::cmSbomBuilder(cmSbomArguments args,
std::vector<cmExportSet*> exportSets,
cmLocalGenerator* lg)
: LocalGenerator(lg)
, ExportSets(std::move(exportSets))
, PackageName(std::move(args.PackageName))
, Namespace(cmStrCat(this->PackageName, "::"_s))
, PackageVersion(std::move(args.Version))
, PackageDescription(std::move(args.Description))
, PackageWebsite(std::move(args.Website))
, PackageUrl(std::move(args.PackageUrl))
, PackageLicense(std::move(args.License))
, PackageFormat(args.GetFormat())
{
}
std::set<cmGeneratorTarget const*> cmSbomBuilder::CollectTargets() const
{
std::set<cmGeneratorTarget const*> targets;
for (cmExportSet* exportSet : this->ExportSets) {
for (auto const& te : exportSet->GetTargetExports()) {
if (cmGeneratorTarget const* gt =
this->LocalGenerator->FindGeneratorTargetToUse(te->TargetName)) {
targets.emplace(gt);
}
}
}
return targets;
}
void cmSbomBuilder::Compute(cmLocalGenerator* lg)
{
this->LocalGenerator = lg;
if (!lg) {
return;
}
for (cmExportSet* es : this->ExportSets) {
es->Compute(lg);
}
// Populate the cache now (rather than at Generate time) so peer SBOMs can
// query CoversTarget() during their own NoteLinkedTarget walks.
this->SbomTargets = this->CollectTargets();
}
bool cmSbomBuilder::CoversTarget(cmGeneratorTarget const* target) const
{
return cm::contains(this->SbomTargets, target);
}
bool cmSbomBuilder::CoversExportSet(cmExportSet const* set) const
{
return std::find(this->ExportSets.cbegin(), this->ExportSets.cend(), set) !=
this->ExportSets.cend();
}
bool cmSbomBuilder::GenerateForTargets(
std::ostream& os, cmGeneratorExpression::PreprocessContext preprocessContext)
{
cmSbomDocument doc;
doc.Graph.reserve(256);
cmSpdxCreationInfo const* ci =
insert_back(doc.Graph, this->GenerateCreationInfo());
cmSpdxDocument* project = insert_back(doc.Graph, this->GenerateSbom(ci));
std::vector<TargetProperties> targetProps;
targetProps.reserve(this->SbomTargets.size());
for (cmGeneratorTarget const* target : this->SbomTargets) {
ImportPropertyMap properties;
this->PopulateLinkLibrariesProperty(target, preprocessContext, properties);
this->PopulateInterfaceLinkLibrariesProperty(target, preprocessContext,
properties);
targetProps.push_back(
TargetProperties{ insert_back(project->RootElements,
this->GenerateImportTarget(ci, target)),
target, std::move(properties) });
}
for (TargetProperties const& target : targetProps) {
this->GenerateProperties(doc, project, ci, target, targetProps);
}
this->WriteSbom(doc, os);
return true;
}
void cmSbomBuilder::WriteSbom(cmSbomDocument& doc, std::ostream& os) const
{
switch (this->PackageFormat) {
case cmSbomArguments::SbomFormat::SPDX_3_0_JSON:
cmSpdxSerializer{}.WriteSbom(os, cmSbomObject(doc));
break;
case cmSbomArguments::SbomFormat::NONE:
break;
}
}
bool cmSbomBuilder::AddPackageInformation(
cmSpdxPackage& artifact, std::string const& name,
cmPackageInformation const& package) const
{
if (name.empty()) {
return false;
}
cmSpdxOrganization org;
org.SpdxId = cmStrCat("urn:", name, "#Organization");
org.Name = name;
org.CreationInfo = artifact.CreationInfo;
artifact.OriginatedBy.emplace_back(std::move(org));
if (package.Description) {
artifact.Description = *package.Description;
}
if (package.Version) {
artifact.PackageVersion = *package.Version;
}
if (package.PackageUrl) {
artifact.PackageUrl = *package.PackageUrl;
}
if (package.License) {
artifact.CopyrightText = *package.License;
}
artifact.BuiltTime = cmSystemTools::GetCurrentDateTime("%FT%TZ");
cmSpdxExternalRef externalRef;
externalRef.Locator = cmStrCat("cmake:find_package(", name, ")");
externalRef.ExternalRefType = "buildSystem";
return true;
}
cmSpdxCreationInfo cmSbomBuilder::GenerateCreationInfo() const
{
cmSpdxCreationInfo ci;
ci.SpdxId = "_:Build#CreationInfo";
ci.Created = cmSystemTools::GetCurrentDateTime("%FT%TZ");
ci.CreatedBy = { "https://gitlab.kitware.com/cmake/cmake" };
ci.Comment = "This SBOM was generated from the CMakeLists.txt File";
ci.SpecVersion = "3.0.1";
return ci;
}
cmSpdxDocument cmSbomBuilder::GenerateSbom(cmSpdxCreationInfo const* ci) const
{
cmSpdxDocument proj;
proj.Name = PackageName;
proj.SpdxId = cmStrCat("urn:", PackageName, "#SPDXDocument");
proj.ProfileConformance = { "core", "software" };
proj.CreationInfo = ci;
if (!this->PackageDescription.empty()) {
proj.Description = this->PackageDescription;
}
if (!this->PackageLicense.empty()) {
proj.DataLicense = this->PackageLicense;
}
return proj;
}
cmSpdxPackage cmSbomBuilder::GenerateImportTarget(
cmSpdxCreationInfo const* ci, cmGeneratorTarget const* target) const
{
cmSpdxPackage package;
package.SpdxId = cmStrCat("urn:", target->GetName(), "#Package");
package.Name = target->GetName();
package.PrimaryPurpose = GetPurpose(target->GetType());
package.CreationInfo = ci;
if (!this->PackageVersion.empty()) {
package.PackageVersion = this->PackageVersion;
}
if (!this->PackageWebsite.empty()) {
package.Homepage = this->PackageWebsite;
}
if (!this->PackageUrl.empty()) {
package.DownloadLocation = this->PackageUrl;
}
return package;
}
void cmSbomBuilder::GenerateLinkProperties(
cmSbomDocument& doc, cmSpdxDocument* project, cmSpdxCreationInfo const* ci,
std::string const& libraries, TargetProperties const& current,
std::vector<TargetProperties> const& allTargets) const
{
auto itProp = current.Properties.find(libraries);
if (itProp == current.Properties.end()) {
return;
}
std::map<std::string, std::vector<std::string>> allowList = { { "LINK_ONLY",
{} } };
std::string interfaceLinkLibraries;
if (!cmGeneratorExpression::ForbidGeneratorExpressions(
current.Target, itProp->first, itProp->second, interfaceLinkLibraries,
allowList)) {
return;
}
auto makeRel = [&](char const* id, char const* desc) {
cmSpdxRelationship r;
r.SpdxId = cmStrCat("urn:", id, "#Relationship");
r.RelationshipType = cmSpdxRelationship::RelationshipTypeId::DEPENDS_ON;
r.Description = desc;
r.From = current.Package;
r.CreationInfo = ci;
return r;
};
auto linkLibraries = makeRel("Static", "Linked Libraries");
auto linkRequires = makeRel("Dynamic", "Required Runtime Libraries");
auto buildRequires = makeRel("Shared", "Required Build-Time Libraries");
auto addArtifact =
[&](std::string const& name) -> std::pair<bool, cmSpdxPackage const*> {
auto it = this->LinkTargets.find(name);
if (it != this->LinkTargets.end()) {
LinkInfo const& linkInfo = it->second;
if (linkInfo.Package.empty()) {
for (auto const& t : allTargets) {
if (t.Target->GetName() == linkInfo.Component) {
return { true, t.Package };
}
}
}
std::string pkgName =
cmStrCat(linkInfo.Package, ":", linkInfo.Component);
cmSpdxPackage pkg;
pkg.Name = pkgName;
pkg.SpdxId = cmStrCat("urn:", pkgName, "#Package");
pkg.CreationInfo = ci;
if (!linkInfo.Package.empty()) {
auto const& pkgIt = this->Requirements.find(linkInfo.Package);
if (pkgIt != this->Requirements.end() &&
pkgIt->second.Components.count(linkInfo.Component) > 0) {
this->AddPackageInformation(pkg, pkgIt->first, pkgIt->second);
}
}
return { true, insert_back(project->Elements, std::move(pkg)) };
}
cmSpdxPackage pkg;
pkg.SpdxId = cmStrCat("urn:", name, "#Package");
pkg.Name = name;
pkg.CreationInfo = ci;
return { false, insert_back(project->Elements, std::move(pkg)) };
};
auto handleDependencies = [&](std::vector<std::string> const& names,
cmSpdxRelationship& internalDeps,
cmSpdxRelationship& externalDeps) {
for (auto const& n : names) {
auto res = addArtifact(n);
if (!res.second) {
continue;
}
if (res.first) {
internalDeps.To.push_back(res.second);
} else {
externalDeps.To.push_back(res.second);
}
}
};
handleDependencies(allowList["LINK_ONLY"], linkLibraries, linkRequires);
handleDependencies(cmList{ interfaceLinkLibraries }, linkLibraries,
buildRequires);
if (!linkLibraries.To.empty()) {
insert_back(doc.Graph, std::move(linkLibraries));
}
if (!linkRequires.To.empty()) {
insert_back(doc.Graph, std::move(linkRequires));
}
if (!buildRequires.To.empty()) {
insert_back(doc.Graph, std::move(buildRequires));
}
}
bool cmSbomBuilder::GenerateProperties(
cmSbomDocument& doc, cmSpdxDocument* proj, cmSpdxCreationInfo const* ci,
TargetProperties const& current,
std::vector<TargetProperties> const& allTargets) const
{
this->GenerateLinkProperties(doc, proj, ci, "LINK_LIBRARIES", current,
allTargets);
this->GenerateLinkProperties(doc, proj, ci, "INTERFACE_LINK_LIBRARIES",
current, allTargets);
return true;
}
bool cmSbomBuilder::PopulateLinkLibrariesProperty(
cmGeneratorTarget const* target,
cmGeneratorExpression::PreprocessContext preprocessRule,
ImportPropertyMap& properties)
{
static std::array<std::string, 3> const linkIfaceProps = {
{ "LINK_LIBRARIES", "LINK_LIBRARIES_DIRECT",
"LINK_LIBRARIES_DIRECT_EXCLUDE" }
};
bool hadLINK_LIBRARIES = false;
for (std::string const& linkIfaceProp : linkIfaceProps) {
if (cmValue input = target->GetProperty(linkIfaceProp)) {
std::string prepro =
cmGeneratorExpression::Preprocess(*input, preprocessRule);
if (!prepro.empty()) {
this->ResolveTargetsInGeneratorExpressions(prepro, target);
properties[linkIfaceProp] = prepro;
hadLINK_LIBRARIES = true;
}
}
}
return hadLINK_LIBRARIES;
}
bool cmSbomBuilder::AddTargetNamespace(std::string& input,
cmGeneratorTarget const* target,
cmLocalGenerator const* lg)
{
cmGeneratorTarget::TargetOrString resolved =
target->ResolveTargetReference(input, lg);
cmGeneratorTarget* tgt = resolved.Target;
if (!tgt) {
input = resolved.String;
return false;
}
if (tgt->IsImported()) {
input = tgt->GetName();
return this->NoteLinkedTarget(target, input, tgt);
}
if (this->SbomTargets.find(tgt) != this->SbomTargets.end()) {
input = this->Namespace + tgt->GetExportName();
} else {
input = tgt->GetName();
}
return this->NoteLinkedTarget(target, input, tgt);
}
bool cmSbomBuilder::NoteLinkedTarget(cmGeneratorTarget const* target,
std::string const& linkedName,
cmGeneratorTarget const* linkedTarget)
{
if (cm::contains(this->SbomTargets, linkedTarget)) {
this->LinkTargets.emplace(linkedName,
LinkInfo{ "", linkedTarget->GetExportName() });
return true;
}
if (linkedTarget->IsImported()) {
using Package = cm::optional<std::pair<std::string, cmPackageInformation>>;
auto pkgInfo = [](cmTarget* t) -> Package {
cmFindPackageStack pkgStack = t->GetFindPackageStack();
if (!pkgStack.Empty()) {
return std::make_pair(pkgStack.Top().Name,
*pkgStack.Top().PackageInfo);
}
std::string const pkgName =
t->GetSafeProperty("EXPORT_FIND_PACKAGE_NAME");
if (pkgName.empty()) {
return cm::nullopt;
}
cmPackageInformation package;
return std::make_pair(pkgName, package);
}(linkedTarget->Target);
if (!pkgInfo) {
target->Makefile->IssueDiagnostic(
cmDiagnostics::CMD_AUTHOR,
cmStrCat("Target \"", target->GetName(),
"\" references imported target \"", linkedName,
"\" which does not come from any known package."));
return false;
}
std::string const& pkgName = pkgInfo->first;
auto const& prefix = cmStrCat(pkgName, "::");
std::string component;
if (!cmHasPrefix(linkedName, prefix)) {
component = linkedName;
} else {
component = linkedName.substr(prefix.length());
}
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
cmPackageInformation& req =
this->Requirements.insert(std::move(*pkgInfo)).first->second;
req.Components.emplace(std::move(component));
return true;
}
// Target belongs to another export from this build or install.
// The leaf class chooses which export map to consult.
auto const& exportInfo = this->FindExportInfoFor(linkedTarget);
if (exportInfo.Namespaces.size() == 1 && exportInfo.Sets.size() == 1) {
auto const& linkNamespace = *exportInfo.Namespaces.begin();
if (!cmHasSuffix(linkNamespace, "::")) {
target->Makefile->IssueDiagnostic(
cmDiagnostics::CMD_AUTHOR,
cmStrCat("Target \"", target->GetName(), "\" references target \"",
linkedName,
"\", whose export does not use the standard namespace "
"separator. The dependency will be recorded by its bare "
"target name without provenance."));
return false;
}
std::string pkgName{ linkNamespace.data(), linkNamespace.size() - 2 };
std::string component = linkedTarget->GetExportName();
if (pkgName == this->GetPackageName()) {
this->LinkTargets.emplace(linkedName, LinkInfo{ "", component });
} else {
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
this->Requirements[pkgName].Components.emplace(std::move(component));
}
return true;
}
if (exportInfo.Sets.empty()) {
// install(export) provenance is unavailable. Fall back to SBOM-level
// attribution: any peer SBOM (of the same build/install mode) that
// covers this target lends its package name. install(export) wins
// when both are available; only reached here when install(export) is
// absent.
auto const& sbomInfo = this->FindSbomInfoFor(linkedTarget);
if (sbomInfo.Packages.empty()) {
target->Makefile->IssueMessage(
MessageType::FATAL_ERROR,
cmStrCat("Target \"", target->GetName(), "\" references target \"",
linkedName,
"\" which has no install(EXPORT)/export(EXPORT) namespace "
"and is not covered by any SBOM. An SBOM cannot attribute "
"this dependency. Give \"",
linkedTarget->GetName(),
"\" an install(EXPORT)/export(EXPORT) with a NAMESPACE, or "
"include it in an install(SBOM)/export(SBOM)."));
return false;
}
std::string const& pkgName = sbomInfo.Packages.front();
if (sbomInfo.Packages.size() > 1) {
target->Makefile->IssueDiagnostic(
cmDiagnostics::CMD_AUTHOR,
cmStrCat(
"Target \"", target->GetName(), "\" references target \"",
linkedName,
"\" which has no install(EXPORT)/export(EXPORT) namespace and is "
"covered by multiple SBOMs: ",
cmJoin(sbomInfo.Packages, ", "), ". Attributing to \"", pkgName,
"\" (first alphabetically)."));
}
std::string component = linkedTarget->GetExportName();
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
this->Requirements[pkgName].Components.emplace(std::move(component));
return true;
}
std::ostringstream e;
e << "Target \"" << target->GetName() << "\" references target \""
<< linkedName << "\" ";
if (exportInfo.Sets.size() == 1) {
e << "that is in an export set which is exported multiple times "
"with different namespaces: ";
} else {
e << "that is in multiple export sets: ";
}
e << cmJoin(exportInfo.Files, ", ") << ".\n"
<< "An SBOM cannot attribute a dependency exported in more than one "
"export set or with more than one namespace. Consider "
"consolidating the exports of the \""
<< linkedTarget->GetName() << "\" target to a single export.";
target->Makefile->IssueMessage(MessageType::FATAL_ERROR, e.str());
return false;
}
void cmSbomBuilder::ResolveTargetsInGeneratorExpression(
std::string& input, cmGeneratorTarget const* target,
cmLocalGenerator const* lg)
{
auto err = cmResolveTargetsInGeneratorExpression(
input, [this, target, lg](std::string& name) {
return this->AddTargetNamespace(name, target, lg);
});
if (err) {
target->GetLocalGenerator()->IssueMessage(MessageType::FATAL_ERROR, *err);
}
}
void cmSbomBuilder::ResolveTargetsInGeneratorExpressions(
std::string& input, cmGeneratorTarget const* target)
{
cmLocalGenerator const* lg = target->GetLocalGenerator();
std::vector<std::string> parts;
cmGeneratorExpression::Split(input, parts);
std::string sep;
input.clear();
for (std::string& li : parts) {
if (target->IsLinkLookupScope(li, lg)) {
continue;
}
if (cmGeneratorExpression::Find(li) == std::string::npos) {
this->AddTargetNamespace(li, target, lg);
} else {
this->ResolveTargetsInGeneratorExpression(li, target, lg);
}
input += sep + li;
sep = ";";
}
}
bool cmSbomBuilder::PopulateInterfaceLinkLibrariesProperty(
cmGeneratorTarget const* target,
cmGeneratorExpression::PreprocessContext preprocessRule,
ImportPropertyMap& properties)
{
if (!target->IsLinkable()) {
return false;
}
static std::array<std::string, 3> const linkIfaceProps = {
{ "INTERFACE_LINK_LIBRARIES", "INTERFACE_LINK_LIBRARIES_DIRECT",
"INTERFACE_LINK_LIBRARIES_DIRECT_EXCLUDE" }
};
bool hadINTERFACE_LINK_LIBRARIES = false;
for (std::string const& linkIfaceProp : linkIfaceProps) {
if (cmValue input = target->GetProperty(linkIfaceProp)) {
std::string prepro =
cmGeneratorExpression::Preprocess(*input, preprocessRule);
if (!prepro.empty()) {
this->ResolveTargetsInGeneratorExpressions(prepro, target);
properties[linkIfaceProp] = prepro;
hadINTERFACE_LINK_LIBRARIES = true;
}
}
}
return hadINTERFACE_LINK_LIBRARIES;
}