Skip to content

Commit

Permalink
Extend "limit to" functionality for notifications
Browse files Browse the repository at this point in the history
to now also include the following subjects:

* `PolicyViolationIdentified`
* `AnalysisDecisionChange`
* `ViolationAnalysisDecisionChange`

Fixes DependencyTrack#975

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Oct 20, 2022
1 parent 15d11cc commit 2cc12c0
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Project> 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<Project> 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;
Expand All @@ -126,7 +121,6 @@ private boolean canRestrictNotificationToRuleProjects(Notification initialNotifi
&& rule.getProjects().size() > 0;
}

@SuppressWarnings("unchecked")
List<NotificationRule> resolveRules(final Notification notification) {
// The notification rules to process for this specific notification
final List<NotificationRule> rules = new ArrayList<>();
Expand All @@ -151,18 +145,16 @@ List<NotificationRule> resolveRules(final Notification notification) {

sb.append("enabled == true && scope == :scope"); //todo: improve this - this only works for testing
query.setFilter(sb.toString());
final List<NotificationRule> result = (List<NotificationRule>)query.execute(NotificationScope.valueOf(notification.getScope()));
query.setParameters(NotificationScope.valueOf(notification.getScope()));
final List<NotificationRule> 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
Expand All @@ -178,21 +170,23 @@ List<NotificationRule> 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()))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 2cc12c0

Please sign in to comment.