From 2cc12c02ed078f11599c35d8b24ae8910b7cdcbc Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 19 Oct 2022 23:30:47 +0200 Subject: [PATCH] Extend "limit to" functionality for notifications to now also include the following subjects: * `PolicyViolationIdentified` * `AnalysisDecisionChange` * `ViolationAnalysisDecisionChange` Fixes #975 Signed-off-by: nscuro --- .../notification/NotificationRouter.java | 44 ++-- .../notification/NotificationRouterTest.java | 225 +++++++++++++++++- 2 files changed, 243 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/dependencytrack/notification/NotificationRouter.java b/src/main/java/org/dependencytrack/notification/NotificationRouter.java index 7066b714f7..249127c04f 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationRouter.java +++ b/src/main/java/org/dependencytrack/notification/NotificationRouter.java @@ -34,6 +34,7 @@ import org.dependencytrack.notification.vo.NewVulnerableDependency; import org.dependencytrack.notification.vo.PolicyViolationIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; +import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; import org.dependencytrack.persistence.QueryManager; import javax.jdo.PersistenceManager; @@ -105,16 +106,10 @@ public Notification restrictNotificationToRuleProjects(Notification initialNotif restrictedNotification.setContent(initialNotification.getContent()); restrictedNotification.setTitle(initialNotification.getTitle()); restrictedNotification.setTimestamp(initialNotification.getTimestamp()); - if(initialNotification.getSubject() instanceof NewVulnerabilityIdentified) { - NewVulnerabilityIdentified subject = (NewVulnerabilityIdentified) initialNotification.getSubject(); + if(initialNotification.getSubject() instanceof final NewVulnerabilityIdentified subject) { Set restrictedProjects = subject.getAffectedProjects().stream().filter(project -> ruleProjectsUuids.contains(project.getUuid().toString())).collect(Collectors.toSet()); NewVulnerabilityIdentified restrictedSubject = new NewVulnerabilityIdentified(subject.getVulnerability(), subject.getComponent(), restrictedProjects, null); restrictedNotification.setSubject(restrictedSubject); - } else if(initialNotification.getSubject() instanceof AnalysisDecisionChange) { - AnalysisDecisionChange subject = (AnalysisDecisionChange) initialNotification.getSubject(); - Set restrictedProjects = subject.getAffectedProjects().stream().filter(project -> ruleProjectsUuids.contains(project.getUuid().toString())).collect(Collectors.toSet()); - AnalysisDecisionChange restrictedSubject = new AnalysisDecisionChange(subject.getVulnerability(), subject.getComponent(), restrictedProjects, subject.getAnalysis()); - restrictedNotification.setSubject(restrictedSubject); } } return restrictedNotification; @@ -126,7 +121,6 @@ private boolean canRestrictNotificationToRuleProjects(Notification initialNotifi && rule.getProjects().size() > 0; } - @SuppressWarnings("unchecked") List resolveRules(final Notification notification) { // The notification rules to process for this specific notification final List rules = new ArrayList<>(); @@ -151,18 +145,16 @@ List resolveRules(final Notification notification) { sb.append("enabled == true && scope == :scope"); //todo: improve this - this only works for testing query.setFilter(sb.toString()); - final List result = (List)query.execute(NotificationScope.valueOf(notification.getScope())); + query.setParameters(NotificationScope.valueOf(notification.getScope())); + final List result = query.executeList(); pm.detachCopyAll(result); if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) - && notification.getSubject() != null && notification.getSubject() instanceof NewVulnerabilityIdentified) { - final NewVulnerabilityIdentified subject = (NewVulnerabilityIdentified) notification.getSubject(); - /* - if the rule specified one or more projects as targets, reduce the execution - of the notification down to those projects that the rule matches and which - also match project the component is included in. - NOTE: This logic is slightly different from what is implemented in limitToProject() - */ + && notification.getSubject() instanceof final NewVulnerabilityIdentified subject) { + // If the rule specified one or more projects as targets, reduce the execution + // of the notification down to those projects that the rule matches and which + // also match project the component is included in. + // NOTE: This logic is slightly different from what is implemented in limitToProject() for (final NotificationRule rule: result) { if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { if (rule.getProjects() != null && rule.getProjects().size() > 0 @@ -178,21 +170,23 @@ List resolveRules(final Notification notification) { } } } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) - && notification.getSubject() != null && notification.getSubject() instanceof NewVulnerableDependency) { - final NewVulnerableDependency subject = (NewVulnerableDependency) notification.getSubject(); + && notification.getSubject() instanceof final NewVulnerableDependency subject) { limitToProject(rules, result, notification, subject.getComponent().getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) - && notification.getSubject() != null && notification.getSubject() instanceof BomConsumedOrProcessed) { - final BomConsumedOrProcessed subject = (BomConsumedOrProcessed) notification.getSubject(); + && notification.getSubject() instanceof final BomConsumedOrProcessed subject) { + limitToProject(rules, result, notification, subject.getProject()); + } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) + && notification.getSubject() instanceof final VexConsumedOrProcessed subject) { limitToProject(rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) - && notification.getSubject() != null && notification.getSubject() instanceof VexConsumedOrProcessed) { - final VexConsumedOrProcessed subject = (VexConsumedOrProcessed) notification.getSubject(); + && notification.getSubject() instanceof final PolicyViolationIdentified subject) { limitToProject(rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) - && notification.getSubject() != null && notification.getSubject() instanceof PolicyViolationIdentified) { - final PolicyViolationIdentified subject = (PolicyViolationIdentified) notification.getSubject(); + && notification.getSubject() instanceof final AnalysisDecisionChange subject) { limitToProject(rules, result, notification, subject.getProject()); + } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) + && notification.getSubject() instanceof final ViolationAnalysisDecisionChange subject) { + limitToProject(rules, result, notification, subject.getComponent().getProject()); } else { for (final NotificationRule rule: result) { if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { diff --git a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java index 8709ea7f69..ad6959c929 100644 --- a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java +++ b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java @@ -22,10 +22,22 @@ import alpine.notification.NotificationLevel; import com.mitchellbosecke.pebble.PebbleEngine; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.*; +import org.dependencytrack.model.Bom; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Vex; +import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.publisher.Publisher; +import org.dependencytrack.notification.vo.AnalysisDecisionChange; +import org.dependencytrack.notification.vo.BomConsumedOrProcessed; import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; +import org.dependencytrack.notification.vo.NewVulnerableDependency; +import org.dependencytrack.notification.vo.PolicyViolationIdentified; +import org.dependencytrack.notification.vo.VexConsumedOrProcessed; +import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; import org.junit.Assert; import org.junit.Test; @@ -298,6 +310,217 @@ public void testDisabledRule() { assertThat(router.resolveRules(notification)).isEmpty(); } + @Test + public void testNewVulnerabilityIdentifiedLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("Component A"); + componentA = qm.createComponent(componentA, false); + + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("Component B"); + componentB = qm.createComponent(componentB, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.NEW_VULNERABILITY)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.NEW_VULNERABILITY.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new NewVulnerabilityIdentified(null, componentB, Set.of(), null)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new NewVulnerabilityIdentified(null, componentA, Set.of(), null)); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testNewVulnerableDependencyLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("Component A"); + componentA = qm.createComponent(componentA, false); + + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("Component B"); + componentB = qm.createComponent(componentB, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.NEW_VULNERABLE_DEPENDENCY)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.NEW_VULNERABLE_DEPENDENCY.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new NewVulnerableDependency(componentB, null)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new NewVulnerableDependency(componentA, null)); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testBomConsumedOrProcessedLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.BOM_CONSUMED)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.BOM_CONSUMED.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new BomConsumedOrProcessed(projectB, "", Bom.Format.CYCLONEDX, "")); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new BomConsumedOrProcessed(projectA, "", Bom.Format.CYCLONEDX, "")); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testVexConsumedOrProcessedLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.VEX_CONSUMED)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.VEX_CONSUMED.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new VexConsumedOrProcessed(projectB, "", Vex.Format.CYCLONEDX, "")); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new VexConsumedOrProcessed(projectA, "", Vex.Format.CYCLONEDX, "")); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testPolicyViolationIdentifiedLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("Component A"); + componentA = qm.createComponent(componentA, false); + + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("Component B"); + componentB = qm.createComponent(componentB, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.POLICY_VIOLATION)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.POLICY_VIOLATION.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new PolicyViolationIdentified(null, componentB, projectB)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new PolicyViolationIdentified(null, componentA, projectA)); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testAnalysisDecisionChangeLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.PROJECT_AUDIT_CHANGE)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.PROJECT_AUDIT_CHANGE.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new AnalysisDecisionChange(null, null, projectB, null)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new AnalysisDecisionChange(null, null, projectA, null)); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + + @Test + public void testViolationAnalysisDecisionChangeLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("Component A"); + componentA = qm.createComponent(componentA, false); + + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("Component B"); + componentB = qm.createComponent(componentB, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.PROJECT_AUDIT_CHANGE)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.PROJECT_AUDIT_CHANGE.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new ViolationAnalysisDecisionChange(null, componentB, null)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new ViolationAnalysisDecisionChange(null, componentA, null)); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + @Test public void testAffectedChild() { NotificationPublisher publisher = createSlackPublisher();