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

com.metaeffekt.artifact.analysis.bom.cyclonedx.ComponentMapper Maven / Gradle / Ivy

The newest version!
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.BomConstants;
import com.metaeffekt.artifact.analysis.bom.LicenseProcessor;
import com.metaeffekt.artifact.analysis.bom.spdx.DocumentSpec;
import com.metaeffekt.artifact.analysis.bom.spdx.LicenseStringConverter;
import com.metaeffekt.artifact.analysis.bom.spdx.facade.SpdxApiFacade;
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.apache.commons.lang3.StringUtils;
import org.cyclonedx.model.*;
import org.cyclonedx.model.license.Expression;
import org.cyclonedx.util.LicenseResolver;
import org.metaeffekt.common.notice.model.NoticeParameters;
import org.metaeffekt.core.inventory.processor.model.*;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

@Slf4j
@Getter
@Setter
public class ComponentMapper {

    public DocumentSpec documentSpec;
    public Set approvedAttributes;
    public Inventory inventory;
    public LicenseStringConverter licenseStringConverter;
    public LicenseTextProvider licenseTextProvider;
    public NormalizationMetaData normalizationMetaData;
    public Set referencedLicenses = new HashSet<>();

    private Map customLicenseMappings;
    private Set writtenAttributes;
    private int currentId;

    /**
     * Maps all assets and artefacts contained in an inventory to a cyclonedx component and translates inventory fields
     * into cyclonedx fields while filtering and converting the information contained within if necessary. All
     * operations directly influencing the fields of a component should be performed here.
     *
     * @param documentSpec           contains metadata and parameters influencing what information will be mapped
     * @param approvedAttributes     list of approved attributes
     * @param inventory              inventory from which to extract information
     * @param licenseStringConverter converts licenses into expressions
     * @param normalizationMetaData  used for licensing
     * @param licenseTextProvider    provides license texts
     */
    public ComponentMapper(DocumentSpec documentSpec, File approvedAttributes, Inventory inventory,
                           LicenseStringConverter licenseStringConverter, NormalizationMetaData normalizationMetaData,
                           LicenseTextProvider licenseTextProvider, Map customLicenseMappings) {
        this.documentSpec = documentSpec;
        this.inventory = inventory;
        this.licenseStringConverter = licenseStringConverter;
        this.licenseTextProvider = licenseTextProvider;
        this.normalizationMetaData = normalizationMetaData;
        this.writtenAttributes = new HashSet<>();
        this.customLicenseMappings = customLicenseMappings;
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            if (approvedAttributes != null && approvedAttributes.length() > 0) {
                List list = objectMapper.readValue(approvedAttributes, List.class);
                this.approvedAttributes = new HashSet<>(list);
            } else {
                this.approvedAttributes = null;
            }
        } catch (IOException e) {
            log.error("ComponentMapper failed to read approvedAttributes File. If all attributes are supposed to be "
                    + "mapped consider setting approvedAttributes parameter for ComponentMapper to null.");
            throw new RuntimeException(e);
        }
    }

    /**
     * Entry point to call when wanting to map a artefact / asset to a component.
     *
     * @param modelBase abstract class which artefact and asset extends. This is used to avoid redundancy and ensure
     *                  the same component quality when mapping artefacts / assets.
     * @return a component which is fully enriched.
     */
    public Component map(AbstractModelBase modelBase) {
        Component component = new Component();

        mapBaseComponent(component, modelBase);
        mapChecksums(component, modelBase);
        mapProjects(component, modelBase);
        mapFileTypeInformation(component, modelBase);
        mapApprovedAttributes(component, modelBase);
        mapType(component, modelBase);

        if (modelBase instanceof Artifact) {
            mapLicenses(component, (Artifact) modelBase);
        }

        return component;
    }

    /**
     * Maps basic information to a component which should be present most of the time or completely depending on in
     * which stage the input inventory was created.
     *
     * @param component the component
     * @param modelBase the artefact or asset
     */
    private void mapBaseComponent(Component component, AbstractModelBase modelBase) {

        // Unique ID
        component.setBomRef(getNextId());

        List nameList = Arrays.asList(Constants.KEY_ID, Constants.KEY_ASSET_ID);
        setIfNotBlank(Component::setName, component, modelBase, nameList);

        List urlList = Collections.singletonList(Constants.KEY_URL);
        setIfNotBlank(Component::setPublisher, component, modelBase, urlList);

        List versionList = Collections.singletonList(Constants.KEY_VERSION);
        setIfNotBlank(Component::setVersion, component, modelBase, versionList);

        List groupsList = Collections.singletonList(Constants.KEY_GROUP_ID);
        setIfNotBlank(Component::setGroup, component, modelBase, groupsList);

        List purlList = Collections.singletonList(Constants.KEY_PURL);
        setIfNotBlank(Component::setPurl, component, modelBase, purlList);
    }

    /**
     * Maps hashes and checksums to the component.
     *
     * @param component the component
     * @param modelBase the artefact / asset
     */
    private void mapChecksums(Component component, AbstractModelBase modelBase) {
        if (StringUtils.isNotEmpty(modelBase.get(Constants.KEY_HASH_SHA1))) {
            component.addHash(new Hash(Hash.Algorithm.SHA1, modelBase.get(Constants.KEY_HASH_SHA1)));
            writtenAttributes.add(Constants.KEY_HASH_SHA1);
        }
        if (StringUtils.isNotEmpty(modelBase.get(Constants.KEY_HASH_SHA256))) {
            component.addHash(new Hash(Hash.Algorithm.SHA_256, modelBase.get(Constants.KEY_HASH_SHA256)));
            writtenAttributes.add(Constants.KEY_HASH_SHA256);
        }
        if (StringUtils.isNotEmpty(modelBase.get(Constants.KEY_CHECKSUM))) {
            component.addHash(new Hash(Hash.Algorithm.MD5, modelBase.get(Constants.KEY_CHECKSUM)));
            writtenAttributes.add(Constants.KEY_CHECKSUM);
        }
        if (StringUtils.isNotEmpty(modelBase.get(Constants.KEY_HASH_SHA512))) {
            component.addHash(new Hash(Hash.Algorithm.SHA_512, modelBase.get(Constants.KEY_HASH_SHA512)));
            writtenAttributes.add(Constants.KEY_HASH_SHA512);
        }
        if (StringUtils.isNotEmpty(modelBase.get(Constants.KEY_DIGEST)) && modelBase.get(Constants.KEY_DIGEST)
                .startsWith("sha256")) {
            component.addHash(new Hash(Hash.Algorithm.SHA_256, modelBase.get(Constants.KEY_DIGEST).substring(7)));
            writtenAttributes.add(Constants.KEY_DIGEST);
        }
    }

    /**
     * Maps the path where the artefact / asset was located.
     *
     * @param component the component
     * @param modelBase the artefact / asset
     */
    private void mapProjects(Component component, AbstractModelBase modelBase) {
        if (StringUtils.isNotEmpty(modelBase.get(Artifact.Attribute.PROJECTS))) {
            Property property = new Property();
            property.setName("Path");
            property.setValue(
                    SpdxApiFacade.getFilePathFromProjectLocations(modelBase.get(Artifact.Attribute.PROJECTS)));
            component.addProperty(property);
            writtenAttributes.add(Constants.KEY_PROJECTS);
        }
    }

    /**
     * Uses a typeMap.json file to map internal types in our inventories to official types supported by cyclonedx. To
     * add support for more types expands the typeMap file.
     *
     * @param component the component
     * @param modelBase the artefact / asset
     */
    private void mapType(Component component, AbstractModelBase modelBase) {
        // Which types to look for was consolidated from many different test inventories and their respective
        // type-field values. The data was provided by YWT.

        if (StringUtils.isBlank(modelBase.get(Constants.KEY_TYPE))) {
            return;
        }

        ObjectMapper objectMapper = new ObjectMapper();
        try {
            JsonNode jsonNode = objectMapper.readTree(getClass().getResource("/sbom/typeMap.json"));
            Iterator> iterator = jsonNode.fields();
            while (iterator.hasNext()) {
                Map.Entry entry = iterator.next();
                List valuesList = new ArrayList<>();
                if (entry.getValue().isArray()) {
                    entry.getValue().forEach(e -> valuesList.add(e.asText()));
                }
                if (valuesList.contains(modelBase.get(Constants.KEY_TYPE))) {
                    component.setType(Component.Type.valueOf(entry.getKey()));
                }
                writtenAttributes.add(Constants.KEY_TYPE);
                // FIXME: Some Assets do not have their respective Types mapped correctly, for example composite ->
                //  container is probably not correct. (Check json file typeMap.json)
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Maps file type information to the component.
     *
     * @param component the component
     * @param modelBase the asset / artefact
     */
    private void mapFileTypeInformation(Component component, AbstractModelBase modelBase) {
        final String archive = modelBase.get("Archive");
        final String structured = modelBase.get("Structured");
        final String executable = modelBase.get("Executable");

        if (StringUtils.isNotBlank(archive)) {
            Property archiveProperty = new Property();
            archiveProperty.setName("Archive");
            archiveProperty.setValue(archive);
            component.addProperty(archiveProperty);
            writtenAttributes.add(Constants.KEY_ARCHIVE);

            if (StringUtils.isNotBlank(structured)) {
                Property structuredProperty = new Property();
                structuredProperty.setName("Structured");
                structuredProperty.setValue(structured);
                component.addProperty(structuredProperty);
                writtenAttributes.add(Constants.KEY_STRUCTURED);
            }
        }
        if (StringUtils.isNotBlank(executable)) {
            Property executableProperty = new Property();
            executableProperty.setName("Executable");
            executableProperty.setValue(executable);
            component.addProperty(executableProperty);
            writtenAttributes.add(Constants.KEY_EXECUTABLE);
        }
    }

    /**
     * This method maps all attributes which have not already been written to a field. If approvedAttributes is null
     * instead of a json File containing attributes the mapper will gather every single artifact attribute and value.
     * This method should ALYWAYS be called after all other mapper methods have run.
     *
     * @param component         CycloneDX component to fill
     * @param abstractModelBase Inventory component from which to gather information from
     */
    private void mapApprovedAttributes(Component component, AbstractModelBase abstractModelBase) {

        if (documentSpec.isIncludeTechnicalProperties()) {
            mapTechnialProperties(component, abstractModelBase);
        }

        if (approvedAttributes != null) {
            for (String attribute : approvedAttributes) {
                Property property = new Property();
                if (StringUtils.isNotBlank(abstractModelBase.get(attribute))) {
                    property.setValue(abstractModelBase.get(attribute));
                    property.setName(attribute);
                    addPropertyIfFieldNotDuplicate(component, property);
                }
            }
        }

        // ID is additionally added as a property because the component name might be changed in further down the chain,
        // for example before the bom is submitted to a customer.
        String idKey = Constants.KEY_ID;
        if (abstractModelBase instanceof AssetMetaData) {
            idKey = Constants.KEY_ASSET_ID;
        }
        Property idProperty = new Property();
        idProperty.setName(BomConstants.METAEFFEKT_ID);
        idProperty.setValue(abstractModelBase.get(idKey));
        component.addProperty(idProperty);

    }

    private void mapTechnialProperties(Component component, AbstractModelBase modelBase) {
        Property property = new Property();
        property.setName(BomConstants.INVENTORY_CLASS);
        if (modelBase instanceof Artifact) {
            property.setValue("artifact");
        } else if (modelBase instanceof AssetMetaData) {
            property.setValue("asset");
        } else {
            log.error("{} is not an Artifact or Asset. That should not be possible.", modelBase.toString());
        }
        component.addProperty(property);
    }

    /**
     * Maps licenses and their texts as either single licenses or license expressions to a component. Makes heavy use
     * of helper methods in {@link LicenseProcessor} to gather licenses information.
     *
     * @param component the component
     * @param artifact  in this case artifact specifically as assets don't have licenses attached.
     */
    private void mapLicenses(Component component, Artifact artifact) {
        final NoticeParameters noticeParameters = LicenseProcessor.readNoticeParameters(artifact);

        if (noticeParameters == null) {
            return;
        }

        List effectiveLicenses = LicenseProcessor.aggregateEffectiveLicenses(noticeParameters);
        List associatedLicenses = noticeParameters.aggregateAssociatedLicenses();

        if (effectiveLicenses.isEmpty() || effectiveLicenses.stream().anyMatch(StringUtils::isBlank)
                || associatedLicenses.isEmpty() || associatedLicenses.stream().anyMatch(StringUtils::isBlank)) {
            return;
        }

        List concludedLicenseResults =
                effectiveLicenses.stream()
                    .map(s -> licenseStringConverter.licenseStringToSpdxExpression(s))
                    .collect(Collectors.toList());

        List declaredLicenseResults =
                associatedLicenses.stream()
                    .map(s -> licenseStringConverter.licenseStringToSpdxExpression(s))
                    .collect(Collectors.toList());

        if (!documentSpec.isUseLicenseExpressions()) {
            if (!concludedLicenseResults.isEmpty()) {
                addSingleLicenses(concludedLicenseResults, documentSpec.isIncludeLicenseTexts(), "concluded",
                        component);
            }
            if (!declaredLicenseResults.isEmpty()) {
                addSingleLicenses(declaredLicenseResults, documentSpec.isIncludeLicenseTexts(), "declared", component);
            }
        } else {
            if (!concludedLicenseResults.isEmpty()) {
                addLicenseExpression(concludedLicenseResults, component, "concluded");
            } else if (!declaredLicenseResults.isEmpty()) {
                addLicenseExpression(declaredLicenseResults, component, "declared");
            }
        }
    }

    /**
     * Responsible for adding single licenses to the component as well as their respective license texts. Still uses
     * some relics from the old license processing pipeline.
     *
     * @param results             a list of licenses attached to the artifact
     * @param includeLicenseTexts switch for if to include license texts or not
     * @param acknowledgement     either derived or concluded
     * @param component           the component
     */
    private void addSingleLicenses(List results, boolean includeLicenseTexts,
                                   String acknowledgement, Component component) {
        LicenseChoice licenseChoice = new LicenseChoice();
        for (LicenseStringConverter.ToSpdxResult result : results) {
            if (StringUtils.isBlank(result.getExpression())) {
                return;
            }

            // Replaces the existing expression with a custom license mapping if it exists.
            String expression = result.getExpression();
            for (String licenseValue : result.getReferencedMissingTmd()) {
                if (customLicenseMappings != null && customLicenseMappings.containsKey(licenseValue)) {
                    expression = customLicenseMappings.get(licenseValue);
                }
            }

            LicenseChoice licenseChoiceResolved = LicenseResolver.resolve(expression,
                    new LicenseResolver.LicenseTextSettings(includeLicenseTexts,
                            LicenseResolver.LicenseEncoding.NONE));

            if (licenseChoiceResolved != null) {
                licenseChoiceResolved.getLicenses().forEach(e -> {
                    e.setAcknowledgement(acknowledgement);
                    licenseChoice.addLicense(e);
                });
                continue;
            }
            Map licenseToText =
                    LicenseProcessor.getReferencedLicenseText(result.getReferencedLicenses(), licenseTextProvider,
                            normalizationMetaData);

            License license = new License();
            if (includeLicenseTexts && licenseToText != null) {
                AttachmentText attachmentText = new AttachmentText();
                attachmentText.setText(licenseToText.get(expression));
                license.setLicenseText(attachmentText);
            }
            license.setName(expression);
            license.setAcknowledgement(acknowledgement);
            licenseChoice.addLicense(license);
        }
        component.setLicenses(licenseChoice);
    }

    /**
     * Responsible for adding licenses expressions to a component as well as their respective license texts. Still uses
     * some relics from the old license processing pipeline.
     *
     * @param results         a list of licenses attached to the artifact
     * @param component       the component
     * @param acknowledgement either derived or concluded
     */
    private void addLicenseExpression(List results, Component component,
                                      String acknowledgement) {

        final StringJoiner joiner = new StringJoiner(" AND ", "(", ")");

        for (LicenseStringConverter.ToSpdxResult converterResult : results) {
            if (StringUtils.isNotBlank(converterResult.getExpression())) {

                // Replaces the existing expression with a custom license mapping if it exists.
                String expression = converterResult.getExpression();
                for (String licenseValue : converterResult.getReferencedMissingTmd()) {
                    if (customLicenseMappings != null && customLicenseMappings.containsKey(licenseValue)) {
                        expression = customLicenseMappings.get(licenseValue);
                    }
                }

                LicenseChoice hasLicenseResolved = LicenseResolver.resolve(expression);
                if (hasLicenseResolved == null) {
                    referencedLicenses.addAll(converterResult.getReferencedLicenses());
                }
                // Expressions which contain exceptions already are in parentheses
                if (expression.startsWith("(") && expression.endsWith(")")) {
                    joiner.add(expression);
                } else {
                    joiner.add("(" + expression + ")");
                }
            }
        }
        LicenseChoice licenseChoice = new LicenseChoice();
        Expression expression = new Expression(joiner.toString());
        expression.setAcknowledgement(acknowledgement);
        licenseChoice.setExpression(expression);
        component.setLicenses(licenseChoice);
    }

    /**
     * Checks if the name of a property to be added already matches the name of a cyclonedx component field. This is
     * mostly done to preserve information we might want to track for the CycloneDXImporter.
     *
     * @param component will contain the property
     * @param property  name will be checked for redundancy.
     */
    private void addPropertyIfFieldNotDuplicate(Component component, Property property) {
        if (property.getName().equals(Constants.KEY_TYPE) && documentSpec.isIncludeTechnicalProperties()) {
            property.setName(BomConstants.SPECIFIC_TYPE);
        }

        // Check if the attribute is already present in another field.
        if (!writtenAttributes.contains(property.getName())) {
            if (!property.getValue().equals(Constants.MARKER_CROSS) || !property.getValue()
                    .equals(Constants.MARKER_CONTAINS)) {
                component.addProperty(property);
            }
        }
    }

    /**
     * Helper method which sets a component field if the value is not null or empty, implemented to avoid redundancy.
     *
     * @param setter    setter method of the component
     * @param component the component
     * @param modelBase the artifact or asset
     * @param keys      list of keys, a list is used because artifact and asset have different attribute keys for the same
     *                  type of value.
     */
    private void setIfNotBlank(BiConsumer setter, Component component, AbstractModelBase modelBase,
                               List keys) {

        for (String key : keys) {
            String value = modelBase.get(key);
            if (StringUtils.isNotBlank(value)) {
                setter.accept(component, value);
            }
            writtenAttributes.add(key);
        }
    }

    /**
     * Very sophisticated ID system.
     *
     * @return the next numerical ID with a prefix attached.
     */
    private String getNextId() {
        currentId += 1;
        return documentSpec.getOrganizationUrl() + "Component-" + currentId;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy