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