com.metaeffekt.artifact.analysis.converter.InventoryToCycloneDxBom Maven / Gradle / Ivy
/*
* 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.converter;
import com.fasterxml.jackson.databind.JsonNode;
import com.metaeffekt.artifact.analysis.spdxbom.LicenseStringConverter;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.vulnerability.CommonEnumerationUtil;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.terms.model.NormalizationMetaData;
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.cyclonedx.model.license.Expression;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.metaeffekt.core.inventory.processor.model.Inventory;
import us.springett.parsers.cpe.Cpe;
import javax.xml.parsers.ParserConfigurationException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* This class converts a given inventory into CycloneDx Bom.
* Set the Bom version using {@link #setVersion(Version)} or {@link #setVersion(String)}.
* Attributes that are converted:
*
* - Name/Id
* - Component
* - Version
* - Group Id
* - Description
* - Checksum
* - Cpe
* - Other vulnerability scanning relevant data
*
*/
public class InventoryToCycloneDxBom {
public static final String KEY_COMPONENT_SPECIFIED_LICENSE = "Component Specified License";
private Version version = Version.VERSION_16;
private final LicenseStringConverter licenseStringConverter;
private boolean prettyPrint = true;
/**
* @param normalizationMetaData The normalization metadata has to be passed in order for the licenses to be
* converted.
*/
public InventoryToCycloneDxBom(NormalizationMetaData normalizationMetaData) {
this(normalizationMetaData, null);
}
public InventoryToCycloneDxBom(NormalizationMetaData normalizationMetaData,
Map spdxStringAssessments) {
licenseStringConverter = new LicenseStringConverter(normalizationMetaData, spdxStringAssessments);
}
public void setVersion(Version version) {
this.version = version;
}
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
}
public void setVersion(String versionString) {
Version version = Arrays.stream(Version.values())
.filter(v -> v.getVersionString().equals(versionString))
.findFirst().orElseThrow(() -> new IllegalStateException("Unknown version " + versionString + "."));
setVersion(version);
}
private String generatedXmlBom = null, generatedJsonBom = null;
/**
* Converts the inventory into a JSON and XML bom and stores the result.
* Use {@link #getGeneratedJsonBom()} and {@link #getGeneratedXmlBom()} to get the generated boms.
*
* @param inventory The inventory to convert.
* @throws ParserConfigurationException If the XML bom could not be generated.
*/
public void convert(Inventory inventory) throws ParserConfigurationException {
Bom bom = convertInventoryToBom(inventory);
BomXmlGenerator xml = BomGeneratorFactory.createXml(version, bom);
xml.generate();
generatedXmlBom = xml.toString();
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();
}
/**
* Run {@link #convert(Inventory)} first.
*
* @return The generated JSON Bom.
*/
public String getGeneratedJsonBom() {
return generatedJsonBom;
}
/**
* Run {@link #convert(Inventory)} first.
*
* @return The generated JSON Bom.
*/
public String getGeneratedXmlBom() {
return generatedXmlBom;
}
private Bom convertInventoryToBom(Inventory inventory) {
final Bom bom = new Bom();
inventory.getArtifacts().stream()
.map(this::createComponentFromArtifact)
.forEach(bom::addComponent);
return bom;
}
/**
* Converts the following field values of an {@link Artifact} into properties of a {@link Component}:
*
* - {@link Component#setName(String)} from
Id
* - {@link Component#setPublisher(String)} from
Component
* - {@link Component#setVersion(String)} from
Version
* - {@link Component#setGroup(String)} from
Group Id
* - {@link Component#setType(Component.Type)} to {@link Component.Type#LIBRARY}
* -
* {@link Component#setDescription(String)} from the first non-null of:
Description
,
* Summary
, Comment
*
* -
* {@link Component#setCpe(String)}:
*
* - Uses {@link CommonEnumerationUtil#parseEffectiveCpe(Artifact)} to find effective CPEs on artifact
* - If none are found or the only CPE is
cpe:/a:none:none
, skips setting property
* - Formats the string as CPE 2.2 or 2.3 as fallback
*
*
* - {@link Component#setPurl(String)} from
PURL
* -
* {@link Component#setLicenseChoice(LicenseChoice)}:
*
* -
* Uses {@link InventoryToCycloneDxBom#createLicenseExpression(String, boolean)} to map the artifact
*
License
s to {@link License} instances
*
*
*
* -
* {@link Component#setHashes(List)}: collect to list from every algorithm in {@link Hash.Algorithm}:
*
* - MD5:
Checksum
* - other:
Checksum ($algorithm)
*
*
* -
* {@link Component#setProperties(List)} from these fields if available:
*
* MS Product ID
* MS Knowledge Base ID
* Addon CVEs
* Inapplicable CVE
* CPE URIs
* Derived CPE URIs
* Additional CPE URIs
* URL
* Comment
*
*
*
*
* @param artifact The artifact to create the component from.
* @return The converted component.
*/
private Component createComponentFromArtifact(Artifact artifact) {
final Component component = new Component();
component.setName(artifact.getId());
component.setPublisher(artifact.getComponent());
component.setVersion(artifact.getVersion());
component.setGroup(artifact.getGroupId());
component.setType(Component.Type.LIBRARY);
component.setDescription(StringUtils.nonNull(artifact.get("Description"), artifact.get("Summary"), artifact.get(Artifact.Attribute.COMMENT)));
final List effectiveArtifactCpes = CommonEnumerationUtil.parseEffectiveCpe(artifact);
if (!effectiveArtifactCpes.isEmpty()
&& !effectiveArtifactCpes.get(0).getVendor().equals("none")
&& !effectiveArtifactCpes.get(0).getProduct().equals("none")) {
final String cpeString = CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(effectiveArtifactCpes.get(0));
component.setCpe(cpeString);
}
component.setPurl(artifact.get(InventoryAttribute.PURL.getKey()));
component.setLicenses(createLicenseChoice(artifact));
final List hashes = new ArrayList<>();
if (component.getHashes() != null) {
hashes.addAll(component.getHashes());
}
if (artifact.getChecksum() != null) {
hashes.add(new Hash(Hash.Algorithm.MD5, artifact.getChecksum()));
}
for (Hash.Algorithm algorithm : Hash.Algorithm.values()) {
final String spec = algorithm.getSpec();
{
// legacy; remove once obsolete
final String value = artifact.get(Artifact.Attribute.CHECKSUM.getKey() + " (" + spec + ")");
if (value != null) {
hashes.add(new Hash(algorithm, value));
}
}
{
final String value = artifact.get("Hash (" + spec + ")");
if (value != null) {
hashes.add(new Hash(algorithm, value));
}
}
}
component.setHashes(hashes);
final List properties = new ArrayList<>();
checkForProperty(artifact, InventoryAttribute.MS_PRODUCT_ID.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.MS_KB_IDENTIFIER.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.ADDON_CVES.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.INAPPLICABLE_CVE.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.INITIAL_CPE_URIS.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.DERIVED_CPE_URIS.getKey(), properties);
checkForProperty(artifact, InventoryAttribute.ADDITIONAL_CPE.getKey(), properties);
checkForProperty(artifact, Artifact.Attribute.URL.getKey(), properties);
checkForProperty(artifact, Artifact.Attribute.COMMENT.getKey(), properties);
if (properties.size() > 0) component.setProperties(properties);
return component;
}
private LicenseChoice createLicenseChoice(Artifact artifact) {
final String curatedAssociatedLicenses = artifact.getLicense();
final String originalLicenses = artifact.get(KEY_COMPONENT_SPECIFIED_LICENSE);
if (StringUtils.hasText(curatedAssociatedLicenses)) {
final LicenseChoice licenseChoice = createLicenseChoice(curatedAssociatedLicenses, true);
if (licenseChoice != null) return licenseChoice;
} else if (StringUtils.hasText(originalLicenses)) {
final LicenseChoice licenseChoice = createLicenseChoice(originalLicenses, false);
if (licenseChoice != null) return licenseChoice;
}
return null;
}
private LicenseChoice createLicenseChoice(String licenses, boolean toSpdx) {
final Expression licenseExpression = createLicenseExpression(licenses, toSpdx);
if (licenseExpression != null) {
final LicenseChoice licenseChoice = new LicenseChoice();
licenseChoice.setExpression(licenseExpression);
return licenseChoice;
}
return null;
}
private void checkForProperty(Artifact artifact, String key, List properties) {
if (artifact.get(key) != null) {
Property property = new Property();
property.setName(key);
property.setValue(artifact.get(key));
properties.add(property);
}
}
/**
* Creates an Spdx License Expression from an artifact license string.
*
* @param artifactLicense An artifact license string to be converted into an spdx expression.
* @param toSpdx Whether to convert to an SPDX identified (required TMD).
*
* @return Returns a converted Spdx expression string.
*/
private Expression createLicenseExpression(String artifactLicense, boolean toSpdx) {
if (artifactLicense != null) {
if (toSpdx) {
final LicenseStringConverter.ToSpdxResult toSpdxResult =
licenseStringConverter.licenseStringToSpdxExpression(artifactLicense);
if (!StringUtils.isEmpty(toSpdxResult.getExpression())) {
return new Expression(toSpdxResult.getExpression());
}
} else {
return new Expression(artifactLicense);
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy