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

com.metaeffekt.artifact.enrichment.matching.VulnerabilitiesFromCpeEnrichment 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.enrichment.matching;

import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.version.AllCategorizedPartsVersionImpl;
import com.metaeffekt.artifact.analysis.version.Version;
import com.metaeffekt.artifact.analysis.version.curation.VersionContext;
import com.metaeffekt.artifact.analysis.vulnerability.CommonEnumerationUtil;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.warnings.InventoryWarningEntry;
import com.metaeffekt.artifact.enrichment.InventoryEnricher;
import com.metaeffekt.artifact.enrichment.configurations.VulnerabilitiesFromCpeEnrichmentConfiguration;
import com.metaeffekt.mirror.contents.base.AmbDataClass;
import com.metaeffekt.mirror.contents.base.DataSourceIndicator;
import com.metaeffekt.mirror.contents.base.VulnerabilityContextInventory;
import com.metaeffekt.mirror.contents.store.ContentIdentifierStore;
import com.metaeffekt.mirror.contents.store.VulnerabilityTypeIdentifier;
import com.metaeffekt.mirror.contents.vulnerability.Vulnerability;
import com.metaeffekt.mirror.contents.vulnerability.VulnerableSoftwareVersionRangeCpe;
import com.metaeffekt.mirror.query.VulnerabilityIndexQuery;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;
import us.springett.parsers.cpe.exceptions.CpeValidationException;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.metaeffekt.core.inventory.processor.model.Constants.ASTERISK;

public abstract class VulnerabilitiesFromCpeEnrichment extends InventoryEnricher {

    private final static Logger LOG = LoggerFactory.getLogger(VulnerabilitiesFromCpeEnrichment.class);

    protected VulnerabilitiesFromCpeEnrichmentConfiguration configuration;

    public VulnerabilitiesFromCpeEnrichment(VulnerabilitiesFromCpeEnrichmentConfiguration configuration) {
        this.configuration = configuration;
    }

    public void setConfiguration(VulnerabilitiesFromCpeEnrichmentConfiguration configuration) {
        this.configuration = configuration;
    }

    protected abstract VulnerabilityIndexQuery getVulnerabilityQuery();

    protected abstract ContentIdentifierStore.ContentIdentifier getVulnerabilitySource();

    protected void enrichVulnerabilitiesForCpe(VulnerabilityContextInventory vInventory, Artifact artifact) {
        final Map> vulnerabilitiesForCpes = this.queryVulnerabilitiesForArtifact(vInventory, artifact);

        // collect matched cpes
        final Set matchedCpes = new HashSet<>();
        vulnerabilitiesForCpes.keySet().forEach(cpe -> matchedCpes.add(CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(cpe)));

        // add matched cpes as field in the artifact
        if (!matchedCpes.isEmpty()) {
            artifact.set(InventoryAttribute.MATCHED_CPES,
                    matchedCpes.stream().filter(cpe -> cpe != null && !cpe.equals("null")).collect(Collectors.joining(", ")));
        } else {
            artifact.set(InventoryAttribute.MATCHED_CPES, null);
        }
    }

    public Map> queryVulnerabilitiesForArtifact(VulnerabilityContextInventory vInventory, Artifact artifact) {
        // represents return value: collects vulnerabilities for artifact
        final Map> aggregatedVulnerabilitiesForCpes = new LinkedHashMap<>();

        // only used for tracking the amount of vulnerabilities for the config parameter
        final Set aggregatedVulnerabilities = new HashSet<>();

        // The aggregation of CPEs is often version agnostic. This is why a specification on the artifact version is
        // required below.
        final List artifactCpes = CommonEnumerationUtil.parseEffectiveCpe(artifact);

        // query CVEs for CPE with matching version
        for (Cpe cpe : artifactCpes) {

            try {
                final Optional optionalQueryCpe = deriveQueryCpe(vInventory, this.getEnrichmentName(), artifact, cpe);
                if (!optionalQueryCpe.isPresent()) {
                    continue;
                }
                final Cpe queryCpe = optionalQueryCpe.get();

                final Map vulnerabilitiesWithSources = getVulnerabilityQuery().findVulnerabilitiesByFlatAffectedConfigurationRetainSource(queryCpe);
                final List vulnerabilitiesForCpes = vulnerabilitiesWithSources.keySet().stream()
                        .map(AmbDataClass::getId)
                        .map(vInventory::findOrCreateVulnerabilityByName)
                        .collect(Collectors.toList());

                if (!vulnerabilitiesForCpes.isEmpty()) {
                    for (Vulnerability vulnerability : vulnerabilitiesForCpes) {
                        final VulnerableSoftwareVersionRangeCpe affectedConfiguration = vulnerabilitiesWithSources.get(vulnerability);
                        final DataSourceIndicator matchingSource = DataSourceIndicator.cpe(artifact, this.getVulnerabilitySource(), queryCpe, affectedConfiguration != null ? affectedConfiguration.toString() : null);
                        vulnerability.addMatchingSource(matchingSource);
                    }
                }

                aggregatedVulnerabilities.addAll(vulnerabilitiesForCpes.stream().map(Vulnerability::getId).collect(Collectors.toSet()));
                aggregatedVulnerabilitiesForCpes.put(cpe, vulnerabilitiesForCpes);

                if (aggregatedVulnerabilities.size() > configuration.getMaxCorrelatedVulnerabilitiesPerArtifact()) {
                    break;
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to query vulnerabilities for artifact [" + artifact.getId() + "] on CPE [" + CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(cpe) + "]: " + e.getMessage(), e);
            }
        }

        if (aggregatedVulnerabilities.size() > configuration.getMaxCorrelatedVulnerabilitiesPerArtifact()) {
            LOG.warn("Found [{}] vulnerabilities for artifact [{}] but only the first [{}] will be considered.",
                    aggregatedVulnerabilities.size(), artifact.getId(), configuration.getMaxCorrelatedVulnerabilitiesPerArtifact());

            vInventory.getInventoryWarnings().addArtifactWarning(new InventoryWarningEntry<>(artifact,
                    "Found " + aggregatedVulnerabilities.size() + " vulnerabilities but only the first "
                            + configuration.getMaxCorrelatedVulnerabilitiesPerArtifact() + " will be considered.",
                    this.getEnrichmentName()
            ));

            int count = 0;
            boolean clearAll = false;
            for (Map.Entry> entry : aggregatedVulnerabilitiesForCpes.entrySet()) {
                final List vulnerabilities = entry.getValue();
                count += vulnerabilities.size();

                if (count > configuration.getMaxCorrelatedVulnerabilitiesPerArtifact()) {
                    if (clearAll) {
                        vulnerabilities.clear();
                    } else {
                        final int removeCount = count - configuration.getMaxCorrelatedVulnerabilitiesPerArtifact();
                        vulnerabilities.subList(vulnerabilities.size() - removeCount, vulnerabilities.size()).clear();
                        clearAll = true;
                    }
                }
            }
        }

        final VulnerabilityTypeIdentifier vulnerabilityIdentifier = this.getVulnerabilityQuery().getVulnerabilityType();
        for (List vulnerabilities : aggregatedVulnerabilitiesForCpes.values()) {
            for (Vulnerability vulnerability : vulnerabilities) {
                vulnerability.setSourceIdentifier(vulnerabilityIdentifier);
            }
        }

        return aggregatedVulnerabilitiesForCpes;
    }

    /**
     * A CPE that can be used to query an index for vulnerabilities. The version and update are derived from the
     * artifact and the cpe in combination.
* This differs from a normal {@link Version#of(String, String)} in that it replaces certain characters in the * version and update before parsing it. * * @param vInventory may be null. If set, warnings will be added to the inventory. * @param enrichmentName The name of the enrichment. * @param artifact The artifact to use the version from to generate the query version. * @param cpe The cpe to use the version and update from to generate the query version. * @return The query cpe. */ public static Optional deriveQueryCpe(VulnerabilityContextInventory vInventory, String enrichmentName, Artifact artifact, Cpe cpe) { final Version queryVersion = deriveQueryVersion(artifact, cpe); try { return Optional.of(CommonEnumerationUtil.builder() .from(cpe) .version(replaceIfNotNull(queryVersion.getVersion(), " ", "_")) .update(replaceIfNotNull(queryVersion.getUpdate(), " ", "_")) .build()); } catch (CpeValidationException e) { LOG.warn("Failed to build CPE for querying vulnerability data: [{}] [{}]: {}", cpe, queryVersion, e.getMessage()); if (vInventory != null) { vInventory.getInventoryWarnings().addArtifactWarning(new InventoryWarningEntry<>(artifact, "Failed to build CPE for querying vulnerability data: " + cpe + " " + queryVersion + ": " + e.getMessage(), enrichmentName != null ? enrichmentName : VulnerabilitiesFromCpeEnrichment.class.getName() )); } return Optional.empty(); } } private static String replaceIfNotNull(String str, String value, String replacement) { if (str != null) { return str.replace(value, replacement); } else { return null; } } /** * Derive the query version. The artifact version and the cpeVersion are combined. * * @param artifact The artifact to get the version from. * @param cpe The cpe to get the version from. * @return The derived version. */ public static Version deriveQueryVersion(Artifact artifact, Cpe cpe) { final Version artifactVersion = deriveArtifactVersion(artifact); final Version cpeVersion = Version.of(cpe.getVersion(), cpe.getUpdate(), VersionContext.fromCpe(cpe)); final Version modulateVersion = modulateVersions(artifactVersion, cpeVersion); if (!artifactVersion.toString().equals(modulateVersion.toString())) { LOG.info("Derived query version for artifact [{}] and CPE [{}]: [{}] + [{}] = [{}]", artifact.getId(), CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(cpe), artifactVersion, cpeVersion, modulateVersion); } return modulateVersion; } public static Version deriveArtifactVersion(Artifact artifact) { String version = extractCpeVersion(artifact); String update = null; final Pattern compile = Pattern.compile("^.*p[0-9]+$"); final Matcher matcher = compile.matcher(version); if (matcher.matches()) { int pIndex = version.lastIndexOf("p"); update = version.substring(pIndex); version = version.substring(0, pIndex); } return Version.of(version, update, VersionContext.fromArtifact(artifact)); } private static Version modulateVersions(Version artifactVersion, Version cpeQueryVersion) { if (artifactVersion instanceof AllCategorizedPartsVersionImpl && (cpeQueryVersion == null || cpeQueryVersion instanceof AllCategorizedPartsVersionImpl)) { final AllCategorizedPartsVersionImpl artifactVersionImpl = (AllCategorizedPartsVersionImpl) artifactVersion; final AllCategorizedPartsVersionImpl cpeQueryVersionImpl = (AllCategorizedPartsVersionImpl) cpeQueryVersion; final String artifactVersionPart = artifactVersionImpl.toStringPreModifierPart(); final String artifactUpdatePart = artifactVersionImpl.toStringModifierPart(); // check whether query was specific if (cpeQueryVersionImpl == null) { return Version.of(artifactVersionPart, artifactUpdatePart); } else { final String cpeQueryVersionPart = cpeQueryVersionImpl.toStringPreModifierPart(); final String cpeQueryUpdatePart = cpeQueryVersionImpl.toStringModifierPart(); final String effectiveVersion; final String effectiveUpdate; if (!ASTERISK.equals(cpeQueryVersion.getVersion()) && StringUtils.hasText(cpeQueryVersion.getVersion()) && StringUtils.hasText(cpeQueryVersionPart)) { effectiveVersion = cpeQueryVersionPart; } else { effectiveVersion = artifactVersionPart; } if (!ASTERISK.equals(cpeQueryVersionImpl.getUpdate()) && StringUtils.hasText(cpeQueryVersion.getUpdate()) && StringUtils.hasText(cpeQueryUpdatePart)) { effectiveUpdate = cpeQueryUpdatePart; } else { effectiveUpdate = artifactUpdatePart; } if (StringUtils.hasText(effectiveVersion) && StringUtils.hasText(effectiveUpdate)) { return Version.of(effectiveVersion, effectiveUpdate); } else if (StringUtils.hasText(effectiveVersion)) { return Version.of(effectiveVersion, artifactVersion.getUpdate()); } else { return Version.of(artifactVersion.getVersion(), artifactVersion.getUpdate()); } } } else { // old implementation LOG.warn("Using old implementation for version modulation. Please update to the new implementation [{}] [{}]", artifactVersion, cpeQueryVersion); String version = artifactVersion.getVersion(); String update = artifactVersion.getUpdate(); // check whether query was specific if (cpeQueryVersion == null) { return artifactVersion; } else if (!ASTERISK.equals(cpeQueryVersion.getVersion())) { // also checks for null version = cpeQueryVersion.getVersion(); if (!ASTERISK.equals(cpeQueryVersion.getUpdate())) { update = cpeQueryVersion.getUpdate(); } } return Version.of(version, update); } } public static String extractCpeVersion(Artifact artifact) { final String strippedVersion = StringUtils.isEmpty(artifact.getVersion()) ? ASTERISK : artifact.getVersion().trim(); if (strippedVersion.equalsIgnoreCase("unspecific") || strippedVersion.equalsIgnoreCase("undefined")) { return ASTERISK; } if (strippedVersion.contains(":")) { return strippedVersion.substring(strippedVersion.indexOf(":") + 1); } if (strippedVersion.contains("+")) { return strippedVersion.substring(0, strippedVersion.indexOf("+")); } if (strippedVersion.contains("~")) { return strippedVersion.substring(0, strippedVersion.indexOf("~")); } if (strippedVersion.contains("-")) { return strippedVersion.substring(0, strippedVersion.indexOf("-")); } if (strippedVersion.contains(".RELEASE")) { return strippedVersion.substring(0, strippedVersion.indexOf(".RELEASE")); } if (strippedVersion.contains(".FINAL")) { return strippedVersion.substring(0, strippedVersion.indexOf(".FINAL")); } if (strippedVersion.contains(".Release")) { return strippedVersion.substring(0, strippedVersion.indexOf(".Release")); } if (strippedVersion.contains(".Final")) { return strippedVersion.substring(0, strippedVersion.indexOf(".Final")); } return strippedVersion; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy