From a0ceff7fc765872f24852b4f209cf3a7f0fe57fb Mon Sep 17 00:00:00 2001 From: Walter de Boer Date: Wed, 1 Mar 2023 23:16:52 +0100 Subject: [PATCH] Support for combined VersionDistances in a VersionDistance policy Signed-off-by: Walter de Boer --- .../VersionDistancePolicyEvaluator.java | 43 +++++++++++++------ .../dependencytrack/util/VersionDistance.java | 38 ++++++++++++++-- .../VersionDistancePolicyEvaluatorTest.java | 10 ++++- .../util/VersionDistanceTest.java | 12 ++++++ 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/dependencytrack/policy/VersionDistancePolicyEvaluator.java b/src/main/java/org/dependencytrack/policy/VersionDistancePolicyEvaluator.java index 95d3bc320c..200f208ca5 100644 --- a/src/main/java/org/dependencytrack/policy/VersionDistancePolicyEvaluator.java +++ b/src/main/java/org/dependencytrack/policy/VersionDistancePolicyEvaluator.java @@ -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; @@ -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 */ @@ -92,16 +90,37 @@ public List 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 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; @@ -109,7 +128,7 @@ private boolean evaluate(final PolicyCondition condition, final VersionDistance 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; } }; diff --git a/src/main/java/org/dependencytrack/util/VersionDistance.java b/src/main/java/org/dependencytrack/util/VersionDistance.java index 7d03dbb3e1..12e2c7a917 100644 --- a/src/main/java/org/dependencytrack/util/VersionDistance.java +++ b/src/main/java/org/dependencytrack/util/VersionDistance.java @@ -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; @@ -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 parse(String combinedDistances) throws NumberFormatException { + final List result = new ArrayList(); + 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) { @@ -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 :..: + * Only the first (most significant) difference number will be set, all + * others will be 0. So the distance will look like :..: * 1:?.?.? * 0:1.?.? * 1:?.?.? diff --git a/src/test/java/org/dependencytrack/policy/VersionDistancePolicyEvaluatorTest.java b/src/test/java/org/dependencytrack/policy/VersionDistancePolicyEvaluatorTest.java index f335bb0281..36dd60f23e 100644 --- a/src/test/java/org/dependencytrack/policy/VersionDistancePolicyEvaluatorTest.java +++ b/src/test/java/org/dependencytrack/policy/VersionDistancePolicyEvaluatorTest.java @@ -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}, @@ -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. diff --git a/src/test/java/org/dependencytrack/util/VersionDistanceTest.java b/src/test/java/org/dependencytrack/util/VersionDistanceTest.java index 164f3d48d0..486992877f 100644 --- a/src/test/java/org/dependencytrack/util/VersionDistanceTest.java +++ b/src/test/java/org/dependencytrack/util/VersionDistanceTest.java @@ -1,5 +1,6 @@ package org.dependencytrack.util; +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -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")); + } + }