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 Mar 1, 2023
1 parent b22ebe3 commit 84d59b3
Show file tree
Hide file tree
Showing 8 changed files with 698 additions and 29 deletions.
6 changes: 0 additions & 6 deletions src/main/java/org/dependencytrack/model/Finding.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,6 @@ public class Finding implements Serializable {
"LEFT JOIN \"ANALYSIS\" ON (\"COMPONENT\".\"ID\" = \"ANALYSIS\".\"COMPONENT_ID\") AND (\"VULNERABILITY\".\"ID\" = \"ANALYSIS\".\"VULNERABILITY_ID\") AND (\"COMPONENT\".\"PROJECT_ID\" = \"ANALYSIS\".\"PROJECT_ID\") " +
"WHERE \"COMPONENT\".\"PROJECT_ID\" = ? ";

/*
* This statement works on Microsoft SQL Server, MySQL, and PostgreSQL. Due to the standardization
* of upper-case table and column names in Dependency-Track, every identifier needs to be wrapped
* in double quotes to satisfy PostgreSQL case-sensitive requirements. This also places a requirement
* on ANSI_QUOTES mode being enabled in MySQL. SQL Server works regardless and is just happy to be invited :-)
*/
public static final String QUERY_OUTDATED = "SELECT " +
"\"COMPONENT\".\"UUID\"," +
"\"COMPONENT\".\"NAME\"," +
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/org/dependencytrack/model/PolicyCondition.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@
*/
package org.dependencytrack.model;

import alpine.common.validation.RegexSequence;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.io.Serializable;
import java.util.UUID;
import javax.jdo.annotations.Column;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
Expand All @@ -33,8 +30,10 @@
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import alpine.common.validation.RegexSequence;

/**
* Defines a Model class for defining a policy condition.
Expand Down Expand Up @@ -76,7 +75,8 @@ public enum Subject {
SWID_TAGID,
VERSION,
COMPONENT_HASH,
CWE
CWE,
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
13 changes: 7 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 @@ -58,6 +57,7 @@ public PolicyEngine() {
evaluators.add(new ComponentAgePolicyEvaluator());
evaluators.add(new ComponentHashPolicyEvaluator());
evaluators.add(new CwePolicyEvaluator());
evaluators.add(new VersionDistancePolicyEvaluator());
}

public List<PolicyViolation> evaluate(final List<Component> components) {
Expand Down Expand Up @@ -144,6 +144,7 @@ private PolicyViolation.Type determineViolationType(final PolicyCondition.Subjec
case SWID_TAGID:
case COMPONENT_HASH:
case VERSION:
case VERSION_DISTANCE:
return PolicyViolation.Type.OPERATIONAL;
case LICENSE:
case LICENSE_GROUP:
Expand All @@ -168,7 +169,7 @@ private boolean isPolicyAssignedToParentProject(Policy policy, Project child) {
}
if (policy.getProjects().stream().anyMatch(p -> p.getId() == child.getParent().getId())) {
return true;
}
}
return isPolicyAssignedToParentProject(policy, child.getParent());
}
}
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 84d59b3

Please sign in to comment.