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

com.metaeffekt.artifact.analysis.version.NormalizationVersionImpl Maven / Gradle / Ivy

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

import com.metaeffekt.artifact.analysis.utils.LruLinkedHashMap;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.version.curation.VersionContext;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.VersionComparator;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

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

@Deprecated
public class NormalizationVersionImpl implements Version {

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

    /**
     * It turns out that normalizing the version is a very expensive operation. This is mainly a problem when
     * generating the {@link VulnerabilityTimeline}, as it checks
     * hundreds of thousands of versions. Therefore, we cache a certain amount of normalized versions.
     */
    private final static Map NORMALISED_VERSION_CACHE = new LruLinkedHashMap<>(2500);

    private final String versionPart;
    private final String updatePart;

    private final String normalized;

    public NormalizationVersionImpl(String versionPart) {
        this.versionPart = versionPart;
        this.updatePart = null;
        this.normalized = normalizeVersion(versionPart);
    }

    public NormalizationVersionImpl(String versionPart, String updatePart) {
        this.versionPart = versionPart;
        this.updatePart = updatePart;
        this.normalized = normalizeVersion(versionPart);
    }

    public boolean equals(Version version) {
        return matchesVersionOf(version);
    }

    public boolean matchesVersionOf(final String checkVersion, final String checkUpdate,
                                    final String versionEndIncluding, final String versionEndExcluding,
                                    final String versionStartExcluding, final String versionStartIncluding,
                                    VersionContext context
    ) {

        // if the version is unknown, match any vulnerable software version
        if (versionPart != null && updatePart == null && (versionPart.equals(ASTERISK) || versionPart.equals("-"))) {
            return true;
        }

        // FIXME: I would say that this is the correct interpretation of the documentation.
        // https://cpe.mitre.org/files/cpe-specification_2.2.pdf page 9
        // a dash in the version or update part means that only versions with a "*" or "" will match that part.
        // if a concrete version or update part is given, it shall not match the version.
        if (checkVersion.equals("-")) {
            if (StringUtils.hasText(versionPart) && !(versionPart.equals(ASTERISK) || versionPart.equals("-"))) {
                return false;
            }
        } else if (checkUpdate.equals("-")) {
            if (StringUtils.hasText(updatePart) && !(updatePart.equals(ASTERISK) || updatePart.equals("-"))) {
                return false;
            }
        }

        // get the normalized version parts
        final String normalisedCheckVersion = normalizePart(checkVersion);
        final String normalisedCheckUpdate = normalizePart(checkUpdate);

        final String normalisedVersionEndIncluding = normalizePart(versionEndIncluding);
        final String normalisedVersionEndExcluding = normalizePart(versionEndExcluding);
        final String normalisedVersionStartExcluding = normalizePart(versionStartExcluding);
        final String normalisedVersionStartIncluding = normalizePart(versionStartIncluding);

        // wildcard version and update: it can be anything
        if (versionPart == null && updatePart == null) return true;

        // check or (included) boundary matchesVersionOf
        if (matchesVersionOf(normalisedCheckVersion, normalisedCheckUpdate)) return true;

        // if the version is excluded it should not match
        if (matchesVersionOf(normalisedVersionStartExcluding, null)) return false;
        if (matchesVersionOf(normalisedVersionEndExcluding, null)) return false;

        // if the version matches the included boundaries, it should match
        if (matchesVersionOf(normalisedVersionStartIncluding, null)) return true;
        if (matchesVersionOf(normalisedVersionEndIncluding, null)) return true;

        // check if version is before the starting boundary
        boolean beforeStart = false;
        // cpeVersion must not be included as it has been confirmed to be null already
        final String startBoundary = StringUtils.nonNull(normalisedVersionStartExcluding, normalisedVersionStartIncluding, normalisedCheckVersion);
        if (startBoundary != null) {
            if (before(startBoundary)) {
                beforeStart = true;
            }
        }

        // check if version is after the ending boundary
        boolean afterEnd = false;
        // cpeVersion must not be included as it has been confirmed to be null already
        final String endBoundary = StringUtils.nonNull(normalisedVersionEndExcluding, normalisedVersionEndIncluding, normalisedCheckVersion);
        if (endBoundary != null) {
            if (after(endBoundary)) {
                afterEnd = true;
            }
        }

        final boolean inRange = !beforeStart && !afterEnd;

        // handle update being set
        if (startBoundary != null && startBoundary.equals(endBoundary) && inRange) {
            if (normalisedCheckUpdate != null) {
                if (!normalisedCheckUpdate.equals(updatePart) && !ASTERISK.equals(updatePart)) {
                    return false;
                }
            }
        }

        return inRange;
    }

    private final static Pattern REGEXP_PATTERN_TRAILING_LETTERS = Pattern.compile("[^a-zA-Z]*[a-zA-Z]");
    private final static Pattern REGEXP_PATTER_TRAILING_LETTERS = Pattern.compile("[a-zA-Z].*");

    protected String normalizeVersion(String version) {
        if (version == null) {
            return null;
        }

        if (version.equals(ASTERISK) || version.equals("-")) {
            return null;
        }

        // FIXME: Is this the way to solve the issue of versions not being parse-able?
        // remove single leading lower or uppercase 'v'
        if (version.startsWith("v") || version.startsWith("V")) {
            version = version.substring(1);
        }

        final String cacheKey = version;
        final String cachedVersion = NORMALISED_VERSION_CACHE.get(cacheKey);
        if (cachedVersion != null) {
            return cachedVersion;
        }

        // find if there is a single identifying letter at the end of the version
        char versionLetter;
        if (REGEXP_PATTERN_TRAILING_LETTERS.matcher(version).matches()) {
            versionLetter = version.charAt(version.length() - 1);
        } else {
            versionLetter = (char) -1;
        }

        version = REGEXP_PATTER_TRAILING_LETTERS.matcher(version).replaceAll("");

        List parts = new ArrayList<>(Arrays.asList(version.split("[\\.-]+")));
        if (versionLetter != (char) -1) {
            while (parts.size() < 3) parts.add("0");
            parts.add(String.valueOf(versionLetter));
        }

        // fill strings
        parts.replaceAll(str -> org.apache.commons.lang3.StringUtils.leftPad(str, 15, "0"));

        final String joined = String.join(".", parts);
        NORMALISED_VERSION_CACHE.put(cacheKey, joined);

        return joined;
    }

    private String normalizeUpdate(String updatePart) {
        if (updatePart == null || updatePart.equals(ASTERISK) || updatePart.equals("-")) {
            return null;
        }
        return updatePart.replace(" ", "").replace("_", "")
                .replace("-", "").replace(".", "")
                .replaceAll("^[^a-zA-Z]+", "").toLowerCase();
    }

    public boolean matchesVersionOf(String version, String update) {
        version = normalizeVersion(version);

        // no comparable information
        if (normalized == null || version == null) {
            return false;
        }

        // extract the parts to compare individually
        final List parts1 = new ArrayList<>(Arrays.asList(normalized.split("\\.")));
        final List parts2 = new ArrayList<>(Arrays.asList(version.split("\\.")));

        // FIXME: This should be the correct behaviour in my opinion
        // if the current version does not contain the same amount of parts than the version that is to be compared
        // (1.23.4 != 1.23), then the compare version is a previous/later version and not equals to the current one.
        if (parts1.size() != parts2.size()) {
            return false;
        }

        // no need to check for parts2 boundary as parts1 has already been confirmed to be shorter.
        // compare every part that both versions share. if a single part is not equal, the entire version is not equal.
        for (int i = 0; i < parts1.size(); i++) {
            String part1 = parts1.get(i);
            String part2 = parts2.get(i);

            if (!part1.equals(part2)) {
                return false;
            }
        }

        // update part: null, *, - and '' are treated as wildcards, but only if the update parts of both versions are wildcards
        if (updatePart == null || updatePart.equals("*") || updatePart.equals("-") || updatePart.equals("")) {
            if (update == null || update.equals("*") || update.equals("-") || update.equals("")) {
                return true;
            }
        }

        // if the update parts are not wildcards, they must be equal. for this they must both be null or both not null.
        if (updatePart == null || update == null) {
            return false;
        }

        return updatePart.equalsIgnoreCase(update);
    }

    // FIXME: This method is unused, what is the difference to the before() method?
    public boolean before_(String v2) {
        String v1 = normalizeVersion(versionPart);
        v2 = normalizeVersion(v2);
        List parts1 = new ArrayList<>(Arrays.asList(v1.split("\\.")));
        List parts2 = new ArrayList<>(Arrays.asList(v2.split("\\.")));

        for (int i = 0; i < parts1.size() && i < parts2.size(); i++) {
            String part1 = parts1.get(i);
            String part2 = parts2.get(i);

            if (String.CASE_INSENSITIVE_ORDER.compare(part1, part2) >= 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param version The version to compare this version against.
     * @return true if this version comes before the version that it is compared against,
     * false otherwise.
     */
    public boolean before(String version) {
        return before(version, null);
    }

    public boolean before(Version version) {
        return before(version.getVersion(), version.getUpdate());
    }

    /**
     * @param version    The version to compare this version against.
     * @param updatePart The update to compare this version against.
     * @return true if this version comes before the version that it is compared against,
     * false otherwise.
     */
    public boolean before(String version, String updatePart) {
        if (normalized == null || normalizeVersion(version) == null) return false;
        final int versionCompareResult = String.CASE_INSENSITIVE_ORDER.compare(normalized, normalizeVersion(version));
        if (versionCompareResult != 0) return versionCompareResult < 0;
        return compareUpdate(updatePart) < 0;
    }

    /**
     * @param version The version to compare this version against.
     * @return true if this version comes after the version that it is compared against,
     * false otherwise.
     */
    public boolean after(String version) {
        return after(version, null);
    }

    public boolean after(Version version) {
        return after(version.getVersion(), version.getUpdate());
    }

    /**
     * @param version    The version to compare this version against.
     * @param updatePart The update part to compare this version against.
     * @return true if this version comes after the version that it is compared against,
     * false otherwise.
     */
    public boolean after(String version, String updatePart) {
        if (normalized == null || normalizeVersion(version) == null) return false;
        final int versionCompareResult = String.CASE_INSENSITIVE_ORDER.compare(normalized, normalizeVersion(version));
        if (versionCompareResult != 0) return versionCompareResult > 0;
        return compareUpdate(updatePart) > 0;
    }

    @Override
    public boolean beforeOrEqual(Version other) {
        return equals(other) || before(other);
    }

    @Override
    public boolean afterOrEqual(Version other) {
        return equals(other) || after(other);
    }

    /**
     * Uses the {@link NormalizationVersionImpl#scoreComparisonUpdate(String)} method to generate scores for the own update part and the
     * update part that is to be compared to. Then uses {@link Integer#compare(int, int)} to compare the scores.
     *
     * @param updatePart The update part to compare against the own update part.
     * @return the value 0 if both are equal; a value less than 0 if the own update comes before; and a value greater
     * than 0 if the own version comes after
     */
    public int compareUpdate(String updatePart) {
        if (updatePart == null || this.updatePart == null) return 0;
        final int ownIndex = scoreComparisonUpdate(this.updatePart);
        final int compareIndex = scoreComparisonUpdate(updatePart);
        return Integer.compare(ownIndex, compareIndex);
    }

    private final static String[] UPDATE_PARTS = {"prealpha", "alpha", "beta", "releasecandidate", "rc", "stable",
            "sr", "rs", "generalavailability", "ga", "longtermsupport", "lts", "update", "sp", "featurecomplete",
            "fc", "endoflife", "eol"};

    /**
     * Creates a compare score for the given update part:
*
    *
  1. Normalizes the update part
  2. *
  3. Extracts trailing numbers and stores the integer value
  4. *
  5. Checks for occurrences of {@link NormalizationVersionImpl#UPDATE_PARTS} and adds a value based on the index of occurrence
  6. *
* Example for release-candidate-12: *
    *
  1. releasecandidate12
  2. *
  3. + 12
  4. *
  5. + (3 + 1) * 100000 = 400000
  6. *
* which makes 400012.
* Returns 0 for version strings (like 1.14.3) * * @param updatePart The update part to generate the score for. * @return The generated score. */ private int scoreComparisonUpdate(String updatePart) { final String comparePart = normalizeUpdate(updatePart); if (comparePart == null) return 0; int value = 0; if (comparePart.matches(".*[0-9].*")) { value += Integer.parseInt(comparePart.replaceAll("[^0-9]", "")); } for (int i = 0; i < UPDATE_PARTS.length; i++) { if (comparePart.contains(UPDATE_PARTS[i])) { value += (i + 1) * 100000; break; } } return value; } public String getVersionPart() { return versionPart; } public String getUpdatePart() { return updatePart; } public String getVersion() { return versionPart; } public String getUpdate() { return updatePart; } @Override public String toString() { return "Version{" + "versionPart='" + versionPart + '\'' + ", updatePart='" + updatePart + '\'' + '}'; } @Override public int compareTo(Version o) { return VersionComparator.INSTANCE.compare(this.getVersion(), o.getVersion()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy