Skip to content

Add option to configure notifications based on severity level #4879

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/main/java/org/dependencytrack/model/NotificationRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@
import javax.jdo.annotations.PrimaryKey;
import javax.jdo.annotations.Unique;
import java.io.Serializable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.TreeSet;
import java.util.UUID;

Expand Down Expand Up @@ -138,6 +140,10 @@ public class NotificationRule implements Serializable {
@Column(name = "NOTIFY_ON", length = 1024)
private String notifyOn;

@Persistent
@Column(name = "NOTIFY_SEVERITIES", length = 1024)
private String notifySeverities;

@Persistent
@Column(name = "MESSAGE", length = 1024)
@Size(max = 1024)
Expand Down Expand Up @@ -325,6 +331,24 @@ public void setNotifyOn(Set<NotificationGroup> groups) {
this.notifyOn = sb.toString();
}

public List<Severity> getNotifySeverities() {
if (notifySeverities == null) {
return List.of(); // empty (user did not pick anything)
}
return Arrays.stream(notifySeverities.split(","))
.map(String::trim)
.map(Severity::valueOf)
.collect(Collectors.toList());
}

public void setNotifySeverities(List<Severity> notifySeverities){
if (notifySeverities == null || notifySeverities.isEmpty()){
this.notifySeverities = null;
return;
}
this.notifySeverities = notifySeverities.stream().map(Enum::name).collect(Collectors.joining(","));
}

public NotificationPublisher getPublisher() {
return publisher;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
import org.dependencytrack.model.NotificationPublisher;
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.Tag;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.notification.publisher.PublishContext;
import org.dependencytrack.notification.publisher.Publisher;
import org.dependencytrack.notification.vo.AnalysisDecisionChange;
Expand Down Expand Up @@ -218,12 +220,50 @@ List<NotificationRule> resolveRules(final PublishContext ctx, final Notification
pm.detachCopyAll(result);
LOGGER.debug("Matched %d notification rules (%s)".formatted(result.size(), ctx));

final List<NotificationRule> severityFiltered = new ArrayList<>();
for (final NotificationRule rule : result) {
List<Severity> severities = rule.getNotifySeverities();
if (severities == null || severities.isEmpty()) {
severities = List.of(Severity.values());
}

// NewVulnerabilityIdentified
if (notification.getSubject() instanceof NewVulnerabilityIdentified vi && vi.getVulnerability() != null) {
Severity s = vi.getVulnerability().getSeverity();
if (s == null || severities.contains(s)) {
severityFiltered.add(rule);
}
// else: skip
continue;
}

// NewVulnerableDependency
if (notification.getSubject() instanceof NewVulnerableDependency nd) {
List<Vulnerability> vs = nd.getVulnerabilities();
if (vs == null || vs.isEmpty()) {
severityFiltered.add(rule);
} else {
List<Severity> finalSeverities = severities;
boolean anyMatch = vs.stream()
.map(Vulnerability::getSeverity)
.anyMatch(sev -> sev != null && finalSeverities.contains(sev));
if (anyMatch) {
severityFiltered.add(rule);
}
}
continue;
}

// everything else (no severity to filter on)
severityFiltered.add(rule);
}

if (NotificationScope.PORTFOLIO.name().equals(notification.getScope())
&& notification.getSubject() instanceof final NewVulnerabilityIdentified subject) {
limitToProject(qm, ctx, rules, result, notification, subject.getComponent().getProject());
limitToProject(qm, ctx, rules, severityFiltered, notification, subject.getComponent().getProject());
} else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope())
&& notification.getSubject() instanceof final NewVulnerableDependency subject) {
limitToProject(qm, ctx, rules, result, notification, subject.getComponent().getProject());
limitToProject(qm, ctx, rules, severityFiltered, notification, subject.getComponent().getProject());
} else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope())
&& notification.getSubject() instanceof final BomConsumedOrProcessed subject) {
limitToProject(qm, ctx, rules, result, notification, subject.getProject());
Expand Down Expand Up @@ -405,4 +445,4 @@ private boolean isChildOfProjectMatching(final Project childProject, final Predi
return false;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public NotificationRule updateNotificationRule(NotificationRule transientRule) {
rule.setNotificationLevel(transientRule.getNotificationLevel());
rule.setPublisherConfig(transientRule.getPublisherConfig());
rule.setNotifyOn(transientRule.getNotifyOn());
rule.setNotifySeverities(transientRule.getNotifySeverities());
bind(rule, resolveTags(transientRule.getTags()));
return rule;
});
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/org/dependencytrack/model/NotificationRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ void testNotifyOn() {
Assertions.assertEquals(2, rule.getNotifyOn().size());
}

@Test
public void testNotifySeverities() {
List<Severity> severities = new ArrayList<>();
severities.add(Severity.LOW);
severities.add(Severity.CRITICAL);
NotificationRule rule = new NotificationRule();
rule.setNotifySeverities(severities);
Assertions.assertEquals(2, rule.getNotifySeverities().size());
}

@Test
void testPublisher() {
NotificationPublisher publisher = new NotificationPublisher();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Tag;
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.Vex;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
Expand Down Expand Up @@ -346,6 +347,85 @@ void testDisabledRule() {
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();
}

@Test
void testNewVulnerabilityIdentifiedShouldTriggerNotification() {
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 = createMockPublisher();

final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher);
rule.setNotifyOn(Set.of(NotificationGroup.NEW_VULNERABILITY));
rule.setProjects(List.of(projectA));

// Set which severities to trigger notification
rule.setNotifySeverities(List.of(Severity.HIGH));

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, qm.detach(componentB), Set.of(), null));

final var router = new NotificationRouter();
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();

// Set a high vulnerability that should trigger a notification
final Vulnerability highVuln = new Vulnerability();
highVuln.setSeverity(Severity.HIGH);
notification.setSubject(new NewVulnerabilityIdentified(highVuln, qm.detach(componentA), Set.of(), null));
assertThat(router.resolveRules(PublishContext.from(notification), notification))
.satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule"));
}

@Test
void testNewVulnerabilityIdentifiedShouldNotTriggerNotification() {
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 = createMockPublisher();

final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher);
rule.setNotifyOn(Set.of(NotificationGroup.NEW_VULNERABILITY));
rule.setProjects(List.of(projectA));

// Set which severities to trigger notification (CRITICAL and HIGH only)
rule.setNotifySeverities(List.of(Severity.CRITICAL, Severity.HIGH));

// Set a low severity that should NOT trigger a notification
final Vulnerability lowVuln = new Vulnerability();
lowVuln.setSeverity(Severity.LOW);

final var notification = new Notification();
notification.setScope(NotificationScope.PORTFOLIO.name());
notification.setGroup(NotificationGroup.NEW_VULNERABILITY.name());
notification.setLevel(NotificationLevel.INFORMATIONAL);
notification.setSubject(new NewVulnerabilityIdentified(lowVuln, qm.detach(componentB), Set.of(), null));

final var router = new NotificationRouter();

// This should not trigger a notification
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();
}

@Test
void testNewVulnerabilityIdentifiedLimitedToProject() {
final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false);
Expand Down Expand Up @@ -414,6 +494,95 @@ void testNewVulnerableDependencyLimitedToProject() {
.satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule"));
}

@Test
void testNewVulnerableDependencyThatShouldTriggerNotification() {
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));

// Set which severities to trigger notification
rule.setNotifySeverities(List.of(Severity.HIGH));

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(qm.detach(componentB), null));

final var router = new NotificationRouter();
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();

// Set two high vulnerabilities
final Vulnerability highVuln1 = new Vulnerability();
highVuln1.setSeverity(Severity.HIGH);
final Vulnerability highVuln2 = new Vulnerability();
highVuln2.setSeverity(Severity.HIGH);
List<Vulnerability> vulnerabilities = List.of(highVuln1, highVuln2);

// Set a new vulnerable dependency that should trigger a notification
notification.setSubject(new NewVulnerableDependency(qm.detach(componentA), vulnerabilities));
assertThat(router.resolveRules(PublishContext.from(notification), notification))
.satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule"));
}

@Test
void testNewVulnerableDependencyThatShouldNotTriggerNotification() {
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));

// Set which severities to trigger notification
rule.setNotifySeverities(List.of(Severity.HIGH));

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(qm.detach(componentB), null));

final var router = new NotificationRouter();
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();

// Set two low vulnerabilities
final Vulnerability highVuln1 = new Vulnerability();
highVuln1.setSeverity(Severity.LOW);
final Vulnerability highVuln2 = new Vulnerability();
highVuln2.setSeverity(Severity.LOW);
List<Vulnerability> vulnerabilities = List.of(highVuln1, highVuln2);

// Set a new vulnerable dependency that should trigger a notification
notification.setSubject(new NewVulnerableDependency(qm.detach(componentA), vulnerabilities));
assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty();
}

@Test
void testBomConsumedOrProcessedLimitedToProject() {
final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false);
Expand Down
Loading