com.metaeffekt.artifact.analysis.version.NormalizationVersionImpl 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.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:
*
* - Normalizes the update part
* - Extracts trailing numbers and stores the integer value
* - Checks for occurrences of {@link NormalizationVersionImpl#UPDATE_PARTS} and adds a value based on the index of occurrence
*
* Example for release-candidate-12
:
*
* releasecandidate12
* + 12
* + (3 + 1) * 100000 = 400000
*
* 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