Skip to content

Commit

Permalink
Support for combined VersionDistances in a VersionDistance policy
Browse files Browse the repository at this point in the history
Signed-off-by: Walter de Boer <walterdeboer@dbso.nl>
  • Loading branch information
Walter de Boer committed Mar 2, 2023
1 parent ed4a1cf commit 87b0b02
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.PolicyCondition.Operator;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.persistence.QueryManager;
Expand All @@ -32,15 +33,12 @@
/**
* Evaluates the {@link VersionDistance} between a {@link Component}'s current and it's latest
* version against a {@link Policy}. This makes it possible to add a policy for checking outdated
* components. The policy "greater than 0:1.0.0" for example means, a difference of only one
* components. The policy "greater than 0:1.?.?" for example means, a difference of only one
* between the curren version's major number and the latest version's major number is allowed.
*
* TODO: add a VersionMatcher to partly match VersionDistances, instead of using
* {@link VersionDistance#compareTo(VersionDistance)}. This could also be userd in
* the VersionPolicyEvaluator to partly match versions. This would enable aadvanced
* policies like "no outdated versions (one major difference), but only if the minor
* version is at least 1 and when it is older than 21 days. (3.0.0 is to early, but
* 3.1.0 is considered safe, when it's older than three weeks without a patch release)"
* VersionDistances van be compined in a policy. For example "greater than 1:1.?.?" means a
* difference of only one epoch number or one major number is allowed. Or "greater than 1.1.?"
* means a difference of only one majr number or one minor number is allowed
*
* @since 4.9.0
*/
Expand Down Expand Up @@ -92,24 +90,45 @@ public List<PolicyConditionViolation> evaluate(final Policy policy, final Compon
return violations;
}

/**
* Evaluate VersionDistance conditions for a given versionDistance. A condition
*
* @param condition operator and value containing combined {@link VersionDistance} values
* @param versionDistance the {@link VersionDistance} to evalue
* @return true if the condition is true for the components versionDistance, false otherwise
*/
private boolean evaluate(final PolicyCondition condition, final VersionDistance versionDistance) {
final VersionDistance policyDistance;
final var operator = condition.getOperator();
final var value = condition.getValue();

final List<VersionDistance> versionDistanceList;
try {
policyDistance = new VersionDistance(condition.getValue());
} catch (NumberFormatException e) {
versionDistanceList = VersionDistance.parse(value);
} catch (IllegalArgumentException e) {
LOGGER.error("Invalid version distance format", e);
return false;
}
if (versionDistanceList.isEmpty()) {
versionDistanceList.add(new VersionDistance(0,0,0));
}

return versionDistanceList.stream().reduce(
false,
(latest, current) -> latest || matches(operator, current, versionDistance),
Boolean::logicalOr
);
}

return switch (condition.getOperator()) {
private boolean matches(final Operator operator, final VersionDistance policyDistance, final VersionDistance versionDistance) {
return switch (operator) {
case NUMERIC_GREATER_THAN -> versionDistance.compareTo(policyDistance) < 0;
case NUMERIC_GREATER_THAN_OR_EQUAL -> versionDistance.compareTo(policyDistance) <= 0;
case NUMERIC_EQUAL -> versionDistance.compareTo(policyDistance) == 0;
case NUMERIC_NOT_EQUAL -> versionDistance.compareTo(policyDistance) != 0;
case NUMERIC_LESSER_THAN_OR_EQUAL -> versionDistance.compareTo(policyDistance) >= 0;
case NUMERIC_LESS_THAN -> versionDistance.compareTo(policyDistance) > 0;
default -> {
LOGGER.warn("Operator %s is not supported for component age conditions".formatted(condition.getOperator()));
LOGGER.warn("Operator %s is not supported for component age conditions".formatted(operator));
yield false;
}
};
Expand Down
38 changes: 34 additions & 4 deletions src/main/java/org/dependencytrack/util/VersionDistance.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package org.dependencytrack.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;

Expand Down Expand Up @@ -150,8 +152,36 @@ private static int parseVersion(String version) throws NumberFormatException {
return Integer.parseInt(version);
}

public static VersionDistance parse(String distance) throws NumberFormatException {
return new VersionDistance(distance);
/**
* Parse a string of combined {@link VersionDistance}s and return tham as a {@link VersionDistance} {@link List}
* @param combinedDistances combined version distance string, e.g 1:1.?.? -> (1:?.?.?, 0:1.?.?)
* @return List of separate {@link VersionDistance}s
* @throws NumberFormatException in case a version distance cannot be parsed
*/
public static List<VersionDistance> parse(String combinedDistances) throws NumberFormatException {
final List<VersionDistance> result = new ArrayList<VersionDistance>();
final var distanceMatcher = DISTANCE_PATTERN.matcher(combinedDistances);
if (distanceMatcher.matches()) {
final var epoch = parseVersion(distanceMatcher.group(GROUP_EPOCH));
if (epoch > 0) {
result.add(new VersionDistance(epoch, -1, -1, -1));
}
final var major = parseVersion(distanceMatcher.group(GROUP_MAJOR));
if (major > 0) {
result.add(new VersionDistance(0, major, -1, -1));
}
final var minor = parseVersion(distanceMatcher.group(GROUP_MINOR));
if (minor > 0) {
result.add(new VersionDistance(0, 0, minor, -1));
}
final var patch = parseVersion(distanceMatcher.group(GROUP_PATCH));
if (patch > 0) {
result.add(new VersionDistance(0, 0, 0, patch));
}
} else {
throw new NumberFormatException("Invallid version distance: " + combinedDistances);
}
return result;
}

public void setEpoch(int epoch) {
Expand Down Expand Up @@ -262,8 +292,8 @@ public String toString() {
* numbers or the patch version numbers. When a number is not foud in the
* version string, 0 is assumed.
*
* Only the first difference number will be set, all others will be 0. So the
* distance will look like <epoch>:<major>.<minor>.<patch>:
* Only the first (most significant) difference number will be set, all
* others will be 0. So the distance will look like <epoch>:<major>.<minor>.<patch>:
* 1:?.?.?
* 0:1.?.?
* 1:?.?.?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static Collection<?> testParameters() {
{"1.2.3", "3.0.1", Operator.NUMERIC_NOT_EQUAL, "2.?.?", false},
{"1.2.3", "3.0.1", Operator.NUMERIC_LESS_THAN, "2.?.?", false},
{"1.2.3", "3.0.1", Operator.NUMERIC_LESSER_THAN_OR_EQUAL, "2.?.?", true},
// Component is exactly as old.
// Component is latest version.
{"1.2.3", "1.2.3", Operator.NUMERIC_GREATER_THAN_OR_EQUAL, "0.0.0", true},
{"1.2.3", "1.2.3", Operator.NUMERIC_GREATER_THAN, "0.0.0", false},
{"1.2.3", "1.2.3", Operator.NUMERIC_EQUAL, "0.0.0", true},
Expand All @@ -75,6 +75,14 @@ public static Collection<?> testParameters() {
{"2.3.4", "1.2.3", Operator.NUMERIC_NOT_EQUAL, "1.?.?", false},
{"2.3.4", "1.2.3", Operator.NUMERIC_LESS_THAN, "1.?.?", false},
{"2.3.4", "1.2.3", Operator.NUMERIC_LESSER_THAN_OR_EQUAL, "1.?.?", true},
// Combined policies.
{"0:2.0.0", "1.0.0", Operator.NUMERIC_EQUAL, "1:1.?.?", true},
{"1:1.0.0", "1.0.0", Operator.NUMERIC_EQUAL, "1:1.?.?", true},
{"1:2.0.0", "1.0.0", Operator.NUMERIC_EQUAL, "1:1.?.?", true},
{"2:2.0.0", "1.0.0", Operator.NUMERIC_EQUAL, "1:1.?.?", false},
{"0:3.2.2", "1.0.0", Operator.NUMERIC_EQUAL, "0:1.1.1", false},
{"0:1.2.2", "1.0.0", Operator.NUMERIC_EQUAL, "0:0.1.1", false},
{"0:0.2.2", "1.0.0", Operator.NUMERIC_EQUAL, "0:0.1.1", false},
// Unsupported operator.
{"1.2.3", "2.1.1", Operator.MATCHES, "1.?.?", false},
// Invalid distanse format.
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/org/dependencytrack/util/VersionDistanceTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.dependencytrack.util;

import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;

Expand Down Expand Up @@ -98,4 +99,15 @@ public void testGetVersionDistance() {
Assert.assertThrows(NumberFormatException.class, () -> VersionDistance.getVersionDistance("1.2a.3", "1"));
Assert.assertThrows(NumberFormatException.class, () -> VersionDistance.getVersionDistance("1.2.3a", "1"));
}

@Test
public void testParse() {
Assert.assertEquals(Arrays.asList(new VersionDistance(0,1,-1)), VersionDistance.parse("0.1.?"));
Assert.assertEquals(Arrays.asList(new VersionDistance(1,-1,-1), new VersionDistance(0,1,-1)), VersionDistance.parse("1.1.?"));
Assert.assertEquals(Arrays.asList(new VersionDistance(1, -1,-1,-1), new VersionDistance(1,-1, -1), new VersionDistance(0,1,-1)), VersionDistance.parse("1:1.1.?"));
Assert.assertEquals(Arrays.asList(), VersionDistance.parse("0:?.?.?"));

Assert.assertThrows(IllegalArgumentException.class, () -> VersionDistance.parse("1.2.3a.1"));
}

}

0 comments on commit 87b0b02

Please sign in to comment.