com.metaeffekt.artifact.analysis.bom.cyclonedx.CycloneDxExporter Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.metaeffekt.artifact.analysis.bom.cyclonedx;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metaeffekt.artifact.analysis.bom.LicenseProcessor;
import com.metaeffekt.artifact.analysis.bom.spdx.LicenseStringConverter;
import com.metaeffekt.artifact.analysis.bom.spdx.DocumentSpec;
import com.metaeffekt.artifact.analysis.bom.spdx.mapper.MappingUtils;
import com.metaeffekt.artifact.analysis.bom.spdx.relationship.RelationshipGraph;
import com.metaeffekt.artifact.analysis.bom.spdx.relationship.RelationshipGraphEdge;
import com.metaeffekt.artifact.analysis.bom.spdx.relationship.RelationshipGraphNode;
import com.metaeffekt.artifact.analysis.utils.FileUtils;
import com.metaeffekt.artifact.analysis.utils.InventoryUtils;
import com.metaeffekt.artifact.terms.model.LicenseTextProvider;
import com.metaeffekt.artifact.terms.model.NormalizationMetaData;
import com.metaeffekt.artifact.terms.model.TermsMetaData;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cyclonedx.Version;
import org.cyclonedx.generators.BomGeneratorFactory;
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.generators.xml.BomXmlGenerator;
import org.cyclonedx.model.*;
import org.metaeffekt.core.inventory.processor.model.*;
import org.spdx.library.model.enumerations.RelationshipType;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
public class CycloneDxExporter {
private final LicenseStringConverter licenseStringConverter;
/**
* NormalizationMetaData, required for license string translation.
*/
private final NormalizationMetaData normalizationMetaData;
/**
* Provider of license texts, used to append license texts that spdx doesn't know about.
*/
private final LicenseTextProvider licenseTextProvider;
private Version version = Version.VERSION_16;
@Setter
private boolean prettyPrint = true;
@Getter
private String generatedXmlBom = null, generatedJsonBom = null;
private final Map customLicenseMappings;
/**
* CycloneDxExporter which is used to export inventories into a cyclonedx bom in either xml or json format. To
* further understand how bom generation works, look into the methods contained in this class as well as
* {@link ComponentMapper}. It is also recommended to understand the influence of the approvedAttributes.json
* file and how to structure it as well as {@link DocumentSpec} and how the optional parameters influence which
* information is contained in the bom.
*
* @param normalizationMetaData needed for licensing
* @param licenseTextProvider provides license texts
*/
public CycloneDxExporter(NormalizationMetaData normalizationMetaData, LicenseTextProvider licenseTextProvider) {
this(normalizationMetaData, licenseTextProvider, null, null);
}
public CycloneDxExporter(NormalizationMetaData normalizationMetaData, LicenseTextProvider licenseTextProvider,
Map spdxStringAssessments, File customLicenseMappings) {
this.normalizationMetaData = normalizationMetaData;
this.licenseTextProvider = licenseTextProvider;
licenseStringConverter = new LicenseStringConverter(normalizationMetaData, spdxStringAssessments);
if (customLicenseMappings == null || customLicenseMappings.length() == 0) {
this.customLicenseMappings = null;
} else {
ObjectMapper objectMapper = new ObjectMapper();
try {
this.customLicenseMappings = objectMapper.readValue(customLicenseMappings, Map.class);
} catch (IOException e) {
throw new RuntimeException("Failed to read custom license mappings file: " + customLicenseMappings, e);
}
}
}
public void setVersion(String versionString) {
if (versionString.equals("latest")) {
final Version[] values = Version.values();
this.version = values[values.length - 1];
return;
}
this.version = Arrays.stream(Version.values())
.filter(v -> v.getVersionString().equals(versionString)).findFirst()
.orElseThrow(() -> new IllegalStateException("Unknown version " + versionString + "."));
}
/**
* Should be called when wanting to export an inventory to xml format. Generated the bom from an inventory and
* exports it to a file if valid. Also generates a correlating ValidationResult containing validation information
* generated by the cyclonedx library.
*
* @param inventory the inventory to export
* @param documentSpec contains metadata and influences document creation
* @param outFile output file which will contain the bom in json format
* @param approvedAttributes influences which attributes will be collected, necessary attributes will always be
* collected anyway. If this is null all attributes will be collected.
*/
public void exportToXml(Inventory inventory, DocumentSpec documentSpec, File outFile, File approvedAttributes) {
Bom bom = createBomFromInventory(inventory, documentSpec, approvedAttributes);
BomXmlGenerator xml = BomGeneratorFactory.createXml(version, bom);
try {
xml.generate();
generatedXmlBom = xml.toString();
if (outFile != null) {
FileUtils.write(outFile, generatedXmlBom, StandardCharsets.UTF_8);
}
} catch (ParserConfigurationException | IOException e) {
throw new RuntimeException(e);
}
}
/**
* Should be called when wanting to export an inventory to json format. Generated the bom from an inventory and
* exports it to a file if valid. Also generates a correlating ValidationResult containing validation information
* generated by the cyclonedx library.
*
* @param inventory the inventory to export
* @param documentSpec contains metadata and influences document creation
* @param outFile output file which will contain the bom in json format
* @param approvedAttributes influences which attributes will be collected, necessary attributes will always be
* collected anyway. If this is null all attributes will be collected.
*
* @throws ParserConfigurationException if json generation failed because of an invalid bom
*/
public void exportToJson(Inventory inventory, DocumentSpec documentSpec, File outFile, File approvedAttributes)
throws ParserConfigurationException {
Bom bom = createBomFromInventory(inventory, documentSpec, approvedAttributes);
BomJsonGenerator json = BomGeneratorFactory.createJson(version, bom);
JsonNode generate = json.toJsonNode();
if (generate == null) {
throw new ParserConfigurationException("Error generating JSON output.");
}
generatedJsonBom = prettyPrint ? generate.toPrettyString() : generate.toString();
try {
if (outFile != null) {
FileUtils.write(outFile, generatedJsonBom, StandardCharsets.UTF_8);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Contains all the relevant steps needed to export an inventory to a cyclonedx bom, from bom creation to filling
* that bom with information.
*
* @param inventory invenory to export
* @param documentSpec contains metadata for document creation as well as parameters used to influence which
* information is contained.
* @param approvedAttributes influences which attributes will be collected, necessary attributes will always be
* collected anyways. If this is null all attributes will be colleceted.
* @return the bom which will be written to a file.
*/
private Bom createBomFromInventory(Inventory inventory, DocumentSpec documentSpec, File approvedAttributes) {
final Bom bom = createBomFromSpec(documentSpec);
List allComponents = new ArrayList<>();
if (!documentSpec.isIncludeAssets()) {
InventoryUtils.removeAssetsAndReferences(inventory);
}
if (approvedAttributes == null || approvedAttributes.length() == 0) {
log.warn("No additional approved attributes were provided. Only exporting predefined attributes from the inventory.");
}
final ComponentMapper componentMapper =
new ComponentMapper(documentSpec, approvedAttributes, inventory, licenseStringConverter,
normalizationMetaData, licenseTextProvider, customLicenseMappings);
for (Artifact artifact : inventory.getArtifacts()) {
allComponents.add(componentMapper.map(artifact));
}
List unmappedAssets = inventory.getAssetMetaData();
unmappedAssets.removeIf(MappingUtils.getAssetToArtifactMap(inventory)::containsKey);
for (AssetMetaData assetMetaData : unmappedAssets) {
allComponents.add(componentMapper.map(assetMetaData));
}
addExternalReferences(bom, documentSpec, componentMapper.getReferencedLicenses());
addComponentsAndCreateAssembly(bom, allComponents, inventory, documentSpec);
return bom;
}
/**
* Creates the bom document itself extracting relevant metadata from document spec. This contains information such
* as Organization data, author etc.
*
* @param documentSpec contains creation metadata
* @return the final bom which will be filled with information
*/
private Bom createBomFromSpec(DocumentSpec documentSpec) {
final Bom bom = new Bom();
final Metadata metadata = new Metadata();
final OrganizationalEntity organization = new OrganizationalEntity();
organization.setName(documentSpec.getOrganization());
organization.setUrls(Collections.singletonList(documentSpec.getOrganizationUrl()));
metadata.setManufacturer(organization);
final Tool tool = new Tool();
tool.setName(documentSpec.getTool());
metadata.addTool(tool);
final OrganizationalContact author = new OrganizationalContact();
author.setName(documentSpec.getPerson());
metadata.addAuthor(author);
Pattern pattern = Pattern.compile("^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
Matcher matcher = pattern.matcher(documentSpec.getDocumentId());
if (matcher.matches()) {
bom.setSerialNumber(documentSpec.getDocumentId());
} else {
throw new RuntimeException("Invalid UUID: " + documentSpec.getDocumentId() +
" provided as DocumentId in DocumentSpec. Does not comply with CycloneDx specification.");
}
metadata.setTimestamp(Calendar.getInstance().getTime());
bom.setMetadata(metadata);
bom.setVersion(documentSpec.getDocumentIteration());
return bom;
}
/**
* Collects all referenced licenses which are custom licenses not present in the spdx license database and adds
* them
* as references which their respective license texts to the bom.
*
* @param bom the document for which to add the references
* @param documentSpec contains parameters which influence if license texts will be included or not
* @param referencedLicenses the set of referenced licenses, collected during component mapping
*/
private void addExternalReferences(Bom bom, DocumentSpec documentSpec, Set referencedLicenses) {
if (referencedLicenses.isEmpty()) {
return;
}
Map licenseIdentifierToText =
LicenseProcessor.getReferencedLicenseText(referencedLicenses, licenseTextProvider,
normalizationMetaData);
if (licenseIdentifierToText != null) {
for (Map.Entry entry : licenseIdentifierToText.entrySet()) {
ExternalReference externalReference = new ExternalReference();
externalReference.setUrl(entry.getKey());
if (entry.getValue() != null && documentSpec.isIncludeLicenseTexts()) {
externalReference.setComment(entry.getValue());
} else {
externalReference.setComment("No license text found.");
}
externalReference.setType(ExternalReference.Type.LICENSE);
bom.addExternalReference(externalReference);
}
}
}
/**
* Adds all mapped and filled components to the bom while respecting their relationship hierarchy in the
* originating inventory.
* DESCRIBES relationships are components added at top-level of the bom. CONTAINS relationships ar portrayed
* through a nested component structure.
*
* @param bom the bom for which to add all components
* @param allComponents a list of all mapped components
* @param inventory the inventory from which the component hierarchy is extracted by RelationshipGraph
* @param documentSpec contains parameters which influence how or if component relationships are tracked.
*/
private void addComponentsAndCreateAssembly(Bom bom, List allComponents, Inventory inventory,
DocumentSpec documentSpec) {
RelationshipGraph relationshipGraph = new RelationshipGraph(inventory, MappingUtils.getAssetToArtifactMap(inventory));
// switch if relationships are disabled or not present in the inventory, adds all components as top-level
if (!documentSpec.isMapRelationships() || relationshipGraph.getAllRelationships().isEmpty()) {
bom.setComponents(allComponents);
return;
}
for (RelationshipGraphEdge edge : relationshipGraph.getAllRelationships()) {
if (edge.getRelationshipType().equals(RelationshipType.CONTAINS)) {
Component fromComponent = allComponents.stream()
.filter(c -> c.getName().equals(edge.getFromNode().getId()))
.findFirst().orElse(null);
if (fromComponent != null) {
final List toNodeNames = edge.getToNodes().stream()
.map(RelationshipGraphNode::getId).collect(Collectors.toList());
final List toComponents = allComponents.stream()
.filter(component -> toNodeNames.contains(component.getName()))
.collect(Collectors.toList());
bom.addComponent(fromComponent);
fromComponent.setComponents(toComponents);
}
} else if (edge.getRelationshipType().equals(RelationshipType.DESCRIBES)) {
List toNodeNames = edge.getToNodes().stream()
.map(RelationshipGraphNode::getId)
.collect(Collectors.toList());
final List toComponents = allComponents.stream()
.filter(component -> toNodeNames.contains(component.getName()))
.collect(Collectors.toList());
toComponents.forEach(bom::addComponent);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy