Skip to content

Commit

Permalink
VersionDistancePolicyEvaluator for detecting outdated components
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 Jun 23, 2023
1 parent 1f435fd commit 1cae90c
Show file tree
Hide file tree
Showing 9 changed files with 684 additions and 17 deletions.
26 changes: 26 additions & 0 deletions src/main/java/org/dependencytrack/exception/PolicyException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This file is part of Dependency-Track.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.exception;

public class PolicyException extends RuntimeException {

public PolicyException(String message) {
super(message);
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/dependencytrack/model/PolicyCondition.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public enum Subject {
VERSION,
COMPONENT_HASH,
CWE,
VULNERABILITY_ID
VULNERABILITY_ID,
VERSION_DISTANCE
}

@PrimaryKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,20 @@
*/
package org.dependencytrack.policy;

import alpine.common.logging.Logger;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.persistence.QueryManager;

import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.persistence.QueryManager;
import alpine.common.logging.Logger;

/**
* Evaluates a {@link Component}'s published date against a {@link Policy}.
Expand Down Expand Up @@ -111,7 +110,7 @@ private boolean evaluate(final PolicyCondition condition, final Date published)
case NUMERIC_EQUAL -> ageDate.isEqual(today);
case NUMERIC_NOT_EQUAL -> !ageDate.isEqual(today);
case NUMERIC_LESSER_THAN_OR_EQUAL -> ageDate.isEqual(today) || ageDate.isAfter(today);
case NUMERIC_LESS_THAN -> ageDate.isAfter(LocalDate.now());
case NUMERIC_LESS_THAN -> ageDate.isAfter(today);
default -> {
LOGGER.warn("Operator %s is not supported for component age conditions".formatted(condition.getOperator()));
yield false;
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/org/dependencytrack/policy/PolicyEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
*/
package org.dependencytrack.policy;

import alpine.common.logging.Logger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
Expand All @@ -27,10 +29,7 @@
import org.dependencytrack.model.Tag;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.NotificationUtil;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import alpine.common.logging.Logger;

/**
* A lightweight policy engine that evaluates a list of components against
Expand Down Expand Up @@ -59,6 +58,7 @@ public PolicyEngine() {
evaluators.add(new ComponentHashPolicyEvaluator());
evaluators.add(new CwePolicyEvaluator());
evaluators.add(new VulnerabilityIdPolicyEvaluator());
evaluators.add(new VersionDistancePolicyEvaluator());
}

public List<PolicyViolation> evaluate(final List<Component> components) {
Expand Down Expand Up @@ -140,7 +140,7 @@ public PolicyViolation.Type determineViolationType(final PolicyCondition.Subject
}
return switch (subject) {
case CWE, SEVERITY, VULNERABILITY_ID -> PolicyViolation.Type.SECURITY;
case AGE, COORDINATES, PACKAGE_URL, CPE, SWID_TAGID, COMPONENT_HASH, VERSION ->
case AGE, COORDINATES, PACKAGE_URL, CPE, SWID_TAGID, COMPONENT_HASH, VERSION, VERSION_DISTANCE ->
PolicyViolation.Type.OPERATIONAL;
case LICENSE, LICENSE_GROUP -> PolicyViolation.Type.LICENSE;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* This file is part of Dependency-Track.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.policy;

import java.util.ArrayList;
import java.util.List;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.VersionDistance;
import alpine.common.logging.Logger;

/**
* 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
* 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)"
*
* @since 4.9.0
*/
public class VersionDistancePolicyEvaluator extends AbstractPolicyEvaluator {

private static final Logger LOGGER = Logger.getLogger(VersionDistancePolicyEvaluator.class);

/**
* {@inheritDoc}
*/
@Override
public PolicyCondition.Subject supportedSubject() {
return PolicyCondition.Subject.VERSION_DISTANCE;
}

/**
* {@inheritDoc}
*/
@Override
public List<PolicyConditionViolation> evaluate(final Policy policy, final Component component) {
final var violations = new ArrayList<PolicyConditionViolation>();
if (component.getPurl() == null) {
return violations;
}

final RepositoryType repoType = RepositoryType.resolve(component.getPurl());
if (RepositoryType.UNSUPPORTED == repoType) {
return violations;
}

final RepositoryMetaComponent metaComponent;
try (final var qm = new QueryManager()) {
metaComponent = qm.getRepositoryMetaComponent(repoType,
component.getPurl().getNamespace(), component.getPurl().getName());
qm.getPersistenceManager().detachCopy(metaComponent);
}
if (metaComponent == null || metaComponent.getLatestVersion() == null) {
return violations;
}

final var versionDistance = VersionDistance.getVersionDistance(component.getVersion(),metaComponent.getLatestVersion());

for (final PolicyCondition condition : super.extractSupportedConditions(policy)) {
if (evaluate(condition, versionDistance)) {
violations.add(new PolicyConditionViolation(condition, component));
}
}

return violations;
}

private boolean evaluate(final PolicyCondition condition, final VersionDistance versionDistance) {
final VersionDistance policyDistance;
try {
policyDistance = new VersionDistance(condition.getValue());
} catch (NumberFormatException e) {
LOGGER.error("Invalid version distance format", e);
return false;
}

return switch (condition.getOperator()) {
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()));
yield false;
}
};
}

}
Loading

0 comments on commit 1cae90c

Please sign in to comment.