All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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