diff --git a/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java b/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java index 5f02b9a74..cfb53cf36 100644 --- a/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java +++ b/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java @@ -589,6 +589,13 @@ private static boolean isExclusiveDependencyOf(final Component leafComponent, fi try (final var qm = new QueryManager(); final Handle jdbiHandle = jdbi(qm).open()) { + // If the component is a direct dependency of the project, + // it can no longer be a dependency exclusively introduced + // through another component. + if (isDirectDependency(jdbiHandle, leafComponent)) { + return false; + } + final Query query = jdbiHandle.createQuery(""" -- Determine the project the given leaf component is part of. WITH RECURSIVE @@ -691,10 +698,6 @@ private static boolean isExclusiveDependencyOf(final Component leafComponent, fi final List> paths = reducePaths(nodes); - // TODO: TBD whether only direct dependency relationships should count. - // Direct only: - // return paths.stream().allMatch(path -> matchedNodeIds.contains(path.get(0))); - // Also transitive (arbitrary distance between matched node and leaf component): return paths.stream().allMatch(path -> path.stream().anyMatch(matchedNodeIds::contains)); } } @@ -898,4 +901,24 @@ private static boolean containsExactly(final List lhs, final List rhs) return Objects.equals(lhs, rhs); } + private static boolean isDirectDependency(final Handle jdbiHandle, final Component component) { + final Query query = jdbiHandle.createQuery(""" + SELECT + 1 + FROM + "COMPONENT" AS "C" + INNER JOIN + "PROJECT" AS "P" ON "P"."ID" = "C"."PROJECT_ID" + WHERE + "C"."UUID" = :leafComponentUuid + AND "P"."DIRECT_DEPENDENCIES" LIKE ('%' || :leafComponentUuid || '%') + """); + + return query + .bind("leafComponentUuid", component.getUuid()) + .mapTo(Boolean.class) + .findOne() + .orElse(false); + } + } diff --git a/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java index 483a8237d..b0f0f5ecd 100644 --- a/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java +++ b/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java @@ -1498,6 +1498,54 @@ public void testEvaluateProjectWithFuncComponentIsDependencyOfExclusiveComponent assertThat(qm.getAllPolicyViolations(componentSpringCore)).hasSize(1); } + @Test + public void testEvaluateProjectWithFuncComponentIsDependencyOfExclusiveComponentWithMultiplePaths5() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var componentA = new Component(); + componentA.setProject(project); + componentA.setName("acme-lib-a"); + qm.persist(componentA); + + final var componentB = new Component(); + componentB.setProject(project); + componentB.setName("acme-lib-b"); + qm.persist(componentB); + + // /-> A -> B + // * ^ + // \-------/ + project.setDirectDependencies("[%s, %s]".formatted( + new ComponentIdentity(componentA).toJSON(), + new ComponentIdentity(componentB).toJSON() + )); + componentA.setDirectDependencies("[%s]".formatted(new ComponentIdentity(componentB).toJSON())); + qm.persist(project); + qm.persist(componentA); + + final var policyEngine = new CelPolicyEngine(); + final var policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + + // Is component introduced exclusively through acme-lib-a? + PolicyCondition condition = qm.createPolicyCondition(policy, + PolicyCondition.Subject.EXPRESSION, PolicyCondition.Operator.MATCHES, """ + component.is_exclusive_dependency_of(v1.Component{name: "acme-lib-a"}) + """, PolicyViolation.Type.OPERATIONAL); + policyEngine.evaluateProject(project.getUuid()); + assertThat(qm.getAllPolicyViolations(componentA)).isEmpty(); + assertThat(qm.getAllPolicyViolations(componentB)).isEmpty(); + + // Is component introduced exclusively through acme-lib-b? + condition.setValue(""" + component.is_exclusive_dependency_of(v1.Component{name: "acme-lib-b"}) + """); + policyEngine.evaluateProject(project.getUuid()); + assertThat(qm.getAllPolicyViolations(componentA)).isEmpty(); + assertThat(qm.getAllPolicyViolations(componentB)).isEmpty(); + } + @Test public void testEvaluateProjectWithFuncMatchesRange() { final var policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL);