diff --git a/src/main/java/org/dependencytrack/model/PolicyCondition.java b/src/main/java/org/dependencytrack/model/PolicyCondition.java index 21474b41e1..b8c1f25995 100644 --- a/src/main/java/org/dependencytrack/model/PolicyCondition.java +++ b/src/main/java/org/dependencytrack/model/PolicyCondition.java @@ -86,10 +86,12 @@ public enum Subject { SEVERITY, SWID_TAGID, VERSION, + IS_INTERNAL, COMPONENT_HASH, CWE, VULNERABILITY_ID, VERSION_DISTANCE, + LATEST_VERSION_STATUS, EPSS } @@ -184,3 +186,4 @@ public void setUuid(UUID uuid) { this.uuid = uuid; } } + diff --git a/src/main/java/org/dependencytrack/policy/InternalStatusPolicyEvaluator.java b/src/main/java/org/dependencytrack/policy/InternalStatusPolicyEvaluator.java new file mode 100644 index 0000000000..78a03308e0 --- /dev/null +++ b/src/main/java/org/dependencytrack/policy/InternalStatusPolicyEvaluator.java @@ -0,0 +1,62 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.policy; + +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; + +import java.util.ArrayList; +import java.util.List; + +/** + * Evaluates whether a component's internal status matches a given policy. + */ +public class InternalStatusPolicyEvaluator extends AbstractPolicyEvaluator { + + @Override + public PolicyCondition.Subject supportedSubject() { + return PolicyCondition.Subject.IS_INTERNAL; + } + + @Override + public List evaluate(final Policy policy, final Component component) { + final List violations = new ArrayList<>(); + + for (final PolicyCondition condition : super.extractSupportedConditions(policy)) { + + final boolean isInternal = Boolean.TRUE.equals(component.isInternal()); + final boolean expectedInternal = Boolean.parseBoolean(condition.getValue()); + + switch (condition.getOperator()) { + case IS: + if (isInternal == expectedInternal) { + violations.add(new PolicyConditionViolation(condition, component)); + } + break; + case IS_NOT: + if (isInternal != expectedInternal) { + violations.add(new PolicyConditionViolation(condition, component)); + } + break; + } + } + return violations; + } +} diff --git a/src/main/java/org/dependencytrack/policy/LatestVersionPolicyEvaluator.java b/src/main/java/org/dependencytrack/policy/LatestVersionPolicyEvaluator.java new file mode 100644 index 0000000000..23eb27187e --- /dev/null +++ b/src/main/java/org/dependencytrack/policy/LatestVersionPolicyEvaluator.java @@ -0,0 +1,101 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.policy; + +import com.github.packageurl.PackageURL; +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 java.util.ArrayList; +import java.util.List; + +public class LatestVersionPolicyEvaluator extends AbstractPolicyEvaluator { + + private static final String STATE_LATEST = "LATEST"; + private static final String STATE_OUTDATED = "OUTDATED"; + private static final String STATE_UNKNOWN = "UNKNOWN"; + + @Override + public List evaluate(final Policy policy, final Component component) { + + final List violations = new ArrayList<>(); + + for (final PolicyCondition condition : extractSupportedConditions(policy)) { + + final String expected = normalize(condition.getValue()); + + RepositoryMetaComponent meta = component.getRepositoryMeta(); + + if (meta == null && qm != null) { + final PackageURL purl = component.getPurlCoordinates(); + if (purl != null) { + final RepositoryType repoType = toRepositoryType(purl.getType()); + final String namespace = purl.getNamespace(); + final String name = purl.getName(); + + if (repoType != null && name != null) { + meta = qm.getRepositoryMetaComponent(repoType, namespace, name); + } + } + } + + final String current = component.getVersion(); + final String latest = (meta != null) ? meta.getLatestVersion() : null; + + final String actualState; + if (meta == null || current == null || latest == null) { + actualState = STATE_UNKNOWN; + } else if (current.equals(latest)) { + actualState = STATE_LATEST; + } else { + actualState = STATE_OUTDATED; + } + + final boolean matches = actualState.equals(expected); + + if (condition.getOperator() == PolicyCondition.Operator.IS) { + if (matches) { + violations.add(new PolicyConditionViolation(condition, component)); + } + } else if (condition.getOperator() == PolicyCondition.Operator.IS_NOT) { + if (!matches) { + violations.add(new PolicyConditionViolation(condition, component)); + } + } + } + + return violations; + } + + @Override + public PolicyCondition.Subject supportedSubject() { + return PolicyCondition.Subject.LATEST_VERSION_STATUS; + } + + private static String normalize(final String v) { + return v == null ? null : v.trim().toUpperCase(); + } + + private static RepositoryType toRepositoryType(final String purlType) { + return RepositoryType.valueOf(purlType.trim().toUpperCase()); + } +} diff --git a/src/main/java/org/dependencytrack/policy/PolicyEngine.java b/src/main/java/org/dependencytrack/policy/PolicyEngine.java index b6ee0a2d20..364d8ac01c 100644 --- a/src/main/java/org/dependencytrack/policy/PolicyEngine.java +++ b/src/main/java/org/dependencytrack/policy/PolicyEngine.java @@ -61,6 +61,8 @@ public PolicyEngine() { evaluators.add(new VulnerabilityIdPolicyEvaluator()); evaluators.add(new VersionDistancePolicyEvaluator()); evaluators.add(new EpssPolicyEvaluator()); + evaluators.add(new InternalStatusPolicyEvaluator()); + evaluators.add(new LatestVersionPolicyEvaluator()); } public List evaluate(final List components) { @@ -145,7 +147,7 @@ public PolicyViolation.Type determineViolationType(final PolicyCondition.Subject } return switch (subject) { case CWE, SEVERITY, VULNERABILITY_ID, EPSS -> PolicyViolation.Type.SECURITY; - case AGE, COORDINATES, PACKAGE_URL, CPE, SWID_TAGID, COMPONENT_HASH, VERSION, VERSION_DISTANCE -> + case AGE, COORDINATES, PACKAGE_URL, CPE, SWID_TAGID, COMPONENT_HASH, VERSION, VERSION_DISTANCE, IS_INTERNAL , LATEST_VERSION_STATUS -> PolicyViolation.Type.OPERATIONAL; case LICENSE, LICENSE_GROUP -> PolicyViolation.Type.LICENSE; }; @@ -176,4 +178,4 @@ private boolean isPolicyAssignedToParentProject(Policy policy, Project child) { } return isPolicyAssignedToParentProject(policy, child.getParent()); } -} \ No newline at end of file +} diff --git a/src/test/java/org/dependencytrack/policy/InternalStatusPolicyEvaluatorTest.java b/src/test/java/org/dependencytrack/policy/InternalStatusPolicyEvaluatorTest.java new file mode 100644 index 0000000000..5ab43a919a --- /dev/null +++ b/src/test/java/org/dependencytrack/policy/InternalStatusPolicyEvaluatorTest.java @@ -0,0 +1,111 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ + +package org.dependencytrack.policy; + +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class InternalStatusPolicyEvaluatorTest { + + private InternalStatusPolicyEvaluator evaluator; + + @BeforeEach + void setUp() { + evaluator = new InternalStatusPolicyEvaluator(); + } + + private Policy policyWith(PolicyCondition condition) { + Policy policy = new Policy(); + policy.setViolationState(Policy.ViolationState.FAIL); + policy.addPolicyCondition(condition); + return policy; + } + + @Test + void testIsTrue_NoViolationWhenInternal() { + Component component = new Component(); + component.setInternal(true); + + PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.IS_INTERNAL); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("true"); + + Policy policy = policyWith(condition); + List result = evaluator.evaluate(policy, component); + + assertFalse(result.isEmpty()); + } + + @Test + void testIsTrue_ViolationWhenNotInternal() { + Component component = new Component(); + component.setInternal(false); + + PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.IS_INTERNAL); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("true"); + + Policy policy = policyWith(condition); + List result = evaluator.evaluate(policy, component); + + assertEquals(0, result.size()); + } + + @Test + void testIsNotTrue_ViolationWhenInternal() { + Component component = new Component(); + component.setInternal(true); + + PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.IS_INTERNAL); + condition.setOperator(PolicyCondition.Operator.IS_NOT); + condition.setValue("true"); + + Policy policy = policyWith(condition); + List result = evaluator.evaluate(policy, component); + + assertEquals(0, result.size()); + } + + @Test + void testIsNotTrue_NoViolationWhenNotInternal() { + Component component = new Component(); + component.setInternal(false); + + PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.IS_INTERNAL); + condition.setOperator(PolicyCondition.Operator.IS_NOT); + condition.setValue("true"); + + Policy policy = policyWith(condition); + List result = evaluator.evaluate(policy, component); + + assertFalse(result.isEmpty()); + } +} diff --git a/src/test/java/org/dependencytrack/policy/LatestVersionPolicyEvaluatorTest.java b/src/test/java/org/dependencytrack/policy/LatestVersionPolicyEvaluatorTest.java new file mode 100644 index 0000000000..efa847f827 --- /dev/null +++ b/src/test/java/org/dependencytrack/policy/LatestVersionPolicyEvaluatorTest.java @@ -0,0 +1,179 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.policy; + +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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class LatestVersionPolicyEvaluatorTest { + + private LatestVersionPolicyEvaluator evaluator; + + @BeforeEach + void setup() { + evaluator = new LatestVersionPolicyEvaluator(); + } + + private Policy policyWith(final PolicyCondition condition) { + final Policy p = new Policy(); + p.setViolationState(Policy.ViolationState.FAIL); + p.addPolicyCondition(condition); + return p; + } + + private RepositoryMetaComponent meta(final String latestVersion) { + final RepositoryMetaComponent m = new RepositoryMetaComponent(); + m.setRepositoryType(RepositoryType.MAVEN); + m.setName("demo"); + m.setNamespace("test"); + m.setLatestVersion(latestVersion); + m.setPublished(new Date()); + m.setLastCheck(new Date()); + return m; + } + + @Test + void testUnknown_NoMetadata_ShouldViolate() { + final Component c = new Component(); + c.setVersion("1.0.0"); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("UNKNOWN"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertEquals(1, result.size()); + } + + @Test + void testUnknown_MetadataButMissingVersion_ShouldViolate() { + final Component c = new Component(); + c.setVersion(null); + c.setRepositoryMeta(meta("1.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("UNKNOWN"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertEquals(1, result.size()); + } + + @Test + void testUnknown_WithMetadata_ShouldNotViolate() { + final Component c = new Component(); + c.setVersion("1.0.0"); + c.setRepositoryMeta(meta("2.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("UNKNOWN"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertTrue(result.isEmpty()); + } + + @Test + void testLatest_WhenComponentIsLatest_ShouldViolate() { + final Component c = new Component(); + c.setVersion("2.0.0"); + c.setRepositoryMeta(meta("2.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("LATEST"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertEquals(1, result.size()); + } + + @Test + void testLatest_WhenNotLatest_ShouldNotViolate() { + final Component c = new Component(); + c.setVersion("1.5.0"); + c.setRepositoryMeta(meta("2.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("LATEST"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertTrue(result.isEmpty()); + } + + @Test + void testOutdated_WhenOlder_ShouldViolate() { + final Component c = new Component(); + c.setVersion("1.5.0"); + c.setRepositoryMeta(meta("2.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("OUTDATED"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertEquals(1, result.size()); + } + + @Test + void testOutdated_WhenLatest_ShouldNotViolate() { + final Component c = new Component(); + c.setVersion("2.0.0"); + c.setRepositoryMeta(meta("2.0.0")); + + final PolicyCondition condition = new PolicyCondition(); + condition.setSubject(PolicyCondition.Subject.LATEST_VERSION_STATUS); + condition.setOperator(PolicyCondition.Operator.IS); + condition.setValue("OUTDATED"); + + final Policy policy = policyWith(condition); + final List result = evaluator.evaluate(policy, c); + + assertTrue(result.isEmpty()); + } +} +