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

com.metaeffekt.artifact.analysis.converter.CycloneDxBomToInventory Maven / Gradle / Ivy

There is a newer version: 0.132.0
Show 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.converter;

import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.vulnerability.CommonEnumerationUtil;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.cyclonedx.exception.ParseException;
import org.cyclonedx.model.*;
import org.cyclonedx.parsers.BomParserFactory;
import org.cyclonedx.parsers.Parser;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.metaeffekt.core.inventory.processor.model.ArtifactType;
import org.metaeffekt.core.inventory.processor.model.Constants;
import org.metaeffekt.core.inventory.processor.model.Inventory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * This class converts a given CycloneDx Bom into an Inventory.
* Attributes that are converted: *
    *
  • Name/Id
  • *
  • Component
  • *
  • Version
  • *
  • Group Id
  • *
  • Description
  • *
  • Cpe
  • *
  • Checksum
  • *
*/ public class CycloneDxBomToInventory { private final static Logger LOG = LoggerFactory.getLogger(CycloneDxBomToInventory.class); public CycloneDxBomToInventory() { } /** * Boolean indicating whether the conversion process shoud include the metadata-level component. *

* False by default for compatibility reasons. */ @Getter @Setter private boolean includeMetadataComponent = false; /** * Read a Bom from a JSON/XML file and convert it into an inventory. * * @param bomFile The file to convert. * @return The converted inventory. * @throws ParseException Thrown if the file is neither XML nor JSON. */ public Inventory convert(File bomFile) throws ParseException { // create json or xml parser, detect from given file format final Parser parser = BomParserFactory.createParser(bomFile); final Bom parsedBom = parser.parse(bomFile); return convert(parsedBom); } /** * Convert a Bom directly into an inventory. * * @param bom The Bom to convert. * @return The converted inventory. */ public Inventory convert(Bom bom) { final Inventory inventory = new Inventory(); final List artifacts = inventory.getArtifacts(); String assetId = null; if (includeMetadataComponent) { final Metadata metadata = bom.getMetadata(); if (metadata != null) { final Component metadataComponent = metadata.getComponent(); if (metadataComponent != null) { final Artifact artifact = createArtifactFromComponent(metadataComponent); artifacts.add(artifact); assetId = "AID-" + artifact.getId(); // include md5 in assetId if available final List hashes = metadataComponent.getHashes(); if (hashes != null) { Optional md5Hash = hashes.stream().filter(h -> "MD5".equals(h.getAlgorithm())).findFirst(); if (md5Hash.isPresent()) { assetId = "AID-" + artifact.getId() + "-" + md5Hash; } } artifact.set(assetId, Constants.MARKER_CROSS); } } } final List components = Optional .ofNullable(bom.getComponents()) .orElse(new ArrayList<>()); for (Component component : components) { final Artifact artifact = createArtifactFromComponent(component); if (assetId != null) { artifact.set(assetId, Constants.MARKER_CONTAINS); } artifacts.add(artifact); } return inventory; } /** * Converts the following properties from a {@link Component} into field values of an {@link Artifact}: *

    *
  • * If present, the values from {@link Component#getProperties()} that are: *
      *
    • known attributes in {@link InventoryAttribute}
    • *
    • URL
    • *
    • Comment
    • *
    *
  • *
  • * If not set via the {@link Component#getProperties()}, the * {@link InventoryAttribute#ADDITIONAL_CPE} from {@link Component#getCpe()} *
  • *
  • Id from {@link Component#getName()}-{@link Component#getVersion()}
  • *
  • Component from {@link Component#getPublisher()}, or as a fallback {@link Component#getName()}
  • *
  • Version from {@link Component#getVersion()}
  • *
  • Group Id from {@link Component#getGroup()}
  • *
  • Description from {@link Component#getDescription()}
  • *
  • Type from {@link Component#getPurl()} or {@link Component#getType()}
  • *
  • License attempts to parse * {@link Component#getLicenseChoice()}{@link LicenseChoice#getExpression() .getExpression()}
  • *
  • * {@link Component#getHashes()} depending on algorithm: *
      *
    • MD5: Checksum
    • *
    • other: Checksum ($algorithm)
    • *
    *
  • *
  • * If the {@link Component#getPurl()} is set, and the following values are not yet set in the artifact, * these values will be extracted: *
      *
    • Ecosystem from {@link PackageURL#getNamespace()}
    • *
    • Id from {@link PackageURL#getName()}
    • *
    • Version from {@link PackageURL#getVersion()}
    • *
    • different attributes based on the namespace
    • *
    *
  • *
  • * If the {@link Component#getSupplier()}is set: *
      *
    • Organization from {@link OrganizationalEntity#getName()}
    • *
    • Organization URL from {@link OrganizationalEntity#getUrls()}, joined as CSV
    • *
    *
  • *
* * @param component The component to create the artifact from. * @return The converted artifact. */ private Artifact createArtifactFromComponent(Component component) { final Artifact artifact = new Artifact(); final List properties = component.getProperties(); if (properties != null) { for (InventoryAttribute value : InventoryAttribute.values()) { transferPropertyAttributeToArtifactIfAvailable(artifact, value.getKey(), properties); } transferPropertyAttributeToArtifactIfAvailable(artifact, Artifact.Attribute.URL.getKey(), properties); transferPropertyAttributeToArtifactIfAvailable(artifact, Artifact.Attribute.COMMENT.getKey(), properties); } // the CPE from the component may either be the only one listed, or there may be additional CPEs stored in the // Additional CPE attribute of the artifact; therefore these need to be merged. final Optional parsedFromComponent = CommonEnumerationUtil.parseCpe(component.getCpe()); if (parsedFromComponent.isPresent()) { final List parsedFromArtifact = CommonEnumerationUtil.parseCpes(artifact.get(InventoryAttribute.ADDITIONAL_CPE.getKey())); final List mergedCpes = CommonEnumerationUtil.distinctAndSortedWithWildcards(Collections.singletonList(parsedFromComponent.get()), parsedFromArtifact); artifact.set(InventoryAttribute.ADDITIONAL_CPE.getKey(), CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(mergedCpes)); } artifact.setId(component.getName()); artifact.setComponent(ObjectUtils.firstNonNull(component.getPublisher(), component.getName())); artifact.setVersion(component.getVersion()); artifact.setGroupId(component.getGroup()); artifact.set("Description", component.getDescription()); // NOTE: the license information from CycloneDx does not match the semantics of any common attribute; therefore, // we need to store the expression in a dedicated attribute artifact.set("Component Specified License", convertLicense(component)); if (component.getHashes() != null) { for (Hash hash : component.getHashes()) { final String algorithm = hash.getAlgorithm(); final String value = hash.getValue(); if (algorithm.equals("MD5")) { artifact.setChecksum(value); } else { artifact.set("Hash (" + algorithm + ")", value); } } } final String purl = component.getPurl(); if (purl != null) { artifact.set(InventoryAttribute.PURL.getKey(), purl); // parse purl and fill missing values in artifact // scheme:type/namespace/name@version?qualifiers#subpath try { PackageURL packageURL = new PackageURL(purl); if (!StringUtils.hasText(artifact.getId())) artifact.setId(packageURL.getName()); artifact.setComponent(packageURL.getName()); if (!StringUtils.hasText(artifact.getVersion())) artifact.setVersion(packageURL.getVersion()); extractEcosystemInformationFromPurlOntoArtifact(packageURL, artifact); } catch (MalformedPackageURLException e) { LOG.warn("Could not parse PURL [{}]", purl); } } final String name = artifact.getId(); final String componentValue = artifact.getComponent(); final String version = artifact.getVersion(); if (shouldVersionBeAppendedToName(name, version)) { artifact.setId(name + "-" + version); } else if (name != null && name.equals(componentValue)) { artifact.setComponent(removeVersionFromName(name, version)); } final OrganizationalEntity supplier = component.getSupplier(); if (supplier != null) { artifact.set(Constants.KEY_ORGANIZATION, supplier.getName()); artifact.set(Constants.KEY_ORGANIZATION_URL, String.join(", ", supplier.getUrls())); } // find type information from either purl or component type final String componentType = component.getType().getTypeName(); final String purlType = artifact.get(InventoryAttribute.ECOSYSTEM); final Pair derivedArtifactTypeInformation = deriveArtifactTypeFromComponentInformation(componentType, purlType); if (derivedArtifactTypeInformation.getLeft() != null) { artifact.set(Artifact.Attribute.TYPE, derivedArtifactTypeInformation.getLeft()); } if (derivedArtifactTypeInformation.getRight() != null) { artifact.set(Artifact.Attribute.COMPONENT_SOURCE_TYPE, derivedArtifactTypeInformation.getRight()); } // FIXME: support dependency tree by setting markers return artifact; } private Pair deriveArtifactTypeFromComponentInformation(String componentType, String purlType) { if (StringUtils.hasText(componentType)) { switch (componentType) { case "operating-system": return Pair.of(ArtifactType.OPERATING_SYSTEM.getCategory(), null); case "device": return Pair.of(ArtifactType.CATEGORY_HARDWARE.getCategory(), null); case "device-driver": return Pair.of(ArtifactType.DRIVER.getCategory(), null); case "firmware": return Pair.of(ArtifactType.BIOS.getCategory(), null); case "file": return Pair.of(ArtifactType.FILE.getCategory(), null); } } // FIXME: When Component Pattern Source and Artifact Type redefinition is complete, rewrite usage of these types here. // https://metaeffekt.atlassian.net/wiki/spaces/~712020304208f6c53c4d029d9adfab7c0a832f/pages/2975727617/Inventory+Attributes if (StringUtils.hasText(purlType)) { switch (purlType) { case "maven": return Pair.of("module", "jar-module"); case "npm": return Pair.of(ArtifactType.WEB_MODULE.getCategory(), null); case "nuget": return Pair.of("module", "nuget-module"); case "pypi": return Pair.of(ArtifactType.PYTHON_MODULE.getCategory(), null); case "deb": case "rpm": case "apk": case "portage": case "opkg": case "swid": return Pair.of(ArtifactType.LINUX_PACKAGE.getCategory(), null); } } return Pair.of(null, null); } private boolean shouldVersionBeAppendedToName(String name, String version) { if (StringUtils.isEmpty(version)) { return false; } if (version.equals("*")) { return false; } if (StringUtils.isEmpty(name)) { return false; } if (name.endsWith(".jar")) { return false; } // the name might already contain the version string as an exact match if (name.contains(version.split(" ")[0])) { return false; } return true; } private String removeVersionFromName(String name, String version) { if (StringUtils.isEmpty(name) || StringUtils.isEmpty(version)) { return name; } version = "-" + version; // try to find full version, then only the part before the first space if (name.contains(version)) { return name.replace(version, "").trim(); } else if (version.contains(" ")) { return name.replace(version.split(" ")[0], "").trim(); } return name; } private void extractEcosystemInformationFromPurlOntoArtifact(PackageURL packageURL, Artifact artifact) { final String type = packageURL.getType(); if (artifact.get(InventoryAttribute.ECOSYSTEM) == null) { artifact.set(InventoryAttribute.ECOSYSTEM, type); } final Optional optionalPackageUrlType = PackageUrlTypes.fromType(type); if (optionalPackageUrlType.isPresent()) { final PackageUrlTypes packageUrlType = optionalPackageUrlType.get(); if (packageUrlType == PackageUrlTypes.MAVEN || packageUrlType == PackageUrlTypes.BITBUCKET) { if (!StringUtils.hasText(artifact.getGroupId())) { artifact.setGroupId(packageURL.getNamespace()); } } } } private void transferPropertyAttributeToArtifactIfAvailable(Artifact artifact, String key, List properties) { for (Property property : properties) { if (property.getName() != null && property.getName().equals(key)) { artifact.set(key, property.getValue()); return; } } } private String convertLicense(Component component) { final LicenseChoice licenses = component.getLicenses(); if (licenses != null) { // "EITHER (list of SPDX licenses and/or named licenses) OR (tuple of one SPDX License Expression)" if (licenses.getExpression() != null) { return licenses.getExpression().getValue(); } final List licenseList = licenses.getLicenses(); if (licenseList != null) { return licenseList.stream().map(l -> licenseToName(l)).collect(Collectors.joining(", ")); } } // NOTE: currently "licensing" is not supported return null; } private String licenseToName(License l) { if (l.getId() != null) return l.getId(); return l.getName(); } @Getter @AllArgsConstructor public enum PackageUrlTypes { DEBIAN("deb"), PYPI("pypi"), NPM("npm"), MAVEN("maven"), NUGET("nuget"), RPM("rpm"), GOLANG("golang"), CARGO("cargo"), GEM("gem"), GITHUB("github"), BITBUCKET("bitbucket"); private final String type; /** * Returns the {@link PackageUrlTypes} instance matching the given type string. * * @param type The type string to match. * @return The matching {@link PackageUrlTypes} instance, if any. */ public static Optional fromType(String type) { for (PackageUrlTypes packageUrlType : values()) { if (packageUrlType.getType().equals(type)) { return Optional.of(packageUrlType); } } return Optional.empty(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy