Skip to content

Commit

Permalink
Merge pull request #1532 from DependencyTrack/port-notification-tags
Browse files Browse the repository at this point in the history
Port : Add tag support for notifications
  • Loading branch information
nscuro authored Sep 26, 2024
2 parents fce736c + e95ca41 commit 16d34df
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ public class NotificationRule extends PanacheEntityBase {
@OrderBy("name ASC, version ASC")
private List<Project> projects;

@OneToMany
@JoinTable(
name = "NOTIFICATIONRULE_TAGS",
joinColumns = @JoinColumn(name = "NOTIFICATIONRULE_ID", referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "TAG_ID", referencedColumnName = "ID")
)
@OrderBy("name ASC")
private List<Tag> tags;

// @Join(column = "NOTIFICATIONRULE_ID")
// @Element(column = "TEAM_ID")
// @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC"))
Expand Down Expand Up @@ -180,6 +189,14 @@ public void setProjects(List<Project> projects) {
this.projects = projects;
}

public List<Tag> getTags() {
return tags;
}

public void setTags(List<Tag> tags) {
this.tags = tags;
}

// public List<Team> getTeams() {
// return teams;
// }
Expand Down
15 changes: 15 additions & 0 deletions commons-persistence/src/main/resources/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,11 @@ CREATE TABLE public."NOTIFICATIONRULE_PROJECTS" (
"PROJECT_ID" bigint
);

CREATE TABLE public."NOTIFICATIONRULE_TAGS" (
"NOTIFICATIONRULE_ID" bigint NOT NULL,
"TAG_ID" bigint
);

CREATE TABLE public."NOTIFICATIONRULE_TEAMS" (
"NOTIFICATIONRULE_ID" bigint NOT NULL,
"TEAM_ID" bigint
Expand Down Expand Up @@ -2469,6 +2474,10 @@ CREATE INDEX "NOTIFICATIONRULE_PROJECTS_PROJECT_ID_IDX" ON public."NOTIFICATIONR

CREATE INDEX "NOTIFICATIONRULE_PUBLISHER_IDX" ON public."NOTIFICATIONRULE" USING btree ("PUBLISHER");

CREATE INDEX "NOTIFICATIONRULE_TAGS_NOTIFICATIONRULE_ID_IDX" ON public."NOTIFICATIONRULE_TAGS" USING btree ("NOTIFICATIONRULE_ID");

CREATE INDEX "NOTIFICATIONRULE_TAGS_TAG_ID_IDX" ON public."NOTIFICATIONRULE_TAGS" USING btree ("TAG_ID");

CREATE INDEX "NOTIFICATIONRULE_TEAMS_NOTIFICATIONRULE_ID_IDX" ON public."NOTIFICATIONRULE_TEAMS" USING btree ("NOTIFICATIONRULE_ID");

CREATE INDEX "NOTIFICATIONRULE_TEAMS_TEAM_ID_IDX" ON public."NOTIFICATIONRULE_TEAMS" USING btree ("TEAM_ID");
Expand Down Expand Up @@ -2729,6 +2738,12 @@ ALTER TABLE ONLY public."NOTIFICATIONRULE_PROJECTS"
ALTER TABLE ONLY public."NOTIFICATIONRULE_PROJECTS"
ADD CONSTRAINT "NOTIFICATIONRULE_PROJECTS_PROJECT_FK" FOREIGN KEY ("PROJECT_ID") REFERENCES public."PROJECT"("ID") DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE ONLY public."NOTIFICATIONRULE_TAGS"
ADD CONSTRAINT "NOTIFICATIONRULE_TAGS_NOTIFICATIONRULE_FK" FOREIGN KEY ("NOTIFICATIONRULE_ID") REFERENCES public."NOTIFICATIONRULE"("ID") DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE ONLY public."NOTIFICATIONRULE_TAGS"
ADD CONSTRAINT "NOTIFICATIONRULE_TAGS_TAG_FK" FOREIGN KEY ("TAG_ID") REFERENCES public."TAG"("ID") DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE ONLY public."NOTIFICATIONRULE_TEAMS"
ADD CONSTRAINT "NOTIFICATIONRULE_TEAMS_NOTIFICATIONRULE_FK" FOREIGN KEY ("NOTIFICATIONRULE_ID") REFERENCES public."NOTIFICATIONRULE"("ID") DEFERRABLE INITIALLY DEFERRED;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ public void testProjects() {
Assertions.assertEquals(project, rule.getProjects().get(0));
}

@Test
public void testTags() {
List<Tag> tags = new ArrayList<>();
Tag tag = new Tag();
tags.add(tag);
NotificationRule rule = new NotificationRule();
rule.setTags(tags);
Assertions.assertEquals(1, rule.getTags().size());
Assertions.assertEquals(tag, rule.getTags().get(0));
}

@Test
public void testMessage() {
NotificationRule rule = new NotificationRule();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.dependencytrack.persistence.model.NotificationRule;
import org.dependencytrack.persistence.model.NotificationScope;
import org.dependencytrack.persistence.model.Project;
import org.dependencytrack.persistence.model.Tag;
import org.dependencytrack.persistence.model.Team;
import org.dependencytrack.persistence.repository.NotificationRuleRepository;
import org.dependencytrack.persistence.repository.TeamRepository;
Expand All @@ -63,6 +64,7 @@
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_KEY;
Expand Down Expand Up @@ -199,25 +201,7 @@ List<NotificationRule> resolveRules(final PublishContext ctx, final Notification
LOGGER.debug("Matched %d notification rules (%s)".formatted(result.size(), ctx));
if (notification.getScope() == SCOPE_PORTFOLIO
&& notification.getSubject().is(NewVulnerabilitySubject.class)) {
final var subject = notification.getSubject().unpack(NewVulnerabilitySubject.class);
// 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(convert(notification.getGroup()))) {
if (rule.getProjects() != null && !rule.getProjects().isEmpty()
&& subject.hasComponent() && subject.hasProject()) {
for (final Project project : rule.getProjects()) {
if (subject.getProject().getUuid().equals(project.getUuid().toString()) || (Boolean.TRUE.equals(rule.isNotifyChildren() && checkIfChildrenAreAffected(project, subject.getProject().getUuid())))) {
rules.add(rule);
}
}
} else {
rules.add(rule);
}
}
}
limitToProject(ctx, rules, result, notification, notification.getSubject().unpack(NewVulnerabilitySubject.class).getProject());
} else if (notification.getScope() == SCOPE_PORTFOLIO
&& notification.getSubject().is(NewVulnerableDependencySubject.class)) {
limitToProject(ctx, rules, result, notification, notification.getSubject().unpack(NewVulnerableDependencySubject.class).getProject());
Expand Down Expand Up @@ -257,43 +241,81 @@ List<NotificationRule> resolveRules(final PublishContext ctx, final Notification
* of the notification down to those projects that the rule matches and which
* also match projects affected by the vulnerability.
*/
private void limitToProject(final PublishContext ctx, final List<NotificationRule> applicableRules, final List<NotificationRule> rules,
final Notification notification, final org.dependencytrack.proto.notification.v1.Project limitToProject) {
private void limitToProject(
final PublishContext ctx,
final List<NotificationRule> applicableRules,
final List<NotificationRule> rules,
final Notification notification,
final org.dependencytrack.proto.notification.v1.Project limitToProject
) {
for (final NotificationRule rule : rules) {
final PublishContext ruleCtx = ctx.withRule(rule);
if (rule.getNotifyOn().contains(convert(notification.getGroup()))) {
if (rule.getProjects() != null && !rule.getProjects().isEmpty()) {
for (final Project project : rule.getProjects()) {
if (project.getUuid().toString().equals(limitToProject.getUuid())) {
LOGGER.debug("Project %s is part of the \"limit to\" list of the rule; Rule is applicable (%s)"
.formatted(limitToProject.getUuid(), ruleCtx));
applicableRules.add(rule);
} else if (rule.isNotifyChildren()) {
final boolean isChildOfLimitToProject = checkIfChildrenAreAffected(project, limitToProject.getUuid());
if (isChildOfLimitToProject) {
LOGGER.debug("Project %s is child of \"limit to\" project %s; Rule is applicable (%s)"
.formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx));
applicableRules.add(rule);
} else {
LOGGER.debug("Project %s is not a child of \"limit to\" project %s; Rule is not applicable (%s)"
.formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx));
}
if (!rule.getNotifyOn().contains(convert(notification.getGroup()))) {
continue;
}

final boolean isLimitedToProjects = rule.getProjects() != null && !rule.getProjects().isEmpty();
final boolean isLimitedToTags = rule.getTags() != null && !rule.getTags().isEmpty();
if (!isLimitedToProjects && !isLimitedToTags) {
LOGGER.debug("Rule is not limited to projects or tags; Rule is applicable (%s)".formatted(ruleCtx));
applicableRules.add(rule);
continue;
}

if (isLimitedToTags) {
final Predicate<org.dependencytrack.proto.notification.v1.Project> tagMatchPredicate = project ->
project.getTagsList() != null
&& rule.getTags().stream()
.map(Tag::getName)
.anyMatch(project.getTagsList()::contains);

if (tagMatchPredicate.test(limitToProject)) {
LOGGER.debug("""
Project %s is tagged with any of the "limit to" tags; \
Rule is applicable (%s)""".formatted(limitToProject.getUuid(), ruleCtx));
applicableRules.add(rule);
continue;
}
} else {
LOGGER.debug("Rule is not limited to tags (%s)".formatted(ruleCtx));
}

if (isLimitedToProjects) {
var matched = false;
for (final Project project : rule.getProjects()) {
if (project.getUuid().toString().equals(limitToProject.getUuid())) {
LOGGER.debug("Project %s is part of the \"limit to\" list of the rule; Rule is applicable (%s)"
.formatted(limitToProject.getUuid(), ruleCtx));
matched = true;
break;
} else if (rule.isNotifyChildren()) {
final boolean isChildOfLimitToProject = checkIfChildrenAreAffected(project, limitToProject.getUuid());
if (isChildOfLimitToProject) {
LOGGER.debug("Project %s is child of \"limit to\" project %s; Rule is applicable (%s)"
.formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx));
matched = true;
break;
} else {
LOGGER.debug("Project %s is not part of the \"limit to\" list of the rule; Rule is not applicable (%s)"
.formatted(limitToProject.getUuid(), ruleCtx));
LOGGER.debug("Project %s is not a child of \"limit to\" project %s (%s)"
.formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx));
}
}
} else {
LOGGER.debug("Rule is not limited to projects; Rule is applicable (%s)".formatted(ruleCtx));
}
if (matched) {
applicableRules.add(rule);
} else {
LOGGER.debug("Project %s is not part of the \"limit to\" list of the rule; Rule is not applicable (%s)"
.formatted(limitToProject.getUuid(), ruleCtx));
}
} else {
LOGGER.debug("Rule is not limited to projects (%s)".formatted(ruleCtx));
}
}
LOGGER.debug("Applicable rules: %s (%s)"
.formatted(applicableRules.stream().map(NotificationRule::getName).collect(Collectors.joining(", ")), ctx));
}

private boolean checkIfChildrenAreAffected(Project parent, String uuid) {
private boolean checkIfChildrenAreAffected (Project parent, String uuid){
boolean isChild = false;
if (parent.getChildren() == null || parent.getChildren().isEmpty()) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,72 @@ void testResolveRulesUserCreatedNotification() throws Exception {
);
}

@Test
@TestTransaction
void testResolveRulesLimitedToProjectTag() throws Exception {
final UUID projectUuidA = UUID.randomUUID();
createProject("Project A", "1.0", true, projectUuidA);

final Long tagId = createTag("test-tag");

final Long publisherId = createConsolePublisher();
final Long ruleId = createRule("Limit To Test Rule",
NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL,
NotificationGroup.NEW_VULNERABILITY, publisherId);
addTagToRule(tagId, ruleId);

final var notificationProjectA = Notification.newBuilder()
.setScope(SCOPE_PORTFOLIO)
.setGroup(GROUP_NEW_VULNERABILITY)
.setLevel(LEVEL_INFORMATIONAL)
.setSubject(Any.pack(NewVulnerabilitySubject.newBuilder()
.setComponent(Component.newBuilder()
.setUuid(UUID.randomUUID().toString()))
.setProject(Project.newBuilder()
.setUuid(projectUuidA.toString())
.addTags("test-tag"))
.setVulnerability(Vulnerability.newBuilder()
.setUuid(UUID.randomUUID().toString()))
.build()))
.build();

Assertions.assertThat(notificationRouter.resolveRules(PublisherTestUtil.createPublisherContext(notificationProjectA), notificationProjectA)).satisfiesExactly(
rule -> Assertions.assertThat(rule.getName()).isEqualTo("Limit To Test Rule")
);
}

@Test
@TestTransaction
void testResolveRulesWithValidNonMatchingTagLimitRule() throws Exception {
final Long publisherId = createConsolePublisher();
final Long ruleId = createRule("Test Rule",
NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL,
NotificationGroup.NEW_VULNERABILITY, publisherId);

final UUID projectUuid = UUID.randomUUID();
createProject("Test Project", "1.0", true, projectUuid);

final Long tagId = createTag("test-tag");
addTagToRule(tagId, ruleId);

final var notification = Notification.newBuilder()
.setScope(SCOPE_PORTFOLIO)
.setGroup(GROUP_NEW_VULNERABILITY)
.setLevel(LEVEL_INFORMATIONAL)
.setSubject(Any.pack(NewVulnerabilitySubject.newBuilder()
.setComponent(Component.newBuilder()
.setUuid(UUID.randomUUID().toString()))
// project is not tagged
.setProject(Project.newBuilder()
.setUuid(UUID.randomUUID().toString()))
.setVulnerability(Vulnerability.newBuilder()
.setUuid(UUID.randomUUID().toString()))
.build()))
.build();
final List<NotificationRule> rules = notificationRouter.resolveRules(PublisherTestUtil.createPublisherContext(notification), notification);
assertThat(rules).isEmpty();
}

private Long createConsolePublisher() {
return (Long) entityManager.createNativeQuery("""
INSERT INTO "NOTIFICATIONPUBLISHER" ("DEFAULT_PUBLISHER", "NAME", "PUBLISHER_CLASS", "TEMPLATE", "TEMPLATE_MIME_TYPE", "UUID") VALUES
Expand Down Expand Up @@ -930,4 +996,22 @@ private void addProjectToRule(final Long projectId, final Long ruleId) {
.executeUpdate();
}

private Long createTag(final String name) {
return (Long) entityManager.createNativeQuery("""
INSERT INTO "TAG" ("NAME") VALUES (:name)
RETURNING "ID";
""")
.setParameter("name", name)
.getSingleResult();
}

private void addTagToRule(final Long tagId, final Long ruleId) {
entityManager.createNativeQuery("""
INSERT INTO "NOTIFICATIONRULE_TAGS" ("TAG_ID", "NOTIFICATIONRULE_ID") VALUES
(:tagId, :ruleId);
""")
.setParameter("tagId", tagId)
.setParameter("ruleId", ruleId)
.executeUpdate();
}
}

0 comments on commit 16d34df

Please sign in to comment.