com.metaeffekt.artifact.analysis.vulnerability.CommonEnumerationUtil 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.vulnerability;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimeline;
import com.metaeffekt.mirror.contents.vulnerability.VulnerableSoftwareVersionRangeCpe;
import org.apache.commons.lang3.tuple.Pair;
import org.metaeffekt.core.inventory.processor.model.AbstractModelBase;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.metaeffekt.core.inventory.processor.model.Inventory;
import org.metaeffekt.core.inventory.processor.model.VulnerabilityMetaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;
import us.springett.parsers.cpe.CpeParser;
import us.springett.parsers.cpe.exceptions.CpeEncodingException;
import us.springett.parsers.cpe.exceptions.CpeParsingException;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
import us.springett.parsers.cpe.values.Part;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Utility class for CPE related tasks.
*
* -
* CPE 2.2 specification
* cpe:/ {part} : {vendor} : {product} : {version} : {update} : {edition} : {language}
*
* -
* CPE 2.3 specification
* cpe : {cpe_version} : {part} : {vendor} : {product} : {version} : {update} : {edition} : {language} : {sw_edition} : {target_sw} : {target_hw} : {other}
*
*
*/
public class CommonEnumerationUtil {
private final static Logger LOG = LoggerFactory.getLogger(CommonEnumerationUtil.class);
public static String reduceCPEUri(String uri) {
int colonIndex1 = uri.indexOf(":");
if (colonIndex1 == -1) return uri;
int colonIndex2 = uri.substring(colonIndex1 + 1).indexOf(":");
if (colonIndex2 == -1) return uri;
int colonIndex3 = uri.substring(colonIndex1 + colonIndex2 + 2).indexOf(":");
if (colonIndex3 == -1) return uri;
int colonIndex4 = uri.substring(colonIndex1 + colonIndex2 + colonIndex3 + 3).indexOf(":");
if (colonIndex4 == -1) return uri;
return uri.substring(0, colonIndex1 + colonIndex2 + colonIndex3 + colonIndex4 + 3);
}
public static List reduceCpeUrisToCommonParts(String... uris) throws CpeValidationException {
if (uris == null) return Collections.emptyList();
final List cpeUris = Arrays.stream(uris)
.map(CommonEnumerationUtil::parseCpe)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
final List commonCpeUris = new ArrayList<>();
for (Cpe uri : cpeUris) {
boolean found = false;
for (Cpe knownCpe : commonCpeUris) {
if (compareCpePartWithoutWildcards(uri.getPart().getAbbreviation(), knownCpe.getPart().getAbbreviation())
&& compareCpePartWithoutWildcards(uri.getVendor(), knownCpe.getVendor())
&& compareCpePartWithoutWildcards(uri.getProduct(), knownCpe.getProduct())) {
CpeBuilder commonCpePartsBuilder = CommonEnumerationUtil.builder().from(knownCpe);
if (!compareCpePartWithoutWildcards(uri.getVersion(), knownCpe.getVersion())) {
commonCpePartsBuilder.version("*");
}
if (!compareCpePartWithoutWildcards(uri.getUpdate(), knownCpe.getUpdate())) {
commonCpePartsBuilder.update("*");
}
if (!compareCpePartWithoutWildcards(uri.getEdition(), knownCpe.getEdition())) {
commonCpePartsBuilder.edition("*");
}
if (!compareCpePartWithoutWildcards(uri.getLanguage(), knownCpe.getLanguage())) {
commonCpePartsBuilder.language("*");
}
if (!compareCpePartWithoutWildcards(uri.getSwEdition(), knownCpe.getSwEdition())) {
commonCpePartsBuilder.swEdition("*");
}
if (!compareCpePartWithoutWildcards(uri.getTargetSw(), knownCpe.getTargetSw())) {
commonCpePartsBuilder.targetSw("*");
}
if (!compareCpePartWithoutWildcards(uri.getTargetHw(), knownCpe.getTargetHw())) {
commonCpePartsBuilder.targetHw("*");
}
if (!compareCpePartWithoutWildcards(uri.getOther(), knownCpe.getOther())) {
commonCpePartsBuilder.other("*");
}
commonCpeUris.remove(knownCpe);
commonCpeUris.add(commonCpePartsBuilder.build());
found = true;
break;
}
}
if (!found) {
commonCpeUris.add(uri);
}
}
return commonCpeUris;
}
private final static Function DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER = old -> toCpe22UriOrFallbackToCpe23FS(distinctAndSortedWithWildcards(parseCpes(old)));
public static void distinctAndSortedWithWildcards(Inventory inventory) {
inventory.getArtifacts().forEach(CommonEnumerationUtil::distinctAndSortedWithWildcards);
inventory.getVulnerabilityMetaData().forEach(CommonEnumerationUtil::distinctAndSortedWithWildcards);
}
public static void distinctAndSortedWithWildcards(Artifact artifact) {
transformAbstractModelBaseAttribute(artifact, InventoryAttribute.INITIAL_CPE_URIS.getKey(), DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER);
transformAbstractModelBaseAttribute(artifact, InventoryAttribute.ADDITIONAL_CPE.getKey(), DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER);
transformAbstractModelBaseAttribute(artifact, InventoryAttribute.INAPPLICABLE_CPE.getKey(), DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER);
transformAbstractModelBaseAttribute(artifact, InventoryAttribute.DERIVED_CPE_URIS.getKey(), DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER);
}
public static void distinctAndSortedWithWildcards(VulnerabilityMetaData vmd) {
transformAbstractModelBaseAttribute(vmd, VulnerabilityMetaData.Attribute.PRODUCT_URIS.getKey(), DISTINCT_AND_SORTED_WITH_WILDCARDS_MAPPER);
}
private static void transformAbstractModelBaseAttribute(AbstractModelBase element, String attributeKey, Function transformation) {
if (StringUtils.hasText(element.get(attributeKey))) {
element.set(attributeKey, transformation.apply(element.get(attributeKey)));
}
}
public static List parseCpe(Collection cpe) {
return cpe.stream()
.map(CommonEnumerationUtil::parseCpe)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
public static List parseCpe(AbstractModelBase vmd, String attribute) {
final String value = vmd.getComplete(attribute);
if (value != null) {
return parseCpe(Arrays.asList(value.split(", ")));
}
return Collections.emptyList();
}
/**
* Computes what CPEs are applicable on the given artifact. The different attributes are applied in this order:
*
* CPE URIs
: if set, these CPEs are parsed and returned directly without evaluating the other attributes
* Derived CPE URIs
: Add all CPEs if not exact match present
* Additional CPE URIs
: Add all CPEs if not exact match present
* Inapplicable CPE URIs
: Remove CPEs if wildcard match present
*
* The resulting list is sorted alphabetically.
*
* @param artifact The artifact to compute the CPEs for
* @return The list of applicable CPEs, empty if none are applicable
*/
public static List parseEffectiveCpe(Artifact artifact) {
final List initialCpes = parseCpe(artifact, InventoryAttribute.INITIAL_CPE_URIS.getKey());
if (initialCpes.size() > 0) {
return initialCpes;
}
final List derivedCpes = parseCpe(artifact, InventoryAttribute.DERIVED_CPE_URIS.getKey());
final List additionalCpes = parseCpe(artifact, InventoryAttribute.ADDITIONAL_CPE.getKey());
final List inapplicableCpes = parseCpe(artifact, InventoryAttribute.INAPPLICABLE_CPE.getKey());
final ArrayList resultingCpes = new ArrayList<>(derivedCpes);
for (Cpe cpe : additionalCpes) {
addCpeIfExactMatchAbsent(resultingCpes, cpe);
}
for (Cpe remove : inapplicableCpes) {
resultingCpes.removeIf(cpe -> compareCpeUsingWildcardsOneWay(cpe, remove));
}
return distinctAndSortedWithoutWildcards(resultingCpes);
}
public static List distinctAndSortedWithoutWildcards(Collection cpe) {
return cpe.stream()
.distinct()
.sorted(Cpe::compareTo)
.collect(Collectors.toList());
}
/**
* Returns a distinct and sorted list of Cpe objects from the provided collection.
* The method ensures uniqueness by comparing Cpe objects using wildcards.
* The returned list is sorted according to the natural ordering of Cpe objects.
*
* @param cpe the collection of Cpe objects to be processed
* @return a distinct and sorted list of Cpe objects
*/
public static List distinctAndSortedWithWildcards(Collection cpe) {
final List list = new ArrayList<>();
final Set uniqueValues = new HashSet<>();
for (Cpe c : cpe) {
if (uniqueValues.add(c)) {
if (list.stream().noneMatch(c2 -> c != c2 && compareCpeUsingWildcards(c, c2))) {
list.add(c);
}
}
}
list.sort(Cpe::compareTo);
return list;
}
/**
* Returns a distinct and sorted list of Cpe objects from the provided collections.
* This method accepts multiple collections and combines them into a single list.
* The method ensures uniqueness by comparing Cpe objects using wildcards.
* The returned list is sorted according to the natural ordering of Cpe objects.
*
* @param cpe the collections of Cpe objects to be processed
* @return a distinct and sorted list of Cpe objects
*/
@SafeVarargs
public static List distinctAndSortedWithWildcards(Collection... cpe) {
return distinctAndSortedWithWildcards(Arrays.stream(cpe)
.flatMap(Collection::stream)
.collect(Collectors.toList()));
}
public static List parseEffectiveCpe(VulnerabilityMetaData vulnerability) {
if (vulnerability == null) {
return Collections.emptyList();
}
if (vulnerability.has(InventoryAttribute.ADDITIONAL_CPE.getKey())) {
LOG.warn("Key '{}' should only be used on artifacts, not on vulnerabilities", InventoryAttribute.ADDITIONAL_CPE.getKey());
} else if (vulnerability.has(InventoryAttribute.INAPPLICABLE_CPE.getKey())) {
LOG.warn("Key '{}' should only be used on artifacts, not on vulnerabilities", InventoryAttribute.INAPPLICABLE_CPE.getKey());
} else if (vulnerability.has(InventoryAttribute.INITIAL_CPE_URIS.getKey())) {
LOG.warn("Key '{}' should only be used on artifacts, not on vulnerabilities", InventoryAttribute.INITIAL_CPE_URIS.getKey());
} else if (vulnerability.has(InventoryAttribute.DERIVED_CPE_URIS.getKey())) {
LOG.warn("Key '{}' should only be used on artifacts, not on vulnerabilities", InventoryAttribute.DERIVED_CPE_URIS.getKey());
}
final List cpes = parseCpe(vulnerability, VulnerabilityMetaData.Attribute.PRODUCT_URIS.getKey());
cpes.sort(Cpe::compareTo);
return cpes;
}
public static void addCpeIfExactMatchAbsent(Collection cpes, Cpe add) {
if (cpes.stream().noneMatch(c -> compareCpeWithoutWildcards(c, add))) {
cpes.add(add);
}
}
public static List> getVendorProducts(Collection cpes) {
return cpes.stream()
.map(e -> Pair.of(e.getVendor(), e.getProduct()))
.distinct()
.collect(Collectors.toList());
}
public static List> getVendorProductsFromVersionRanges(Collection cpes) {
return cpes.stream()
.map(e -> Pair.of(e.getCpe().getVendor(), e.getCpe().getProduct()))
.distinct()
.collect(Collectors.toList());
}
public static List> getVendorProductsFromTimelines(Collection timelines) {
return timelines.stream()
.map(e -> Pair.of(e.getVendor(), e.getProduct()))
.distinct()
.collect(Collectors.toList());
}
public static Optional parseCPESilent(String cpe) {
return parseCpe(cpe, true);
}
public static Optional parseCpe(String cpe) {
return parseCpe(cpe, false);
}
public static List parseCpes(String cpe) {
if (cpe == null) return new ArrayList<>();
return parseCpe(Arrays.stream(cpe.split(", "))
.map(String::trim)
.filter(StringUtils::hasText)
.collect(Collectors.toList()));
}
private static Optional parseCpe(String cpe, boolean silent) {
if (cpe == null) {
return Optional.empty();
}
final String trimmedCpe = cpe.trim();
try {
return Optional.ofNullable(CpeParser.parse(trimmedCpe));
} catch (CpeParsingException e) {
try {
if (trimmedCpe.startsWith("cpe:/")) {
final String[] parts = trimmedCpe.substring(5).split(":");
final StringJoiner sb = new StringJoiner(":");
for (String part : parts) {
if (part.equals("*")) {
sb.add("*");
} else {
sb.add(URLEncoder.encode(part, "UTF-8"));
}
}
final String encodedCpe = "cpe:/" + sb;
return Optional.ofNullable(CpeParser.parse(encodedCpe));
} else if (trimmedCpe.startsWith("cpe:2.3")) {
return Optional.ofNullable(CpeParser.parse(fillCpeComponents(trimmedCpe)));
}
} catch (CpeParsingException | UnsupportedEncodingException ignored) {
}
if (!silent) {
LOG.warn("Could not parse CPE [{}]: {}", trimmedCpe, e.getMessage());
}
}
return Optional.empty();
}
public static String fillCpeComponents(String cpe) {
if (cpe.startsWith("cpe:2.3:")) {
final String[] parts = cpe.substring(8).split("(? cpes) {
if (cpes == null || cpes.isEmpty()) {
return null;
}
return cpes.stream()
.map(CommonEnumerationUtil::toCpe22UriOrFallbackToCpe23FS)
.collect(Collectors.joining(", "));
}
private final static String[] ESCAPE_CPE_22_CHARS = new String[]{
"_", "\\+", "\\.", "/", "-"
};
private static String encodeValidCpe22Part(String part) throws UnsupportedEncodingException {
if (StringUtils.isEmpty(part) || part.equals("*")) {
return "*";
}
for (String character : ESCAPE_CPE_22_CHARS) {
part = part.replaceAll("(? keepOnlyPartVendorProduct(Cpe cpe) {
try {
// remove every part behind the product
return Optional.of(CommonEnumerationUtil.builder().from(cpe).keepOnlyPartVendorProduct().build());
} catch (CpeValidationException e) {
LOG.error("Unable to parse CPE [{}] provided by vendor/product: {}", CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(cpe), e.getMessage());
return Optional.empty();
}
}
public static CpeBuilder builder() {
return new CommonEnumerationUtil.CpeBuilder();
}
public static class CpeBuilder {
private Part part;
private String vendor = "*";
private String product = "*";
private String version = "*";
private String update = "*";
private String edition = "*";
private String language = "*";
private String swEdition = "*";
private String targetSw = "*";
private String targetHw = "*";
private String other = "*";
private CpeBuilder() {
}
public CpeBuilder from(Cpe cpe) {
part = cpe.getPart();
vendor = cpe.getVendor();
product = cpe.getProduct();
version = cpe.getVersion();
update = cpe.getUpdate();
edition = cpe.getEdition();
language = cpe.getLanguage();
swEdition = cpe.getSwEdition();
targetSw = cpe.getTargetSw();
targetHw = cpe.getTargetHw();
other = cpe.getOther();
return this;
}
public CpeBuilder from(String cpe) throws CpeValidationException {
return from(CommonEnumerationUtil.parseCpe(cpe).orElseThrow(() -> new CpeValidationException("Failed to parse CPE: " + cpe)));
}
public CpeBuilder part(Part part) {
if (part == null) part = Part.ANY;
this.part = part;
return this;
}
public CpeBuilder vendor(String vendor) {
this.vendor = transformNullToAny(vendor);
return this;
}
public CpeBuilder product(String product) {
this.product = transformNullToAny(product);
return this;
}
public CpeBuilder version(String version) {
this.version = transformNullToAny(version);
return this;
}
public CpeBuilder update(String update) {
this.update = transformNullToAny(update);
return this;
}
public CpeBuilder edition(String edition) {
this.edition = transformNullToAny(edition);
return this;
}
public CpeBuilder language(String language) {
this.language = transformNullToAny(language);
return this;
}
public CpeBuilder swEdition(String swEdition) {
this.swEdition = transformNullToAny(swEdition);
return this;
}
public CpeBuilder targetSw(String targetSw) {
this.targetSw = transformNullToAny(targetSw);
return this;
}
public CpeBuilder targetHw(String targetHw) {
this.targetHw = transformNullToAny(targetHw);
return this;
}
public CpeBuilder other(String other) {
this.other = transformNullToAny(other);
return this;
}
public CpeBuilder keepOnlyPartVendorProduct() {
version = "*";
update = "*";
edition = "*";
language = "*";
swEdition = "*";
targetSw = "*";
targetHw = "*";
other = "*";
return this;
}
private String transformNullToAny(String s) {
if (s == null) return "*";
return s;
}
public Cpe build() throws CpeValidationException {
return new Cpe(part, vendor, product, version, update, edition, language, swEdition, targetSw, targetHw, other);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy