From fa060573e984b2869b651a659de9f39cf1e936a5 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 29 Apr 2024 17:13:29 +0200 Subject: [PATCH 01/98] added javacron dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 2c325d6b23..d37ea06192 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,7 @@ 5.4 2.0.16 1.323 + 1.4.0 12.8.1.jre11 8.2.0 @@ -411,6 +412,12 @@ ${lib.org-kohsuke-github-api.version} + + com.asahaf.javacron + javacron + ${lib.com-asahaf-javacron.version} + + junit From 30fd0cfb4eedf26b5d10d1b0288017c3f51ed11d Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 29 Apr 2024 17:23:59 +0200 Subject: [PATCH 02/98] added scheduled properties in NotificationRule, added configurable default cron interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/ConfigPropertyConstants.java | 1 + .../model/NotificationRule.java | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index db989d7489..64c2912231 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -98,6 +98,7 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates"), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates"), + NOTIFICATION_CRON_DEFAULT_INTERVAL("notification", "cron.default.interval", "0 12 * * *", PropertyType.STRING, "The default interval of scheduled notifications as cron expression (every day at 12pm)"), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP"), TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories"), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database"), diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index 9fdad4c536..cae9b984af 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -22,6 +22,9 @@ import alpine.model.Team; import alpine.notification.NotificationLevel; import alpine.server.json.TrimmedStringDeserializer; + +import com.asahaf.javacron.InvalidExpressionException; +import com.asahaf.javacron.Schedule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -45,6 +48,7 @@ import javax.jdo.annotations.PrimaryKey; import javax.jdo.annotations.Unique; import java.io.Serializable; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -154,6 +158,20 @@ public class NotificationRule implements Serializable { @NotNull private UUID uuid; + @Persistent(defaultFetchGroup = "true") + @Column(name = "CRON_CONFIG", allowsNull = "true") // new column, must allow nulls on existing databases + @JsonDeserialize(using = TrimmedStringDeserializer.class) + // @Pattern(regexp = RegexSequence.Definition.CRON, message = "The message may only contain characters valid in cron strings") + private String cronConfig; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "LAST_EXECUTION_TIME", allowsNull = "true") // new column, must allow nulls on existing databases + private ZonedDateTime lastExecutionTime; + + @Persistent + @Column(name = "PUBLISH_ONLY_WITH_UPDATES", allowsNull = "true") // new column, must allow nulls on existing databases + private boolean publishOnlyWithUpdates; + public long getId() { return id; } @@ -296,4 +314,36 @@ public UUID getUuid() { public void setUuid(@NotNull UUID uuid) { this.uuid = uuid; } + + public Schedule getCronConfig() throws InvalidExpressionException { + var cronSchedule = Schedule.create(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue()); + if (this.cronConfig != null) { + cronSchedule = Schedule.create(this.cronConfig); + } + return cronSchedule; + } + + public void setCronConfig(Schedule cronSchedule) { + if (cronSchedule == null) { + this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); + return; + } + this.cronConfig = cronSchedule.getExpression(); + } + + public ZonedDateTime getLastExecutionTime() { + return lastExecutionTime; + } + + public void setLastExecutionTime(ZonedDateTime lastExecutionTime) { + this.lastExecutionTime = lastExecutionTime; + } + + public boolean getPublishOnlyWithUpdates() { + return publishOnlyWithUpdates; + } + + public void setPublishOnlyWithUpdates(boolean publishOnlyWithUpdates) { + this.publishOnlyWithUpdates = publishOnlyWithUpdates; + } } From 02b815e4bd9641ac5bc59d3992a9f536a3a75e37 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 29 Apr 2024 18:00:17 +0200 Subject: [PATCH 03/98] changed type of cron configuration to string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/model/NotificationRule.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index cae9b984af..02e26dc75f 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -23,8 +23,6 @@ import alpine.notification.NotificationLevel; import alpine.server.json.TrimmedStringDeserializer; -import com.asahaf.javacron.InvalidExpressionException; -import com.asahaf.javacron.Schedule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -315,20 +313,20 @@ public void setUuid(@NotNull UUID uuid) { this.uuid = uuid; } - public Schedule getCronConfig() throws InvalidExpressionException { - var cronSchedule = Schedule.create(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue()); + public String getCronConfig() { + var cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); if (this.cronConfig != null) { - cronSchedule = Schedule.create(this.cronConfig); + cronConfig = this.cronConfig; } - return cronSchedule; + return cronConfig; } - public void setCronConfig(Schedule cronSchedule) { - if (cronSchedule == null) { + public void setCronConfig(String cronConfig) { + if (cronConfig == null) { this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); return; } - this.cronConfig = cronSchedule.getExpression(); + this.cronConfig = cronConfig; } public ZonedDateTime getLastExecutionTime() { From bd1cddff2498ef1234c6dd16de1665a5ee9e3f81 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 29 Apr 2024 18:00:46 +0200 Subject: [PATCH 04/98] added fallback for last execution time if not set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- src/main/java/org/dependencytrack/model/NotificationRule.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index 02e26dc75f..71254c34e4 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -330,6 +330,9 @@ public void setCronConfig(String cronConfig) { } public ZonedDateTime getLastExecutionTime() { + if (lastExecutionTime == null) { + return ZonedDateTime.now(); + } return lastExecutionTime; } From 5ac8ade397e6f00bdbf542c7b9c95ced1595dac1 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 2 May 2024 17:12:40 +0200 Subject: [PATCH 05/98] moved scheduled properties from NotificationRule to new class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/NotificationRule.java | 50 ----------- .../model/ScheduledNotificationRule.java | 88 +++++++++++++++++++ 2 files changed, 88 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index 71254c34e4..8b82d7efb0 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -46,7 +46,6 @@ import javax.jdo.annotations.PrimaryKey; import javax.jdo.annotations.Unique; import java.io.Serializable; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -156,20 +155,6 @@ public class NotificationRule implements Serializable { @NotNull private UUID uuid; - @Persistent(defaultFetchGroup = "true") - @Column(name = "CRON_CONFIG", allowsNull = "true") // new column, must allow nulls on existing databases - @JsonDeserialize(using = TrimmedStringDeserializer.class) - // @Pattern(regexp = RegexSequence.Definition.CRON, message = "The message may only contain characters valid in cron strings") - private String cronConfig; - - @Persistent(defaultFetchGroup = "true") - @Column(name = "LAST_EXECUTION_TIME", allowsNull = "true") // new column, must allow nulls on existing databases - private ZonedDateTime lastExecutionTime; - - @Persistent - @Column(name = "PUBLISH_ONLY_WITH_UPDATES", allowsNull = "true") // new column, must allow nulls on existing databases - private boolean publishOnlyWithUpdates; - public long getId() { return id; } @@ -312,39 +297,4 @@ public UUID getUuid() { public void setUuid(@NotNull UUID uuid) { this.uuid = uuid; } - - public String getCronConfig() { - var cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); - if (this.cronConfig != null) { - cronConfig = this.cronConfig; - } - return cronConfig; - } - - public void setCronConfig(String cronConfig) { - if (cronConfig == null) { - this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); - return; - } - this.cronConfig = cronConfig; - } - - public ZonedDateTime getLastExecutionTime() { - if (lastExecutionTime == null) { - return ZonedDateTime.now(); - } - return lastExecutionTime; - } - - public void setLastExecutionTime(ZonedDateTime lastExecutionTime) { - this.lastExecutionTime = lastExecutionTime; - } - - public boolean getPublishOnlyWithUpdates() { - return publishOnlyWithUpdates; - } - - public void setPublishOnlyWithUpdates(boolean publishOnlyWithUpdates) { - this.publishOnlyWithUpdates = publishOnlyWithUpdates; - } } diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java new file mode 100644 index 0000000000..a826d62e46 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -0,0 +1,88 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import alpine.server.json.TrimmedStringDeserializer; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import javax.jdo.annotations.Column; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import java.time.ZonedDateTime; + +/** + * Defines a Model class for scheduled notification configurations. + * + * @author Max Schiller + */ +@PersistenceCapable +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ScheduledNotificationRule extends NotificationRule { + @Persistent(defaultFetchGroup = "true") + @Column(name = "CRON_CONFIG", allowsNull = "true") // new column, must allow nulls on existing databases + @JsonDeserialize(using = TrimmedStringDeserializer.class) + // @Pattern(regexp = RegexSequence.Definition.CRON, message = "The message may only contain characters valid in cron strings") + private String cronConfig; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "LAST_EXECUTION_TIME", allowsNull = "true") // new column, must allow nulls on existing databases + private ZonedDateTime lastExecutionTime; + + @Persistent + @Column(name = "PUBLISH_ONLY_WITH_UPDATES", allowsNull = "true") // new column, must allow nulls on existing databases + private boolean publishOnlyWithUpdates; + + public String getCronConfig() { + var cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); + if (this.cronConfig != null) { + cronConfig = this.cronConfig; + } + return cronConfig; + } + + public void setCronConfig(String cronConfig) { + if (cronConfig == null) { + this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); + return; + } + this.cronConfig = cronConfig; + } + + public ZonedDateTime getLastExecutionTime() { + if (lastExecutionTime == null) { + return ZonedDateTime.now(); + } + return lastExecutionTime; + } + + public void setLastExecutionTime(ZonedDateTime lastExecutionTime) { + this.lastExecutionTime = lastExecutionTime; + } + + public boolean getPublishOnlyWithUpdates() { + return publishOnlyWithUpdates; + } + + public void setPublishOnlyWithUpdates(boolean publishOnlyWithUpdates) { + this.publishOnlyWithUpdates = publishOnlyWithUpdates; + } +} \ No newline at end of file From 465624f7e5f4f40514447193d5c89d111f120c65 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 2 May 2024 17:12:59 +0200 Subject: [PATCH 06/98] added persistence entry for ScheduledNotificationRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- src/main/resources/META-INF/persistence.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 54ea30d611..26da474e71 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -34,6 +34,7 @@ org.dependencytrack.model.LicenseGroup org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule + org.dependencytrack.model.ScheduledNotificationRule org.dependencytrack.model.Policy org.dependencytrack.model.PolicyCondition org.dependencytrack.model.PolicyViolation From 5f9a6cfee42910e869a7dc30ea66eb71bf4d83ad Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 2 May 2024 17:40:25 +0200 Subject: [PATCH 07/98] added scheduled crud methods to query managers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/NotificationQueryManager.java | 65 +++++++++++++++++++ .../persistence/QueryManager.java | 13 ++++ 2 files changed, 78 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index 3f4b6e71ad..b5cf44be28 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -22,10 +22,13 @@ import alpine.notification.NotificationLevel; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; + +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.Publisher; @@ -33,6 +36,8 @@ import javax.jdo.Query; import java.util.ArrayList; import java.util.Collection; + +import java.time.ZonedDateTime; import java.util.List; import static org.dependencytrack.util.PersistenceUtil.assertPersistent; @@ -80,6 +85,29 @@ public NotificationRule createNotificationRule(String name, NotificationScope sc }); } + /** + * Creates a new ScheduledNotificationRule. + * @param name the name of the rule + * @param scope the scope + * @param level the level + * @param publisher the publisher + * @return a new ScheduledNotificationRule + */ + public ScheduledNotificationRule createScheduledNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { + final ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setName(name); + rule.setScope(scope); + rule.setNotificationLevel(level); + rule.setPublisher(publisher); + rule.setEnabled(true); + rule.setNotifyChildren(true); + rule.setLogSuccessfulPublish(false); + rule.setCronConfig(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue()); + rule.setLastExecutionTime(ZonedDateTime.now()); + rule.setPublishOnlyWithUpdates(false); + return persist(rule); + } + /** * Updated an existing NotificationRule. * @param transientRule the rule to update @@ -99,6 +127,26 @@ public NotificationRule updateNotificationRule(NotificationRule transientRule) { return persist(rule); }); } + + /** + * Updated an existing ScheduledNotificationRule. + * @param transientRule the rule to update + * @return a ScheduledNotificationRule + */ + public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotificationRule transientRule) { + final ScheduledNotificationRule rule = getObjectByUuid(ScheduledNotificationRule.class, transientRule.getUuid()); + rule.setName(transientRule.getName()); + rule.setEnabled(transientRule.isEnabled()); + rule.setNotifyChildren(transientRule.isNotifyChildren()); + rule.setLogSuccessfulPublish(transientRule.isLogSuccessfulPublish()); + rule.setNotificationLevel(transientRule.getNotificationLevel()); + rule.setPublisherConfig(transientRule.getPublisherConfig()); + rule.setNotifyOn(transientRule.getNotifyOn()); + rule.setCronConfig(transientRule.getCronConfig()); + rule.setLastExecutionTime(transientRule.getLastExecutionTime()); + rule.setPublishOnlyWithUpdates(transientRule.getPublishOnlyWithUpdates()); + return persist(rule); + } /** * Returns a paginated list of all notification rules. @@ -116,6 +164,23 @@ public PaginatedResult getNotificationRules() { } return execute(query); } + + /** + * Returns a paginated list of all scheduled notification rules. + * @return a paginated list of ScheduledNotificationRules + */ + public PaginatedResult getScheduledNotificationRules() { + final Query query = pm.newQuery(ScheduledNotificationRule.class); + if (orderBy == null) { + query.setOrdering("name asc"); + } + if (filter != null) { + query.setFilter("name.toLowerCase().matches(:name) || publisher.name.toLowerCase().matches(:name)"); + final String filterString = ".*" + filter.toLowerCase() + ".*"; + return execute(query, filterString); + } + return execute(query); + } /** * Retrieves all NotificationPublishers. diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index ac7db84931..b4f0a69618 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -66,6 +66,7 @@ import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vex; @@ -1287,6 +1288,18 @@ public void removeTeamFromNotificationRules(final Team team) { getNotificationQueryManager().removeTeamFromNotificationRules(team); } + public ScheduledNotificationRule createScheduledNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { + return getNotificationQueryManager().createScheduledNotificationRule(name, scope, level, publisher); + } + + public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotificationRule transientRule) { + return getNotificationQueryManager().updateScheduledNotificationRule(transientRule); + } + + public PaginatedResult getScheduledNotificationRules() { + return getNotificationQueryManager().getScheduledNotificationRules(); + } + /** * Determines if a config property is enabled or not. * @param configPropertyConstants the property to query From 8edf14f574316a9e2580f2456ebf84eab069bde7 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 2 May 2024 18:09:54 +0200 Subject: [PATCH 08/98] added api for scheduled notification rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../v1/ScheduledNotificationRuleResource.java | 351 ++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java new file mode 100644 index 0000000000..76daf4a399 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -0,0 +1,351 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import alpine.common.logging.Logger; +import alpine.model.Team; +import alpine.persistence.PaginatedResult; +import alpine.server.auth.PermissionRequired; +import alpine.server.resources.AlpineResource; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.ResponseHeader; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ScheduledNotificationRule; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.SendMailPublisher; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; + +import javax.validation.Validator; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * JAX-RS resources for processing scheduled notification rules. + * + * @author Max Schiller + */ +@Path("/v1/schedulednotification/rule") +@Api(value = "schedulednotification", authorizations = @Authorization(value = "X-Api-Key")) +public class ScheduledNotificationRuleResource extends AlpineResource { + + private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationRuleResource.class); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all scheduled notification rules", + response = ScheduledNotificationRule.class, + responseContainer = "List", + responseHeaders = @ResponseHeader(name = TOTAL_COUNT_HEADER, response = Long.class, description = "The total number of scheduled notification rules"), + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @PaginatedApi + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response getAllScheduledNotificationRules() { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final PaginatedResult result = qm.getScheduledNotificationRules(); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Creates a new scheduled notification rule", + response = ScheduledNotificationRule.class, + code = 201, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The UUID of the notification publisher could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response createScheduledNotificationRule(ScheduledNotificationRule jsonRule) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(jsonRule, "name") + ); + + try (QueryManager qm = new QueryManager()) { + NotificationPublisher publisher = null; + if (jsonRule.getPublisher() != null) { + publisher =qm.getObjectByUuid(NotificationPublisher.class, jsonRule.getPublisher().getUuid()); + } + if (publisher == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the notification publisher could not be found.").build(); + } + final ScheduledNotificationRule rule = qm.createScheduledNotificationRule( + StringUtils.trimToNull(jsonRule.getName()), + jsonRule.getScope(), + jsonRule.getNotificationLevel(), + publisher + ); + return Response.status(Response.Status.CREATED).entity(rule).build(); + } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Updates a scheduled notification rule", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The UUID of the scheduled notification rule could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response updateScheduledNotificationRule(ScheduledNotificationRule jsonRule) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(jsonRule, "name"), + validator.validateProperty(jsonRule, "publisherConfig") + ); + + try (QueryManager qm = new QueryManager()) { + ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, jsonRule.getUuid()); + if (rule != null) { + jsonRule.setName(StringUtils.trimToNull(jsonRule.getName())); + rule = qm.updateScheduledNotificationRule(jsonRule); + return Response.ok(rule).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the scheduled notification rule could not be found.").build(); + } + } + } + + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Deletes a scheduled notification rule", + code = 204, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The UUID of the scheduled notification rule could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response deleteScheduledNotificationRule(ScheduledNotificationRule jsonRule) { + try (QueryManager qm = new QueryManager()) { + final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, jsonRule.getUuid()); + if (rule != null) { + qm.delete(rule); + return Response.status(Response.Status.NO_CONTENT).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the scheduled notification rule could not be found.").build(); + } + } + } + + @POST + @Path("/{ruleUuid}/project/{projectUuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Adds a project to a scheduled notification rule", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 304, message = "The rule already has the specified project assigned"), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The scheduled notification rule or project could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response addProjectToRule( + @ApiParam(value = "The UUID of the rule to add a project to", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to add to the rule", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { + try (QueryManager qm = new QueryManager()) { + final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, ruleUuid); + if (rule == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The scheduled notification rule could not be found.").build(); + } + if (rule.getScope() != NotificationScope.PORTFOLIO) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Project limitations are only possible on scheduled notification rules with PORTFOLIO scope.").build(); + } + final Project project = qm.getObjectByUuid(Project.class, projectUuid); + if (project == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + } + final List projects = rule.getProjects(); + if (projects != null && !projects.contains(project)) { + rule.getProjects().add(project); + qm.persist(rule); + return Response.ok(rule).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + + @DELETE + @Path("/{ruleUuid}/project/{projectUuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Removes a project from a scheduled notification rule", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 304, message = "The rule does not have the specified project assigned"), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The scheduled notification rule or project could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response removeProjectFromRule( + @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @PathParam("projectUuid") @ValidUuid String projectUuid) { + try (QueryManager qm = new QueryManager()) { + final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, ruleUuid); + if (rule == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The scheduled notification rule could not be found.").build(); + } + if (rule.getScope() != NotificationScope.PORTFOLIO) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Project limitations are only possible on scheduled notification rules with PORTFOLIO scope.").build(); + } + final Project project = qm.getObjectByUuid(Project.class, projectUuid); + if (project == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + } + final List projects = rule.getProjects(); + if (projects != null && projects.contains(project)) { + rule.getProjects().remove(project); + qm.persist(rule); + return Response.ok(rule).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + + @POST + @Path("/{ruleUuid}/team/{teamUuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Adds a team to a scheduled scheduled notification rule", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 304, message = "The rule already has the specified team assigned"), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The scheduled notification rule or team could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response addTeamToRule( + @ApiParam(value = "The UUID of the rule to add a team to", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the team to add to the rule", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid String teamUuid) { + try (QueryManager qm = new QueryManager()) { + final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, ruleUuid); + if (rule == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The scheduled notification rule could not be found.").build(); + } + if (!rule.getPublisher().getPublisherClass().equals(SendMailPublisher.class.getName())) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Team subscriptions are only possible on scheduled notification rules with EMAIL publisher.").build(); + } + final Team team = qm.getObjectByUuid(Team.class, teamUuid); + if (team == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + } + final List teams = rule.getTeams(); + if (teams != null && !teams.contains(team)) { + rule.getTeams().add(team); + qm.persist(rule); + return Response.ok(rule).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + + @DELETE + @Path("/{ruleUuid}/team/{teamUuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Removes a team from a scheduled notification rule", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 304, message = "The rule does not have the specified team assigned"), + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The scheduled notification rule or team could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response removeTeamFromRule( + @ApiParam(value = "The UUID of the rule to remove the project from", format = "uuid", required = true) + @PathParam("ruleUuid") @ValidUuid String ruleUuid, + @ApiParam(value = "The UUID of the project to remove from the rule", format = "uuid", required = true) + @PathParam("teamUuid") @ValidUuid String teamUuid) { + try (QueryManager qm = new QueryManager()) { + final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, ruleUuid); + if (rule == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The scheduled notification rule could not be found.").build(); + } + if (!rule.getPublisher().getPublisherClass().equals(SendMailPublisher.class.getName())) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Team subscriptions are only possible on scheduled notification rules with EMAIL publisher.").build(); + } + final Team team = qm.getObjectByUuid(Team.class, teamUuid); + if (team == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + } + final List teams = rule.getTeams(); + if (teams != null && teams.contains(team)) { + rule.getTeams().remove(team); + qm.persist(rule); + return Response.ok(rule).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } +} From 69e29ddd18bfe0757a94d0e5f8774541eecd94ce Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 3 May 2024 10:37:47 +0200 Subject: [PATCH 09/98] added some minor validation in scheduled api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../resources/v1/ScheduledNotificationRuleResource.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index 76daf4a399..f068ce7646 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -103,7 +103,9 @@ public Response getAllScheduledNotificationRules() { public Response createScheduledNotificationRule(ScheduledNotificationRule jsonRule) { final Validator validator = super.getValidator(); failOnValidationError( - validator.validateProperty(jsonRule, "name") + validator.validateProperty(jsonRule, "name"), + validator.validateProperty(jsonRule, "cronConfig"), + validator.validateProperty(jsonRule, "lastExecutionTime") ); try (QueryManager qm = new QueryManager()) { @@ -141,7 +143,9 @@ public Response updateScheduledNotificationRule(ScheduledNotificationRule jsonRu final Validator validator = super.getValidator(); failOnValidationError( validator.validateProperty(jsonRule, "name"), - validator.validateProperty(jsonRule, "publisherConfig") + validator.validateProperty(jsonRule, "publisherConfig"), + validator.validateProperty(jsonRule, "cronConfig"), + validator.validateProperty(jsonRule, "lastExecutionTime") ); try (QueryManager qm = new QueryManager()) { From 49352eb58496dcbdf145d9db081dbbdfcf811fb4 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 3 May 2024 12:43:11 +0200 Subject: [PATCH 10/98] fixed wrong database usage (data stored in notificationrule table), workaround for unknown possibility of JDO Inheritance setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/ScheduledNotificationRule.java | 250 +++++++++++++++++- 1 file changed, 249 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index a826d62e46..6c0b505a9f 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -18,15 +18,42 @@ */ package org.dependencytrack.model; +import alpine.common.validation.RegexSequence; +import alpine.model.Team; +import alpine.notification.NotificationLevel; import alpine.server.json.TrimmedStringDeserializer; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import org.apache.commons.collections4.CollectionUtils; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; + +import java.io.Serializable; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; /** * Defines a Model class for scheduled notification configurations. @@ -36,7 +63,92 @@ @PersistenceCapable @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class ScheduledNotificationRule extends NotificationRule { +public class ScheduledNotificationRule implements Serializable { + private static final long serialVersionUID = 2534439091019367263L; + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + /** + * The String representation of the name of the notification. + */ + @Persistent + @Column(name = "NAME", allowsNull = "false") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters") + private String name; + + @Persistent + @Column(name = "ENABLED") + private boolean enabled; + + @Persistent + @Column(name = "NOTIFY_CHILDREN", allowsNull = "true") // New column, must allow nulls on existing data bases) + private boolean notifyChildren; + + /** + * In addition to warnings and errors, also emit a log message upon successful publishing. + *

+ * Intended to aid in debugging of missing notifications, or environments where notification + * delivery is critical and subject to auditing. + * + * @since 4.10.0 + */ + @Persistent + @Column(name = "LOG_SUCCESSFUL_PUBLISH", allowsNull = "true") + private boolean logSuccessfulPublish; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "SCOPE", jdbcType = "VARCHAR", allowsNull = "false") + @NotNull + private NotificationScope scope; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "NOTIFICATION_LEVEL", jdbcType = "VARCHAR") + private NotificationLevel notificationLevel; + + @Persistent(table = "SCHEDULED_NOTIFICATIONRULE_PROJECTS", defaultFetchGroup = "true") + @Join(column = "SCHEDULED_NOTIFICATIONRULE_ID") + @Element(column = "PROJECT_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC, version ASC")) + private List projects; + + @Persistent(table = "SCHEDULED_NOTIFICATIONRULE_TEAMS", defaultFetchGroup = "true") + @Join(column = "SCHEDULED_NOTIFICATIONRULE_ID") + @Element(column = "TEAM_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) + private List teams; + + @Persistent + @Column(name = "NOTIFY_ON", length = 1024) + private String notifyOn; + + @Persistent + @Column(name = "MESSAGE", length = 1024) + @Size(max = 1024) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The message may only contain printable characters") + private String message; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "PUBLISHER") + private NotificationPublisher publisher; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "PUBLISHER_CONFIG", jdbcType = "CLOB") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + private String publisherConfig; + + @Persistent(defaultFetchGroup = "true", customValueStrategy = "uuid") + @Unique(name = "SCHEDULED_NOTIFICATIONRULE_UUID_IDX") + @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false") + @NotNull + private UUID uuid; + @Persistent(defaultFetchGroup = "true") @Column(name = "CRON_CONFIG", allowsNull = "true") // new column, must allow nulls on existing databases @JsonDeserialize(using = TrimmedStringDeserializer.class) @@ -51,6 +163,142 @@ public class ScheduledNotificationRule extends NotificationRule { @Column(name = "PUBLISH_ONLY_WITH_UPDATES", allowsNull = "true") // new column, must allow nulls on existing databases private boolean publishOnlyWithUpdates; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @NotNull + public String getName() { + return name; + } + + public void setName(@NotNull String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isNotifyChildren() { + return notifyChildren; + } + + public void setNotifyChildren(boolean notifyChildren) { + this.notifyChildren = notifyChildren; + } + + public boolean isLogSuccessfulPublish() { + return logSuccessfulPublish; + } + + public void setLogSuccessfulPublish(final boolean logSuccessfulPublish) { + this.logSuccessfulPublish = logSuccessfulPublish; + } + + @NotNull + public NotificationScope getScope() { + return scope; + } + + public void setScope(@NotNull NotificationScope scope) { + this.scope = scope; + } + + public NotificationLevel getNotificationLevel() { + return notificationLevel; + } + + public void setNotificationLevel(NotificationLevel notificationLevel) { + this.notificationLevel = notificationLevel; + } + + public List getProjects() { + return projects; + } + + public void setProjects(List projects) { + this.projects = projects; + } + + public List getTeams() { + return teams; + } + + public void setTeams(List teams) { + this.teams = teams; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Set getNotifyOn() { + Set result = new TreeSet<>(); + if (notifyOn != null) { + String[] groups = notifyOn.split(","); + for (String s: groups) { + result.add(NotificationGroup.valueOf(s.trim())); + } + } + return result; + } + + public void setNotifyOn(Set groups) { + if (CollectionUtils.isEmpty(groups)) { + this.notifyOn = null; + return; + } + StringBuilder sb = new StringBuilder(); + List list = new ArrayList<>(groups); + Collections.sort(list); + for (int i=0; i Date: Tue, 7 May 2024 11:57:01 +0200 Subject: [PATCH 11/98] Updated NotificationQueryManager to use UTC time for ScheduledNotificationRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/persistence/NotificationQueryManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index b5cf44be28..8e891436a4 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; @@ -103,7 +104,7 @@ public ScheduledNotificationRule createScheduledNotificationRule(String name, No rule.setNotifyChildren(true); rule.setLogSuccessfulPublish(false); rule.setCronConfig(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue()); - rule.setLastExecutionTime(ZonedDateTime.now()); + rule.setLastExecutionTime(ZonedDateTime.now(ZoneOffset.UTC)); rule.setPublishOnlyWithUpdates(false); return persist(rule); } From 5b79e26a2a670245ef340983051c7d057b3d4dd2 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 7 May 2024 11:58:14 +0200 Subject: [PATCH 12/98] Add new methods for retrieving new policy violations and vulnerabilities for scheduled notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/PolicyQueryManager.java | 37 ++++++++++++++++- .../persistence/QueryManager.java | 9 ++++ .../VulnerabilityQueryManager.java | 41 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index 3aa3b07190..8e21f4a3e5 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -23,6 +23,8 @@ import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; + +import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; @@ -36,14 +38,17 @@ import org.dependencytrack.model.ViolationAnalysisComment; import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.util.DateUtil; - import javax.jdo.PersistenceManager; import javax.jdo.Query; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -53,6 +58,7 @@ import static org.dependencytrack.util.PersistenceUtil.assertNonPersistentAll; import static org.dependencytrack.util.PersistenceUtil.assertPersistent; import static org.dependencytrack.util.PersistenceUtil.assertPersistentAll; +import java.util.Map; final class PolicyQueryManager extends QueryManager implements IQueryManager { @@ -392,6 +398,35 @@ public List getAllPolicyViolations(final Project project) { return (List)query.execute(project.getId()); } + /** + * Returns a List of all Policy objects since given datetime for given project ids. + * @param dateTime DateTime, since which the policy violations shall be fetched + * @param projectIds IDs of the projects, that shall be used for filtering + * @return a List of {@link PolicyViolation}s + */ + @SuppressWarnings("unchecked") + public Map> getNewPolicyViolationsForProjectsSince(ZonedDateTime dateTime, List projectIds){ + String queryString = "SELECT PROJECT_ID, ID " + + "FROM POLICYVIOLATION " + + "WHERE (TIMESTAMP BETWEEN ? AND ?) "; + if(projectIds != null && !projectIds.isEmpty()){ + queryString.concat("AND (PROJECT_ID IN ?) "); + } + queryString.concat("ORDER BY PROJECT_ID ASC"); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, queryString); + final List totalList = (List)query.execute(dateTime, ZonedDateTime.now(ZoneOffset.UTC), projectIds); + Map> projectPolicyViolations = new HashMap<>(); + for(Object[] obj : totalList){ + Project project = getObjectById(Project.class, obj[0]); + PolicyViolation policyViolation = getObjectById(PolicyViolation.class, obj[1]); + if(!projectPolicyViolations.containsKey(project)){ + projectPolicyViolations.put(project, new ArrayList<>()); + } + projectPolicyViolations.get(project).add(policyViolation); + } + return projectPolicyViolations; + } + /** * Returns a List of all Policy violations for a specific project. * @param project the project to retrieve violations for diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index b4f0a69618..7b21745a11 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -89,6 +89,7 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; import java.security.Principal; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -718,6 +719,10 @@ public List getAllPolicyViolations(final Project project) { return getPolicyQueryManager().getAllPolicyViolations(project); } + public Map> getNewPolicyViolationsForProjectsSince(ZonedDateTime zonedDateTime, List projectIds){ + return getPolicyQueryManager().getNewPolicyViolationsForProjectsSince(zonedDateTime, projectIds); + } + public PaginatedResult getPolicyViolations(final Project project, boolean includeSuppressed) { return getPolicyQueryManager().getPolicyViolations(project, includeSuppressed); } @@ -815,6 +820,10 @@ public Vulnerability getVulnerabilityByVulnId(Vulnerability.Source source, Strin return getVulnerabilityQueryManager().getVulnerabilityByVulnId(source, vulnId, includeVulnerableSoftware); } + public Map> getNewVulnerabilitiesForProjectsSince(ZonedDateTime zonedDateTime, List projectIds){ + return getVulnerabilityQueryManager().getNewVulnerabilitiesForProjectsSince(zonedDateTime, projectIds); + } + public void addVulnerability(Vulnerability vulnerability, Component component, AnalyzerIdentity analyzerIdentity) { getVulnerabilityQueryManager().addVulnerability(vulnerability, component, analyzerIdentity); } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 4594e539d8..05867ea402 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -22,6 +22,7 @@ import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import org.apache.commons.lang3.StringUtils; +import org.datanucleus.api.jdo.JDOQuery; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Analysis; @@ -37,6 +38,9 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -180,6 +184,43 @@ public Vulnerability getVulnerabilityByVulnId(Vulnerability.Source source, Strin return getVulnerabilityByVulnId(source.name(), vulnId, includeVulnerableSoftware); } + /** + * Returns vulnerabilities for the specified npm module + * @param module the NPM module to query on + * @return a list of Vulnerability objects + */ + @Deprecated + @SuppressWarnings("unchecked") + //todo: determine if this is needed and delete + public List getVulnerabilitiesForNpmModule(String module) { + final Query query = pm.newQuery(Vulnerability.class, "source == :source && subtitle == :module"); + query.getFetchPlan().addGroup(Vulnerability.FetchGroup.COMPONENTS.name()); + return (List) query.execute(Vulnerability.Source.NPM.name(), module); + } + + @SuppressWarnings("unchecked") + public Map> getNewVulnerabilitiesForProjectsSince(ZonedDateTime lastExecution, List projectIds){ + String queryString = "PROJECT_ID, VULNERABILITY_ID " + + "FROM FINDINGATTRIBUTION " + + "WHERE ATTRIBUTED_ON BETWEEN ? AND ? "; + if(projectIds != null && !projectIds.isEmpty()){ + queryString.concat("AND PROJECT_ID IN ? "); + } + queryString.concat("ORDER BY PROJECT_ID ASC, VULNERABILITY_ID ASC"); + final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, queryString); + final List totalList = (List)query.execute(lastExecution, ZonedDateTime.now(ZoneOffset.UTC), projectIds); + Map> projectVulnerabilities = new HashMap<>(); + for(Object[] obj : totalList){ + Project project = getObjectById(Project.class, obj[0]); + Vulnerability vulnerability = getObjectById(Vulnerability.class, obj[1]); + if(!projectVulnerabilities.containsKey(project)){ + projectVulnerabilities.put(project, new ArrayList<>()); + } + projectVulnerabilities.get(project).add(vulnerability); + } + return projectVulnerabilities; + } + /** * Adds a vulnerability to a component. * @param vulnerability the vulnerability to add From ee6745b18300bb5908aff1b46642dfaa71113ba8 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 7 May 2024 13:09:52 +0200 Subject: [PATCH 13/98] Added basic Task for sending scheduled notifications (originates mainly of previous work from MGE, may be changed in future) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java new file mode 100644 index 0000000000..8b2f537b26 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -0,0 +1,163 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.tasks; + +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import javax.json.Json; +import javax.json.JsonObject; + +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ScheduledNotificationRule; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.QueryManager; + +import alpine.common.logging.Logger; +import alpine.security.crypto.DataEncryption; +import alpine.server.mail.SendMail; +import alpine.server.mail.SendMailException; +import io.pebbletemplates.pebble.PebbleEngine; +import io.pebbletemplates.pebble.template.PebbleTemplate; + +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_FROM_ADDR; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_PASSWORD; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_HOSTNAME; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_PORT; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SSLTLS; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_TRUSTCERT; +import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_USERNAME; +import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_KEY; +import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_MIME_TYPE_KEY; + +public class SendScheduledNotificationTask implements Runnable { + private ScheduledNotificationRule scheduledNotificationRule; + private ScheduledExecutorService service; + private static final Logger LOGGER = Logger.getLogger(SendScheduledNotificationTask.class); + + public SendScheduledNotificationTask(ScheduledNotificationRule scheduledNotificationRule, ScheduledExecutorService service) { + this.scheduledNotificationRule = scheduledNotificationRule; + this.service = service; + } + + @Override + public void run() { + String content = ""; + final String mimeType; + final boolean smtpEnabled; + final String smtpFrom; + final String smtpHostname; + final int smtpPort; + final String smtpUser; + final String encryptedSmtpPassword; + final boolean smtpSslTls; + final boolean smtpTrustCert; + Map> newProjectVulnerabilities; + Map> newProjectPolicyViolations; + + try (QueryManager qm = new QueryManager()) { + scheduledNotificationRule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRule.getUuid()); + if (scheduledNotificationRule == null) { + LOGGER.info("shutdown ExecutorService for Scheduled notification " + scheduledNotificationRule.getUuid()); + service.shutdown(); + } else { + // if (scheduledNotificationRule.getLastExecutionTime().equals(scheduledNotificationRule.getCreated())) { + // LOGGER.info("schedulednotification just created. No Information to show"); + // } else { + final List projectIds = scheduledNotificationRule.getProjects().stream().map(proj -> proj.getId()).toList(); + newProjectVulnerabilities = qm.getNewVulnerabilitiesForProjectsSince(scheduledNotificationRule.getLastExecutionTime(), projectIds); + newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(scheduledNotificationRule.getLastExecutionTime(), projectIds); + + NotificationPublisher notificationPublisher = qm.getNotificationPublisher("Email"); + + JsonObject notificationPublisherConfig = Json.createObjectBuilder() + .add(CONFIG_TEMPLATE_MIME_TYPE_KEY, notificationPublisher.getTemplateMimeType()) + .add(CONFIG_TEMPLATE_KEY, notificationPublisher.getTemplate()) + .build(); + + PebbleEngine pebbleEngine = new PebbleEngine.Builder().build(); + String literalTemplate = notificationPublisherConfig.getString(CONFIG_TEMPLATE_KEY); + final PebbleTemplate template = pebbleEngine.getLiteralTemplate(literalTemplate); + mimeType = notificationPublisherConfig.getString(CONFIG_TEMPLATE_MIME_TYPE_KEY); + + final Map context = new HashMap<>(); + context.put("length", newProjectVulnerabilities.size()); + context.put("vulnerabilities", newProjectVulnerabilities); + context.put("policyviolations", newProjectPolicyViolations); + final Writer writer = new StringWriter(); + template.evaluate(writer, context); + content = writer.toString(); + + smtpEnabled = qm.isEnabled(EMAIL_SMTP_ENABLED); + if (!smtpEnabled) { + System.out.println("SMTP is not enabled; Skipping notification "); + return; + } + smtpFrom = qm.getConfigProperty(EMAIL_SMTP_FROM_ADDR.getGroupName(),EMAIL_SMTP_FROM_ADDR.getPropertyName()).getPropertyValue(); + smtpHostname = qm.getConfigProperty(EMAIL_SMTP_SERVER_HOSTNAME.getGroupName(),EMAIL_SMTP_SERVER_HOSTNAME.getPropertyName()).getPropertyValue(); + smtpPort = Integer.parseInt(qm.getConfigProperty(EMAIL_SMTP_SERVER_PORT.getGroupName(),EMAIL_SMTP_SERVER_PORT.getPropertyName()).getPropertyValue()); + smtpUser = qm.getConfigProperty(EMAIL_SMTP_USERNAME.getGroupName(),EMAIL_SMTP_USERNAME.getPropertyName()).getPropertyValue(); + encryptedSmtpPassword = qm.getConfigProperty(EMAIL_SMTP_PASSWORD.getGroupName(),EMAIL_SMTP_PASSWORD.getPropertyName()).getPropertyValue(); + smtpSslTls = qm.isEnabled(EMAIL_SMTP_SSLTLS); + smtpTrustCert = qm.isEnabled(EMAIL_SMTP_TRUSTCERT); + final boolean smtpAuth = (smtpUser != null && encryptedSmtpPassword != null); + final String decryptedSmtpPassword; + try { + decryptedSmtpPassword = (encryptedSmtpPassword != null) ? DataEncryption.decryptAsString(encryptedSmtpPassword) : null; + } catch (Exception e) { + System.out.println("Failed to decrypt SMTP password"); + return; + } + // String[] destinations = scheduledNotificationRule.getDestinations().split(" "); + try { + final SendMail sendMail = new SendMail() + .from(smtpFrom) + // .to(destinations) + .subject("[Dependency-Track] " + "ScheduledNotification") + .body(content) + .bodyMimeType(mimeType) + .host(smtpHostname) + .port(smtpPort) + .username(smtpUser) + .password(decryptedSmtpPassword) + .smtpauth(smtpAuth) + .useStartTLS(smtpSslTls) + .trustCert(smtpTrustCert); + sendMail.send(); + } catch (SendMailException | RuntimeException e) { + LOGGER.debug("Failed to send notification email "); + LOGGER.debug(e.getMessage()); + } + // } + // qm.updateScheduledNotificationInfoNextExecution(scheduledNotificationRule); + } + + } catch (Exception e) { + LOGGER.debug(e.getMessage()); + } + + } + +} From e13092cb36664973429972cc77110437f2408c34 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 8 May 2024 13:20:13 +0200 Subject: [PATCH 14/98] added update method for last execution after scheduled task completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/NotificationQueryManager.java | 6 ++++++ .../java/org/dependencytrack/persistence/QueryManager.java | 4 ++++ .../tasks/SendScheduledNotificationTask.java | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index 8e891436a4..0505969f69 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -149,6 +149,12 @@ public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotifi return persist(rule); } + public ScheduledNotificationRule updateScheduledNotificationRuleLastExecutionTimeToNowUtc(ScheduledNotificationRule transientRule) { + final ScheduledNotificationRule rule = getObjectByUuid(ScheduledNotificationRule.class, transientRule.getUuid()); + rule.setLastExecutionTime(ZonedDateTime.now(ZoneOffset.UTC)); + return persist(rule); + } + /** * Returns a paginated list of all notification rules. * @return a paginated list of NotificationRules diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 7b21745a11..65d0dc9472 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1305,6 +1305,10 @@ public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotifi return getNotificationQueryManager().updateScheduledNotificationRule(transientRule); } + public ScheduledNotificationRule updateScheduledNotificationRuleToNowUtc(ScheduledNotificationRule transientRule) { + return getNotificationQueryManager().updateScheduledNotificationRuleToNowUtc(transientRule); + } + public PaginatedResult getScheduledNotificationRules() { return getNotificationQueryManager().getScheduledNotificationRules(); } diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 8b2f537b26..1fd1441f7f 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -151,7 +151,7 @@ public void run() { LOGGER.debug(e.getMessage()); } // } - // qm.updateScheduledNotificationInfoNextExecution(scheduledNotificationRule); + qm.updateScheduledNotificationRuleToNowUtc(scheduledNotificationRule); } } catch (Exception e) { From d3d50be82aaa6933ba576e00f7d5c3bfe8d59d11 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 10 May 2024 13:13:36 +0200 Subject: [PATCH 15/98] fixed VulnerabilityQueryManager SQL query for new vulnerabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/persistence/VulnerabilityQueryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 05867ea402..4047fbc643 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -200,7 +200,7 @@ public List getVulnerabilitiesForNpmModule(String module) { @SuppressWarnings("unchecked") public Map> getNewVulnerabilitiesForProjectsSince(ZonedDateTime lastExecution, List projectIds){ - String queryString = "PROJECT_ID, VULNERABILITY_ID " + + String queryString = "SELECT PROJECT_ID, VULNERABILITY_ID " + "FROM FINDINGATTRIBUTION " + "WHERE ATTRIBUTED_ON BETWEEN ? AND ? "; if(projectIds != null && !projectIds.isEmpty()){ From 3e50a58e6d76dfa460a7fa38d2d0fd944c4dea3d Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 13 May 2024 15:47:21 +0200 Subject: [PATCH 16/98] added basic support for scheduled publishing in notification publishers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/NotificationPublisher.java | 13 ++++++++++ .../DefaultNotificationPublishers.java | 15 ++++++++++- .../persistence/NotificationQueryManager.java | 25 ++++++++++-------- .../persistence/QueryManager.java | 8 +++++- .../v1/NotificationPublisherResource.java | 26 ++++++++++--------- .../util/NotificationUtil.java | 3 ++- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/NotificationPublisher.java b/src/main/java/org/dependencytrack/model/NotificationPublisher.java index d4016e3f39..689182cc5f 100644 --- a/src/main/java/org/dependencytrack/model/NotificationPublisher.java +++ b/src/main/java/org/dependencytrack/model/NotificationPublisher.java @@ -52,6 +52,7 @@ @Persistent(name = "template"), @Persistent(name = "templateMimeType"), @Persistent(name = "defaultPublisher"), + @Persistent(name = "publishScheduled"), @Persistent(name = "uuid"), }) }) @@ -108,6 +109,10 @@ public enum FetchGroup { @Column(name = "DEFAULT_PUBLISHER") private boolean defaultPublisher; + @Persistent(defaultFetchGroup = "true") + @Column(name = "PUBLISH_SCHEDULED") + private boolean publishScheduled; + @Persistent(defaultFetchGroup = "true", customValueStrategy = "uuid") @Unique(name = "NOTIFICATIONPUBLISHER_UUID_IDX") @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false") @@ -173,6 +178,14 @@ public void setDefaultPublisher(boolean defaultPublisher) { this.defaultPublisher = defaultPublisher; } + public boolean isPublishScheduled(){ + return publishScheduled; + } + + public void setPublishScheduled(boolean publishScheduled){ + this.publishScheduled = publishScheduled; + } + @NotNull public UUID getUuid() { return uuid; diff --git a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java index 3c5ee7c262..e602fe2b60 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java +++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java @@ -26,7 +26,9 @@ public enum DefaultNotificationPublishers { MS_TEAMS("Microsoft Teams", "Publishes notifications to a Microsoft Teams channel", MsTeamsPublisher.class, "/templates/notification/publisher/msteams.peb", MediaType.APPLICATION_JSON, true), MATTERMOST("Mattermost", "Publishes notifications to a Mattermost channel", MattermostPublisher.class, "/templates/notification/publisher/mattermost.peb", MediaType.APPLICATION_JSON, true), EMAIL("Email", "Sends notifications to an email address", SendMailPublisher.class, "/templates/notification/publisher/email.peb", "text/plain; charset=utf-8", true), + // SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email.peb", MediaType.TEXT_PLAIN, true, true), CONSOLE("Console", "Displays notifications on the system console", ConsolePublisher.class, "/templates/notification/publisher/console.peb", MediaType.TEXT_PLAIN, true), + // SCHEDULED_CONSOLE("Scheduled Console", "Displays summarized notifications on the system console in a defined schedule", ConsolePublisher.class, "/templates/notification/publisher/scheduled_console.peb", MediaType.TEXT_PLAIN, true, true), WEBHOOK("Outbound Webhook", "Publishes notifications to a configurable endpoint", WebhookPublisher.class, "/templates/notification/publisher/webhook.peb", MediaType.APPLICATION_JSON, true), CS_WEBEX("Cisco Webex", "Publishes notifications to a Cisco Webex Teams channel", CsWebexPublisher.class, "/templates/notification/publisher/cswebex.peb", MediaType.APPLICATION_JSON, true), JIRA("Jira", "Creates a Jira issue in a configurable Jira instance and queue", JiraPublisher.class, "/templates/notification/publisher/jira.peb", MediaType.APPLICATION_JSON, true); @@ -37,15 +39,22 @@ public enum DefaultNotificationPublishers { private final String templateFile; private final String templateMimeType; private final boolean defaultPublisher; + private final boolean publishScheduled; DefaultNotificationPublishers(final String name, final String description, final Class publisherClass, - final String templateFile, final String templateMimeType, final boolean defaultPublisher) { + final String templateFile, final String templateMimeType, final boolean defaultPublisher, final boolean publishScheduled) { this.name = name; this.description = description; this.publisherClass = publisherClass; this.templateFile = templateFile; this.templateMimeType = templateMimeType; this.defaultPublisher = defaultPublisher; + this.publishScheduled = publishScheduled; + } + + DefaultNotificationPublishers(final String name, final String description, final Class publisherClass, + final String templateFile, final String templateMimeType, final boolean defaultPublisher) { + this(name, description, publisherClass, templateFile, templateMimeType, defaultPublisher, false); } public String getPublisherName() { @@ -71,4 +80,8 @@ public String getTemplateMimeType() { public boolean isDefaultPublisher() { return defaultPublisher; } + + public boolean isPublishScheduled() { + return publishScheduled; + } } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index 0505969f69..e652ddbda6 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -241,17 +241,20 @@ private NotificationPublisher getDefaultNotificationPublisher(final String clazz */ public NotificationPublisher createNotificationPublisher(final String name, final String description, final Class publisherClass, final String templateContent, - final String templateMimeType, final boolean defaultPublisher) { - return callInTransaction(() -> { - final NotificationPublisher publisher = new NotificationPublisher(); - publisher.setName(name); - publisher.setDescription(description); - publisher.setPublisherClass(publisherClass.getName()); - publisher.setTemplate(templateContent); - publisher.setTemplateMimeType(templateMimeType); - publisher.setDefaultPublisher(defaultPublisher); - return pm.makePersistent(publisher); - }); + final String templateMimeType, final boolean defaultPublisher, final boolean publishScheduled) { + pm.currentTransaction().begin(); + final NotificationPublisher publisher = new NotificationPublisher(); + publisher.setName(name); + publisher.setDescription(description); + publisher.setPublisherClass(publisherClass.getName()); + publisher.setTemplate(templateContent); + publisher.setTemplateMimeType(templateMimeType); + publisher.setDefaultPublisher(defaultPublisher); + publisher.setPublishScheduled(publishScheduled); + pm.makePersistent(publisher); + pm.currentTransaction().commit(); + pm.getFetchPlan().addGroup(NotificationPublisher.FetchGroup.ALL.name()); + return getObjectById(NotificationPublisher.class, publisher.getId()); } /** diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 65d0dc9472..e733319352 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1278,7 +1278,13 @@ public NotificationPublisher getDefaultNotificationPublisher(final Class publisherClass, final String templateContent, final String templateMimeType, final boolean defaultPublisher) { - return getNotificationQueryManager().createNotificationPublisher(name, description, publisherClass, templateContent, templateMimeType, defaultPublisher); + return createNotificationPublisher(name, description, publisherClass, templateContent, templateMimeType, defaultPublisher, false); + } + + public NotificationPublisher createNotificationPublisher(final String name, final String description, + final Class publisherClass, final String templateContent, + final String templateMimeType, final boolean defaultPublisher, final boolean publishScheduled) { + return getNotificationQueryManager().createNotificationPublisher(name, description, publisherClass, templateContent, templateMimeType, defaultPublisher, publishScheduled); } public NotificationPublisher updateNotificationPublisher(NotificationPublisher transientPublisher) { diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 105c660c96..18c90128e1 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -145,18 +145,20 @@ public Response createNotificationPublisher(NotificationPublisher jsonNotificati return Response.status(Response.Status.BAD_REQUEST).entity("The creation of a new default publisher is forbidden").build(); } - final Class publisherClass = Class.forName(jsonNotificationPublisher.getPublisherClass()).asSubclass(Publisher.class); - final NotificationPublisher notificationPublisherCreated = qm.createNotificationPublisher( - jsonNotificationPublisher.getName(), - jsonNotificationPublisher.getDescription(), - publisherClass, - jsonNotificationPublisher.getTemplate(), - jsonNotificationPublisher.getTemplateMimeType(), - jsonNotificationPublisher.isDefaultPublisher() - ); - return Response.status(Response.Status.CREATED).entity(notificationPublisherCreated).build(); - } catch (ClassCastException e) { - return Response.status(Response.Status.BAD_REQUEST).entity("The class " + jsonNotificationPublisher.getPublisherClass() + " does not implement " + Publisher.class.getName()).build(); + Class publisherClass = Class.forName(jsonNotificationPublisher.getPublisherClass()); + + if (Publisher.class.isAssignableFrom(publisherClass)) { + Class castedPublisherClass = (Class) publisherClass; + NotificationPublisher notificationPublisherCreated = qm.createNotificationPublisher( + jsonNotificationPublisher.getName(), jsonNotificationPublisher.getDescription(), + castedPublisherClass, jsonNotificationPublisher.getTemplate(), jsonNotificationPublisher.getTemplateMimeType(), + jsonNotificationPublisher.isDefaultPublisher(), jsonNotificationPublisher.isPublishScheduled() + ); + return Response.status(Response.Status.CREATED).entity(notificationPublisherCreated).build(); + } else { + return Response.status(Response.Status.BAD_REQUEST).entity("The class "+jsonNotificationPublisher.getPublisherClass()+" does not implement "+Publisher.class.getName()).build(); + } + } catch (ClassNotFoundException e) { return Response.status(Response.Status.BAD_REQUEST).entity("The class " + jsonNotificationPublisher.getPublisherClass() + " cannot be found").build(); } diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index 32a74388bf..e00058c725 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -583,7 +583,7 @@ public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOE qm.createNotificationPublisher( publisher.getPublisherName(), publisher.getPublisherDescription(), publisher.getPublisherClass(), templateContent, publisher.getTemplateMimeType(), - publisher.isDefaultPublisher() + publisher.isDefaultPublisher(), publisher.isPublishScheduled() ); } else { existingPublisher.setName(publisher.getPublisherName()); @@ -592,6 +592,7 @@ public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOE existingPublisher.setTemplate(templateContent); existingPublisher.setTemplateMimeType(publisher.getTemplateMimeType()); existingPublisher.setDefaultPublisher(publisher.isDefaultPublisher()); + existingPublisher.setPublishScheduled(publisher.isPublishScheduled()); qm.updateNotificationPublisher(existingPublisher); } } From 5d42aba2bbcc362e01a108927c4050e5a8fcb039 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 13 May 2024 18:49:40 +0200 Subject: [PATCH 17/98] Added API endpoints for filtering publishers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/model/PublishTrigger.java | 31 ++++++++++++++ .../persistence/NotificationQueryManager.java | 25 ++++++++++- .../persistence/QueryManager.java | 5 +++ .../v1/NotificationPublisherResource.java | 41 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/PublishTrigger.java diff --git a/src/main/java/org/dependencytrack/model/PublishTrigger.java b/src/main/java/org/dependencytrack/model/PublishTrigger.java new file mode 100644 index 0000000000..43c682e4f2 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/PublishTrigger.java @@ -0,0 +1,31 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ + +package org.dependencytrack.model; + +/** + * Provides a list of available triggers for publishers to send notifications. + * + * @author Max Schiller + */ +public enum PublishTrigger { + ALL, + EVENT, + SCHEDULE, +} diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index e652ddbda6..e7a981dc02 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -28,6 +28,7 @@ import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.PublishTrigger; import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.Publisher; @@ -194,12 +195,32 @@ public PaginatedResult getScheduledNotificationRules() { * This method if designed NOT to provide paginated results. * @return list of all NotificationPublisher objects */ - @SuppressWarnings("unchecked") public List getAllNotificationPublishers() { + return getAllNotificationPublishersOfType(PublishTrigger.ALL); + } + + /** + * Retrieves all NotificationPublishers matching the corresponding trigger type. + * This methoid is designed NOT to provide paginated results. + * @param trigger + * @return list of all matching NotificationPublisher objects + */ + public List getAllNotificationPublishersOfType(PublishTrigger trigger) { final Query query = pm.newQuery(NotificationPublisher.class); query.getFetchPlan().addGroup(NotificationPublisher.FetchGroup.ALL.name()); query.setOrdering("name asc"); - return (List)query.execute(); + switch (trigger) { + case SCHEDULE: + query.setFilter("publishScheduled == :publishScheduled"); + query.setParameters(true); + return List.copyOf(query.executeList()); + case EVENT: + query.setFilter("publishScheduled == :publishScheduled || publishScheduled == null"); + query.setParameters(false); + return List.copyOf(query.executeList()); + default: + return List.copyOf(query.executeList()); + } } /** diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e733319352..289037be3d 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -63,6 +63,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.PublishTrigger; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -1267,6 +1268,10 @@ public List getAllNotificationPublishers() { return getNotificationQueryManager().getAllNotificationPublishers(); } + public List getAllNotificationPublishersOfType(PublishTrigger trigger) { + return getNotificationQueryManager().getAllNotificationPublishersOfType(trigger); + } + public NotificationPublisher getNotificationPublisher(final String name) { return getNotificationQueryManager().getNotificationPublisher(name); } diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 18c90128e1..4d20f95c54 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -39,6 +39,7 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.PublishTrigger; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -107,6 +108,46 @@ public Response getAllNotificationPublishers() { } } + @GET + @Path("/event") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all event-driven notification publishers", + response = NotificationPublisher.class, + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response getAllEventNotificationPublishers() { + try (QueryManager qm = new QueryManager()) { + final List publishers = qm.getAllNotificationPublishersOfType(PublishTrigger.EVENT); + return Response.ok(publishers).build(); + } + } + + @GET + @Path("/scheduled") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Returns a list of all scheduled notification publishers", + response = NotificationPublisher.class, + responseContainer = "List", + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response getAllScheduledNotificationPublishers() { + try (QueryManager qm = new QueryManager()) { + final List publishers = qm.getAllNotificationPublishersOfType(PublishTrigger.SCHEDULE); + return Response.ok(publishers).build(); + } + } + @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) From 0b4bc6fd8b09c6c87261c4f842eb0385c3bca85a Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 14 May 2024 09:48:49 +0200 Subject: [PATCH 18/98] Unique serialVersionUID for ScheduledNotificationRule instead of same as NotificationRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/model/ScheduledNotificationRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index 6c0b505a9f..7f9e9e9b4a 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -64,7 +64,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class ScheduledNotificationRule implements Serializable { - private static final long serialVersionUID = 2534439091019367263L; + private static final long serialVersionUID = 3390485832822256096L; @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) From 468bfd342b9292a6276d6c926d0086a31ef6e105 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 14 May 2024 14:41:03 +0200 Subject: [PATCH 19/98] fixed setting last execution time on update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/persistence/NotificationQueryManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index e7a981dc02..c23a77371a 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -145,7 +145,6 @@ public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotifi rule.setPublisherConfig(transientRule.getPublisherConfig()); rule.setNotifyOn(transientRule.getNotifyOn()); rule.setCronConfig(transientRule.getCronConfig()); - rule.setLastExecutionTime(transientRule.getLastExecutionTime()); rule.setPublishOnlyWithUpdates(transientRule.getPublishOnlyWithUpdates()); return persist(rule); } From 96e0ae1c4d196133913d03d21e9cd867b83162bf Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 15 May 2024 10:39:38 +0200 Subject: [PATCH 20/98] fixed wrong method usage for updating last execution time in QueryManager and Scheduled Task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../java/org/dependencytrack/persistence/QueryManager.java | 4 ++-- .../dependencytrack/tasks/SendScheduledNotificationTask.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 289037be3d..bffc72886e 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1316,8 +1316,8 @@ public ScheduledNotificationRule updateScheduledNotificationRule(ScheduledNotifi return getNotificationQueryManager().updateScheduledNotificationRule(transientRule); } - public ScheduledNotificationRule updateScheduledNotificationRuleToNowUtc(ScheduledNotificationRule transientRule) { - return getNotificationQueryManager().updateScheduledNotificationRuleToNowUtc(transientRule); + public ScheduledNotificationRule updateScheduledNotificationRuleLastExecutionTimeToNowUtc(ScheduledNotificationRule transientRule) { + return getNotificationQueryManager().updateScheduledNotificationRuleLastExecutionTimeToNowUtc(transientRule); } public PaginatedResult getScheduledNotificationRules() { diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 1fd1441f7f..8d9a1ca135 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -151,7 +151,7 @@ public void run() { LOGGER.debug(e.getMessage()); } // } - qm.updateScheduledNotificationRuleToNowUtc(scheduledNotificationRule); + qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(scheduledNotificationRule); } } catch (Exception e) { From ef9c21b5d25641c62541ab8345eac3fdc0fd5113 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 15 May 2024 11:13:48 +0200 Subject: [PATCH 21/98] fixed last execution to only update after successful publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/tasks/SendScheduledNotificationTask.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 8d9a1ca135..43aff51acf 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -146,12 +146,11 @@ public void run() { .useStartTLS(smtpSslTls) .trustCert(smtpTrustCert); sendMail.send(); + qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(scheduledNotificationRule); } catch (SendMailException | RuntimeException e) { LOGGER.debug("Failed to send notification email "); LOGGER.debug(e.getMessage()); } - // } - qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(scheduledNotificationRule); } } catch (Exception e) { From d4e06e0213e05d6926662fac3b75bef94d88216b Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 16 May 2024 17:14:26 +0200 Subject: [PATCH 22/98] code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/model/ScheduledNotificationRule.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index 7f9e9e9b4a..2421e86515 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -152,7 +152,6 @@ public class ScheduledNotificationRule implements Serializable { @Persistent(defaultFetchGroup = "true") @Column(name = "CRON_CONFIG", allowsNull = "true") // new column, must allow nulls on existing databases @JsonDeserialize(using = TrimmedStringDeserializer.class) - // @Pattern(regexp = RegexSequence.Definition.CRON, message = "The message may only contain characters valid in cron strings") private String cronConfig; @Persistent(defaultFetchGroup = "true") From fec62ef6041a6ddc65b094b7e24fcdd5bff1b35a Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 16 May 2024 18:43:00 +0200 Subject: [PATCH 23/98] abstracted NotificationRule with interface for reusing existing PublishContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/NotificationRule.java | 2 +- .../java/org/dependencytrack/model/Rule.java | 25 +++++++++++++++++++ .../model/ScheduledNotificationRule.java | 2 +- .../publisher/PublishContext.java | 3 ++- 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/Rule.java diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index 8b82d7efb0..5cdf76bfac 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -62,7 +62,7 @@ @PersistenceCapable @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class NotificationRule implements Serializable { +public class NotificationRule implements Rule, Serializable { private static final long serialVersionUID = 2534439091019367263L; diff --git a/src/main/java/org/dependencytrack/model/Rule.java b/src/main/java/org/dependencytrack/model/Rule.java new file mode 100644 index 0000000000..e8d29a61e8 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/Rule.java @@ -0,0 +1,25 @@ +package org.dependencytrack.model; + +import java.util.List; +import java.util.Set; + +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; + +import alpine.model.Team; +import alpine.notification.NotificationLevel; + +public interface Rule { + public String getName(); + public boolean isEnabled(); + public boolean isNotifyChildren(); + public boolean isLogSuccessfulPublish(); + public NotificationScope getScope(); + public NotificationLevel getNotificationLevel(); + public NotificationPublisher getPublisher(); + public String getPublisherConfig(); + public Set getNotifyOn(); + public String getMessage(); + public List getProjects(); + public List getTeams(); +} diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index 2421e86515..39bd037964 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -63,7 +63,7 @@ @PersistenceCapable @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class ScheduledNotificationRule implements Serializable { +public class ScheduledNotificationRule implements Rule, Serializable { private static final long serialVersionUID = 3390485832822256096L; @PrimaryKey diff --git a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java index e60a1e9287..6cb1e6dd32 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java +++ b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java @@ -21,6 +21,7 @@ import alpine.notification.Notification; import com.google.common.base.MoreObjects; import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.Rule; import org.dependencytrack.notification.vo.AnalysisDecisionChange; import org.dependencytrack.notification.vo.BomConsumedOrProcessed; import org.dependencytrack.notification.vo.BomProcessingFailed; @@ -114,7 +115,7 @@ public static PublishContext from(final Notification notification) { * @param rule The applicable {@link NotificationRule} * @return This {@link PublishContext} */ - public PublishContext withRule(final NotificationRule rule) { + public PublishContext withRule(final Rule rule) { return new PublishContext(this.notificationGroup, this.notificationLevel, this.notificationScope, this.notificationTimestamp, this.notificationSubjects, rule.getName(), rule.getScope().name(), rule.getNotificationLevel().name(), rule.isLogSuccessfulPublish()); } From 601e5fc3b5b621aac6613a2aa022213ec18f974b Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 16 May 2024 18:44:13 +0200 Subject: [PATCH 24/98] basic rebuild of scheduled publish task to match idea of multiple publishing per notification group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 204 ++++++++---------- 1 file changed, 90 insertions(+), 114 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 43aff51acf..10edf1a87b 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -18,145 +18,121 @@ */ package org.dependencytrack.tasks; -import java.io.StringWriter; -import java.io.Writer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; +import java.io.StringReader; +import java.lang.reflect.InvocationTargetException; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + import javax.json.Json; import javax.json.JsonObject; +import javax.json.JsonReader; +import org.dependencytrack.exception.PublisherException; import org.dependencytrack.model.NotificationPublisher; -import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Rule; import org.dependencytrack.model.ScheduledNotificationRule; -import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.publisher.PublishContext; +import org.dependencytrack.notification.publisher.Publisher; +import org.dependencytrack.notification.publisher.SendMailPublisher; +import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; import org.dependencytrack.persistence.QueryManager; import alpine.common.logging.Logger; -import alpine.security.crypto.DataEncryption; -import alpine.server.mail.SendMail; -import alpine.server.mail.SendMailException; -import io.pebbletemplates.pebble.PebbleEngine; -import io.pebbletemplates.pebble.template.PebbleTemplate; - -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_ENABLED; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_FROM_ADDR; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_PASSWORD; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_HOSTNAME; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SERVER_PORT; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_SSLTLS; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_TRUSTCERT; -import static org.dependencytrack.model.ConfigPropertyConstants.EMAIL_SMTP_USERNAME; +import alpine.notification.Notification; import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_KEY; import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_MIME_TYPE_KEY; public class SendScheduledNotificationTask implements Runnable { - private ScheduledNotificationRule scheduledNotificationRule; - private ScheduledExecutorService service; + private UUID scheduledNotificationRuleUuid; private static final Logger LOGGER = Logger.getLogger(SendScheduledNotificationTask.class); - public SendScheduledNotificationTask(ScheduledNotificationRule scheduledNotificationRule, ScheduledExecutorService service) { - this.scheduledNotificationRule = scheduledNotificationRule; - this.service = service; + public SendScheduledNotificationTask(UUID scheduledNotificationRuleUuid) { + this.scheduledNotificationRuleUuid = scheduledNotificationRuleUuid; } @Override public void run() { - String content = ""; - final String mimeType; - final boolean smtpEnabled; - final String smtpFrom; - final String smtpHostname; - final int smtpPort; - final String smtpUser; - final String encryptedSmtpPassword; - final boolean smtpSslTls; - final boolean smtpTrustCert; - Map> newProjectVulnerabilities; - Map> newProjectPolicyViolations; - - try (QueryManager qm = new QueryManager()) { - scheduledNotificationRule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRule.getUuid()); - if (scheduledNotificationRule == null) { - LOGGER.info("shutdown ExecutorService for Scheduled notification " + scheduledNotificationRule.getUuid()); - service.shutdown(); - } else { - // if (scheduledNotificationRule.getLastExecutionTime().equals(scheduledNotificationRule.getCreated())) { - // LOGGER.info("schedulednotification just created. No Information to show"); - // } else { - final List projectIds = scheduledNotificationRule.getProjects().stream().map(proj -> proj.getId()).toList(); - newProjectVulnerabilities = qm.getNewVulnerabilitiesForProjectsSince(scheduledNotificationRule.getLastExecutionTime(), projectIds); - newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(scheduledNotificationRule.getLastExecutionTime(), projectIds); - - NotificationPublisher notificationPublisher = qm.getNotificationPublisher("Email"); - - JsonObject notificationPublisherConfig = Json.createObjectBuilder() - .add(CONFIG_TEMPLATE_MIME_TYPE_KEY, notificationPublisher.getTemplateMimeType()) - .add(CONFIG_TEMPLATE_KEY, notificationPublisher.getTemplate()) - .build(); - - PebbleEngine pebbleEngine = new PebbleEngine.Builder().build(); - String literalTemplate = notificationPublisherConfig.getString(CONFIG_TEMPLATE_KEY); - final PebbleTemplate template = pebbleEngine.getLiteralTemplate(literalTemplate); - mimeType = notificationPublisherConfig.getString(CONFIG_TEMPLATE_MIME_TYPE_KEY); - - final Map context = new HashMap<>(); - context.put("length", newProjectVulnerabilities.size()); - context.put("vulnerabilities", newProjectVulnerabilities); - context.put("policyviolations", newProjectPolicyViolations); - final Writer writer = new StringWriter(); - template.evaluate(writer, context); - content = writer.toString(); - - smtpEnabled = qm.isEnabled(EMAIL_SMTP_ENABLED); - if (!smtpEnabled) { - System.out.println("SMTP is not enabled; Skipping notification "); - return; + try (var qm = new QueryManager()) { + var rule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRuleUuid); + for (NotificationGroup group : rule.getNotifyOn()) { + final Notification notificationProxy = new Notification() + .scope(rule.getScope()) + .group(group) + .title(rule.getName()) + .level(rule.getNotificationLevel()) + .content("") // TODO: evaluate use and creation of content here + .subject(null); // TODO: generate helper class here + + final PublishContext ctx = PublishContext.from(notificationProxy); + final PublishContext ruleCtx =ctx.withRule(rule); + + // Not all publishers need configuration (i.e. ConsolePublisher) + JsonObject config = Json.createObjectBuilder().build(); + if (rule.getPublisherConfig() != null) { + try (StringReader stringReader = new StringReader(rule.getPublisherConfig()); + final JsonReader jsonReader = Json.createReader(stringReader)) { + config = jsonReader.readObject(); + } catch (Exception e) { + LOGGER.error("An error occurred while preparing the configuration for the notification publisher (%s)".formatted(ruleCtx), e); + } } - smtpFrom = qm.getConfigProperty(EMAIL_SMTP_FROM_ADDR.getGroupName(),EMAIL_SMTP_FROM_ADDR.getPropertyName()).getPropertyValue(); - smtpHostname = qm.getConfigProperty(EMAIL_SMTP_SERVER_HOSTNAME.getGroupName(),EMAIL_SMTP_SERVER_HOSTNAME.getPropertyName()).getPropertyValue(); - smtpPort = Integer.parseInt(qm.getConfigProperty(EMAIL_SMTP_SERVER_PORT.getGroupName(),EMAIL_SMTP_SERVER_PORT.getPropertyName()).getPropertyValue()); - smtpUser = qm.getConfigProperty(EMAIL_SMTP_USERNAME.getGroupName(),EMAIL_SMTP_USERNAME.getPropertyName()).getPropertyValue(); - encryptedSmtpPassword = qm.getConfigProperty(EMAIL_SMTP_PASSWORD.getGroupName(),EMAIL_SMTP_PASSWORD.getPropertyName()).getPropertyValue(); - smtpSslTls = qm.isEnabled(EMAIL_SMTP_SSLTLS); - smtpTrustCert = qm.isEnabled(EMAIL_SMTP_TRUSTCERT); - final boolean smtpAuth = (smtpUser != null && encryptedSmtpPassword != null); - final String decryptedSmtpPassword; try { - decryptedSmtpPassword = (encryptedSmtpPassword != null) ? DataEncryption.decryptAsString(encryptedSmtpPassword) : null; - } catch (Exception e) { - System.out.println("Failed to decrypt SMTP password"); - return; - } - // String[] destinations = scheduledNotificationRule.getDestinations().split(" "); - try { - final SendMail sendMail = new SendMail() - .from(smtpFrom) - // .to(destinations) - .subject("[Dependency-Track] " + "ScheduledNotification") - .body(content) - .bodyMimeType(mimeType) - .host(smtpHostname) - .port(smtpPort) - .username(smtpUser) - .password(decryptedSmtpPassword) - .smtpauth(smtpAuth) - .useStartTLS(smtpSslTls) - .trustCert(smtpTrustCert); - sendMail.send(); - qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(scheduledNotificationRule); - } catch (SendMailException | RuntimeException e) { - LOGGER.debug("Failed to send notification email "); - LOGGER.debug(e.getMessage()); + NotificationPublisher notificationPublisher = rule.getPublisher(); + final Class publisherClass = Class.forName(notificationPublisher.getPublisherClass()); + if (Publisher.class.isAssignableFrom(publisherClass)) { + final Publisher publisher = (Publisher) publisherClass.getDeclaredConstructor().newInstance(); + JsonObject notificationPublisherConfig = Json.createObjectBuilder() + .add(CONFIG_TEMPLATE_MIME_TYPE_KEY, notificationPublisher.getTemplateMimeType()) + .add(CONFIG_TEMPLATE_KEY, notificationPublisher.getTemplate()) + .addAll(Json.createObjectBuilder(config)) + .build(); + if (publisherClass != SendMailPublisher.class || rule.getTeams().isEmpty() || rule.getTeams() == null) { + publisher.inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig); + } else { + ((SendMailPublisher) publisher).inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig, rule.getTeams()); + } + } else { + LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName() + " (%s)".formatted(ruleCtx)); + } + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException + | InvocationTargetException | IllegalAccessException e) { + LOGGER.error( + "An error occurred while instantiating a notification publisher (%s)".formatted(ruleCtx), + e); + } catch (PublisherException publisherException) { + LOGGER.error("An error occurred during the publication of the notification (%s)".formatted(ruleCtx), + publisherException); } } - - } catch (Exception e) { - LOGGER.debug(e.getMessage()); } + } + private Notification restrictNotificationToRuleProjects(final Notification initialNotification, final Rule rule) { + Notification restrictedNotification = initialNotification; + if (canRestrictNotificationToRuleProjects(initialNotification, rule)) { + Set ruleProjectsUuids = rule.getProjects().stream().map(Project::getUuid).map(UUID::toString).collect(Collectors.toSet()); + restrictedNotification = new Notification(); + restrictedNotification.setGroup(initialNotification.getGroup()); + restrictedNotification.setLevel(initialNotification.getLevel()); + restrictedNotification.scope(initialNotification.getScope()); + restrictedNotification.setContent(initialNotification.getContent()); + restrictedNotification.setTitle(initialNotification.getTitle()); + restrictedNotification.setTimestamp(initialNotification.getTimestamp()); + 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); + } + } + return restrictedNotification; } + private boolean canRestrictNotificationToRuleProjects(final Notification initialNotification, final Rule rule) { + return initialNotification.getSubject() instanceof NewVulnerabilityIdentified + && rule.getProjects() != null + && !rule.getProjects().isEmpty(); + } } From ed8107b07ae957876ac4792060460c9a6f82c7c6 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 17 May 2024 12:22:22 +0200 Subject: [PATCH 25/98] null checks in query managers for new events since last scheduled execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/persistence/PolicyQueryManager.java | 3 +++ .../dependencytrack/persistence/VulnerabilityQueryManager.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index 8e21f4a3e5..22ed0edb76 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -419,6 +419,9 @@ public Map> getNewPolicyViolationsForProjectsSinc for(Object[] obj : totalList){ Project project = getObjectById(Project.class, obj[0]); PolicyViolation policyViolation = getObjectById(PolicyViolation.class, obj[1]); + if(project == null || policyViolation == null){ + continue; + } if(!projectPolicyViolations.containsKey(project)){ projectPolicyViolations.put(project, new ArrayList<>()); } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 4047fbc643..5bd0bd7f74 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -213,6 +213,9 @@ public Map> getNewVulnerabilitiesForProjectsSince(Z for(Object[] obj : totalList){ Project project = getObjectById(Project.class, obj[0]); Vulnerability vulnerability = getObjectById(Vulnerability.class, obj[1]); + if(project == null || vulnerability == null){ + continue; + } if(!projectVulnerabilities.containsKey(project)){ projectVulnerabilities.put(project, new ArrayList<>()); } From e321106c0d97d08febe572af828278ba35fedd47 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 17 May 2024 12:23:16 +0200 Subject: [PATCH 26/98] generation of basic notification content in task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 10edf1a87b..a067e5a0e7 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,6 +20,9 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -30,9 +33,11 @@ import org.dependencytrack.exception.PublisherException; import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; import org.dependencytrack.model.Rule; import org.dependencytrack.model.ScheduledNotificationRule; +import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; @@ -58,13 +63,40 @@ public void run() { try (var qm = new QueryManager()) { var rule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRuleUuid); for (NotificationGroup group : rule.getNotifyOn()) { + final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) - .title(rule.getName()) - .level(rule.getNotificationLevel()) - .content("") // TODO: evaluate use and creation of content here - .subject(null); // TODO: generate helper class here + .title(generateNotificationTitle(rule, group)) + .level(rule.getNotificationLevel()); + + switch (group) { + case NEW_VULNERABILITY: + var newProjectVulnerabilities = qm.getNewVulnerabilitiesForProjectsSince(rule.getLastExecutionTime(), projectIds); + if(newProjectVulnerabilities.isEmpty() && rule.getPublishOnlyWithUpdates()) + continue; + notificationProxy + .content(generateVulnerabilityNotificationContent(rule, + newProjectVulnerabilities.values().stream().flatMap(List::stream).toList(), + newProjectVulnerabilities.keySet().stream().toList(), + rule.getLastExecutionTime())) + .subject(null); // TODO: generate helper class here + break; + case POLICY_VIOLATION: + var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(rule.getLastExecutionTime(), projectIds); + if(newProjectPolicyViolations.isEmpty() && rule.getPublishOnlyWithUpdates()) + continue; + notificationProxy + .content(generatePolicyNotificationContent(rule, + newProjectPolicyViolations.values().stream().flatMap(List::stream).toList(), + newProjectPolicyViolations.keySet().stream().toList(), + rule.getLastExecutionTime())) + .subject(null); // TODO: generate helper class here + break; + default: + LOGGER.error(group.name() + " is not a supported notification group for scheduled publishing"); + continue; + } final PublishContext ctx = PublishContext.from(notificationProxy); final PublishContext ruleCtx =ctx.withRule(rule); @@ -135,4 +167,32 @@ private boolean canRestrictNotificationToRuleProjects(final Notification initial && rule.getProjects() != null && !rule.getProjects().isEmpty(); } + + private String generateNotificationTitle(final Rule rule, final NotificationGroup group) { + return "Scheduled Notification: " + group.name(); + } + + private String generateVulnerabilityNotificationContent(final Rule rule, final List vulnerabilities, final List projects, final ZonedDateTime lastExecutionTime) { + final String content; + + if (vulnerabilities.isEmpty()) { + content = "No new vulnerabilities found."; + } else { + content = "In total, " + vulnerabilities.size() + " new vulnerabilities in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; + } + + return content; + } + + private String generatePolicyNotificationContent(final Rule rule, final List policyViolations, final List projects, final ZonedDateTime lastExecutionTime) { + final String content; + + if (policyViolations.isEmpty()) { + content = "No new policy violations found."; + } else { + content = "In total, " + policyViolations.size() + " new policy violations in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; + } + + return content; + } } From 69cd738073477d970d1b362feb87d5895a94e6de Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 24 May 2024 09:20:21 +0200 Subject: [PATCH 27/98] fixed missing header part in Rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../java/org/dependencytrack/model/Rule.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/dependencytrack/model/Rule.java b/src/main/java/org/dependencytrack/model/Rule.java index e8d29a61e8..e3ac5e3f81 100644 --- a/src/main/java/org/dependencytrack/model/Rule.java +++ b/src/main/java/org/dependencytrack/model/Rule.java @@ -1,3 +1,21 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ package org.dependencytrack.model; import java.util.List; From 19926564b56d10927fb678ed63baec7ac5002769 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 24 May 2024 09:22:21 +0200 Subject: [PATCH 28/98] fixed query in policy and vulnerability querymanagers when project limitation is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/PolicyQueryManager.java | 11 +++++++---- .../persistence/VulnerabilityQueryManager.java | 7 +++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index 22ed0edb76..ba39c942ba 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -407,14 +407,17 @@ public List getAllPolicyViolations(final Project project) { @SuppressWarnings("unchecked") public Map> getNewPolicyViolationsForProjectsSince(ZonedDateTime dateTime, List projectIds){ String queryString = "SELECT PROJECT_ID, ID " + - "FROM POLICYVIOLATION " + - "WHERE (TIMESTAMP BETWEEN ? AND ?) "; - if(projectIds != null && !projectIds.isEmpty()){ + "FROM POLICYVIOLATION " + + "WHERE (TIMESTAMP BETWEEN ? AND ?) "; + boolean hasProjectLimitation = projectIds != null && !projectIds.isEmpty(); + if(hasProjectLimitation){ queryString.concat("AND (PROJECT_ID IN ?) "); } queryString.concat("ORDER BY PROJECT_ID ASC"); final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, queryString); - final List totalList = (List)query.execute(dateTime, ZonedDateTime.now(ZoneOffset.UTC), projectIds); + final List totalList = hasProjectLimitation + ? (List) query.execute(dateTime, ZonedDateTime.now(ZoneOffset.UTC), projectIds) + : (List) query.execute(dateTime, ZonedDateTime.now(ZoneOffset.UTC)); Map> projectPolicyViolations = new HashMap<>(); for(Object[] obj : totalList){ Project project = getObjectById(Project.class, obj[0]); diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 5bd0bd7f74..3e62cf5680 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -203,12 +203,15 @@ public Map> getNewVulnerabilitiesForProjectsSince(Z String queryString = "SELECT PROJECT_ID, VULNERABILITY_ID " + "FROM FINDINGATTRIBUTION " + "WHERE ATTRIBUTED_ON BETWEEN ? AND ? "; - if(projectIds != null && !projectIds.isEmpty()){ + boolean hasProjectLimitation = projectIds != null && !projectIds.isEmpty(); + if(hasProjectLimitation){ queryString.concat("AND PROJECT_ID IN ? "); } queryString.concat("ORDER BY PROJECT_ID ASC, VULNERABILITY_ID ASC"); final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, queryString); - final List totalList = (List)query.execute(lastExecution, ZonedDateTime.now(ZoneOffset.UTC), projectIds); + final List totalList = hasProjectLimitation + ? (List) query.execute(lastExecution, ZonedDateTime.now(ZoneOffset.UTC), projectIds) + : (List) query.execute(lastExecution, ZonedDateTime.now(ZoneOffset.UTC)); Map> projectVulnerabilities = new HashMap<>(); for(Object[] obj : totalList){ Project project = getObjectById(Project.class, obj[0]); From 8a76884697019ef05805a5c1b3be87226d5d74a1 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 24 May 2024 09:39:38 +0200 Subject: [PATCH 29/98] changed retrieval of default publishers from db to support multiple default publishers with same publisher class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/NotificationQueryManager.java | 13 +++++++------ .../dependencytrack/persistence/QueryManager.java | 5 +++-- .../resources/v1/NotificationPublisherResource.java | 8 ++++---- .../org/dependencytrack/util/NotificationUtil.java | 2 +- .../v1/NotificationPublisherResourceTest.java | 10 +++++----- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index c23a77371a..f5187d0be2 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -31,6 +31,7 @@ import org.dependencytrack.model.PublishTrigger; import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.publisher.Publisher; import javax.jdo.PersistenceManager; @@ -238,8 +239,8 @@ public NotificationPublisher getNotificationPublisher(final String name) { * @param clazz The Class of the NotificationPublisher * @return a NotificationPublisher */ - public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { - return getDefaultNotificationPublisher(clazz.getCanonicalName()); + public NotificationPublisher getDefaultNotificationPublisher(final DefaultNotificationPublishers defaultPublisher) { + return getDefaultNotificationPublisher(defaultPublisher.getPublisherName(), defaultPublisher.getPublisherClass().getCanonicalName()); } /** @@ -247,11 +248,11 @@ public NotificationPublisher getDefaultNotificationPublisher(final Class query = pm.newQuery(NotificationPublisher.class, "publisherClass == :publisherClass && defaultPublisher == true"); + private NotificationPublisher getDefaultNotificationPublisher(final String publisherName, final String clazz) { + final Query query = pm.newQuery(NotificationPublisher.class, "name == :name && publisherClass == :publisherClass && defaultPublisher == true"); query.getFetchPlan().addGroup(NotificationPublisher.FetchGroup.ALL.name()); query.setRange(0, 1); - return singleResult(query.execute(clazz)); + return singleResult(query.execute(publisherName, clazz)); } /** @@ -286,7 +287,7 @@ public NotificationPublisher updateNotificationPublisher(NotificationPublisher t if (transientPublisher.getId() > 0) { publisher = getObjectById(NotificationPublisher.class, transientPublisher.getId()); } else if (transientPublisher.isDefaultPublisher()) { - publisher = getDefaultNotificationPublisher(transientPublisher.getPublisherClass()); + publisher = getDefaultNotificationPublisher(transientPublisher.getName(), transientPublisher.getPublisherClass()); } if (publisher != null) { publisher.setName(transientPublisher.getName()); diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index bffc72886e..521a7daff6 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -80,6 +80,7 @@ import org.dependencytrack.model.VulnerabilityMetrics; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.resources.v1.vo.AffectedProject; import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; @@ -1276,8 +1277,8 @@ public NotificationPublisher getNotificationPublisher(final String name) { return getNotificationQueryManager().getNotificationPublisher(name); } - public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { - return getNotificationQueryManager().getDefaultNotificationPublisher(clazz); + public NotificationPublisher getDefaultNotificationPublisher(final DefaultNotificationPublishers defaultPublisher) { + return getNotificationQueryManager().getDefaultNotificationPublisher(defaultPublisher); } public NotificationPublisher createNotificationPublisher(final String name, final String description, diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 4d20f95c54..356fab64ad 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -44,9 +44,9 @@ import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; -import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.NotificationUtil; @@ -347,9 +347,9 @@ public Response restoreDefaultTemplates() { @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response testSmtpPublisherConfig(@FormParam("destination") String destination) { try(QueryManager qm = new QueryManager()) { - Class defaultEmailPublisherClass = SendMailPublisher.class; - NotificationPublisher emailNotificationPublisher = qm.getDefaultNotificationPublisher(defaultEmailPublisherClass); - final Publisher emailPublisher = defaultEmailPublisherClass.getDeclaredConstructor().newInstance(); + DefaultNotificationPublishers defaultEmailPublisher = DefaultNotificationPublishers.EMAIL; + NotificationPublisher emailNotificationPublisher = qm.getDefaultNotificationPublisher(defaultEmailPublisher); + final Publisher emailPublisher = (Publisher) defaultEmailPublisher.getPublisherClass().getDeclaredConstructor().newInstance(); final JsonObject config = Json.createObjectBuilder() .add(Publisher.CONFIG_DESTINATION, destination) .add(Publisher.CONFIG_TEMPLATE_KEY, emailNotificationPublisher.getTemplate()) diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index e00058c725..0c14108522 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -578,7 +578,7 @@ public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOE } } final String templateContent = FileUtils.readFileToString(templateFile, UTF_8); - final NotificationPublisher existingPublisher = qm.getDefaultNotificationPublisher(publisher.getPublisherClass()); + final NotificationPublisher existingPublisher = qm.getDefaultNotificationPublisher(publisher); if (existingPublisher == null) { qm.createNotificationPublisher( publisher.getPublisherName(), publisher.getPublisherDescription(), diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index 677303f372..add6158cb8 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -219,7 +219,7 @@ public void updateUnknownNotificationPublisherTest() { @Test public void updateExistingDefaultNotificationPublisherTest() { - NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(SendMailPublisher.class); + NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.EMAIL); notificationPublisher.setName(notificationPublisher.getName() + " Updated"); Response response = jersey.target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) @@ -324,8 +324,8 @@ public void deleteUnknownNotificationPublisherTest() { @Test public void deleteDefaultNotificationPublisherTest() { - NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(SendMailPublisher.class); - Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/" + notificationPublisher.getUuid()).request() + NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.EMAIL); + Response response = target(V1_NOTIFICATION_PUBLISHER + "/" + notificationPublisher.getUuid()).request() .header(X_API_KEY, apiKey) .delete(); Assert.assertEquals(400, response.getStatus(), 0); @@ -370,7 +370,7 @@ public void testNotificationRuleTest() { @Test public void restoreDefaultTemplatesTest() { - NotificationPublisher slackPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherClass()); + NotificationPublisher slackPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.SLACK); slackPublisher.setName(slackPublisher.getName()+" Updated"); qm.persist(slackPublisher); qm.detach(NotificationPublisher.class, slackPublisher.getId()); @@ -387,7 +387,7 @@ public void restoreDefaultTemplatesTest() { qm.getPersistenceManager().refreshAll(); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertFalse(qm.isEnabled(ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED)); - slackPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherClass()); + slackPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.SLACK); Assert.assertEquals(DefaultNotificationPublishers.SLACK.getPublisherName(), slackPublisher.getName()); } } From d43990b7951c45935ca51043fec8b5ed1edab553 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 24 May 2024 12:12:20 +0200 Subject: [PATCH 30/98] fixed missing detach for scheduled notification items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/persistence/PolicyQueryManager.java | 3 ++- .../dependencytrack/persistence/VulnerabilityQueryManager.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index ba39c942ba..05569e8177 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -425,10 +425,11 @@ public Map> getNewPolicyViolationsForProjectsSinc if(project == null || policyViolation == null){ continue; } + var detachedPolicyViolation = pm.detachCopy(policyViolation); if(!projectPolicyViolations.containsKey(project)){ projectPolicyViolations.put(project, new ArrayList<>()); } - projectPolicyViolations.get(project).add(policyViolation); + projectPolicyViolations.get(project).add(detachedPolicyViolation); } return projectPolicyViolations; } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 3e62cf5680..47c6cb6b4c 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -219,10 +219,11 @@ public Map> getNewVulnerabilitiesForProjectsSince(Z if(project == null || vulnerability == null){ continue; } + var detachedVulnerability = pm.detachCopy(vulnerability); if(!projectVulnerabilities.containsKey(project)){ projectVulnerabilities.put(project, new ArrayList<>()); } - projectVulnerabilities.get(project).add(vulnerability); + projectVulnerabilities.get(project).add(detachedVulnerability); } return projectVulnerabilities; } From 62d77bf8b7d357f5657f4ac081489bff4dd83a70 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 27 May 2024 15:59:33 +0200 Subject: [PATCH 31/98] added scheduled default publisher with testing email template, support for first iteration of new vulnerabilities and policy violation summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../DefaultNotificationPublishers.java | 4 +- .../publisher/PublishContext.java | 7 ++ .../notification/publisher/Publisher.java | 8 ++ ...ScheduledNewVulnerabilitiesIdentified.java | 63 +++++++++++++ .../ScheduledPolicyViolationsIdentified.java | 45 +++++++++ .../util/NotificationUtil.java | 91 +++++++++++++++++++ .../publisher/scheduled_email.peb | 44 +++++++++ 7 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java create mode 100644 src/main/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentified.java create mode 100644 src/main/resources/templates/notification/publisher/scheduled_email.peb diff --git a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java index e602fe2b60..8e22a3141b 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java +++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java @@ -25,8 +25,8 @@ public enum DefaultNotificationPublishers { SLACK("Slack", "Publishes notifications to a Slack channel", SlackPublisher.class, "/templates/notification/publisher/slack.peb", MediaType.APPLICATION_JSON, true), MS_TEAMS("Microsoft Teams", "Publishes notifications to a Microsoft Teams channel", MsTeamsPublisher.class, "/templates/notification/publisher/msteams.peb", MediaType.APPLICATION_JSON, true), MATTERMOST("Mattermost", "Publishes notifications to a Mattermost channel", MattermostPublisher.class, "/templates/notification/publisher/mattermost.peb", MediaType.APPLICATION_JSON, true), - EMAIL("Email", "Sends notifications to an email address", SendMailPublisher.class, "/templates/notification/publisher/email.peb", "text/plain; charset=utf-8", true), - // SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email.peb", MediaType.TEXT_PLAIN, true, true), + EMAIL("Email", "Sends notifications to an email address", SendMailPublisher.class, "/templates/notification/publisher/email.peb", MediaType.TEXT_PLAIN, true), + SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email.peb", MediaType.TEXT_PLAIN, true, true), CONSOLE("Console", "Displays notifications on the system console", ConsolePublisher.class, "/templates/notification/publisher/console.peb", MediaType.TEXT_PLAIN, true), // SCHEDULED_CONSOLE("Scheduled Console", "Displays summarized notifications on the system console in a defined schedule", ConsolePublisher.class, "/templates/notification/publisher/scheduled_console.peb", MediaType.TEXT_PLAIN, true, true), WEBHOOK("Outbound Webhook", "Publishes notifications to a configurable endpoint", WebhookPublisher.class, "/templates/notification/publisher/webhook.peb", MediaType.APPLICATION_JSON, true), diff --git a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java index 6cb1e6dd32..208b9ff5ad 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java +++ b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java @@ -28,6 +28,8 @@ import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; import org.dependencytrack.notification.vo.NewVulnerableDependency; import org.dependencytrack.notification.vo.PolicyViolationIdentified; +import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; +import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; @@ -102,6 +104,11 @@ public static PublishContext from(final Notification notification) { notificationSubjects.put(SUBJECT_VULNERABILITY, Vulnerability.convert(subject.getVulnerability())); } else if (notification.getSubject() instanceof final VexConsumedOrProcessed subject) { notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + } else if (notification.getSubject() instanceof final ScheduledNewVulnerabilitiesIdentified subject) { + notificationSubjects.put(SUBJECT_PROJECTS, subject.getNewProjectVulnerabilities().keySet().stream().map(Project::convert).toList()); + notificationSubjects.put(SUBJECT_VULNERABILITIES, subject.getNewVulnerabilitiesTotal().stream().map(Vulnerability::convert).toList()); + } else if (notification.getSubject() instanceof final ScheduledPolicyViolationsIdentified subject) { + notificationSubjects.put(SUBJECT_PROJECTS, subject.getNewProjectPolicyViolations().keySet().stream().map(Project::convert).toList()); } return new PublishContext(notification.getGroup(), Optional.ofNullable(notification.getLevel()).map(Enum::name).orElse(null), diff --git a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java index 50a319d92f..c69eade286 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java @@ -34,6 +34,8 @@ import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; import org.dependencytrack.notification.vo.NewVulnerableDependency; import org.dependencytrack.notification.vo.PolicyViolationIdentified; +import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; +import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; import org.dependencytrack.persistence.QueryManager; @@ -130,6 +132,12 @@ default String prepareTemplate(final Notification notification, final PebbleTemp } else if (notification.getSubject() instanceof final PolicyViolationIdentified subject) { context.put("subject", subject); context.put("subjectJson", NotificationUtil.toJson(subject)); + } else if (notification.getSubject() instanceof final ScheduledNewVulnerabilitiesIdentified subject) { + context.put("subject", subject); + context.put("subjectJson", NotificationUtil.toJson(subject)); + } else if (notification.getSubject() instanceof final ScheduledPolicyViolationsIdentified subject) { + context.put("subject", subject); + context.put("subjectJson", NotificationUtil.toJson(subject)); } } else if (NotificationScope.SYSTEM.name().equals(notification.getScope())) { if (notification.getSubject() instanceof final UserPrincipal subject) { diff --git a/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java b/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java new file mode 100644 index 0000000000..6a25b94a2e --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java @@ -0,0 +1,63 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification.vo; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; + +public class ScheduledNewVulnerabilitiesIdentified { + private final Map> newProjectVulnerabilities; + private final Map>> newProjectVulnerabilitiesBySeverity; + private final List newVulnerabilitiesTotal; + private final Map> newVulnerabilitiesTotalBySeverity; + + public ScheduledNewVulnerabilitiesIdentified(Map> newProjectVulnerabilities) { + this.newProjectVulnerabilities = newProjectVulnerabilities; + this.newProjectVulnerabilitiesBySeverity = newProjectVulnerabilities.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream() + .collect(Collectors.groupingBy(Vulnerability::getSeverity, () -> new EnumMap<>(Severity.class), Collectors.toList())))); + this.newVulnerabilitiesTotal = newProjectVulnerabilities.values().stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + this.newVulnerabilitiesTotalBySeverity = newVulnerabilitiesTotal.stream() + .collect(Collectors.groupingBy(Vulnerability::getSeverity, () -> new EnumMap<>(Severity.class), Collectors.toList())); + } + + public Map> getNewProjectVulnerabilities() { + return newProjectVulnerabilities; + } + + public Map>> getNewProjectVulnerabilitiesBySeverity() { + return newProjectVulnerabilitiesBySeverity; + } + + public List getNewVulnerabilitiesTotal() { + return newVulnerabilitiesTotal; + } + + public Map> getNewVulnerabilitiesTotalBySeverity() { + return newVulnerabilitiesTotalBySeverity; + } +} diff --git a/src/main/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentified.java b/src/main/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentified.java new file mode 100644 index 0000000000..e046415ffa --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentified.java @@ -0,0 +1,45 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification.vo; + +import java.util.List; +import java.util.Map; + +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; + +public class ScheduledPolicyViolationsIdentified { + private final Map> newProjectPolicyViolations; + private final List newPolicyViolationsTotal; + + public ScheduledPolicyViolationsIdentified(Map> newProjectPolicyViolations) { + this.newProjectPolicyViolations = newProjectPolicyViolations; + this.newPolicyViolationsTotal = newProjectPolicyViolations.values().stream() + .flatMap(List::stream) + .collect(java.util.stream.Collectors.toList()); + } + + public Map> getNewProjectPolicyViolations() { + return newProjectPolicyViolations; + } + + public List getNewPolicyViolationsTotal() { + return newPolicyViolationsTotal; + } +} diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index 0c14108522..ba1b9d9ef3 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -54,6 +54,8 @@ import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; import org.dependencytrack.notification.vo.NewVulnerableDependency; import org.dependencytrack.notification.vo.PolicyViolationIdentified; +import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; +import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; import org.dependencytrack.parser.common.resolver.CweResolver; @@ -564,6 +566,95 @@ public static JsonObject toJson(final Policy policy) { return builder.build(); } + public static JsonObject toJson(final ScheduledNewVulnerabilitiesIdentified vo) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + + if (vo.getNewProjectVulnerabilities() != null && vo.getNewProjectVulnerabilities().size() > 0) { + final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); + for (final Map.Entry> entry : vo.getNewProjectVulnerabilities().entrySet()) { + final JsonObjectBuilder projectBuilder = Json.createObjectBuilder(); + projectBuilder.add("project", toJson(entry.getKey())); + final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); + for (final Vulnerability vulnerability : entry.getValue()) { + vulnsBuilder.add(toJson(vulnerability)); + } + projectBuilder.add("vulnerabilities", vulnsBuilder.build()); + projectsBuilder.add(projectBuilder.build()); + } + builder.add("newProjectVulnerabilities", projectsBuilder.build()); + } + if(vo.getNewProjectVulnerabilitiesBySeverity() != null && vo.getNewProjectVulnerabilitiesBySeverity().size() > 0) { + final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); + for (final Map.Entry>> entry : vo.getNewProjectVulnerabilitiesBySeverity().entrySet()) { + final JsonObjectBuilder projectBuilder = Json.createObjectBuilder(); + projectBuilder.add("project", toJson(entry.getKey())); + final JsonArrayBuilder vulnsBySeverityBuilder = Json.createArrayBuilder(); + for (final Map.Entry> vulnEntry : entry.getValue().entrySet()) { + final JsonObjectBuilder severityBuilder = Json.createObjectBuilder(); + severityBuilder.add("severity", vulnEntry.getKey().name()); + final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); + for (final Vulnerability vulnerability : vulnEntry.getValue()) { + vulnsBuilder.add(toJson(vulnerability)); + } + severityBuilder.add("vulnerabilities", vulnsBuilder.build()); + vulnsBySeverityBuilder.add(severityBuilder.build()); + } + projectBuilder.add("vulnerabilitiesBySeverity", vulnsBySeverityBuilder.build()); + projectsBuilder.add(projectBuilder.build()); + } + builder.add("newProjectVulnerabilitiesBySeverity", projectsBuilder.build()); + } + if (vo.getNewVulnerabilitiesTotal() != null && vo.getNewVulnerabilitiesTotal().size() > 0) { + final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); + for (final Vulnerability vulnerability : vo.getNewVulnerabilitiesTotal()) { + vulnsBuilder.add(toJson(vulnerability)); + } + builder.add("newVulnerabilitiesTotal", vulnsBuilder.build()); + } + if(vo.getNewVulnerabilitiesTotalBySeverity() != null && vo.getNewVulnerabilitiesTotalBySeverity().size() > 0) { + final JsonArrayBuilder vulnsBySeverityBuilder = Json.createArrayBuilder(); + for (final Map.Entry> vulnEntry : vo.getNewVulnerabilitiesTotalBySeverity().entrySet()) { + final JsonObjectBuilder severityBuilder = Json.createObjectBuilder(); + severityBuilder.add("severity", vulnEntry.getKey().name()); + final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); + for (final Vulnerability vulnerability : vulnEntry.getValue()) { + vulnsBuilder.add(toJson(vulnerability)); + } + severityBuilder.add("vulnerabilities", vulnsBuilder.build()); + vulnsBySeverityBuilder.add(severityBuilder.build()); + } + builder.add("newVulnerabilitiesTotalBySeverity", vulnsBySeverityBuilder.build()); + } + + return builder.build(); + } + + public static JsonObject toJson(ScheduledPolicyViolationsIdentified vo) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + if (vo.getNewProjectPolicyViolations() != null && vo.getNewProjectPolicyViolations().size() > 0) { + final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); + for (final Map.Entry> entry : vo.getNewProjectPolicyViolations().entrySet()) { + final JsonObjectBuilder projectBuilder = Json.createObjectBuilder(); + projectBuilder.add("project", toJson(entry.getKey())); + final JsonArrayBuilder violationsBuilder = Json.createArrayBuilder(); + for (final PolicyViolation policyViolation : entry.getValue()) { + violationsBuilder.add(toJson(policyViolation)); + } + projectBuilder.add("policyViolations", violationsBuilder.build()); + projectsBuilder.add(projectBuilder.build()); + } + builder.add("newProjectPolicyViolations", projectsBuilder.build()); + } + if (vo.getNewPolicyViolationsTotal() != null && vo.getNewPolicyViolationsTotal().size() > 0) { + final JsonArrayBuilder violationsBuilder = Json.createArrayBuilder(); + for (final PolicyViolation policyViolation : vo.getNewPolicyViolationsTotal()) { + violationsBuilder.add(toJson(policyViolation)); + } + builder.add("newPolicyViolationsTotal", violationsBuilder.build()); + } + return builder.build(); + } + public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOException { for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) { File templateFile = new File(URLDecoder.decode(NotificationUtil.class.getResource(publisher.getPublisherTemplateFile()).getFile(), UTF_8.name())); diff --git a/src/main/resources/templates/notification/publisher/scheduled_email.peb b/src/main/resources/templates/notification/publisher/scheduled_email.peb new file mode 100644 index 0000000000..b5c3df931a --- /dev/null +++ b/src/main/resources/templates/notification/publisher/scheduled_email.peb @@ -0,0 +1,44 @@ +{{ notification.title }} + +{{ notification.content }} + +=================================================================================================== + +{% if notification.group == "NEW_VULNERABILITY" %} +{% for entry in subject.newProjectVulnerabilitiesBySeverity %} +Project-Name: {{ entry.key.name }} +Project-URL: {{ baseUrl }}/projects/{{ entry.key.uuid }} +{% for severityEntry in entry.value %} + - {{ severityEntry.key }}: {{ severityEntry.value|length }} new vulnerabilities +{% endfor %} +{% endfor %} + +--------------------------------------------------------------------------------------------------- + +Details to all {{ subject.newVulnerabilitiesTotal|length }} new vulnerabilities: +{% for entry in subject.newProjectVulnerabilities %} +------------------------------------------------- +Project-Name: {{ entry.key.name }} +{% for vuln in entry.value %} + +ID: {{ vuln.vulnId }} +Severity: {{ vuln.severity }} +Description: {{ vuln.description }} +Source: {{ vuln.source }} +{% endfor %} +{% endfor %} +{% elseif notification.group == "POLICY_VIOLATION" %} + +New Policy Violations: {{ subject.newPolicyViolationsTotal|length }} +PolicyViolations: +{% for projectEntry in subject.newProjectPolicyViolations %} +{{ projectEntry.key.name }} ({{ projectEntry.key.uuid }}) +{% for policyEntry in projectEntry.value %} +- [{{ policyEntry.type }}] {{ policyEntry.timestamp }} {{ policyEntry.policyCondition }} +{% endfor %} +{% endfor %} +{% endif %} + +=================================================================================================== + +{{ timestamp }} \ No newline at end of file From 4b7e26c058469b2c693cd82dbef854eabe51738d Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 28 May 2024 14:59:16 +0200 Subject: [PATCH 32/98] modified scheduled task to deliver test data with new subject classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index a067e5a0e7..c2c42a4afb 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,6 +20,7 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; @@ -43,6 +44,8 @@ import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; +import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; +import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.persistence.QueryManager; import alpine.common.logging.Logger; @@ -62,8 +65,9 @@ public SendScheduledNotificationTask(UUID scheduledNotificationRuleUuid) { public void run() { try (var qm = new QueryManager()) { var rule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRuleUuid); + final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); + for (NotificationGroup group : rule.getNotifyOn()) { - final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) @@ -75,23 +79,25 @@ public void run() { var newProjectVulnerabilities = qm.getNewVulnerabilitiesForProjectsSince(rule.getLastExecutionTime(), projectIds); if(newProjectVulnerabilities.isEmpty() && rule.getPublishOnlyWithUpdates()) continue; + ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(newProjectVulnerabilities); notificationProxy .content(generateVulnerabilityNotificationContent(rule, - newProjectVulnerabilities.values().stream().flatMap(List::stream).toList(), + vulnSubject.getNewVulnerabilitiesTotal(), newProjectVulnerabilities.keySet().stream().toList(), rule.getLastExecutionTime())) - .subject(null); // TODO: generate helper class here + .subject(vulnSubject); break; case POLICY_VIOLATION: - var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(rule.getLastExecutionTime(), projectIds); + var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(ZonedDateTime.of(2023, 05, 20, 0, 0, 0, 0, ZoneId.systemDefault())/* rule.getLastExecutionTime() */, projectIds); if(newProjectPolicyViolations.isEmpty() && rule.getPublishOnlyWithUpdates()) continue; + ScheduledPolicyViolationsIdentified policySubject = new ScheduledPolicyViolationsIdentified(newProjectPolicyViolations); notificationProxy .content(generatePolicyNotificationContent(rule, - newProjectPolicyViolations.values().stream().flatMap(List::stream).toList(), + policySubject.getNewPolicyViolationsTotal(), newProjectPolicyViolations.keySet().stream().toList(), rule.getLastExecutionTime())) - .subject(null); // TODO: generate helper class here + .subject(policySubject); break; default: LOGGER.error(group.name() + " is not a supported notification group for scheduled publishing"); From 44e272ffc271274aec102c42623500c922c473f9 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 28 May 2024 15:01:21 +0200 Subject: [PATCH 33/98] added cron task management on CRUD operations with automatic re-scheduling after completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../v1/ScheduledNotificationRuleResource.java | 62 +++++++++++++++++++ .../tasks/ActionOnDoneFutureTask.java | 44 +++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/main/java/org/dependencytrack/tasks/ActionOnDoneFutureTask.java diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index f068ce7646..cd71a442ee 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -40,6 +40,11 @@ import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.tasks.ActionOnDoneFutureTask; +import org.dependencytrack.tasks.SendScheduledNotificationTask; + +import com.asahaf.javacron.InvalidExpressionException; +import com.asahaf.javacron.Schedule; import javax.validation.Validator; import javax.ws.rs.Consumes; @@ -52,7 +57,17 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; /** * JAX-RS resources for processing scheduled notification rules. @@ -64,6 +79,7 @@ public class ScheduledNotificationRuleResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationRuleResource.class); + private static final HashMap> SCHEDULED_NOTIFY_TASKS = new HashMap>(); @GET @Produces(MediaType.APPLICATION_JSON) @@ -122,10 +138,42 @@ public Response createScheduledNotificationRule(ScheduledNotificationRule jsonRu jsonRule.getNotificationLevel(), publisher ); + + if(rule.isEnabled()) { + Schedule schedule; + try { + schedule = Schedule.create(jsonRule.getCronConfig()); + scheduleNextRuleTask(rule.getUuid(), schedule); + } catch (InvalidExpressionException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); + } + } + return Response.status(Response.Status.CREATED).entity(rule).build(); } } + private void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule) { + var scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + var futureTask = new ActionOnDoneFutureTask(new SendScheduledNotificationTask(ruleUuid), () -> scheduleNextRuleTask(ruleUuid, schedule)); + + var future = scheduledExecutor.schedule( + futureTask, + schedule.nextDuration(TimeUnit.MILLISECONDS), + TimeUnit.MILLISECONDS); + SCHEDULED_NOTIFY_TASKS.put(ruleUuid, future); + + LOGGER.info(">>>>>>>>>> Scheduled notification task for rule " + ruleUuid + " @ " + LocalDateTime.ofInstant(Instant.ofEpochMilli(schedule.nextDuration(TimeUnit.MILLISECONDS)), ZoneId.systemDefault()).truncatedTo(ChronoUnit.SECONDS) + " >>>>>>>>>>"); + } + + private void cancelActiveRuleTask(UUID ruleUuid) { + if (SCHEDULED_NOTIFY_TASKS.containsKey(ruleUuid)) { + SCHEDULED_NOTIFY_TASKS.get(ruleUuid).cancel(true); + SCHEDULED_NOTIFY_TASKS.remove(ruleUuid); + LOGGER.info("<<<<<<<<<< Canceled scheduled notification task for rule " + ruleUuid + " <<<<<<<<<<<<"); + } + } + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -153,6 +201,17 @@ public Response updateScheduledNotificationRule(ScheduledNotificationRule jsonRu if (rule != null) { jsonRule.setName(StringUtils.trimToNull(jsonRule.getName())); rule = qm.updateScheduledNotificationRule(jsonRule); + + try { + cancelActiveRuleTask(jsonRule.getUuid()); + if (rule.isEnabled()) { + var schedule = Schedule.create(jsonRule.getCronConfig()); + scheduleNextRuleTask(jsonRule.getUuid(), schedule); + } + } catch (InvalidExpressionException e) { + return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); + } + return Response.ok(rule).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the scheduled notification rule could not be found.").build(); @@ -178,6 +237,9 @@ public Response deleteScheduledNotificationRule(ScheduledNotificationRule jsonRu final ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, jsonRule.getUuid()); if (rule != null) { qm.delete(rule); + + cancelActiveRuleTask(jsonRule.getUuid()); + return Response.status(Response.Status.NO_CONTENT).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the scheduled notification rule could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/tasks/ActionOnDoneFutureTask.java b/src/main/java/org/dependencytrack/tasks/ActionOnDoneFutureTask.java new file mode 100644 index 0000000000..f926fa11b9 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/ActionOnDoneFutureTask.java @@ -0,0 +1,44 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.tasks; + +import java.util.concurrent.FutureTask; + +import alpine.common.logging.Logger; + +public class ActionOnDoneFutureTask extends FutureTask { + private static final Logger LOGGER = Logger.getLogger(ActionOnDoneFutureTask.class); + private final Runnable action; + + public ActionOnDoneFutureTask(Runnable runnable, Runnable actionOnDone) { + super(runnable, null); + this.action = actionOnDone; + } + + @Override + protected void done() { + super.done(); + try { + this.action.run(); + } catch (Exception e) { + // just catch and log, do not interfere with completion + LOGGER.warn(e.toString()); + } + } +} From bba7f8feed3d59179f5150ac79f36dc55b6fe4f0 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 28 May 2024 15:08:19 +0200 Subject: [PATCH 34/98] removed test date in scheduled task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/tasks/SendScheduledNotificationTask.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index c2c42a4afb..86b0ec9a8e 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,7 +20,6 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; @@ -88,7 +87,7 @@ public void run() { .subject(vulnSubject); break; case POLICY_VIOLATION: - var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(ZonedDateTime.of(2023, 05, 20, 0, 0, 0, 0, ZoneId.systemDefault())/* rule.getLastExecutionTime() */, projectIds); + var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(rule.getLastExecutionTime(), projectIds); if(newProjectPolicyViolations.isEmpty() && rule.getPublishOnlyWithUpdates()) continue; ScheduledPolicyViolationsIdentified policySubject = new ScheduledPolicyViolationsIdentified(newProjectPolicyViolations); From 2a828d15e2ebe6274f559a302431e7acdd780df1 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Tue, 28 May 2024 15:08:56 +0200 Subject: [PATCH 35/98] fixed missing update of last execution time after successful publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../dependencytrack/tasks/SendScheduledNotificationTask.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 86b0ec9a8e..f6584fe53d 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -131,6 +131,9 @@ public void run() { } else { ((SendMailPublisher) publisher).inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig, rule.getTeams()); } + + // update last execution time after successful publication to avoid duplicate notifications in the next run + qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(rule); } else { LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName() + " (%s)".formatted(ruleCtx)); } From 5606fe424ab0baa8a0184278dcef50bf9a4aa172 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 10:31:43 +0200 Subject: [PATCH 36/98] initialize scheduled notification tasks at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../ScheduledNotificationTaskInitializer.java | 58 +++++++++++++++ .../ScheduledNotificationTaskManager.java | 71 +++++++++++++++++++ .../v1/ScheduledNotificationRuleResource.java | 47 +++--------- src/main/webapp/WEB-INF/web.xml | 3 + 4 files changed, 140 insertions(+), 39 deletions(-) create mode 100644 src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java create mode 100644 src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java diff --git a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java new file mode 100644 index 0000000000..99f18dbb2b --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java @@ -0,0 +1,58 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.dependencytrack.model.ScheduledNotificationRule; +import org.dependencytrack.persistence.QueryManager; + +import com.asahaf.javacron.InvalidExpressionException; +import com.asahaf.javacron.Schedule; + +import alpine.common.logging.Logger; + +public class ScheduledNotificationTaskInitializer implements ServletContextListener { + private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationTaskInitializer.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + try (var qm = new QueryManager()) { + var paginatedScheduledRules = qm.getScheduledNotificationRules(); + var scheduledRulesList = paginatedScheduledRules.getList(ScheduledNotificationRule.class); + for (var scheduledRule : scheduledRulesList) { + try { + if (scheduledRule.isEnabled()) { + ScheduledNotificationTaskManager.scheduleNextRuleTask( + scheduledRule.getUuid(), + Schedule.create(scheduledRule.getCronConfig())); + } + } catch (InvalidExpressionException e) { + LOGGER.error("Invalid cron expression in rule " + scheduledRule.getUuid() + ", no cron task could be created!"); + } + } + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + ScheduledNotificationTaskManager.cancelAllActiveRuleTasks(); + } +} diff --git a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java new file mode 100644 index 0000000000..6da003aca4 --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java @@ -0,0 +1,71 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.dependencytrack.tasks.ActionOnDoneFutureTask; +import org.dependencytrack.tasks.SendScheduledNotificationTask; + +import com.asahaf.javacron.Schedule; + +import alpine.common.logging.Logger; + +public final class ScheduledNotificationTaskManager { + private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationTaskManager.class); + private static final HashMap> SCHEDULED_NOTIFY_TASKS = new HashMap>(); + + public static void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule) { + var scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + var futureTask = new ActionOnDoneFutureTask(new SendScheduledNotificationTask(ruleUuid), () -> scheduleNextRuleTask(ruleUuid, schedule)); + + var future = scheduledExecutor.schedule( + futureTask, + schedule.nextDuration(TimeUnit.MILLISECONDS), + TimeUnit.MILLISECONDS); + SCHEDULED_NOTIFY_TASKS.put(ruleUuid, future); + + LOGGER.info(">>>>>>>>>> Scheduled notification task for rule " + ruleUuid + " @ " + LocalDateTime + .ofInstant(Instant.ofEpochMilli(schedule.nextDuration(TimeUnit.MILLISECONDS)), ZoneId.systemDefault()) + .truncatedTo(ChronoUnit.SECONDS) + " >>>>>>>>>>"); + } + + public static void cancelActiveRuleTask(UUID ruleUuid) { + if (SCHEDULED_NOTIFY_TASKS.containsKey(ruleUuid)) { + SCHEDULED_NOTIFY_TASKS.get(ruleUuid).cancel(true); + SCHEDULED_NOTIFY_TASKS.remove(ruleUuid); + LOGGER.info("<<<<<<<<<< Canceled scheduled notification task for rule " + ruleUuid + " <<<<<<<<<<<<"); + } + } + + public static void cancelAllActiveRuleTasks(){ + for (var future : SCHEDULED_NOTIFY_TASKS.values()) { + future.cancel(true); + } + SCHEDULED_NOTIFY_TASKS.clear(); + } +} diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index cd71a442ee..46b53b67df 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -37,12 +37,10 @@ import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.ScheduledNotificationTaskManager; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.openapi.PaginatedApi; -import org.dependencytrack.tasks.ActionOnDoneFutureTask; -import org.dependencytrack.tasks.SendScheduledNotificationTask; - import com.asahaf.javacron.InvalidExpressionException; import com.asahaf.javacron.Schedule; @@ -58,16 +56,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.HashMap; import java.util.List; -import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; /** * JAX-RS resources for processing scheduled notification rules. @@ -79,7 +68,6 @@ public class ScheduledNotificationRuleResource extends AlpineResource { private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationRuleResource.class); - private static final HashMap> SCHEDULED_NOTIFY_TASKS = new HashMap>(); @GET @Produces(MediaType.APPLICATION_JSON) @@ -143,8 +131,9 @@ public Response createScheduledNotificationRule(ScheduledNotificationRule jsonRu Schedule schedule; try { schedule = Schedule.create(jsonRule.getCronConfig()); - scheduleNextRuleTask(rule.getUuid(), schedule); - } catch (InvalidExpressionException e) { + ScheduledNotificationTaskManager.scheduleNextRuleTask(rule.getUuid(), schedule); + } catch (InvalidExpressionException e) { + LOGGER.error("Cron expression is invalid: " + jsonRule.getCronConfig()); return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); } } @@ -153,27 +142,6 @@ public Response createScheduledNotificationRule(ScheduledNotificationRule jsonRu } } - private void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule) { - var scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - var futureTask = new ActionOnDoneFutureTask(new SendScheduledNotificationTask(ruleUuid), () -> scheduleNextRuleTask(ruleUuid, schedule)); - - var future = scheduledExecutor.schedule( - futureTask, - schedule.nextDuration(TimeUnit.MILLISECONDS), - TimeUnit.MILLISECONDS); - SCHEDULED_NOTIFY_TASKS.put(ruleUuid, future); - - LOGGER.info(">>>>>>>>>> Scheduled notification task for rule " + ruleUuid + " @ " + LocalDateTime.ofInstant(Instant.ofEpochMilli(schedule.nextDuration(TimeUnit.MILLISECONDS)), ZoneId.systemDefault()).truncatedTo(ChronoUnit.SECONDS) + " >>>>>>>>>>"); - } - - private void cancelActiveRuleTask(UUID ruleUuid) { - if (SCHEDULED_NOTIFY_TASKS.containsKey(ruleUuid)) { - SCHEDULED_NOTIFY_TASKS.get(ruleUuid).cancel(true); - SCHEDULED_NOTIFY_TASKS.remove(ruleUuid); - LOGGER.info("<<<<<<<<<< Canceled scheduled notification task for rule " + ruleUuid + " <<<<<<<<<<<<"); - } - } - @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -203,12 +171,13 @@ public Response updateScheduledNotificationRule(ScheduledNotificationRule jsonRu rule = qm.updateScheduledNotificationRule(jsonRule); try { - cancelActiveRuleTask(jsonRule.getUuid()); + ScheduledNotificationTaskManager.cancelActiveRuleTask(jsonRule.getUuid()); if (rule.isEnabled()) { var schedule = Schedule.create(jsonRule.getCronConfig()); - scheduleNextRuleTask(jsonRule.getUuid(), schedule); + ScheduledNotificationTaskManager.scheduleNextRuleTask(jsonRule.getUuid(), schedule); } } catch (InvalidExpressionException e) { + LOGGER.error("Cron expression is invalid: " + jsonRule.getCronConfig()); return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); } @@ -238,7 +207,7 @@ public Response deleteScheduledNotificationRule(ScheduledNotificationRule jsonRu if (rule != null) { qm.delete(rule); - cancelActiveRuleTask(jsonRule.getUuid()); + ScheduledNotificationTaskManager.cancelActiveRuleTask(jsonRule.getUuid()); return Response.status(Response.Status.NO_CONTENT).build(); } else { diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 339838a5f3..ad86cd00d5 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -57,6 +57,9 @@ org.dependencytrack.persistence.H2WebConsoleInitializer + + org.dependencytrack.notification.ScheduledNotificationTaskInitializer + WhitelistUrlFilter From 4fbcd2e7fa244a09ab9b662f38b64e8c574e3bce Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 12:19:48 +0200 Subject: [PATCH 37/98] added option to run scheduled notification rule manually instant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../ScheduledNotificationTaskManager.java | 28 +++++------ .../v1/ScheduledNotificationRuleResource.java | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java index 6da003aca4..c70f281ae2 100644 --- a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java +++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java @@ -18,10 +18,6 @@ */ package org.dependencytrack.notification; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.Executors; @@ -33,32 +29,36 @@ import com.asahaf.javacron.Schedule; -import alpine.common.logging.Logger; - public final class ScheduledNotificationTaskManager { - private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationTaskManager.class); private static final HashMap> SCHEDULED_NOTIFY_TASKS = new HashMap>(); + public static void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule, long customDelay, TimeUnit delayUnit) { + scheduleNextRuleTask(ruleUuid, schedule, customDelay, delayUnit, () -> scheduleNextRuleTask(ruleUuid, schedule)); + } + public static void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule) { + scheduleNextRuleTask(ruleUuid, schedule, schedule.nextDuration(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + + public static void scheduleNextRuleTaskOnce(UUID ruleUuid, long customDelay, TimeUnit delayUnit){ + scheduleNextRuleTask(ruleUuid, null, customDelay, delayUnit, () -> cancelActiveRuleTask(ruleUuid)); + } + + private static void scheduleNextRuleTask(UUID ruleUuid, Schedule schedule, long customDelay, TimeUnit delayUnit, Runnable actionAfterTaskCompletion){ var scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - var futureTask = new ActionOnDoneFutureTask(new SendScheduledNotificationTask(ruleUuid), () -> scheduleNextRuleTask(ruleUuid, schedule)); + var futureTask = new ActionOnDoneFutureTask(new SendScheduledNotificationTask(ruleUuid), actionAfterTaskCompletion); var future = scheduledExecutor.schedule( futureTask, - schedule.nextDuration(TimeUnit.MILLISECONDS), + customDelay, TimeUnit.MILLISECONDS); SCHEDULED_NOTIFY_TASKS.put(ruleUuid, future); - - LOGGER.info(">>>>>>>>>> Scheduled notification task for rule " + ruleUuid + " @ " + LocalDateTime - .ofInstant(Instant.ofEpochMilli(schedule.nextDuration(TimeUnit.MILLISECONDS)), ZoneId.systemDefault()) - .truncatedTo(ChronoUnit.SECONDS) + " >>>>>>>>>>"); } public static void cancelActiveRuleTask(UUID ruleUuid) { if (SCHEDULED_NOTIFY_TASKS.containsKey(ruleUuid)) { SCHEDULED_NOTIFY_TASKS.get(ruleUuid).cancel(true); SCHEDULED_NOTIFY_TASKS.remove(ruleUuid); - LOGGER.info("<<<<<<<<<< Canceled scheduled notification task for rule " + ruleUuid + " <<<<<<<<<<<<"); } } diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index 46b53b67df..60e345ac7b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -57,6 +57,7 @@ import javax.ws.rs.core.Response; import java.util.List; +import java.util.concurrent.TimeUnit; /** * JAX-RS resources for processing scheduled notification rules. @@ -342,6 +343,53 @@ public Response addTeamToRule( } } + @POST + @Path("/execute") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Executes a scheduled notification rule instantly ignoring the cron expression", + response = ScheduledNotificationRule.class, + notes = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The UUID of the scheduled notification rule could not be found") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response executeScheduledNotificationRuleNow(ScheduledNotificationRule jsonRule) { + final Validator validator = super.getValidator(); + failOnValidationError( + validator.validateProperty(jsonRule, "name"), + validator.validateProperty(jsonRule, "publisherConfig"), + validator.validateProperty(jsonRule, "cronConfig"), + validator.validateProperty(jsonRule, "lastExecutionTime") + ); + + try (QueryManager qm = new QueryManager()) { + ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, jsonRule.getUuid()); + if (rule != null) { + try { + ScheduledNotificationTaskManager.cancelActiveRuleTask(jsonRule.getUuid()); + if (rule.isEnabled()) { + // schedule must be passed too, to schedule the next execution according to cron expression again + var schedule = Schedule.create(jsonRule.getCronConfig()); + ScheduledNotificationTaskManager.scheduleNextRuleTask(jsonRule.getUuid(), schedule, 0, TimeUnit.MILLISECONDS); + } else { + ScheduledNotificationTaskManager.scheduleNextRuleTaskOnce(jsonRule.getUuid(), 0, TimeUnit.MILLISECONDS); + } + } catch (InvalidExpressionException e) { + LOGGER.error("Cron expression is invalid: " + jsonRule.getCronConfig()); + return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); + } + + return Response.ok(rule).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the scheduled notification rule could not be found.").build(); + } + } + } + @DELETE @Path("/{ruleUuid}/team/{teamUuid}") @Consumes(MediaType.APPLICATION_JSON) From 2dd4f71076734874378d3838ae41a6d736ab8475 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 14:06:51 +0200 Subject: [PATCH 38/98] support to read default cron expression from environment variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- .../org/dependencytrack/model/ScheduledNotificationRule.java | 4 ++-- .../dependencytrack/persistence/NotificationQueryManager.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 64c2912231..5482da61cb 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -98,7 +98,7 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates"), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates"), - NOTIFICATION_CRON_DEFAULT_INTERVAL("notification", "cron.default.interval", "0 12 * * *", PropertyType.STRING, "The default interval of scheduled notifications as cron expression (every day at 12pm)"), + NOTIFICATION_CRON_DEFAULT_EXPRESSION("notification", "cron.default.expression", SystemUtils.getEnvironmentVariable("DEFAULT_SCHEDULED_CRON_EXPRESSION", "0 12 * * *"), PropertyType.STRING, "The default interval of scheduled notifications as cron expression"), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP"), TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories"), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database"), diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index 39bd037964..ceec5483af 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -299,7 +299,7 @@ public void setUuid(@NotNull UUID uuid) { } public String getCronConfig() { - var cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); + var cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_EXPRESSION.getDefaultPropertyValue(); if (this.cronConfig != null) { cronConfig = this.cronConfig; } @@ -308,7 +308,7 @@ public String getCronConfig() { public void setCronConfig(String cronConfig) { if (cronConfig == null) { - this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue(); + this.cronConfig = ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_EXPRESSION.getDefaultPropertyValue(); return; } this.cronConfig = cronConfig; diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index f5187d0be2..25797837ee 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -105,7 +105,7 @@ public ScheduledNotificationRule createScheduledNotificationRule(String name, No rule.setEnabled(true); rule.setNotifyChildren(true); rule.setLogSuccessfulPublish(false); - rule.setCronConfig(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_INTERVAL.getDefaultPropertyValue()); + rule.setCronConfig(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_EXPRESSION.getDefaultPropertyValue()); rule.setLastExecutionTime(ZonedDateTime.now(ZoneOffset.UTC)); rule.setPublishOnlyWithUpdates(false); return persist(rule); From 80b9e01ca68d73b2baa3ae2c95721a96916029a2 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 14:30:45 +0200 Subject: [PATCH 39/98] update last execution time of rule without publishing, if no errors occured during task execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index f6584fe53d..e3bd21396a 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -65,6 +65,8 @@ public void run() { try (var qm = new QueryManager()) { var rule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRuleUuid); final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); + Boolean errorsDuringExecution = false; + Boolean atLeastOneSuccessfulPublish = false; for (NotificationGroup group : rule.getNotifyOn()) { final Notification notificationProxy = new Notification() @@ -100,6 +102,7 @@ public void run() { break; default: LOGGER.error(group.name() + " is not a supported notification group for scheduled publishing"); + errorsDuringExecution |= true; continue; } @@ -114,6 +117,7 @@ public void run() { config = jsonReader.readObject(); } catch (Exception e) { LOGGER.error("An error occurred while preparing the configuration for the notification publisher (%s)".formatted(ruleCtx), e); + errorsDuringExecution |= true; } } try { @@ -131,22 +135,27 @@ public void run() { } else { ((SendMailPublisher) publisher).inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig, rule.getTeams()); } - - // update last execution time after successful publication to avoid duplicate notifications in the next run - qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(rule); + atLeastOneSuccessfulPublish |= true; } else { LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName() + " (%s)".formatted(ruleCtx)); } } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) { - LOGGER.error( - "An error occurred while instantiating a notification publisher (%s)".formatted(ruleCtx), - e); + LOGGER.error("An error occurred while instantiating a notification publisher (%s)".formatted(ruleCtx), e); + errorsDuringExecution |= true; } catch (PublisherException publisherException) { - LOGGER.error("An error occurred during the publication of the notification (%s)".formatted(ruleCtx), - publisherException); + LOGGER.error("An error occurred during the publication of the notification (%s)".formatted(ruleCtx), publisherException); + errorsDuringExecution |= true; } } + if (!errorsDuringExecution || atLeastOneSuccessfulPublish) { + /* + * Update last execution time after successful operation (even without + * publishing) to avoid duplicate notifications in the next run and signalize + * user indirectly, that operation has ended without failure + */ + qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(rule); + } } } From 70a4fbef02c21d42ef6491d0ad108c77fd9fc95f Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 14:31:07 +0200 Subject: [PATCH 40/98] added informational logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../ScheduledNotificationTaskInitializer.java | 2 ++ .../tasks/SendScheduledNotificationTask.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java index 99f18dbb2b..ada3ccb2f7 100644 --- a/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java +++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.java @@ -34,6 +34,7 @@ public class ScheduledNotificationTaskInitializer implements ServletContextListe @Override public void contextInitialized(ServletContextEvent sce) { + LOGGER.info("Initializing scheduled notification task service"); try (var qm = new QueryManager()) { var paginatedScheduledRules = qm.getScheduledNotificationRules(); var scheduledRulesList = paginatedScheduledRules.getList(ScheduledNotificationRule.class); @@ -53,6 +54,7 @@ public void contextInitialized(ServletContextEvent sce) { @Override public void contextDestroyed(ServletContextEvent sce) { + LOGGER.info("Shutting down scheduled notification task service"); ScheduledNotificationTaskManager.cancelAllActiveRuleTasks(); } } diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index e3bd21396a..361b3c934e 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -67,6 +67,8 @@ public void run() { final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); Boolean errorsDuringExecution = false; Boolean atLeastOneSuccessfulPublish = false; + + LOGGER.info("Processing notification publishing for scheduled notification rule " + rule.getUuid()); for (NotificationGroup group : rule.getNotifyOn()) { final Notification notificationProxy = new Notification() @@ -101,7 +103,7 @@ public void run() { .subject(policySubject); break; default: - LOGGER.error(group.name() + " is not a supported notification group for scheduled publishing"); + LOGGER.warn(group.name() + " is not a supported notification group for scheduled publishing"); errorsDuringExecution |= true; continue; } @@ -138,6 +140,7 @@ public void run() { atLeastOneSuccessfulPublish |= true; } else { LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName() + " (%s)".formatted(ruleCtx)); + errorsDuringExecution |= true; } } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) { @@ -155,6 +158,10 @@ public void run() { * user indirectly, that operation has ended without failure */ qm.updateScheduledNotificationRuleLastExecutionTimeToNowUtc(rule); + LOGGER.info("Successfuly processed notification publishing for scheduled notification rule " + scheduledNotificationRuleUuid); + } + else { + LOGGER.error("Errors occured while processing notification publishing for scheduled notification rule " + scheduledNotificationRuleUuid); } } } From 54034aec9911e5f7480107f307b62d30b5dbe59e Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 15:41:17 +0200 Subject: [PATCH 41/98] removed author tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- src/main/java/org/dependencytrack/model/PublishTrigger.java | 2 -- .../org/dependencytrack/model/ScheduledNotificationRule.java | 2 -- .../resources/v1/ScheduledNotificationRuleResource.java | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/PublishTrigger.java b/src/main/java/org/dependencytrack/model/PublishTrigger.java index 43c682e4f2..9e402e63ae 100644 --- a/src/main/java/org/dependencytrack/model/PublishTrigger.java +++ b/src/main/java/org/dependencytrack/model/PublishTrigger.java @@ -21,8 +21,6 @@ /** * Provides a list of available triggers for publishers to send notifications. - * - * @author Max Schiller */ public enum PublishTrigger { ALL, diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index ceec5483af..b16ca821d8 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -57,8 +57,6 @@ /** * Defines a Model class for scheduled notification configurations. - * - * @author Max Schiller */ @PersistenceCapable @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index 60e345ac7b..21a00dd1c2 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -61,8 +61,6 @@ /** * JAX-RS resources for processing scheduled notification rules. - * - * @author Max Schiller */ @Path("/v1/schedulednotification/rule") @Api(value = "schedulednotification", authorizations = @Authorization(value = "X-Api-Key")) From 10ebedc57fb8cb04ab471805ed4c3cf654a15ef1 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 15:41:40 +0200 Subject: [PATCH 42/98] removed unnecessary code in publisher task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 361b3c934e..455de666a0 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -23,10 +23,7 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; - import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; @@ -42,7 +39,6 @@ import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; -import org.dependencytrack.notification.vo.NewVulnerabilityIdentified; import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.persistence.QueryManager; @@ -133,9 +129,9 @@ public void run() { .addAll(Json.createObjectBuilder(config)) .build(); if (publisherClass != SendMailPublisher.class || rule.getTeams().isEmpty() || rule.getTeams() == null) { - publisher.inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig); + publisher.inform(ruleCtx, notificationProxy, notificationPublisherConfig); } else { - ((SendMailPublisher) publisher).inform(ruleCtx, restrictNotificationToRuleProjects(notificationProxy, rule), notificationPublisherConfig, rule.getTeams()); + ((SendMailPublisher) publisher).inform(ruleCtx, notificationProxy, notificationPublisherConfig, rule.getTeams()); } atLeastOneSuccessfulPublish |= true; } else { @@ -166,32 +162,6 @@ public void run() { } } - private Notification restrictNotificationToRuleProjects(final Notification initialNotification, final Rule rule) { - Notification restrictedNotification = initialNotification; - if (canRestrictNotificationToRuleProjects(initialNotification, rule)) { - Set ruleProjectsUuids = rule.getProjects().stream().map(Project::getUuid).map(UUID::toString).collect(Collectors.toSet()); - restrictedNotification = new Notification(); - restrictedNotification.setGroup(initialNotification.getGroup()); - restrictedNotification.setLevel(initialNotification.getLevel()); - restrictedNotification.scope(initialNotification.getScope()); - restrictedNotification.setContent(initialNotification.getContent()); - restrictedNotification.setTitle(initialNotification.getTitle()); - restrictedNotification.setTimestamp(initialNotification.getTimestamp()); - 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); - } - } - return restrictedNotification; - } - - private boolean canRestrictNotificationToRuleProjects(final Notification initialNotification, final Rule rule) { - return initialNotification.getSubject() instanceof NewVulnerabilityIdentified - && rule.getProjects() != null - && !rule.getProjects().isEmpty(); - } - private String generateNotificationTitle(final Rule rule, final NotificationGroup group) { return "Scheduled Notification: " + group.name(); } From 624a96b8805c7ca8b3ef6c075491488dadca51eb Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 29 May 2024 17:06:35 +0200 Subject: [PATCH 43/98] moved notification title and content generation to NotificationUtil class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 57 +++++-------------- .../util/NotificationUtil.java | 50 ++++++++++++++++ 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 455de666a0..7d449122eb 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,8 +20,6 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.List; import java.util.UUID; import javax.json.Json; @@ -30,11 +28,7 @@ import org.dependencytrack.exception.PublisherException; import org.dependencytrack.model.NotificationPublisher; -import org.dependencytrack.model.PolicyViolation; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.Rule; import org.dependencytrack.model.ScheduledNotificationRule; -import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; @@ -42,6 +36,7 @@ import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.NotificationUtil; import alpine.common.logging.Logger; import alpine.notification.Notification; @@ -70,7 +65,7 @@ public void run() { final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) - .title(generateNotificationTitle(rule, group)) + .title(NotificationUtil.generateNotificationTitle(group, rule.getProjects())) .level(rule.getNotificationLevel()); switch (group) { @@ -80,10 +75,11 @@ public void run() { continue; ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(newProjectVulnerabilities); notificationProxy - .content(generateVulnerabilityNotificationContent(rule, - vulnSubject.getNewVulnerabilitiesTotal(), - newProjectVulnerabilities.keySet().stream().toList(), - rule.getLastExecutionTime())) + .content(NotificationUtil.generateVulnerabilityScheduledNotificationContent( + rule, + vulnSubject.getNewVulnerabilitiesTotal(), + newProjectVulnerabilities.keySet().stream().toList(), + rule.getLastExecutionTime())) .subject(vulnSubject); break; case POLICY_VIOLATION: @@ -92,11 +88,12 @@ public void run() { continue; ScheduledPolicyViolationsIdentified policySubject = new ScheduledPolicyViolationsIdentified(newProjectPolicyViolations); notificationProxy - .content(generatePolicyNotificationContent(rule, - policySubject.getNewPolicyViolationsTotal(), - newProjectPolicyViolations.keySet().stream().toList(), - rule.getLastExecutionTime())) - .subject(policySubject); + .content(NotificationUtil.generatePolicyScheduledNotificationContent( + rule, + policySubject.getNewPolicyViolationsTotal(), + newProjectPolicyViolations.keySet().stream().toList(), + rule.getLastExecutionTime())) + .subject(policySubject); break; default: LOGGER.warn(group.name() + " is not a supported notification group for scheduled publishing"); @@ -161,32 +158,4 @@ public void run() { } } } - - private String generateNotificationTitle(final Rule rule, final NotificationGroup group) { - return "Scheduled Notification: " + group.name(); - } - - private String generateVulnerabilityNotificationContent(final Rule rule, final List vulnerabilities, final List projects, final ZonedDateTime lastExecutionTime) { - final String content; - - if (vulnerabilities.isEmpty()) { - content = "No new vulnerabilities found."; - } else { - content = "In total, " + vulnerabilities.size() + " new vulnerabilities in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; - } - - return content; - } - - private String generatePolicyNotificationContent(final Rule rule, final List policyViolations, final List projects, final ZonedDateTime lastExecutionTime) { - final String content; - - if (policyViolations.isEmpty()) { - content = "No new policy violations found."; - } else { - content = "In total, " + policyViolations.size() + " new policy violations in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; - } - - return content; - } } diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index ba1b9d9ef3..6eaaa873b8 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -36,6 +36,7 @@ import org.dependencytrack.model.PolicyCondition.Operator; import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Rule; import org.dependencytrack.model.Severity; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vex; @@ -72,6 +73,8 @@ import java.net.URLDecoder; import java.nio.file.Path; import java.util.Date; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -727,6 +730,30 @@ private static String generateNotificationContent(final ViolationAnalysis violat return "An violation analysis decision was made to a policy violation affecting a project"; } + public static String generateVulnerabilityScheduledNotificationContent(final Rule rule, final List vulnerabilities, final List projects, final ZonedDateTime lastExecutionTime) { + final String content; + + if (vulnerabilities.isEmpty()) { + content = "No new vulnerabilities found."; + } else { + content = "In total, " + vulnerabilities.size() + " new vulnerabilities in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; + } + + return content; + } + + public static String generatePolicyScheduledNotificationContent(final Rule rule, final List policyViolations, final List projects, final ZonedDateTime lastExecutionTime) { + final String content; + + if (policyViolations.isEmpty()) { + content = "No new policy violations found."; + } else { + content = "In total, " + policyViolations.size() + " new policy violations in " + projects.size() + " projects were found since " + lastExecutionTime.toLocalDateTime().truncatedTo(ChronoUnit.SECONDS) + "."; + } + + return content; + } + public static String generateNotificationTitle(String messageType, Project project) { if (project != null) { return messageType + " on Project: [" + project.toString() + "]"; @@ -734,6 +761,29 @@ public static String generateNotificationTitle(String messageType, Project proje return messageType; } + public static String generateNotificationTitle(NotificationGroup notificationGroup, List projects) { + String messageType; + + switch (notificationGroup) { + case NEW_VULNERABILITY: + messageType = NotificationConstants.Title.NEW_VULNERABILITY; + break; + case POLICY_VIOLATION: + messageType = NotificationConstants.Title.POLICY_VIOLATION; + break; + default: + return notificationGroup.name(); + } + + if (projects != null) { + if (projects.size() == 1) { + return generateNotificationTitle(messageType, projects.get(0)); + } + } + + return messageType + " on " + projects.size() + " projects"; + } + public static Object generateSubject(String group) { final Project project = createProject(); final Vulnerability vuln = createVulnerability(); From 439b7fb3e9413d6c1c452b19c0187ab1af383d6a Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 30 May 2024 17:21:27 +0200 Subject: [PATCH 44/98] removed check for instant execution api payload to match UI changes (uuid only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../v1/ScheduledNotificationRuleResource.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java index 21a00dd1c2..54ab9e6d3a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResource.java @@ -356,28 +356,20 @@ public Response addTeamToRule( }) @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) public Response executeScheduledNotificationRuleNow(ScheduledNotificationRule jsonRule) { - final Validator validator = super.getValidator(); - failOnValidationError( - validator.validateProperty(jsonRule, "name"), - validator.validateProperty(jsonRule, "publisherConfig"), - validator.validateProperty(jsonRule, "cronConfig"), - validator.validateProperty(jsonRule, "lastExecutionTime") - ); - try (QueryManager qm = new QueryManager()) { ScheduledNotificationRule rule = qm.getObjectByUuid(ScheduledNotificationRule.class, jsonRule.getUuid()); if (rule != null) { try { - ScheduledNotificationTaskManager.cancelActiveRuleTask(jsonRule.getUuid()); + ScheduledNotificationTaskManager.cancelActiveRuleTask(rule.getUuid()); if (rule.isEnabled()) { // schedule must be passed too, to schedule the next execution according to cron expression again - var schedule = Schedule.create(jsonRule.getCronConfig()); - ScheduledNotificationTaskManager.scheduleNextRuleTask(jsonRule.getUuid(), schedule, 0, TimeUnit.MILLISECONDS); + var schedule = Schedule.create(rule.getCronConfig()); + ScheduledNotificationTaskManager.scheduleNextRuleTask(rule.getUuid(), schedule, 0, TimeUnit.MILLISECONDS); } else { - ScheduledNotificationTaskManager.scheduleNextRuleTaskOnce(jsonRule.getUuid(), 0, TimeUnit.MILLISECONDS); + ScheduledNotificationTaskManager.scheduleNextRuleTaskOnce(rule.getUuid(), 0, TimeUnit.MILLISECONDS); } } catch (InvalidExpressionException e) { - LOGGER.error("Cron expression is invalid: " + jsonRule.getCronConfig()); + LOGGER.error("Cron expression is invalid: " + rule.getCronConfig()); return Response.status(Response.Status.BAD_REQUEST).entity("Invalid cron expression").build(); } From 8c57996fe2bdc8382d2a4b231010573f263edd69 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 31 May 2024 15:37:49 +0200 Subject: [PATCH 45/98] added json serializer for ZonedDateTime for better readability in api json results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/ScheduledNotificationRule.java | 7 ++- .../Iso8601ZonedDateTimeSerializer.java | 46 +++++++++++++++++++ .../util/ZonedDateTimeUtil.java | 36 +++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/dependencytrack/resources/v1/serializers/Iso8601ZonedDateTimeSerializer.java create mode 100644 src/main/java/org/dependencytrack/util/ZonedDateTimeUtil.java diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index b16ca821d8..5a1fce9601 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -27,6 +27,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -45,8 +47,10 @@ import org.apache.commons.collections4.CollectionUtils; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.resources.v1.serializers.Iso8601ZonedDateTimeSerializer; import java.io.Serializable; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; @@ -154,6 +158,7 @@ public class ScheduledNotificationRule implements Rule, Serializable { @Persistent(defaultFetchGroup = "true") @Column(name = "LAST_EXECUTION_TIME", allowsNull = "true") // new column, must allow nulls on existing databases + @JsonSerialize(using = Iso8601ZonedDateTimeSerializer.class) private ZonedDateTime lastExecutionTime; @Persistent @@ -314,7 +319,7 @@ public void setCronConfig(String cronConfig) { public ZonedDateTime getLastExecutionTime() { if (lastExecutionTime == null) { - return ZonedDateTime.now(); + return ZonedDateTime.now(ZoneOffset.UTC); } return lastExecutionTime; } diff --git a/src/main/java/org/dependencytrack/resources/v1/serializers/Iso8601ZonedDateTimeSerializer.java b/src/main/java/org/dependencytrack/resources/v1/serializers/Iso8601ZonedDateTimeSerializer.java new file mode 100644 index 0000000000..0716a7d1c9 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/serializers/Iso8601ZonedDateTimeSerializer.java @@ -0,0 +1,46 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.serializers; + +import java.io.IOException; +import java.time.ZonedDateTime; +import org.dependencytrack.util.ZonedDateTimeUtil; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class Iso8601ZonedDateTimeSerializer extends StdSerializer { + + public Iso8601ZonedDateTimeSerializer() { + this(null); + } + + public Iso8601ZonedDateTimeSerializer(Class t) { + super(t); + } + + @Override + public void serialize(ZonedDateTime value, JsonGenerator gen, SerializerProvider arg2) + throws IOException, JsonProcessingException { + gen.writeString(ZonedDateTimeUtil.toISO8601(value)); + } + +} diff --git a/src/main/java/org/dependencytrack/util/ZonedDateTimeUtil.java b/src/main/java/org/dependencytrack/util/ZonedDateTimeUtil.java new file mode 100644 index 0000000000..2998de5bd1 --- /dev/null +++ b/src/main/java/org/dependencytrack/util/ZonedDateTimeUtil.java @@ -0,0 +1,36 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.util; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class ZonedDateTimeUtil { + public static String toISO8601(final ZonedDateTime date) { + return date.withZoneSameInstant(ZoneId.of("UTC")).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + public static ZonedDateTime fromISO8601(final String dateString) { + if (dateString == null) { + return null; + } + return ZonedDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } +} From 6ead108aa3fac457f07f90678bdf29c419fd5930 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 3 Jun 2024 16:05:42 +0200 Subject: [PATCH 46/98] Merge branch 'msr-scheduled-tests' into msr-issue-322 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/ResourceTest.java | 15 + .../model/NotificationPublisherTest.java | 7 + .../model/ScheduledNotificationRuleTest.java | 207 +++++++ .../DefaultNotificationPublishersTest.java | 19 + ...duledNewVulnerabilitiesIdentifiedTest.java | 80 +++ ...heduledPolicyViolationsIdentifiedTest.java | 51 ++ .../v1/NotificationRuleResourceTest.java | 1 + ...ScheduledNotificationRuleResourceTest.java | 570 ++++++++++++++++++ 8 files changed, 950 insertions(+) create mode 100644 src/test/java/org/dependencytrack/model/ScheduledNotificationRuleTest.java create mode 100644 src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java create mode 100644 src/test/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentifiedTest.java create mode 100644 src/test/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResourceTest.java diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index 4177c1e0ba..9b9f98191d 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -30,6 +30,12 @@ import org.junit.Before; import org.junit.BeforeClass; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -56,6 +62,7 @@ public abstract class ResourceTest { protected final String V1_METRICS = "/v1/metrics"; protected final String V1_NOTIFICATION_PUBLISHER = "/v1/notification/publisher"; protected final String V1_NOTIFICATION_RULE = "/v1/notification/rule"; + protected final String V1_SCHEDULED_NOTIFICATION_RULE = "/v1/schedulednotification/rule"; protected final String V1_OIDC = "/v1/oidc"; protected final String V1_PERMISSION = "/v1/permission"; protected final String V1_OSV_ECOSYSTEM = "/v1/integration/osv/ecosystem"; @@ -89,6 +96,14 @@ public abstract class ResourceTest { protected QueryManager qm; protected Team team; protected String apiKey; + protected JsonMapper jsonMapper; + + public ResourceTest() { + // needed to deserialize Java time objects in tests + jsonMapper = JsonMapper.builder() + .addModule(new JavaTimeModule()) + .build(); + } @BeforeClass public static void init() { diff --git a/src/test/java/org/dependencytrack/model/NotificationPublisherTest.java b/src/test/java/org/dependencytrack/model/NotificationPublisherTest.java index 72bcd1b68f..e0a239fbd1 100644 --- a/src/test/java/org/dependencytrack/model/NotificationPublisherTest.java +++ b/src/test/java/org/dependencytrack/model/NotificationPublisherTest.java @@ -74,6 +74,13 @@ public void testDefaultPublisher() { Assert.assertTrue(publisher.isDefaultPublisher()); } + @Test + public void testPublishScheduled() { + NotificationPublisher publisher = new NotificationPublisher(); + publisher.setPublishScheduled(true); + Assert.assertTrue(publisher.isPublishScheduled()); + } + @Test public void testUuid() { UUID uuid = UUID.randomUUID(); diff --git a/src/test/java/org/dependencytrack/model/ScheduledNotificationRuleTest.java b/src/test/java/org/dependencytrack/model/ScheduledNotificationRuleTest.java new file mode 100644 index 0000000000..b4afa9974e --- /dev/null +++ b/src/test/java/org/dependencytrack/model/ScheduledNotificationRuleTest.java @@ -0,0 +1,207 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.junit.Assert; +import org.junit.Test; + +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; +import alpine.model.Team; +import alpine.notification.NotificationLevel; + +public class ScheduledNotificationRuleTest { + @Test + public void testId() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setId(111L); + Assert.assertEquals(111L, rule.getId()); + } + + @Test + public void testName() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setName("Test Name"); + Assert.assertEquals("Test Name", rule.getName()); + } + + @Test + public void testEnabled() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setEnabled(true); + Assert.assertTrue(rule.isEnabled()); + } + + @Test + public void testScope() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setScope(NotificationScope.PORTFOLIO); + Assert.assertEquals(NotificationScope.PORTFOLIO, rule.getScope()); + } + + @Test + public void testNotificationLevel() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setNotificationLevel(NotificationLevel.INFORMATIONAL); + Assert.assertEquals(NotificationLevel.INFORMATIONAL, rule.getNotificationLevel()); + } + + @Test + public void testProjects() { + List projects = new ArrayList<>(); + Project project = new Project(); + projects.add(project); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setProjects(projects); + Assert.assertEquals(1, rule.getProjects().size()); + Assert.assertEquals(project, rule.getProjects().get(0)); + } + + @Test + public void testMessage() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setMessage("Test Message"); + Assert.assertEquals("Test Message", rule.getMessage()); + } + + @Test + public void testNotifyOn() { + Set groups = new HashSet<>(); + groups.add(NotificationGroup.POLICY_VIOLATION); + groups.add(NotificationGroup.NEW_VULNERABILITY); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setNotifyOn(groups); + Assert.assertEquals(2, rule.getNotifyOn().size()); + } + + @Test + public void testPublisher() { + NotificationPublisher publisher = new NotificationPublisher(); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setPublisher(publisher); + Assert.assertEquals(publisher, rule.getPublisher()); + } + + @Test + public void testPublisherConfig() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setPublisherConfig("{ \"config\": \"configured\" }"); + Assert.assertEquals("{ \"config\": \"configured\" }", rule.getPublisherConfig()); + } + + @Test + public void testUuid() { + UUID uuid = UUID.randomUUID(); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setUuid(uuid); + Assert.assertEquals(uuid.toString(), rule.getUuid().toString()); + } + + @Test + public void testTeams(){ + List teams = new ArrayList<>(); + Team team = new Team(); + teams.add(team); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setTeams(teams); + Assert.assertEquals(1, rule.getTeams().size()); + Assert.assertEquals(team, rule.getTeams().get(0)); + } + + @Test + public void testManagedUsers(){ + List teams = new ArrayList<>(); + Team team = new Team(); + List managedUsers = new ArrayList<>(); + ManagedUser managedUser = new ManagedUser(); + managedUsers.add(managedUser); + team.setManagedUsers(managedUsers); + teams.add(team); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setTeams(teams); + Assert.assertEquals(1, rule.getTeams().size()); + Assert.assertEquals(team, rule.getTeams().get(0)); + Assert.assertEquals(managedUser, rule.getTeams().get(0).getManagedUsers().get(0)); + } + + @Test + public void testLdapUsers(){ + List teams = new ArrayList<>(); + Team team = new Team(); + List ldapUsers = new ArrayList<>(); + LdapUser ldapUser = new LdapUser(); + ldapUsers.add(ldapUser); + team.setLdapUsers(ldapUsers); + teams.add(team); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setTeams(teams); + Assert.assertEquals(1, rule.getTeams().size()); + Assert.assertEquals(team, rule.getTeams().get(0)); + Assert.assertEquals(ldapUser, rule.getTeams().get(0).getLdapUsers().get(0)); + } + + @Test + public void testOidcUsers(){ + List teams = new ArrayList<>(); + Team team = new Team(); + List oidcUsers = new ArrayList<>(); + OidcUser oidcUser = new OidcUser(); + oidcUsers.add(oidcUser); + team.setOidcUsers(oidcUsers); + teams.add(team); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setTeams(teams); + Assert.assertEquals(1, rule.getTeams().size()); + Assert.assertEquals(team, rule.getTeams().get(0)); + Assert.assertEquals(oidcUser, rule.getTeams().get(0).getOidcUsers().get(0)); + } + + @Test + public void testCronExpression() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setCronConfig("0 0 12 * *"); + Assert.assertEquals("0 0 12 * *", rule.getCronConfig()); + } + + @Test + public void testLastExecutionTime() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + ZonedDateTime zdt = ZonedDateTime.of(2024, 5, 20, 12, 10, 13, 0, ZoneOffset.UTC); + rule.setLastExecutionTime(zdt); + Assert.assertEquals(zdt, rule.getLastExecutionTime()); + } + + @Test + public void testPublishOnlyWithUpdates() { + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setPublishOnlyWithUpdates(true); + Assert.assertTrue(rule.getPublishOnlyWithUpdates()); + } +} diff --git a/src/test/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishersTest.java b/src/test/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishersTest.java index b125d50766..153be61c1f 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishersTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishersTest.java @@ -31,6 +31,7 @@ public void testEnums() { Assert.assertEquals("MS_TEAMS", DefaultNotificationPublishers.MS_TEAMS.name()); Assert.assertEquals("MATTERMOST", DefaultNotificationPublishers.MATTERMOST.name()); Assert.assertEquals("EMAIL", DefaultNotificationPublishers.EMAIL.name()); + Assert.assertEquals("SCHEDULED_EMAIL", DefaultNotificationPublishers.SCHEDULED_EMAIL.name()); Assert.assertEquals("CONSOLE", DefaultNotificationPublishers.CONSOLE.name()); Assert.assertEquals("WEBHOOK", DefaultNotificationPublishers.WEBHOOK.name()); Assert.assertEquals("JIRA", DefaultNotificationPublishers.JIRA.name()); @@ -44,6 +45,7 @@ public void testSlack() { Assert.assertEquals("/templates/notification/publisher/slack.peb", DefaultNotificationPublishers.SLACK.getPublisherTemplateFile()); Assert.assertEquals(MediaType.APPLICATION_JSON, DefaultNotificationPublishers.SLACK.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.SLACK.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.SLACK.isPublishScheduled()); } @Test @@ -54,6 +56,7 @@ public void testMsTeams() { Assert.assertEquals("/templates/notification/publisher/msteams.peb", DefaultNotificationPublishers.MS_TEAMS.getPublisherTemplateFile()); Assert.assertEquals(MediaType.APPLICATION_JSON, DefaultNotificationPublishers.MS_TEAMS.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.MS_TEAMS.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.MS_TEAMS.isPublishScheduled()); } @Test @@ -64,6 +67,7 @@ public void testMattermost() { Assert.assertEquals("/templates/notification/publisher/mattermost.peb", DefaultNotificationPublishers.MATTERMOST.getPublisherTemplateFile()); Assert.assertEquals(MediaType.APPLICATION_JSON, DefaultNotificationPublishers.MATTERMOST.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.MATTERMOST.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.MATTERMOST.isPublishScheduled()); } @Test @@ -74,6 +78,18 @@ public void testEmail() { Assert.assertEquals("/templates/notification/publisher/email.peb", DefaultNotificationPublishers.EMAIL.getPublisherTemplateFile()); Assert.assertEquals("text/plain; charset=utf-8", DefaultNotificationPublishers.EMAIL.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.EMAIL.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.EMAIL.isPublishScheduled()); + } + + @Test + public void testScheduledEmail() { + Assert.assertEquals("Scheduled Email", DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + Assert.assertEquals("Sends summarized notifications to an email address in a defined schedule", DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherDescription()); + Assert.assertEquals(SendMailPublisher.class, DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherClass()); + Assert.assertEquals("/templates/notification/publisher/scheduled_email.peb", DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherTemplateFile()); + Assert.assertEquals(MediaType.TEXT_PLAIN, DefaultNotificationPublishers.SCHEDULED_EMAIL.getTemplateMimeType()); + Assert.assertTrue(DefaultNotificationPublishers.SCHEDULED_EMAIL.isDefaultPublisher()); + Assert.assertTrue(DefaultNotificationPublishers.SCHEDULED_EMAIL.isPublishScheduled()); } @Test @@ -84,6 +100,7 @@ public void testConsole() { Assert.assertEquals("/templates/notification/publisher/console.peb", DefaultNotificationPublishers.CONSOLE.getPublisherTemplateFile()); Assert.assertEquals(MediaType.TEXT_PLAIN, DefaultNotificationPublishers.CONSOLE.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.CONSOLE.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.CONSOLE.isPublishScheduled()); } @Test @@ -94,6 +111,7 @@ public void testWebhook() { Assert.assertEquals("/templates/notification/publisher/webhook.peb", DefaultNotificationPublishers.WEBHOOK.getPublisherTemplateFile()); Assert.assertEquals(MediaType.APPLICATION_JSON, DefaultNotificationPublishers.WEBHOOK.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.WEBHOOK.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.WEBHOOK.isPublishScheduled()); } @Test @@ -104,5 +122,6 @@ public void testJira() { Assert.assertEquals("/templates/notification/publisher/jira.peb", DefaultNotificationPublishers.JIRA.getPublisherTemplateFile()); Assert.assertEquals(MediaType.APPLICATION_JSON, DefaultNotificationPublishers.JIRA.getTemplateMimeType()); Assert.assertTrue(DefaultNotificationPublishers.JIRA.isDefaultPublisher()); + Assert.assertFalse(DefaultNotificationPublishers.JIRA.isPublishScheduled()); } } diff --git a/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java b/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java new file mode 100644 index 0000000000..438561be08 --- /dev/null +++ b/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java @@ -0,0 +1,80 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification.vo; + +import java.util.LinkedHashMap; +import java.util.List; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.junit.Assert; +import org.junit.Test; + +public class ScheduledNewVulnerabilitiesIdentifiedTest { + @Test + public void testVo() { + Vulnerability critVuln = new Vulnerability(); + critVuln.setTitle("Critical Vulnerability"); + critVuln.setSeverity(Severity.CRITICAL); + Vulnerability highVuln = new Vulnerability(); + highVuln.setTitle("High Vulnerability"); + highVuln.setSeverity(Severity.HIGH); + Vulnerability mediumVuln = new Vulnerability(); + mediumVuln.setTitle("Medium Vulnerability"); + mediumVuln.setSeverity(Severity.MEDIUM); + Vulnerability lowVuln = new Vulnerability(); + lowVuln.setTitle("Low Vulnerability"); + lowVuln.setSeverity(Severity.LOW); + Vulnerability infoVuln = new Vulnerability(); + infoVuln.setTitle("Info Vulnerability"); + infoVuln.setSeverity(Severity.INFO); + + Project project1 = new Project(); + var project1Vulns = List.of(critVuln, highVuln, infoVuln); + var projectVulnerabilitiesMap = new LinkedHashMap>(); + projectVulnerabilitiesMap.put(project1, project1Vulns); + Project project2 = new Project(); + var project2Vulns = List.of(mediumVuln, lowVuln); + projectVulnerabilitiesMap.put(project2, project2Vulns); + + ScheduledNewVulnerabilitiesIdentified vo = new ScheduledNewVulnerabilitiesIdentified(projectVulnerabilitiesMap); + + Assert.assertEquals(2, vo.getNewProjectVulnerabilities().size()); + Assert.assertEquals(project1Vulns, vo.getNewProjectVulnerabilities().get(project1)); + Assert.assertEquals(project2Vulns, vo.getNewProjectVulnerabilities().get(project2)); + Assert.assertEquals(2, vo.getNewProjectVulnerabilitiesBySeverity().size()); + var projectVulnerabilitiesBySeverityMap = vo.getNewProjectVulnerabilitiesBySeverity(); + Assert.assertEquals(3, projectVulnerabilitiesBySeverityMap.get(project1).size()); + Assert.assertEquals(2, projectVulnerabilitiesBySeverityMap.get(project2).size()); + Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.CRITICAL).size()); + Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.HIGH).size()); + Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.INFO).size()); + Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.MEDIUM).size()); + Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.LOW).size()); + Assert.assertEquals(5, vo.getNewVulnerabilitiesTotal().size()); + Assert.assertEquals(List.of(critVuln, highVuln, infoVuln, mediumVuln, lowVuln), vo.getNewVulnerabilitiesTotal()); + Assert.assertEquals(5, vo.getNewVulnerabilitiesTotalBySeverity().size()); + var vulnerabilitiesBySeverity = vo.getNewVulnerabilitiesTotalBySeverity(); + Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.CRITICAL).size()); + Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.HIGH).size()); + Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.MEDIUM).size()); + Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.LOW).size()); + Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.INFO).size()); + } +} diff --git a/src/test/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentifiedTest.java b/src/test/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentifiedTest.java new file mode 100644 index 0000000000..e46a90d64f --- /dev/null +++ b/src/test/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentifiedTest.java @@ -0,0 +1,51 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification.vo; + +import java.util.LinkedHashMap; +import java.util.List; + +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; +import org.junit.Assert; +import org.junit.Test; + +public class ScheduledPolicyViolationsIdentifiedTest { + @Test + public void testVo() { + Project project1 = new Project(); + PolicyViolation policyViolation1 = new PolicyViolation(); + PolicyViolation policyViolation2 = new PolicyViolation(); + Project project2 = new Project(); + PolicyViolation policyViolation3 = new PolicyViolation(); + PolicyViolation policyViolation4 = new PolicyViolation(); + PolicyViolation policyViolation5 = new PolicyViolation(); + var projectPolicyViolations = new LinkedHashMap>(); + projectPolicyViolations.put(project1, List.of(policyViolation1, policyViolation2)); + projectPolicyViolations.put(project2, List.of(policyViolation3, policyViolation4, policyViolation5)); + + ScheduledPolicyViolationsIdentified vo = new ScheduledPolicyViolationsIdentified(projectPolicyViolations); + + Assert.assertEquals(2, vo.getNewProjectPolicyViolations().size()); + Assert.assertEquals(List.of(policyViolation1, policyViolation2), vo.getNewProjectPolicyViolations().get(project1)); + Assert.assertEquals(List.of(policyViolation3, policyViolation4, policyViolation5), vo.getNewProjectPolicyViolations().get(project2)); + Assert.assertEquals(5, vo.getNewPolicyViolationsTotal().size()); + Assert.assertEquals(List.of(policyViolation1, policyViolation2, policyViolation3, policyViolation4, policyViolation5), vo.getNewPolicyViolationsTotal()); + } +} diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java index 9906483b4a..194f9b949a 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java @@ -545,6 +545,7 @@ public void addTeamToRuleWithCustomEmailPublisherTest() { "publisherClass": "org.dependencytrack.notification.publisher.SendMailPublisher", "templateMimeType": "templateMimeType", "defaultPublisher": false, + "publishScheduled": false, "uuid": "${json-unit.matches:publisherUuid}" }, "uuid": "${json-unit.matches:ruleUuid}" diff --git a/src/test/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResourceTest.java new file mode 100644 index 0000000000..6c8378d2f7 --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/ScheduledNotificationRuleResourceTest.java @@ -0,0 +1,570 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonValue; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ScheduledNotificationRule; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; +import org.dependencytrack.notification.publisher.SendMailPublisher; +import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import alpine.common.util.UuidUtil; +import alpine.model.Team; +import alpine.notification.NotificationLevel; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class ScheduledNotificationRuleResourceTest extends ResourceTest { + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer( + new ResourceConfig(ScheduledNotificationRuleResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class))) + .build(); + } + + @Before + public void before() throws Exception { + super.before(); + DefaultObjectGenerator generator = new DefaultObjectGenerator(); + generator.contextInitialized(null); + } + + @Test + public void getAllScheduledNotificationRulesTest(){ + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule r1 = qm.createScheduledNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + qm.createScheduledNotificationRule("Rule 2", NotificationScope.PORTFOLIO, NotificationLevel.WARNING, publisher); + qm.createScheduledNotificationRule("Rule 3", NotificationScope.SYSTEM, NotificationLevel.ERROR, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(String.valueOf(3), response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(3, json.size()); + Assert.assertEquals("Rule 1", json.getJsonObject(0).getString("name")); + Assert.assertTrue(json.getJsonObject(0).getBoolean("enabled")); + Assert.assertEquals("PORTFOLIO", json.getJsonObject(0).getString("scope")); + Assert.assertEquals("INFORMATIONAL", json.getJsonObject(0).getString("notificationLevel")); + Assert.assertEquals(0, json.getJsonObject(0).getJsonArray("notifyOn").size()); + Assert.assertTrue(UuidUtil.isValidUUID(json.getJsonObject(0).getString("uuid"))); + Assert.assertEquals("Slack", json.getJsonObject(0).getJsonObject("publisher").getString("name")); + Assert.assertFalse(json.getJsonObject(0).getBoolean("logSuccessfulPublish")); + Assert.assertEquals(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_EXPRESSION.getDefaultPropertyValue(), json.getJsonObject(0).getString("cronConfig")); + JsonValue jsonValue = json.getJsonObject(0).get("lastExecutionTime"); + try { + Assert.assertEquals(r1.getLastExecutionTime(), jsonMapper.readValue(jsonValue.toString(), ZonedDateTime.class).withZoneSameInstant(r1.getLastExecutionTime().getZone())); + } catch (JsonProcessingException e) { + Assert.fail(); + } + Assert.assertFalse(json.getJsonObject(0).getBoolean("publishOnlyWithUpdates")); + } + + @Test + public void createScheduledNotificationRuleTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setName("Example Rule"); + rule.setNotificationLevel(NotificationLevel.WARNING); + rule.setScope(NotificationScope.SYSTEM); + rule.setPublisher(publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Example Rule", json.getString("name")); + Assert.assertTrue(json.getBoolean("enabled")); + Assert.assertTrue(json.getBoolean("notifyChildren")); + Assert.assertFalse(json.getBoolean("logSuccessfulPublish")); + Assert.assertEquals("SYSTEM", json.getString("scope")); + Assert.assertEquals("WARNING", json.getString("notificationLevel")); + Assert.assertEquals(0, json.getJsonArray("notifyOn").size()); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid"))); + Assert.assertEquals("Slack", json.getJsonObject("publisher").getString("name")); + Assert.assertEquals(ConfigPropertyConstants.NOTIFICATION_CRON_DEFAULT_EXPRESSION.getDefaultPropertyValue(), json.getString("cronConfig")); + Assert.assertFalse(json.getBoolean("publishOnlyWithUpdates")); + } + + @Test + public void createScheduledNotificationRuleInvalidPublisherTest() { + NotificationPublisher publisher = new NotificationPublisher(); + publisher.setUuid(UUID.randomUUID()); + ScheduledNotificationRule rule = new ScheduledNotificationRule(); + rule.setName("Example Rule"); + rule.setEnabled(true); + rule.setPublisherConfig("{ \"foo\": \"bar\" }"); + rule.setMessage("A message"); + rule.setNotificationLevel(NotificationLevel.WARNING); + rule.setScope(NotificationScope.SYSTEM); + rule.setPublisher(publisher); + rule.setCronConfig("0 * * * *"); + rule.setLastExecutionTime(ZonedDateTime.now()); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The UUID of the notification publisher could not be found.", body); + } + + @Test + public void updateScheduledNotificationRuleTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setName("Example Rule"); + rule.setNotifyOn(Collections.singleton(NotificationGroup.NEW_VULNERABILITY)); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Example Rule", json.getString("name")); + Assert.assertTrue(json.getBoolean("enabled")); + Assert.assertEquals("PORTFOLIO", json.getString("scope")); + Assert.assertEquals("INFORMATIONAL", json.getString("notificationLevel")); + Assert.assertEquals("NEW_VULNERABILITY", json.getJsonArray("notifyOn").getString(0)); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid"))); + Assert.assertEquals("Slack", json.getJsonObject("publisher").getString("name")); + } + + @Test + public void updateScheduledNotificationRuleInvalidTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule = qm.detach(ScheduledNotificationRule.class, rule.getId()); + rule.setUuid(UUID.randomUUID()); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The UUID of the scheduled notification rule could not be found.", body); + } + + @Test + public void deleteScheduledNotificationRuleTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setName("Example Rule"); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE).request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK + .method("DELETE", Entity.entity(rule, MediaType.APPLICATION_JSON)); // HACK + // Hack: Workaround to https://github.com/eclipse-ee4j/jersey/issues/3798 + Assert.assertEquals(204, response.getStatus(), 0); + } + + @Test + public void addProjectToRuleTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Example Rule", json.getString("name")); + Assert.assertEquals(1, json.getJsonArray("projects").size()); + Assert.assertEquals("Acme Example", json.getJsonArray("projects").getJsonObject(0).getString("name")); + Assert.assertEquals(project.getUuid().toString(), json.getJsonArray("projects").getJsonObject(0).getString("uuid")); + } + + @Test + public void addProjectToRuleInvalidRuleTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The scheduled notification rule could not be found.", body); + } + + @Test + public void addProjectToRuleInvalidScopeTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.SYSTEM, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(406, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("Project limitations are only possible on scheduled notification rules with PORTFOLIO scope.", body); + } + + @Test + public void addProjectToRuleInvalidProjectTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The project could not be found.", body); + } + + @Test + public void addProjectToRuleDuplicateProjectTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + List projects = new ArrayList<>(); + projects.add(project); + rule.setProjects(projects); + qm.persist(rule); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(304, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void removeProjectFromRuleTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + List projects = new ArrayList<>(); + projects.add(project); + rule.setProjects(projects); + qm.persist(rule); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void removeProjectFromRuleInvalidRuleTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The scheduled notification rule could not be found.", body); + } + + @Test + public void removeProjectFromRuleInvalidScopeTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.SYSTEM, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(406, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("Project limitations are only possible on scheduled notification rules with PORTFOLIO scope.", body); + } + + @Test + public void removeProjectFromRuleInvalidProjectTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + UUID.randomUUID().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The project could not be found.", body); + } + + @Test + public void removeProjectFromRuleDuplicateProjectTest() { + Project project = qm.createProject("Acme Example", null, null, null, null, null, true, false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(304, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void addTeamToRuleTest(){ + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Example Rule", json.getString("name")); + Assert.assertEquals(1, json.getJsonArray("teams").size()); + Assert.assertEquals("Team Example", json.getJsonArray("teams").getJsonObject(0).getString("name")); + Assert.assertEquals(team.getUuid().toString(), json.getJsonArray("teams").getJsonObject(0).getString("uuid")); + } + + @Test + public void addTeamToRuleInvalidRuleTest(){ + Team team = qm.createTeam("Team Example", false); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The scheduled notification rule could not be found.", body); + } + + @Test + public void addTeamToRuleInvalidTeamTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The team could not be found.", body); + } + + @Test + public void addTeamToRuleDuplicateTeamTest() { + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + List teams = new ArrayList<>(); + teams.add(team); + rule.setTeams(teams); + qm.persist(rule); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(304, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void addTeamToRuleInvalidPublisherTest(){ + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + Assert.assertEquals(406, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("Team subscriptions are only possible on scheduled notification rules with EMAIL publisher.", body); + } + + @Test + public void addTeamToRuleWithCustomEmailPublisherTest() { + final Team team = qm.createTeam("Team Example", false); + final NotificationPublisher publisher = qm.createNotificationPublisher("foo", "description", SendMailPublisher.class, "template", "templateMimeType", false, true); + final ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + final ZonedDateTime testTime = ZonedDateTime.parse("2024-05-31T13:24:46Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + rule.setLastExecutionTime(testTime); + final Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid() + "/team/" + team.getUuid()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .withMatcher("publisherUuid", equalTo(publisher.getUuid().toString())) + .withMatcher("ruleUuid", equalTo(rule.getUuid().toString())) + .withMatcher("teamUuid", equalTo(team.getUuid().toString())) + .withMatcher("cronConfig", equalTo(rule.getCronConfig())) + .isEqualTo(""" + { + "name": "Example Rule", + "enabled": true, + "notifyChildren": true, + "logSuccessfulPublish": false, + "scope": "PORTFOLIO", + "notificationLevel": "INFORMATIONAL", + "projects": [], + "teams": [ + { + "uuid": "${json-unit.matches:teamUuid}", + "name": "Team Example", + "permissions": [] + } + ], + "notifyOn": [], + "publisher": { + "name": "foo", + "description": "description", + "publisherClass": "org.dependencytrack.notification.publisher.SendMailPublisher", + "templateMimeType": "templateMimeType", + "defaultPublisher": false, + "publishScheduled": true, + "uuid": "${json-unit.matches:publisherUuid}" + }, + "uuid": "${json-unit.matches:ruleUuid}", + "cronConfig": "${json-unit.matches:cronConfig}", + "lastExecutionTime": "2024-05-31T13:24:46Z", + "publishOnlyWithUpdates": false + } + """); + } + + @Test + public void removeTeamFromRuleTest() { + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + List teams = new ArrayList<>(); + teams.add(team); + rule.setTeams(teams); + qm.persist(rule); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void removeTeamFromRuleInvalidRuleTest() { + Team team = qm.createTeam("Team Example", false); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + UUID.randomUUID().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The scheduled notification rule could not be found.", body); + } + + @Test + public void removeTeamFromRuleInvalidTeamTest() { + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + UUID.randomUUID().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The team could not be found.", body); + } + + @Test + public void removeTeamFromRuleDuplicateTeamTest() { + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(304, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + } + + @Test + public void removeTeamToRuleInvalidPublisherTest(){ + Team team = qm.createTeam("Team Example", false); + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/team/" + team.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + Assert.assertEquals(406, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("Team subscriptions are only possible on scheduled notification rules with EMAIL publisher.", body); + } + + @Test + public void executeScheduledNotificationRuleNowTest(){ + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/execute").request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Example Rule", json.getString("name")); + Assert.assertEquals(rule.getUuid().toString(), json.getString("uuid")); + } + + @Test + public void executeScheduledNotificationRuleNowInvalidRuleTest(){ + NotificationPublisher publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SCHEDULED_EMAIL.getPublisherName()); + ScheduledNotificationRule rule = qm.createScheduledNotificationRule("Example Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + // detach the rule to uncouple from database, else setUuid(...) will update the persistent entry and the request will be valid with http code 200 + rule = qm.detach(ScheduledNotificationRule.class, rule.getId()); + rule.setUuid(UUID.randomUUID()); + Response response = target(V1_SCHEDULED_NOTIFICATION_RULE + "/execute").request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(rule, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The UUID of the scheduled notification rule could not be found.", body); + } +} From d6bec6b8e318de728911e8ad0db36da325a44f9b Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 5 Jun 2024 13:23:18 +0200 Subject: [PATCH 47/98] added new data models to match new provided pebble template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/scheduled/DetailInfo.java | 93 +++++++++++++ .../model/scheduled/Details.java | 45 +++++++ .../model/scheduled/Overview.java | 91 +++++++++++++ .../model/scheduled/Summary.java | 45 +++++++ .../model/scheduled/SummaryInfo.java | 62 +++++++++ .../publisher/scheduled_email_summary.peb | 125 ++++++++++++++++++ 6 files changed, 461 insertions(+) create mode 100644 src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java create mode 100644 src/main/java/org/dependencytrack/model/scheduled/Details.java create mode 100644 src/main/java/org/dependencytrack/model/scheduled/Overview.java create mode 100644 src/main/java/org/dependencytrack/model/scheduled/Summary.java create mode 100644 src/main/java/org/dependencytrack/model/scheduled/SummaryInfo.java create mode 100644 src/main/resources/templates/notification/publisher/scheduled_email_summary.peb diff --git a/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java b/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java new file mode 100644 index 0000000000..159f23c62d --- /dev/null +++ b/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java @@ -0,0 +1,93 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model.scheduled; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; +import org.dependencytrack.model.Finding; + +import alpine.common.logging.Logger; + +public class DetailInfo { + private static final Logger LOGGER = Logger.getLogger(DetailInfo.class); + + private final String componentName; + private final String componentVersion; + private final String componentGroup; + private final String vulnerabilityId; + private final String vulnerabilitySeverity; + private final String analyzer; + private Date attributedOn; + private final String analysisState; + private final Boolean suppressed; + + public DetailInfo(Finding finding) { + this.componentName = (String) finding.getComponent().get("name"); + this.componentVersion = (String) finding.getComponent().get("version"); + this.componentGroup = (String) finding.getComponent().get("group"); + this.vulnerabilityId = (String) finding.getVulnerability().get("vulnId"); + this.vulnerabilitySeverity = (String) finding.getVulnerability().get("severity"); + this.analyzer = (String) finding.getAttribution().get("analyzerIdentity"); + try { + this.attributedOn = DateFormat.getDateTimeInstance().parse((String) finding.getAttribution().get("attributedOn")); + } catch (ParseException e) { + this.attributedOn = null; + LOGGER.error("An error occurred while parsing the attributedOn date for component" + this.componentName); + } + this.analysisState = (String) finding.getAnalysis().get("state"); + this.suppressed = (Boolean) finding.getAnalysis().get("isSuppressed"); + } + + public String getComponentName() { + return componentName; + } + + public String getComponentVersion() { + return componentVersion; + } + + public String getComponentGroup() { + return componentGroup; + } + + public String getVulnerabilityId() { + return vulnerabilityId; + } + + public String getVulnerabilitySeverity() { + return vulnerabilitySeverity; + } + + public String getAnalyzer() { + return analyzer; + } + + public Date getAttributedOn() { + return attributedOn; + } + + public String getAnalysisState() { + return analysisState; + } + + public Boolean getSuppressed() { + return suppressed; + } +} diff --git a/src/main/java/org/dependencytrack/model/scheduled/Details.java b/src/main/java/org/dependencytrack/model/scheduled/Details.java new file mode 100644 index 0000000000..7ca7b228b6 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/scheduled/Details.java @@ -0,0 +1,45 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model.scheduled; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.QueryManager; + +public final class Details { + private final Map> affectedProjectFindings = new TreeMap<>(); + + public Details(final List affectedProjects, ZonedDateTime lastExecution) { + try (var qm = new QueryManager()) { + for (Project project : affectedProjects) { + var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + affectedProjectFindings.put(project, findings.stream().map(f -> new DetailInfo(f)).toList()); + } + } + } + + public Map> getAffectedProjectFindings() { + return affectedProjectFindings; + } +} diff --git a/src/main/java/org/dependencytrack/model/scheduled/Overview.java b/src/main/java/org/dependencytrack/model/scheduled/Overview.java new file mode 100644 index 0000000000..f5a65ceaf2 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/scheduled/Overview.java @@ -0,0 +1,91 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model.scheduled; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Finding; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.QueryManager; + +public final class Overview { + private final Integer affectedProjectsCount; + private final Integer newVulnerabilitiesCount; + private final Map newVulnerabilitiesBySeverity = new EnumMap<>(Severity.class); + private final Integer affectedComponentsCount; + private final Integer suppressedNewVulnerabilitiesCount; + + public Overview(final List affectedProjects, ZonedDateTime lastExecution) { + var componentCache = new HashSet(); + var vulnerabilityCache = new HashSet(); + var suppressedVulnerabilityCache = new HashSet(); + + try (var qm = new QueryManager()) { + for (Project project : affectedProjects) { + var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + for (Finding finding : findings) { + Component component = qm.getObjectByUuid(Component.class, (String) finding.getComponent().get("uuid")); + componentCache.add(component); + + Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid")); + if (finding.getAnalysis().get("istSuppressed") instanceof Boolean suppressed) { + if (suppressed) { + suppressedVulnerabilityCache.add(vulnerability); + } else { + vulnerabilityCache.add(vulnerability); + newVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); + } + } + } + } + } + + affectedProjectsCount = affectedProjects.size(); + newVulnerabilitiesCount = vulnerabilityCache.size(); + affectedComponentsCount = componentCache.size(); + suppressedNewVulnerabilitiesCount = suppressedVulnerabilityCache.size(); + } + + public Integer getAffectedProjectsCount() { + return affectedProjectsCount; + } + + public Integer getNewVulnerabilitiesCount() { + return newVulnerabilitiesCount; + } + + public Map getNewVulnerabilitiesBySeverity() { + return newVulnerabilitiesBySeverity; + } + + public Integer getAffectedComponentsCount() { + return affectedComponentsCount; + } + + public Integer getSuppressedNewVulnerabilitiesCount() { + return suppressedNewVulnerabilitiesCount; + } +} diff --git a/src/main/java/org/dependencytrack/model/scheduled/Summary.java b/src/main/java/org/dependencytrack/model/scheduled/Summary.java new file mode 100644 index 0000000000..1e4fd64c80 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/scheduled/Summary.java @@ -0,0 +1,45 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model.scheduled; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.QueryManager; + +public final class Summary { + private final Map affectedProjectSummaries = new TreeMap<>(); + + public Summary(final List affectedProjects, ZonedDateTime lastExecution) { + try (var qm = new QueryManager()) { + for (Project project : affectedProjects) { + var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + affectedProjectSummaries.put(project, new SummaryInfo(findings)); + } + } + } + + public Map getAffectedProjectSummaries() { + return affectedProjectSummaries; + } +} diff --git a/src/main/java/org/dependencytrack/model/scheduled/SummaryInfo.java b/src/main/java/org/dependencytrack/model/scheduled/SummaryInfo.java new file mode 100644 index 0000000000..51319b2376 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/scheduled/SummaryInfo.java @@ -0,0 +1,62 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model.scheduled; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import org.dependencytrack.model.Finding; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.QueryManager; + +public final class SummaryInfo { + private final Map newVulnerabilitiesBySeverity = new EnumMap<>(Severity.class); + private final Map totalProjectVulnerabilitiesBySeverity = new EnumMap<>(Severity.class); + private final Map suppressedNewVulnerabilitiesBySeverity = new EnumMap<>(Severity.class); + + public SummaryInfo(final List findings) { + try (var qm = new QueryManager()) { + for (Finding finding : findings) { + Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid")); + if (finding.getAnalysis().get("isSuppressed") instanceof Boolean suppressed) { + if (suppressed) { + suppressedNewVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); + } else { + newVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); + } + totalProjectVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); + } + } + } + } + + public Map getNewVulnerabilitiesBySeverity() { + return newVulnerabilitiesBySeverity; + } + + public Map getTotalProjectVulnerabilitiesBySeverity() { + return totalProjectVulnerabilitiesBySeverity; + } + + public Map getSuppressedNewVulnerabilitiesBySeverity() { + return suppressedNewVulnerabilitiesBySeverity; + } +} diff --git a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb new file mode 100644 index 0000000000..4e03e83960 --- /dev/null +++ b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb @@ -0,0 +1,125 @@ + + + + {{ notification.title }} + ------------- + {{ notification.description }} + + + +

Overview

+ + + + + + + + + + {% if subject.overview.newVulnerabilitiesBySeverity|length > 0 %}{% for entry in subject.overview.newVulnerabilitiesBySeverity %} + + + + {% endfor %}{% endif %} + + + + + + + + + +
Projects included in this rule{{ subject.overview.affectedProjectsCount }}
Total new vulnerabilities{{ subject.overview.newVulnerabilitiesCount }}
New vulnerabilities ({{ entry.key }}){{ entry.value }}
Components affected by new vulnerabilities{{ subject.overview.affectedComponentsCount }}
Suppressed new vulnerabilities (not included above){{ subject.overview.suppressedNewVulnerabilitiesCount }}
+ +

Summary per project in this rule

+ + + + + + + + + + + {% for entry in subject.summary.affectedProjectSummaries %} + + + + + + + {% endfor %} + +
Project NameVersionNew VulnerabilitiesAll VulnerabilitiesSuppressed Vulnerabilities
+ + {{ entry.key.name }} + + {{ entry.key.version }} + {% for newVulnEntry in entry.value.newVulnerabilitiesBySeverity %} + {{ newVulnEntry.key }}: {{ newVulnEntry.value }}
+ {% endfor %} +
+ {% for projVulnEntry in entry.value.totalProjectVulnerabilitiesBySeverity %} + {{ projVulnEntry.key }}: {{ projVulnEntry.value }}
+ {% endfor %} +
+ {% for supprVulnEntry in entry.value.suppressedNewVulnerabilitiesBySeverity %} + {{ supprVulnEntry.key }}: {{ supprVulnEntry.value }}
+ {% endfor %} +
+ +

New vulnerabilities per project

+ + {% if subject.details.affectedProjectFindings|length > 0 %}{% for affProjEntry in subject.details.affectedProjectFindings %} +

Project "{{ affProjEntry.key.name }}"" [Version: {{ affProjEntry.key.version }}]

+ + + + + + + + + + + + + + + {% for finding in findings %} + + + + + + + + + + + {% endfor %} + +
ComponentVersionGroupVulnerabilitySeverityAnalyzerAttributed OnAnalysisSuppressed
+ + {{ affProjEntry.value.componentName }} + + {{ affProjEntry.value.componentVersion }} + + {{ affProjEntry.value.componentGroup }} + + + + {{ affProjEntry.value.vulnerabilityId }} + + {{ affProjEntry.value.vulnerabilitySeverity }}{{ affProjEntry.value.analyzer }}{{ affProjEntry.value.attributedOn }}{{ affProjEntry.value.analysisState }}{{ affProjEntry.value.suppressed }}
+ {% endfor %}{% endif %} + +
+

+ {{ timestamp }} +

+
+ From 7e9f5c8bc46dccf383c81582bed9ed741d6786a0 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 5 Jun 2024 13:24:38 +0200 Subject: [PATCH 48/98] changed depending classes to use new template models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/model/Finding.java | 46 ++++++ .../model/ScheduledNotificationRule.java | 2 +- .../DefaultNotificationPublishers.java | 2 +- .../publisher/PublishContext.java | 3 +- ...ScheduledNewVulnerabilitiesIdentified.java | 50 +++---- .../persistence/FindingsQueryManager.java | 102 +++++--------- .../persistence/QueryManager.java | 4 + .../tasks/SendScheduledNotificationTask.java | 48 ++++--- .../util/NotificationUtil.java | 132 +++++++++++------- ...duledNewVulnerabilitiesIdentifiedTest.java | 44 +++--- 10 files changed, 237 insertions(+), 196 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 13867227b2..e986a120b1 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -104,6 +104,52 @@ public class Finding implements Serializable { AND (:includeSuppressed = :true OR "ANALYSIS"."SUPPRESSED" IS NULL OR "ANALYSIS"."SUPPRESSED" = :false) """; + // language=SQL + public static final String QUERY_SINCE_ATTRIBUTION = """ + SELECT "COMPONENT"."UUID" + , "COMPONENT"."NAME" + , "COMPONENT"."GROUP" + , "COMPONENT"."VERSION" + , "COMPONENT"."PURL" + , "COMPONENT"."CPE" + , "VULNERABILITY"."UUID" + , "VULNERABILITY"."SOURCE" + , "VULNERABILITY"."VULNID" + , "VULNERABILITY"."TITLE" + , "VULNERABILITY"."SUBTITLE" + , "VULNERABILITY"."DESCRIPTION" + , "VULNERABILITY"."RECOMMENDATION" + , "VULNERABILITY"."SEVERITY" + , "VULNERABILITY"."CVSSV2BASESCORE" + , "VULNERABILITY"."CVSSV3BASESCORE" + , "VULNERABILITY"."OWASPRRLIKELIHOODSCORE" + , "VULNERABILITY"."OWASPRRTECHNICALIMPACTSCORE" + , "VULNERABILITY"."OWASPRRBUSINESSIMPACTSCORE" + , "VULNERABILITY"."EPSSSCORE" + , "VULNERABILITY"."EPSSPERCENTILE" + , "VULNERABILITY"."CWES" + , "FINDINGATTRIBUTION"."ANALYZERIDENTITY" + , "FINDINGATTRIBUTION"."ATTRIBUTED_ON" + , "FINDINGATTRIBUTION"."ALT_ID" + , "FINDINGATTRIBUTION"."REFERENCE_URL" + , "ANALYSIS"."STATE" + , "ANALYSIS"."SUPPRESSED" + FROM "COMPONENT" + INNER JOIN "COMPONENTS_VULNERABILITIES" + ON "COMPONENT"."ID" = "COMPONENTS_VULNERABILITIES"."COMPONENT_ID" + INNER JOIN "VULNERABILITY" + ON "COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" = "VULNERABILITY"."ID" + INNER JOIN "FINDINGATTRIBUTION" + ON "COMPONENT"."ID" = "FINDINGATTRIBUTION"."COMPONENT_ID" + AND "VULNERABILITY"."ID" = "FINDINGATTRIBUTION"."VULNERABILITY_ID" + LEFT JOIN "ANALYSIS" + ON "COMPONENT"."ID" = "ANALYSIS"."COMPONENT_ID" + AND "VULNERABILITY"."ID" = "ANALYSIS"."VULNERABILITY_ID" + AND "COMPONENT"."PROJECT_ID" = "ANALYSIS"."PROJECT_ID" + WHERE "COMPONENT"."PROJECT_ID" = ? + AND "FINDINGATTRIBUTION"."ATTRIBUTED_ON" BETWEEN ? AND ? + """; + // language=SQL public static final String QUERY_ALL_FINDINGS = """ SELECT "COMPONENT"."UUID" diff --git a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java index 5a1fce9601..7d9535c2e8 100644 --- a/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java +++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java @@ -321,7 +321,7 @@ public ZonedDateTime getLastExecutionTime() { if (lastExecutionTime == null) { return ZonedDateTime.now(ZoneOffset.UTC); } - return lastExecutionTime; + return lastExecutionTime.withZoneSameInstant(ZoneOffset.UTC); } public void setLastExecutionTime(ZonedDateTime lastExecutionTime) { diff --git a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java index 8e22a3141b..3df8c7b635 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java +++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java @@ -26,7 +26,7 @@ public enum DefaultNotificationPublishers { MS_TEAMS("Microsoft Teams", "Publishes notifications to a Microsoft Teams channel", MsTeamsPublisher.class, "/templates/notification/publisher/msteams.peb", MediaType.APPLICATION_JSON, true), MATTERMOST("Mattermost", "Publishes notifications to a Mattermost channel", MattermostPublisher.class, "/templates/notification/publisher/mattermost.peb", MediaType.APPLICATION_JSON, true), EMAIL("Email", "Sends notifications to an email address", SendMailPublisher.class, "/templates/notification/publisher/email.peb", MediaType.TEXT_PLAIN, true), - SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email.peb", MediaType.TEXT_PLAIN, true, true), + SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email_summary.peb", MediaType.TEXT_HTML, true, true), CONSOLE("Console", "Displays notifications on the system console", ConsolePublisher.class, "/templates/notification/publisher/console.peb", MediaType.TEXT_PLAIN, true), // SCHEDULED_CONSOLE("Scheduled Console", "Displays summarized notifications on the system console in a defined schedule", ConsolePublisher.class, "/templates/notification/publisher/scheduled_console.peb", MediaType.TEXT_PLAIN, true, true), WEBHOOK("Outbound Webhook", "Publishes notifications to a configurable endpoint", WebhookPublisher.class, "/templates/notification/publisher/webhook.peb", MediaType.APPLICATION_JSON, true), diff --git a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java index 208b9ff5ad..d01b69a255 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java +++ b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java @@ -105,8 +105,7 @@ public static PublishContext from(final Notification notification) { } else if (notification.getSubject() instanceof final VexConsumedOrProcessed subject) { notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); } else if (notification.getSubject() instanceof final ScheduledNewVulnerabilitiesIdentified subject) { - notificationSubjects.put(SUBJECT_PROJECTS, subject.getNewProjectVulnerabilities().keySet().stream().map(Project::convert).toList()); - notificationSubjects.put(SUBJECT_VULNERABILITIES, subject.getNewVulnerabilitiesTotal().stream().map(Vulnerability::convert).toList()); + notificationSubjects.put(SUBJECT_PROJECTS, subject.getSummary().getAffectedProjectSummaries().keySet().stream().map(Project::convert).toList()); } else if (notification.getSubject() instanceof final ScheduledPolicyViolationsIdentified subject) { notificationSubjects.put(SUBJECT_PROJECTS, subject.getNewProjectPolicyViolations().keySet().stream().map(Project::convert).toList()); } diff --git a/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java b/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java index 6a25b94a2e..a834cc8cd5 100644 --- a/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java +++ b/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java @@ -18,46 +18,34 @@ */ package org.dependencytrack.notification.vo; -import java.util.EnumMap; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import org.dependencytrack.model.Project; -import org.dependencytrack.model.Severity; -import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.scheduled.Details; +import org.dependencytrack.model.scheduled.Overview; +import org.dependencytrack.model.scheduled.Summary; public class ScheduledNewVulnerabilitiesIdentified { - private final Map> newProjectVulnerabilities; - private final Map>> newProjectVulnerabilitiesBySeverity; - private final List newVulnerabilitiesTotal; - private final Map> newVulnerabilitiesTotalBySeverity; - - public ScheduledNewVulnerabilitiesIdentified(Map> newProjectVulnerabilities) { - this.newProjectVulnerabilities = newProjectVulnerabilities; - this.newProjectVulnerabilitiesBySeverity = newProjectVulnerabilities.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream() - .collect(Collectors.groupingBy(Vulnerability::getSeverity, () -> new EnumMap<>(Severity.class), Collectors.toList())))); - this.newVulnerabilitiesTotal = newProjectVulnerabilities.values().stream() - .flatMap(List::stream) - .collect(Collectors.toList()); - this.newVulnerabilitiesTotalBySeverity = newVulnerabilitiesTotal.stream() - .collect(Collectors.groupingBy(Vulnerability::getSeverity, () -> new EnumMap<>(Severity.class), Collectors.toList())); - } - - public Map> getNewProjectVulnerabilities() { - return newProjectVulnerabilities; + private final Overview overview; + private final Summary summary; + private final Details details; + + public ScheduledNewVulnerabilitiesIdentified(final List ruleProjects, ZonedDateTime lastExecution) { + this.overview = new Overview(ruleProjects, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + this.summary = new Summary(ruleProjects, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + this.details = new Details(ruleProjects, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); } - public Map>> getNewProjectVulnerabilitiesBySeverity() { - return newProjectVulnerabilitiesBySeverity; + public Overview getOverview() { + return overview; } - public List getNewVulnerabilitiesTotal() { - return newVulnerabilitiesTotal; + public Summary getSummary() { + return summary; } - public Map> getNewVulnerabilitiesTotalBySeverity() { - return newVulnerabilitiesTotalBySeverity; + public Details getDetails() { + return details; } } diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index d56394f2d1..2abded25f7 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -37,6 +37,9 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.Date; import java.util.List; @@ -258,81 +261,52 @@ void deleteAnalysisTrail(Project project) { */ @SuppressWarnings("unchecked") public List getFindings(Project project) { - return getFindings(project, false); + return getFindings(project, false, null); } /** * Returns a List of Finding objects for the specified project. * @param project the project to retrieve findings for * @param includeSuppressed determines if suppressed vulnerabilities should be included or not + * @param sinceAttributedOn only include findings that have been attributed on or after this datetime (optional) * @return a List of Finding objects */ @SuppressWarnings("unchecked") - public List getFindings(Project project, boolean includeSuppressed) { - final Query query = pm.newQuery(Query.SQL, Finding.QUERY); - query.setNamedParameters(Map.ofEntries( - Map.entry("projectId", project.getId()), - Map.entry("includeSuppressed", includeSuppressed), - // NB: These are required for MSSQL, apparently it doesn't have - // a native boolean type, or DataNucleus maps booleans to a type - // that doesn't have boolean semantics. Fun! - Map.entry("false", false), - Map.entry("true", true) - )); - final List queryResultRows = executeAndCloseList(query); - - final List findings = queryResultRows.stream() - .map(row -> new Finding(project.getUuid(), row)) - .toList(); - - final Map> findingsByVulnIdAndSource = findings.stream() - .collect(Collectors.groupingBy( - finding -> new VulnIdAndSource( - (String) finding.getVulnerability().get("vulnId"), - (String) finding.getVulnerability().get("source") - ) - )); - final Map> aliasesByVulnIdAndSource = - getVulnerabilityAliases(findingsByVulnIdAndSource.keySet()); - for (final VulnIdAndSource vulnIdAndSource : findingsByVulnIdAndSource.keySet()) { - final List affectedFindings = findingsByVulnIdAndSource.get(vulnIdAndSource); - final List aliases = aliasesByVulnIdAndSource.getOrDefault(vulnIdAndSource, Collections.emptyList()); - - for (final Finding finding : affectedFindings) { - finding.addVulnerabilityAliases(aliases); - } + public List getFindings(Project project, boolean includeSuppressed, ZonedDateTime sinceAttributedOn) { + final Query query; + if (sinceAttributedOn != null) { + query = pm.newQuery(Query.SQL, Finding.QUERY); + query.setParameters(project.getId()); + } else { + query = pm.newQuery(Query.SQL, Finding.QUERY_SINCE_ATTRIBUTION); + query.setParameters(project.getId(), sinceAttributedOn, ZonedDateTime.now(ZoneOffset.UTC)); } - - final Map> findingsByMetaComponentSearch = findings.stream() - .filter(finding -> finding.getComponent().get("purl") != null) - .map(finding -> { - final PackageURL purl = PurlUtil.silentPurl((String) finding.getComponent().get("purl")); - if (purl == null) { - return null; + final List list = query.executeList(); + final List findings = new ArrayList<>(); + for (final Object[] o: list) { + final Finding finding = new Finding(project.getUuid(), o); + final Component component = getObjectByUuid(Component.class, (String)finding.getComponent().get("uuid")); + final Vulnerability vulnerability = getObjectByUuid(Vulnerability.class, (String)finding.getVulnerability().get("uuid")); + final Analysis analysis = getAnalysis(component, vulnerability); + final List aliases = detach(getVulnerabilityAliases(vulnerability)); + finding.addVulnerabilityAliases(aliases); + if (includeSuppressed || analysis == null || !analysis.isSuppressed()) { // do not add globally suppressed findings + // These are CLOB fields. Handle these here so that database-specific deserialization doesn't need to be performed (in Finding) + finding.getVulnerability().put("description", vulnerability.getDescription()); + finding.getVulnerability().put("recommendation", vulnerability.getRecommendation()); + final PackageURL purl = component.getPurl(); + if (purl != null) { + final RepositoryType type = RepositoryType.resolve(purl); + if (RepositoryType.UNSUPPORTED != type) { + final RepositoryMetaComponent repoMetaComponent = getRepositoryMetaComponent(type, purl.getNamespace(), purl.getName()); + if (repoMetaComponent != null) { + finding.getComponent().put("latestVersion", repoMetaComponent.getLatestVersion()); + } } - - final var repositoryType = RepositoryType.resolve(purl); - if (repositoryType == RepositoryType.UNSUPPORTED) { - return null; - } - - final var search = new RepositoryMetaComponentSearch(repositoryType, purl.getNamespace(), purl.getName()); - return Map.entry(search, finding); - }) - .filter(Objects::nonNull) - .collect(Collectors.groupingBy( - Map.Entry::getKey, - Collectors.mapping(Map.Entry::getValue, Collectors.toList()) - )); - getRepositoryMetaComponents(List.copyOf(findingsByMetaComponentSearch.keySet())) - .forEach(metaComponent -> { - final var search = new RepositoryMetaComponentSearch(metaComponent.getRepositoryType(), metaComponent.getNamespace(), metaComponent.getName()); - final List affectedFindings = findingsByMetaComponentSearch.get(search); - for (final Finding finding : affectedFindings) { - finding.getComponent().put("latestVersion", metaComponent.getLatestVersion()); - } - }); - + } + findings.add(finding); + } + } return findings; } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 521a7daff6..c8c6e5d329 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1157,6 +1157,10 @@ public List getFindings(Project project, boolean includeSuppressed) { return getFindingsQueryManager().getFindings(project, includeSuppressed); } + public List getFindingsSince(Project project, boolean includeSuppressed, ZonedDateTime sinceAttributedOn) { + return getFindingsQueryManager().getFindings(project, includeSuppressed, sinceAttributedOn); + } + public PaginatedResult getAllFindings(final Map filters, final boolean showSuppressed, final boolean showInactive) { return getFindingsSearchQueryManager().getAllFindings(filters, showSuppressed, showInactive); } diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 7d449122eb..f907438579 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,6 +20,9 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; import javax.json.Json; @@ -28,13 +31,13 @@ import org.dependencytrack.exception.PublisherException; import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.Project; import org.dependencytrack.model.ScheduledNotificationRule; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; -import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.NotificationUtil; @@ -55,45 +58,46 @@ public SendScheduledNotificationTask(UUID scheduledNotificationRuleUuid) { public void run() { try (var qm = new QueryManager()) { var rule = qm.getObjectByUuid(ScheduledNotificationRule.class, scheduledNotificationRuleUuid); - final List projectIds = rule.getProjects().stream().map(proj -> proj.getId()).toList(); Boolean errorsDuringExecution = false; Boolean atLeastOneSuccessfulPublish = false; LOGGER.info("Processing notification publishing for scheduled notification rule " + rule.getUuid()); for (NotificationGroup group : rule.getNotifyOn()) { + List affectedProjects = rule.getProjects(); + // if rule does not limit to specific projects, get all projects + if (affectedProjects.isEmpty()) + affectedProjects = qm.getProjects().getList(Project.class); final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) - .title(NotificationUtil.generateNotificationTitle(group, rule.getProjects())) + .title(NotificationUtil.generateNotificationTitle(group, affectedProjects)) .level(rule.getNotificationLevel()); switch (group) { case NEW_VULNERABILITY: - var newProjectVulnerabilities = qm.getNewVulnerabilitiesForProjectsSince(rule.getLastExecutionTime(), projectIds); - if(newProjectVulnerabilities.isEmpty() && rule.getPublishOnlyWithUpdates()) + ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(affectedProjects, ZonedDateTime.of(2023, 05, 20, 0, 0, 0, 0, ZoneId.systemDefault())/* rule.getLastExecutionTime() */); + if(vulnSubject.getOverview().getNewVulnerabilitiesCount() == 0 && rule.getPublishOnlyWithUpdates()) continue; - ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(newProjectVulnerabilities); notificationProxy - .content(NotificationUtil.generateVulnerabilityScheduledNotificationContent( - rule, - vulnSubject.getNewVulnerabilitiesTotal(), - newProjectVulnerabilities.keySet().stream().toList(), - rule.getLastExecutionTime())) + .title("[Dependency-Track] Scheduled Notification of Rule “" + rule.getName() + "”") + .content("Find below a summary of new vulnerabilities since " + + rule.getLastExecutionTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + " in Scheduled Notification Rule “" + rule.getName() + "”.") .subject(vulnSubject); break; case POLICY_VIOLATION: - var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(rule.getLastExecutionTime(), projectIds); - if(newProjectPolicyViolations.isEmpty() && rule.getPublishOnlyWithUpdates()) - continue; - ScheduledPolicyViolationsIdentified policySubject = new ScheduledPolicyViolationsIdentified(newProjectPolicyViolations); - notificationProxy - .content(NotificationUtil.generatePolicyScheduledNotificationContent( - rule, - policySubject.getNewPolicyViolationsTotal(), - newProjectPolicyViolations.keySet().stream().toList(), - rule.getLastExecutionTime())) - .subject(policySubject); + // var newProjectPolicyViolations = qm.getNewPolicyViolationsForProjectsSince(rule.getLastExecutionTime(), projectIds); + // if(newProjectPolicyViolations.isEmpty() && rule.getPublishOnlyWithUpdates()) + // continue; + // ScheduledPolicyViolationsIdentified policySubject = new ScheduledPolicyViolationsIdentified(newProjectPolicyViolations); + // notificationProxy + // .content(NotificationUtil.generatePolicyScheduledNotificationContent( + // rule, + // policySubject.getNewPolicyViolationsTotal(), + // newProjectPolicyViolations.keySet().stream().toList(), + // rule.getLastExecutionTime())) + // .subject(policySubject); break; default: LOGGER.warn(group.name() + " is not a supported notification group for scheduled publishing"); diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index 6eaaa873b8..a426a5ec8a 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -44,6 +44,11 @@ import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAnalysisLevel; +import org.dependencytrack.model.scheduled.DetailInfo; +import org.dependencytrack.model.scheduled.Details; +import org.dependencytrack.model.scheduled.Overview; +import org.dependencytrack.model.scheduled.Summary; +import org.dependencytrack.model.scheduled.SummaryInfo; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; @@ -68,6 +73,8 @@ import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import javax.jdo.FetchPlan; +import javax.json.JsonValue; + import java.io.File; import java.io.IOException; import java.net.URLDecoder; @@ -571,64 +578,83 @@ public static JsonObject toJson(final Policy policy) { public static JsonObject toJson(final ScheduledNewVulnerabilitiesIdentified vo) { final JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add("overview", toJson(vo.getOverview())); + builder.add("summary", toJson(vo.getSummary())); + builder.add("details", toJson(vo.getDetails())); + return builder.build(); + } + + public static JsonObject toJson(final Overview overview) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add("affectedProjectsCount", overview.getAffectedProjectsCount()); + builder.add("newVulnerabilitiesCount", overview.getNewVulnerabilitiesCount()); + builder.add("affectedComponentsCount", overview.getAffectedComponentsCount()); + builder.add("suppressedNewVulnerabilitiesCount", overview.getSuppressedNewVulnerabilitiesCount()); + final JsonObjectBuilder newVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); + for (final Map.Entry entry : overview.getNewVulnerabilitiesBySeverity().entrySet()) { + newVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); + } + builder.add("newVulnerabilitiesBySeverity", newVulnerabilitiesBySeverityBuilder.build()); + return builder.build(); + } + + public static JsonObject toJson(final Summary summary){ + final JsonObjectBuilder builder = Json.createObjectBuilder(); + final JsonObjectBuilder affectedProjectSummariesBuilder = Json.createObjectBuilder(); + for (final Map.Entry entry : summary.getAffectedProjectSummaries().entrySet()) { + affectedProjectSummariesBuilder.add("projectName", entry.getKey().getName()); + affectedProjectSummariesBuilder.add("projectVersion", entry.getKey().getVersion()); + affectedProjectSummariesBuilder.add("projectSummary", toJson(entry.getValue())); + } + builder.add("projectSummaries", affectedProjectSummariesBuilder.build()); + return builder.build(); + } + + private static JsonValue toJson(SummaryInfo info) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); - if (vo.getNewProjectVulnerabilities() != null && vo.getNewProjectVulnerabilities().size() > 0) { - final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); - for (final Map.Entry> entry : vo.getNewProjectVulnerabilities().entrySet()) { - final JsonObjectBuilder projectBuilder = Json.createObjectBuilder(); - projectBuilder.add("project", toJson(entry.getKey())); - final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); - for (final Vulnerability vulnerability : entry.getValue()) { - vulnsBuilder.add(toJson(vulnerability)); - } - projectBuilder.add("vulnerabilities", vulnsBuilder.build()); - projectsBuilder.add(projectBuilder.build()); - } - builder.add("newProjectVulnerabilities", projectsBuilder.build()); + final JsonObjectBuilder newVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); + for (final Map.Entry entry : info.getNewVulnerabilitiesBySeverity().entrySet()) { + newVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); } - if(vo.getNewProjectVulnerabilitiesBySeverity() != null && vo.getNewProjectVulnerabilitiesBySeverity().size() > 0) { - final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); - for (final Map.Entry>> entry : vo.getNewProjectVulnerabilitiesBySeverity().entrySet()) { - final JsonObjectBuilder projectBuilder = Json.createObjectBuilder(); - projectBuilder.add("project", toJson(entry.getKey())); - final JsonArrayBuilder vulnsBySeverityBuilder = Json.createArrayBuilder(); - for (final Map.Entry> vulnEntry : entry.getValue().entrySet()) { - final JsonObjectBuilder severityBuilder = Json.createObjectBuilder(); - severityBuilder.add("severity", vulnEntry.getKey().name()); - final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); - for (final Vulnerability vulnerability : vulnEntry.getValue()) { - vulnsBuilder.add(toJson(vulnerability)); - } - severityBuilder.add("vulnerabilities", vulnsBuilder.build()); - vulnsBySeverityBuilder.add(severityBuilder.build()); - } - projectBuilder.add("vulnerabilitiesBySeverity", vulnsBySeverityBuilder.build()); - projectsBuilder.add(projectBuilder.build()); - } - builder.add("newProjectVulnerabilitiesBySeverity", projectsBuilder.build()); + builder.add("newVulnerabilitiesBySeverity", newVulnerabilitiesBySeverityBuilder.build()); + + final JsonObjectBuilder totalProjectVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); + for (final Map.Entry entry : info.getTotalProjectVulnerabilitiesBySeverity().entrySet()) { + totalProjectVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); } - if (vo.getNewVulnerabilitiesTotal() != null && vo.getNewVulnerabilitiesTotal().size() > 0) { - final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); - for (final Vulnerability vulnerability : vo.getNewVulnerabilitiesTotal()) { - vulnsBuilder.add(toJson(vulnerability)); - } - builder.add("newVulnerabilitiesTotal", vulnsBuilder.build()); - } - if(vo.getNewVulnerabilitiesTotalBySeverity() != null && vo.getNewVulnerabilitiesTotalBySeverity().size() > 0) { - final JsonArrayBuilder vulnsBySeverityBuilder = Json.createArrayBuilder(); - for (final Map.Entry> vulnEntry : vo.getNewVulnerabilitiesTotalBySeverity().entrySet()) { - final JsonObjectBuilder severityBuilder = Json.createObjectBuilder(); - severityBuilder.add("severity", vulnEntry.getKey().name()); - final JsonArrayBuilder vulnsBuilder = Json.createArrayBuilder(); - for (final Vulnerability vulnerability : vulnEntry.getValue()) { - vulnsBuilder.add(toJson(vulnerability)); - } - severityBuilder.add("vulnerabilities", vulnsBuilder.build()); - vulnsBySeverityBuilder.add(severityBuilder.build()); - } - builder.add("newVulnerabilitiesTotalBySeverity", vulnsBySeverityBuilder.build()); + builder.add("totalProjectVulnerabilitiesBySeverity", totalProjectVulnerabilitiesBySeverityBuilder.build()); + + final JsonObjectBuilder suppressedNewVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); + for (final Map.Entry entry : info.getSuppressedNewVulnerabilitiesBySeverity().entrySet()) { + suppressedNewVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); } + builder.add("suppressedNewVulnerabilitiesBySeverity", suppressedNewVulnerabilitiesBySeverityBuilder.build()); + + return builder.build(); + } + private static JsonObject toJson(Details details) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + final JsonObjectBuilder affectedProjectFindingsBuilder = Json.createObjectBuilder(); + for (final Map.Entry> entry : details.getAffectedProjectFindings().entrySet()) { + affectedProjectFindingsBuilder.add("projectName", entry.getKey().getName()); + affectedProjectFindingsBuilder.add("projectVersion", entry.getKey().getVersion()); + final JsonObjectBuilder findingsBuilder = Json.createObjectBuilder(); + for (final DetailInfo detailInfo : entry.getValue()) { + findingsBuilder.add("componentName", detailInfo.getComponentName()); + findingsBuilder.add("componentVersion", detailInfo.getComponentVersion()); + findingsBuilder.add("componentGroup", detailInfo.getComponentGroup()); + findingsBuilder.add("vulnerabilityId", detailInfo.getVulnerabilityId()); + findingsBuilder.add("vulnerabilitySeverity", detailInfo.getVulnerabilitySeverity()); + findingsBuilder.add("analyzer", detailInfo.getAnalyzer()); + findingsBuilder.add("attributedOn", detailInfo.getAttributedOn() == null ? "---" : DateUtil.toISO8601(detailInfo.getAttributedOn())); + findingsBuilder.add("analysisState", detailInfo.getAnalysisState()); + findingsBuilder.add("suppressed", detailInfo.getSuppressed()); + } + affectedProjectFindingsBuilder.add("projectFindings", findingsBuilder.build()); + } + builder.add("projectDetails", affectedProjectFindingsBuilder.build()); return builder.build(); } diff --git a/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java b/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java index 438561be08..3a07be3a5d 100644 --- a/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java +++ b/src/test/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentifiedTest.java @@ -53,28 +53,28 @@ public void testVo() { var project2Vulns = List.of(mediumVuln, lowVuln); projectVulnerabilitiesMap.put(project2, project2Vulns); - ScheduledNewVulnerabilitiesIdentified vo = new ScheduledNewVulnerabilitiesIdentified(projectVulnerabilitiesMap); + // ScheduledNewVulnerabilitiesIdentified vo = new ScheduledNewVulnerabilitiesIdentified(projectVulnerabilitiesMap); - Assert.assertEquals(2, vo.getNewProjectVulnerabilities().size()); - Assert.assertEquals(project1Vulns, vo.getNewProjectVulnerabilities().get(project1)); - Assert.assertEquals(project2Vulns, vo.getNewProjectVulnerabilities().get(project2)); - Assert.assertEquals(2, vo.getNewProjectVulnerabilitiesBySeverity().size()); - var projectVulnerabilitiesBySeverityMap = vo.getNewProjectVulnerabilitiesBySeverity(); - Assert.assertEquals(3, projectVulnerabilitiesBySeverityMap.get(project1).size()); - Assert.assertEquals(2, projectVulnerabilitiesBySeverityMap.get(project2).size()); - Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.CRITICAL).size()); - Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.HIGH).size()); - Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.INFO).size()); - Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.MEDIUM).size()); - Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.LOW).size()); - Assert.assertEquals(5, vo.getNewVulnerabilitiesTotal().size()); - Assert.assertEquals(List.of(critVuln, highVuln, infoVuln, mediumVuln, lowVuln), vo.getNewVulnerabilitiesTotal()); - Assert.assertEquals(5, vo.getNewVulnerabilitiesTotalBySeverity().size()); - var vulnerabilitiesBySeverity = vo.getNewVulnerabilitiesTotalBySeverity(); - Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.CRITICAL).size()); - Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.HIGH).size()); - Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.MEDIUM).size()); - Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.LOW).size()); - Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.INFO).size()); + // Assert.assertEquals(2, vo.getNewProjectVulnerabilities().size()); + // Assert.assertEquals(project1Vulns, vo.getNewProjectVulnerabilities().get(project1)); + // Assert.assertEquals(project2Vulns, vo.getNewProjectVulnerabilities().get(project2)); + // Assert.assertEquals(2, vo.getNewProjectVulnerabilitiesBySeverity().size()); + // var projectVulnerabilitiesBySeverityMap = vo.getNewProjectVulnerabilitiesBySeverity(); + // Assert.assertEquals(3, projectVulnerabilitiesBySeverityMap.get(project1).size()); + // Assert.assertEquals(2, projectVulnerabilitiesBySeverityMap.get(project2).size()); + // Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.CRITICAL).size()); + // Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.HIGH).size()); + // Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project1).get(Severity.INFO).size()); + // Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.MEDIUM).size()); + // Assert.assertEquals(1, projectVulnerabilitiesBySeverityMap.get(project2).get(Severity.LOW).size()); + // Assert.assertEquals(5, vo.getNewVulnerabilitiesTotal().size()); + // Assert.assertEquals(List.of(critVuln, highVuln, infoVuln, mediumVuln, lowVuln), vo.getNewVulnerabilitiesTotal()); + // Assert.assertEquals(5, vo.getNewVulnerabilitiesTotalBySeverity().size()); + // var vulnerabilitiesBySeverity = vo.getNewVulnerabilitiesTotalBySeverity(); + // Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.CRITICAL).size()); + // Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.HIGH).size()); + // Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.MEDIUM).size()); + // Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.LOW).size()); + // Assert.assertEquals(1, vulnerabilitiesBySeverity.get(Severity.INFO).size()); } } From 74b757e839f5af15d8f08340242c56eae27b7298 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 5 Jun 2024 18:05:43 +0200 Subject: [PATCH 49/98] fixed wrong query in getting findings with since-date-filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../org/dependencytrack/persistence/FindingsQueryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index 2abded25f7..5a59a599c8 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -274,7 +274,7 @@ public List getFindings(Project project) { @SuppressWarnings("unchecked") public List getFindings(Project project, boolean includeSuppressed, ZonedDateTime sinceAttributedOn) { final Query query; - if (sinceAttributedOn != null) { + if (sinceAttributedOn == null) { query = pm.newQuery(Query.SQL, Finding.QUERY); query.setParameters(project.getId()); } else { From 9031bd9e8a07a960ad19dc04bb38fe53d80ca401 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Wed, 5 Jun 2024 18:30:36 +0200 Subject: [PATCH 50/98] fixed typo in overview model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- src/main/java/org/dependencytrack/model/scheduled/Overview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/scheduled/Overview.java b/src/main/java/org/dependencytrack/model/scheduled/Overview.java index f5a65ceaf2..94ec9858a2 100644 --- a/src/main/java/org/dependencytrack/model/scheduled/Overview.java +++ b/src/main/java/org/dependencytrack/model/scheduled/Overview.java @@ -51,7 +51,7 @@ public Overview(final List affectedProjects, ZonedDateTime lastExecutio componentCache.add(component); Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, (String) finding.getVulnerability().get("uuid")); - if (finding.getAnalysis().get("istSuppressed") instanceof Boolean suppressed) { + if (finding.getAnalysis().get("isSuppressed") instanceof Boolean suppressed) { if (suppressed) { suppressedVulnerabilityCache.add(vulnerability); } else { From 904a8a9f518c78e6ddf4d39ef09969e87565fc1b Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 6 Jun 2024 11:17:53 +0200 Subject: [PATCH 51/98] fixed StackOverflowException due to missing method definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../persistence/FindingsQueryManager.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java index 5a59a599c8..ba98cd3b9f 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java @@ -259,11 +259,20 @@ void deleteAnalysisTrail(Project project) { * @param project the project to retrieve findings for * @return a List of Finding objects */ - @SuppressWarnings("unchecked") public List getFindings(Project project) { return getFindings(project, false, null); } + /** + * Returns a List of Finding objects for the specified project. + * @param project the project to retrieve findings for + * @param includeSuppressed determines if suppressed vulnerabilities should be included or not + * @return a List of Finding objects + */ + public List getFindings(Project project, boolean includeSuppressed) { + return getFindings(project, includeSuppressed, null); + } + /** * Returns a List of Finding objects for the specified project. * @param project the project to retrieve findings for From 49b597b9ec01e64c13cbd974bcc2d626ec1c7d0d Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 6 Jun 2024 13:42:38 +0200 Subject: [PATCH 52/98] fixed ignore of suppressed violations, fixed error on pebble template population with null values in json, added important fields for template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../model/scheduled/DetailInfo.java | 62 ++-- .../model/scheduled/Details.java | 7 +- .../model/scheduled/Overview.java | 10 +- .../model/scheduled/Summary.java | 7 +- .../org/dependencytrack/util/JsonUtil.java | 14 + .../util/NotificationUtil.java | 43 +-- .../publisher/scheduled_email_summary.peb | 284 +++++++++++------- 7 files changed, 267 insertions(+), 160 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java b/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java index 159f23c62d..20abffec6e 100644 --- a/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java +++ b/src/main/java/org/dependencytrack/model/scheduled/DetailInfo.java @@ -18,41 +18,55 @@ */ package org.dependencytrack.model.scheduled; -import java.text.DateFormat; -import java.text.ParseException; import java.util.Date; import org.dependencytrack.model.Finding; +import org.dependencytrack.util.DateUtil; import alpine.common.logging.Logger; public class DetailInfo { private static final Logger LOGGER = Logger.getLogger(DetailInfo.class); + private final String componentUuid; private final String componentName; private final String componentVersion; private final String componentGroup; + private final String vulnerabilitySource; private final String vulnerabilityId; private final String vulnerabilitySeverity; private final String analyzer; - private Date attributedOn; + private final String attributionReferenceUrl; + private final String attributedOn; private final String analysisState; - private final Boolean suppressed; + private final String suppressed; public DetailInfo(Finding finding) { - this.componentName = (String) finding.getComponent().get("name"); - this.componentVersion = (String) finding.getComponent().get("version"); - this.componentGroup = (String) finding.getComponent().get("group"); - this.vulnerabilityId = (String) finding.getVulnerability().get("vulnId"); - this.vulnerabilitySeverity = (String) finding.getVulnerability().get("severity"); - this.analyzer = (String) finding.getAttribution().get("analyzerIdentity"); - try { - this.attributedOn = DateFormat.getDateTimeInstance().parse((String) finding.getAttribution().get("attributedOn")); - } catch (ParseException e) { - this.attributedOn = null; - LOGGER.error("An error occurred while parsing the attributedOn date for component" + this.componentName); - } - this.analysisState = (String) finding.getAnalysis().get("state"); - this.suppressed = (Boolean) finding.getAnalysis().get("isSuppressed"); + this.componentUuid = getValueOrUnknownIfNull(finding.getComponent().get("uuid")); + this.componentName = getValueOrUnknownIfNull(finding.getComponent().get("name")); + this.componentVersion = getValueOrUnknownIfNull(finding.getComponent().get("version")); + this.componentGroup = getValueOrUnknownIfNull(finding.getComponent().get("group")); + this.vulnerabilitySource = getValueOrUnknownIfNull(finding.getVulnerability().get("source")); + this.vulnerabilityId = getValueOrUnknownIfNull(finding.getVulnerability().get("vulnId")); + this.vulnerabilitySeverity = getValueOrUnknownIfNull(finding.getVulnerability().get("severity")); + this.analyzer = getValueOrUnknownIfNull(finding.getAttribution().get("analyzerIdentity")); + this.attributionReferenceUrl = getValueOrUnknownIfNull(finding.getAttribution().get("referenceUrl")); + this.attributedOn = getDateOrUnknownIfNull((Date) finding.getAttribution().get("attributedOn")); + this.analysisState = getValueOrUnknownIfNull(finding.getAnalysis().get("state")); + this.suppressed = finding.getAnalysis().get("isSuppressed") instanceof Boolean + ? (Boolean) finding.getAnalysis().get("isSuppressed") ? "Yes" : "No" + : "No"; + } + + private static String getValueOrUnknownIfNull(Object value) { + return value == null ? "" : value.toString(); + } + + private static String getDateOrUnknownIfNull(Date date) { + return date == null ? "Unknown" : DateUtil.toISO8601(date); + } + + public String getComponentUuid() { + return componentUuid; } public String getComponentName() { @@ -67,6 +81,10 @@ public String getComponentGroup() { return componentGroup; } + public String getVulnerabilitySource() { + return vulnerabilitySource; + } + public String getVulnerabilityId() { return vulnerabilityId; } @@ -79,7 +97,11 @@ public String getAnalyzer() { return analyzer; } - public Date getAttributedOn() { + public String getAttributionReferenceUrl() { + return attributionReferenceUrl; + } + + public String getAttributedOn() { return attributedOn; } @@ -87,7 +109,7 @@ public String getAnalysisState() { return analysisState; } - public Boolean getSuppressed() { + public String getSuppressed() { return suppressed; } } diff --git a/src/main/java/org/dependencytrack/model/scheduled/Details.java b/src/main/java/org/dependencytrack/model/scheduled/Details.java index 7ca7b228b6..f817b80706 100644 --- a/src/main/java/org/dependencytrack/model/scheduled/Details.java +++ b/src/main/java/org/dependencytrack/model/scheduled/Details.java @@ -20,20 +20,19 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; - import org.dependencytrack.model.Project; import org.dependencytrack.persistence.QueryManager; public final class Details { - private final Map> affectedProjectFindings = new TreeMap<>(); + private final Map> affectedProjectFindings = new LinkedHashMap<>(); public Details(final List affectedProjects, ZonedDateTime lastExecution) { try (var qm = new QueryManager()) { for (Project project : affectedProjects) { - var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + var findings = qm.getFindingsSince(project, true, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); affectedProjectFindings.put(project, findings.stream().map(f -> new DetailInfo(f)).toList()); } } diff --git a/src/main/java/org/dependencytrack/model/scheduled/Overview.java b/src/main/java/org/dependencytrack/model/scheduled/Overview.java index 94ec9858a2..186dea1269 100644 --- a/src/main/java/org/dependencytrack/model/scheduled/Overview.java +++ b/src/main/java/org/dependencytrack/model/scheduled/Overview.java @@ -45,7 +45,7 @@ public Overview(final List affectedProjects, ZonedDateTime lastExecutio try (var qm = new QueryManager()) { for (Project project : affectedProjects) { - var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + var findings = qm.getFindingsSince(project, true, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); for (Finding finding : findings) { Component component = qm.getObjectByUuid(Component.class, (String) finding.getComponent().get("uuid")); componentCache.add(component); @@ -56,13 +56,19 @@ public Overview(final List affectedProjects, ZonedDateTime lastExecutio suppressedVulnerabilityCache.add(vulnerability); } else { vulnerabilityCache.add(vulnerability); - newVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); } } } } } + for (Severity severity : Severity.values()) { + newVulnerabilitiesBySeverity.put(severity, 0); + } + for (Vulnerability vulnerability : vulnerabilityCache) { + newVulnerabilitiesBySeverity.merge(vulnerability.getSeverity(), 1, Integer::sum); + } + affectedProjectsCount = affectedProjects.size(); newVulnerabilitiesCount = vulnerabilityCache.size(); affectedComponentsCount = componentCache.size(); diff --git a/src/main/java/org/dependencytrack/model/scheduled/Summary.java b/src/main/java/org/dependencytrack/model/scheduled/Summary.java index 1e4fd64c80..4e38e476af 100644 --- a/src/main/java/org/dependencytrack/model/scheduled/Summary.java +++ b/src/main/java/org/dependencytrack/model/scheduled/Summary.java @@ -20,20 +20,19 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; - import org.dependencytrack.model.Project; import org.dependencytrack.persistence.QueryManager; public final class Summary { - private final Map affectedProjectSummaries = new TreeMap<>(); + private final Map affectedProjectSummaries = new LinkedHashMap<>(); public Summary(final List affectedProjects, ZonedDateTime lastExecution) { try (var qm = new QueryManager()) { for (Project project : affectedProjects) { - var findings = qm.getFindingsSince(project, false, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); + var findings = qm.getFindingsSince(project, true, lastExecution.withZoneSameInstant(ZoneOffset.UTC)); affectedProjectSummaries.put(project, new SummaryInfo(findings)); } } diff --git a/src/main/java/org/dependencytrack/util/JsonUtil.java b/src/main/java/org/dependencytrack/util/JsonUtil.java index 5bf7e3c370..afaeade08d 100644 --- a/src/main/java/org/dependencytrack/util/JsonUtil.java +++ b/src/main/java/org/dependencytrack/util/JsonUtil.java @@ -38,6 +38,13 @@ public static JsonObjectBuilder add(final JsonObjectBuilder builder, final Strin return builder; } + public static JsonObjectBuilder add(final JsonObjectBuilder builder, final String key, final Integer value) { + if (value != null) { + builder.add(key, value); + } + return builder; + } + public static JsonObjectBuilder add(final JsonObjectBuilder builder, final String key, final BigInteger value) { if (value != null) { builder.add(key, value); @@ -51,6 +58,13 @@ public static JsonObjectBuilder add(final JsonObjectBuilder builder, final Strin } return builder; } + + public static JsonObjectBuilder add(final JsonObjectBuilder builder, final String key, final Boolean value) { + if (value != null) { + builder.add(key, value); + } + return builder; + } public static JsonObjectBuilder add(final JsonObjectBuilder builder, final String key, final Enum value) { if (value != null) { diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index a426a5ec8a..415276c1be 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -586,13 +586,13 @@ public static JsonObject toJson(final ScheduledNewVulnerabilitiesIdentified vo) public static JsonObject toJson(final Overview overview) { final JsonObjectBuilder builder = Json.createObjectBuilder(); - builder.add("affectedProjectsCount", overview.getAffectedProjectsCount()); - builder.add("newVulnerabilitiesCount", overview.getNewVulnerabilitiesCount()); - builder.add("affectedComponentsCount", overview.getAffectedComponentsCount()); - builder.add("suppressedNewVulnerabilitiesCount", overview.getSuppressedNewVulnerabilitiesCount()); + JsonUtil.add(builder, "affectedProjectsCount", overview.getAffectedProjectsCount()); + JsonUtil.add(builder, "newVulnerabilitiesCount", overview.getNewVulnerabilitiesCount()); + JsonUtil.add(builder, "affectedComponentsCount", overview.getAffectedComponentsCount()); + JsonUtil.add(builder, "suppressedNewVulnerabilitiesCount", overview.getSuppressedNewVulnerabilitiesCount()); final JsonObjectBuilder newVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); for (final Map.Entry entry : overview.getNewVulnerabilitiesBySeverity().entrySet()) { - newVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); + JsonUtil.add(newVulnerabilitiesBySeverityBuilder, entry.getKey().name(), entry.getValue()); } builder.add("newVulnerabilitiesBySeverity", newVulnerabilitiesBySeverityBuilder.build()); return builder.build(); @@ -602,8 +602,7 @@ public static JsonObject toJson(final Summary summary){ final JsonObjectBuilder builder = Json.createObjectBuilder(); final JsonObjectBuilder affectedProjectSummariesBuilder = Json.createObjectBuilder(); for (final Map.Entry entry : summary.getAffectedProjectSummaries().entrySet()) { - affectedProjectSummariesBuilder.add("projectName", entry.getKey().getName()); - affectedProjectSummariesBuilder.add("projectVersion", entry.getKey().getVersion()); + affectedProjectSummariesBuilder.add("project", toJson(entry.getKey())); affectedProjectSummariesBuilder.add("projectSummary", toJson(entry.getValue())); } builder.add("projectSummaries", affectedProjectSummariesBuilder.build()); @@ -615,19 +614,19 @@ private static JsonValue toJson(SummaryInfo info) { final JsonObjectBuilder newVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); for (final Map.Entry entry : info.getNewVulnerabilitiesBySeverity().entrySet()) { - newVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); + JsonUtil.add(newVulnerabilitiesBySeverityBuilder, entry.getKey().name(), entry.getValue()); } builder.add("newVulnerabilitiesBySeverity", newVulnerabilitiesBySeverityBuilder.build()); final JsonObjectBuilder totalProjectVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); for (final Map.Entry entry : info.getTotalProjectVulnerabilitiesBySeverity().entrySet()) { - totalProjectVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); + JsonUtil.add(totalProjectVulnerabilitiesBySeverityBuilder, entry.getKey().name(), entry.getValue()); } builder.add("totalProjectVulnerabilitiesBySeverity", totalProjectVulnerabilitiesBySeverityBuilder.build()); final JsonObjectBuilder suppressedNewVulnerabilitiesBySeverityBuilder = Json.createObjectBuilder(); for (final Map.Entry entry : info.getSuppressedNewVulnerabilitiesBySeverity().entrySet()) { - suppressedNewVulnerabilitiesBySeverityBuilder.add(entry.getKey().name(), entry.getValue()); + JsonUtil.add(suppressedNewVulnerabilitiesBySeverityBuilder, entry.getKey().name(), entry.getValue()); } builder.add("suppressedNewVulnerabilitiesBySeverity", suppressedNewVulnerabilitiesBySeverityBuilder.build()); @@ -638,19 +637,21 @@ private static JsonObject toJson(Details details) { final JsonObjectBuilder builder = Json.createObjectBuilder(); final JsonObjectBuilder affectedProjectFindingsBuilder = Json.createObjectBuilder(); for (final Map.Entry> entry : details.getAffectedProjectFindings().entrySet()) { - affectedProjectFindingsBuilder.add("projectName", entry.getKey().getName()); - affectedProjectFindingsBuilder.add("projectVersion", entry.getKey().getVersion()); + affectedProjectFindingsBuilder.add("project", toJson(entry.getKey())); final JsonObjectBuilder findingsBuilder = Json.createObjectBuilder(); for (final DetailInfo detailInfo : entry.getValue()) { - findingsBuilder.add("componentName", detailInfo.getComponentName()); - findingsBuilder.add("componentVersion", detailInfo.getComponentVersion()); - findingsBuilder.add("componentGroup", detailInfo.getComponentGroup()); - findingsBuilder.add("vulnerabilityId", detailInfo.getVulnerabilityId()); - findingsBuilder.add("vulnerabilitySeverity", detailInfo.getVulnerabilitySeverity()); - findingsBuilder.add("analyzer", detailInfo.getAnalyzer()); - findingsBuilder.add("attributedOn", detailInfo.getAttributedOn() == null ? "---" : DateUtil.toISO8601(detailInfo.getAttributedOn())); - findingsBuilder.add("analysisState", detailInfo.getAnalysisState()); - findingsBuilder.add("suppressed", detailInfo.getSuppressed()); + JsonUtil.add(findingsBuilder, "componentUuid", detailInfo.getComponentUuid()); + JsonUtil.add(findingsBuilder, "componentName", detailInfo.getComponentName()); + JsonUtil.add(findingsBuilder, "componentVersion", detailInfo.getComponentVersion()); + JsonUtil.add(findingsBuilder, "componentGroup", detailInfo.getComponentGroup()); + JsonUtil.add(findingsBuilder, "vulnerabilitySource", detailInfo.getVulnerabilitySource()); + JsonUtil.add(findingsBuilder, "vulnerabilityId", detailInfo.getVulnerabilityId()); + JsonUtil.add(findingsBuilder, "vulnerabilitySeverity", detailInfo.getVulnerabilitySeverity()); + JsonUtil.add(findingsBuilder, "analyzer", detailInfo.getAnalyzer()); + JsonUtil.add(findingsBuilder, "attributionReferenceUrl", detailInfo.getAttributionReferenceUrl()); + JsonUtil.add(findingsBuilder, "attributedOn", detailInfo.getAttributedOn()); + JsonUtil.add(findingsBuilder, "analysisState", detailInfo.getAnalysisState()); + JsonUtil.add(findingsBuilder, "suppressed", detailInfo.getSuppressed()); } affectedProjectFindingsBuilder.add("projectFindings", findingsBuilder.build()); } diff --git a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb index 4e03e83960..d22938bb28 100644 --- a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb +++ b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb @@ -1,125 +1,191 @@ - - {{ notification.title }} - ------------- - {{ notification.description }} - + +

{{ notification.title }}

+

-------------

+

{{ notification.content }}

+

Overview

- +
+
- - - - - - - - {% if subject.overview.newVulnerabilitiesBySeverity|length > 0 %}{% for entry in subject.overview.newVulnerabilitiesBySeverity %} - - - - {% endfor %}{% endif %} - - - - - - - - + + + + + + + + + {% if subject.overview.newVulnerabilitiesBySeverity|length > 0 %}{% + for entry in subject.overview.newVulnerabilitiesBySeverity %}{% if entry.value > 0 %} + + + + + {% endif %}{% endfor %}{% endif %} + + + + + + + + -
Projects included in this rule{{ subject.overview.affectedProjectsCount }}
Total new vulnerabilities{{ subject.overview.newVulnerabilitiesCount }}
New vulnerabilities ({{ entry.key }}){{ entry.value }}
Components affected by new vulnerabilities{{ subject.overview.affectedComponentsCount }}
Suppressed new vulnerabilities (not included above){{ subject.overview.suppressedNewVulnerabilitiesCount }}
Projects included in this rule{{ subject.overview.affectedProjectsCount }}
Total new vulnerabilities{{ subject.overview.newVulnerabilitiesCount }}
New vulnerabilities ({{ entry.key }}){{ entry.value }}
Components affected by new vulnerabilities{{ subject.overview.affectedComponentsCount }}
Suppressed new vulnerabilities (not included above){{ subject.overview.suppressedNewVulnerabilitiesCount }}
+ +

Summary per project in this rule

- - - - - - - - - - - {% for entry in subject.summary.affectedProjectSummaries %} - - - - - - - {% endfor %} - -
Project NameVersionNew VulnerabilitiesAll VulnerabilitiesSuppressed Vulnerabilities
- - {{ entry.key.name }} - - {{ entry.key.version }} - {% for newVulnEntry in entry.value.newVulnerabilitiesBySeverity %} - {{ newVulnEntry.key }}: {{ newVulnEntry.value }}
- {% endfor %} -
- {% for projVulnEntry in entry.value.totalProjectVulnerabilitiesBySeverity %} - {{ projVulnEntry.key }}: {{ projVulnEntry.value }}
- {% endfor %} -
- {% for supprVulnEntry in entry.value.suppressedNewVulnerabilitiesBySeverity %} - {{ supprVulnEntry.key }}: {{ supprVulnEntry.value }}
- {% endfor %} -
+
+ + + + + + + + + + + + {% for entry in subject.summary.affectedProjectSummaries %} + + + + + + + + {% endfor %} + +
Project NameVersionNew VulnerabilitiesAll VulnerabilitiesSuppressed Vulnerabilities
+ + {{ entry.key.name }} + + {{ entry.key.version }} + {% for newVulnEntry in entry.value.newVulnerabilitiesBySeverity %} + {{ newVulnEntry.key }}: {{ newVulnEntry.value }}
+ {% endfor %} +
+ {% for projVulnEntry in + entry.value.totalProjectVulnerabilitiesBySeverity %} + {{ projVulnEntry.key }}: {{ projVulnEntry.value }}
+ {% endfor %} +
+ {% for supprVulnEntry in + entry.value.suppressedNewVulnerabilitiesBySeverity %} + {{ supprVulnEntry.key }}: {{ supprVulnEntry.value }}
+ {% endfor %} +
+

New vulnerabilities per project

- {% if subject.details.affectedProjectFindings|length > 0 %}{% for affProjEntry in subject.details.affectedProjectFindings %} -

Project "{{ affProjEntry.key.name }}"" [Version: {{ affProjEntry.key.version }}]

- - - - - - - - - - - - - - - {% for finding in findings %} - - - - - - - - - - - {% endfor %} - -
ComponentVersionGroupVulnerabilitySeverityAnalyzerAttributed OnAnalysisSuppressed
- - {{ affProjEntry.value.componentName }} - - {{ affProjEntry.value.componentVersion }} - - {{ affProjEntry.value.componentGroup }} - - - - {{ affProjEntry.value.vulnerabilityId }} - - {{ affProjEntry.value.vulnerabilitySeverity }}{{ affProjEntry.value.analyzer }}{{ affProjEntry.value.attributedOn }}{{ affProjEntry.value.analysisState }}{{ affProjEntry.value.suppressed }}
- {% endfor %}{% endif %} + {% if subject.details.affectedProjectFindings|length > 0 %}{% for + affProjEntry in subject.details.affectedProjectFindings %}{% if affProjEntry.value|length > 0 %} +

+ Project "{{ affProjEntry.key.name }}" [Version: + {{ affProjEntry.key.version }}] +

+
+ + + + + + + + + + + + + + + + {% for vulnerableComponent in affProjEntry.value %} + + + + + + + + + + + + {% endfor %} + +
ComponentVersionGroupVulnerabilitySeverityAnalyzerAttributed OnAnalysisSuppressed
+ {% if vulnerableComponent.componentUuid is empty %} + {{ vulnerableComponent.componentName }} + {% else %} + + {{ vulnerableComponent.componentName }} + + {% endif %} + + {{ vulnerableComponent.componentVersion }} + + {{ vulnerableComponent.componentGroup }} + + {% if vulnerableComponent.vulnerabilityId is empty %} + {{ vulnerableComponent.vulnerabilityId }} + {% else %} + + {{ vulnerableComponent.vulnerabilityId }} + + {% endif %} + + {{ vulnerableComponent.vulnerabilitySeverity }} + + {% if vulnerableComponent.attributionReferenceUrl is empty %} + {{ vulnerableComponent.analyzer }} + {% else %} + + {{ vulnerableComponent.analyzer }} + + {% endif %} + + {{ vulnerableComponent.attributedOn }} + + {{ vulnerableComponent.analysisState }} + + {{ vulnerableComponent.suppressed }} +
+
+ {% endif %}{% endfor %}{% endif %}
-

- {{ timestamp }} -

+

Executed: {{ timestamp }}

From 0328433290031d0e0976664b83144317a2bf1e97 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 6 Jun 2024 13:45:15 +0200 Subject: [PATCH 53/98] added child projects audit in scheduled notification mail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index f907438579..7935182225 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -64,10 +64,20 @@ public void run() { LOGGER.info("Processing notification publishing for scheduled notification rule " + rule.getUuid()); for (NotificationGroup group : rule.getNotifyOn()) { - List affectedProjects = rule.getProjects(); + List affectedProjects = rule.getProjects() == null ? List.of() : rule.getProjects(); // if rule does not limit to specific projects, get all projects - if (affectedProjects.isEmpty()) - affectedProjects = qm.getProjects().getList(Project.class); + if (affectedProjects.isEmpty()) { + List allProjects = qm.getProjects().getList(Project.class); + affectedProjects.addAll(allProjects); + } + + // detach the projects to avoid issues while modifying the list + affectedProjects = qm.detach(affectedProjects); + + if (!affectedProjects.isEmpty() && rule.isNotifyChildren()) { + extendProjectListWithChildren(affectedProjects); + } + final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) @@ -161,5 +171,19 @@ public void run() { LOGGER.error("Errors occured while processing notification publishing for scheduled notification rule " + scheduledNotificationRuleUuid); } } + catch (Exception e) { + LOGGER.error("An error occurred while processing scheduled notification rule " + scheduledNotificationRuleUuid, e); + } + } + + private void extendProjectListWithChildren(final List affectedProjects) { + var allProjects = List.copyOf(affectedProjects); + for (Project project : allProjects) { + if (project == null || project.getChildren() == null || project.getChildren().isEmpty()) { + continue; + } + var parentIndex = affectedProjects.indexOf(project); + affectedProjects.addAll(++parentIndex, project.getChildren()); + } } } From bff4c79db2d8fcdbaf9ea87bdd6a2039e87182a8 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 6 Jun 2024 13:51:55 +0200 Subject: [PATCH 54/98] ignore version label in template if not set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../notification/publisher/scheduled_email_summary.peb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb index d22938bb28..dda04ffdf8 100644 --- a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb +++ b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb @@ -112,8 +112,8 @@ {% if subject.details.affectedProjectFindings|length > 0 %}{% for affProjEntry in subject.details.affectedProjectFindings %}{% if affProjEntry.value|length > 0 %}

- Project "{{ affProjEntry.key.name }}" [Version: - {{ affProjEntry.key.version }}] + Project "{{ affProjEntry.key.name }}"{% if affProjEntry.key.version %} [Version: + {{ affProjEntry.key.version }}]{% endif %}

From c64efc195be1f8a96e7e95322d5731b81125795b Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Thu, 6 Jun 2024 14:19:28 +0200 Subject: [PATCH 55/98] fixed detach in scheduled task to avoid implicit modification of notification rule project limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index 7935182225..bacff14efc 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -64,16 +64,13 @@ public void run() { LOGGER.info("Processing notification publishing for scheduled notification rule " + rule.getUuid()); for (NotificationGroup group : rule.getNotifyOn()) { - List affectedProjects = rule.getProjects() == null ? List.of() : rule.getProjects(); + List affectedProjects = rule.getProjects() == null ? List.of() : qm.detach(rule.getProjects()); // if rule does not limit to specific projects, get all projects if (affectedProjects.isEmpty()) { List allProjects = qm.getProjects().getList(Project.class); - affectedProjects.addAll(allProjects); + affectedProjects.addAll(qm.detach(allProjects)); } - // detach the projects to avoid issues while modifying the list - affectedProjects = qm.detach(affectedProjects); - if (!affectedProjects.isEmpty() && rule.isNotifyChildren()) { extendProjectListWithChildren(affectedProjects); } From 66d7ac26454b44ca682f461b4c3b08f4c4f1aa44 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Fri, 7 Jun 2024 16:25:33 +0200 Subject: [PATCH 56/98] fixed determination of affected project in scheduled notification rule to consider limiting projects and children setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../tasks/SendScheduledNotificationTask.java | 76 +++++++++++++------ 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java index bacff14efc..f75b35ddfe 100644 --- a/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java +++ b/src/main/java/org/dependencytrack/tasks/SendScheduledNotificationTask.java @@ -20,7 +20,7 @@ import java.io.StringReader; import java.lang.reflect.InvocationTargetException; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @@ -28,6 +28,7 @@ import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; +import java.util.stream.Collectors; import org.dependencytrack.exception.PublisherException; import org.dependencytrack.model.NotificationPublisher; @@ -39,7 +40,6 @@ import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified; import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.util.NotificationUtil; import alpine.common.logging.Logger; import alpine.notification.Notification; @@ -62,35 +62,27 @@ public void run() { Boolean atLeastOneSuccessfulPublish = false; LOGGER.info("Processing notification publishing for scheduled notification rule " + rule.getUuid()); + final ZonedDateTime lastExecutionTime = ZonedDateTime.of(2024, 05, 16, 0, 0, 0, 0, ZoneOffset.UTC); // rule.getLastExecutionTime(); for (NotificationGroup group : rule.getNotifyOn()) { - List affectedProjects = rule.getProjects() == null ? List.of() : qm.detach(rule.getProjects()); - // if rule does not limit to specific projects, get all projects - if (affectedProjects.isEmpty()) { - List allProjects = qm.getProjects().getList(Project.class); - affectedProjects.addAll(qm.detach(allProjects)); - } - - if (!affectedProjects.isEmpty() && rule.isNotifyChildren()) { - extendProjectListWithChildren(affectedProjects); - } + List affectedProjects = List.of(); + affectedProjects = evaluateAffectedProjects(qm, rule); final Notification notificationProxy = new Notification() .scope(rule.getScope()) .group(group) - .title(NotificationUtil.generateNotificationTitle(group, affectedProjects)) .level(rule.getNotificationLevel()); switch (group) { case NEW_VULNERABILITY: - ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(affectedProjects, ZonedDateTime.of(2023, 05, 20, 0, 0, 0, 0, ZoneId.systemDefault())/* rule.getLastExecutionTime() */); + ScheduledNewVulnerabilitiesIdentified vulnSubject = new ScheduledNewVulnerabilitiesIdentified(affectedProjects, lastExecutionTime); if(vulnSubject.getOverview().getNewVulnerabilitiesCount() == 0 && rule.getPublishOnlyWithUpdates()) continue; notificationProxy - .title("[Dependency-Track] Scheduled Notification of Rule “" + rule.getName() + "”") + .title(vulnSubject.getOverview().getNewVulnerabilitiesCount() + " new Vulnerabilities in " + vulnSubject.getOverview().getAffectedComponentsCount() + " components in Scheduled Rule '" + rule.getName() + "'") .content("Find below a summary of new vulnerabilities since " - + rule.getLastExecutionTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - + " in Scheduled Notification Rule “" + rule.getName() + "”.") + + lastExecutionTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + " in Scheduled Notification Rule '" + rule.getName() + "'.") .subject(vulnSubject); break; case POLICY_VIOLATION: @@ -173,14 +165,54 @@ public void run() { } } + private List evaluateAffectedProjects(QueryManager qm, ScheduledNotificationRule rule) { + List affectedProjects; + /* + * TODO: + * To workaround the inconsitent parent-child relationship in projects delivered + * by QueryManager.getAllProjects() (and some other multi-project methods), we + * need to retrieve them one by one by their UUIDs. This way it was empirically + * proven that the parent-child relationship is (more) consistent. + */ + if(rule.getProjects().isEmpty()){ + // if rule does not limit to specific projects, get all projects and their children, if configured + affectedProjects = qm.detach(qm.getAllProjects()) + .stream() + .filter(p -> rule.isNotifyChildren() ? true : p.getParent() == null) + .collect(Collectors.toList()); + } else { + // use projects defined in rule and with children if rule is set to notify children + affectedProjects = qm.detach(rule.getProjects()) + .stream() + .collect(Collectors.toList()); + if (rule.isNotifyChildren()) { + extendProjectListWithChildren(affectedProjects); + } + } + return affectedProjects; + } + private void extendProjectListWithChildren(final List affectedProjects) { var allProjects = List.copyOf(affectedProjects); - for (Project project : allProjects) { - if (project == null || project.getChildren() == null || project.getChildren().isEmpty()) { - continue; + try (var qm = new QueryManager()) { + for (Project project : allProjects) { + if (project == null || project.getChildren() == null || project.getChildren().isEmpty()) { + continue; + } + var parentIndex = affectedProjects.indexOf(project); + var childCounter = 0; + for (Project child : project.getChildren()) { + if (affectedProjects + .stream() + .filter(p -> p.getUuid().equals(child.getUuid())) + .findAny() + .isPresent()) { + continue; + } + childCounter++; + affectedProjects.add(parentIndex + childCounter, child); + } } - var parentIndex = affectedProjects.indexOf(project); - affectedProjects.addAll(++parentIndex, project.getChildren()); } } } From 4465e63d506cc6609845aa7a1d57ad82a98dd15e Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 10 Jun 2024 10:42:33 +0200 Subject: [PATCH 57/98] updated console default publisher and template to support scheduled notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../DefaultNotificationPublishers.java | 2 +- .../publisher/scheduled_console.peb | 10 +++++ .../publisher/scheduled_email.peb | 44 ------------------- 3 files changed, 11 insertions(+), 45 deletions(-) create mode 100644 src/main/resources/templates/notification/publisher/scheduled_console.peb delete mode 100644 src/main/resources/templates/notification/publisher/scheduled_email.peb diff --git a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java index 3df8c7b635..218a3a75c9 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java +++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java @@ -28,7 +28,7 @@ public enum DefaultNotificationPublishers { EMAIL("Email", "Sends notifications to an email address", SendMailPublisher.class, "/templates/notification/publisher/email.peb", MediaType.TEXT_PLAIN, true), SCHEDULED_EMAIL("Scheduled Email", "Sends summarized notifications to an email address in a defined schedule", SendMailPublisher.class, "/templates/notification/publisher/scheduled_email_summary.peb", MediaType.TEXT_HTML, true, true), CONSOLE("Console", "Displays notifications on the system console", ConsolePublisher.class, "/templates/notification/publisher/console.peb", MediaType.TEXT_PLAIN, true), - // SCHEDULED_CONSOLE("Scheduled Console", "Displays summarized notifications on the system console in a defined schedule", ConsolePublisher.class, "/templates/notification/publisher/scheduled_console.peb", MediaType.TEXT_PLAIN, true, true), + SCHEDULED_CONSOLE("Scheduled Console", "Displays summarized notifications on the system console in a defined schedule", ConsolePublisher.class, "/templates/notification/publisher/scheduled_console.peb", MediaType.TEXT_PLAIN, true, true), WEBHOOK("Outbound Webhook", "Publishes notifications to a configurable endpoint", WebhookPublisher.class, "/templates/notification/publisher/webhook.peb", MediaType.APPLICATION_JSON, true), CS_WEBEX("Cisco Webex", "Publishes notifications to a Cisco Webex Teams channel", CsWebexPublisher.class, "/templates/notification/publisher/cswebex.peb", MediaType.APPLICATION_JSON, true), JIRA("Jira", "Creates a Jira issue in a configurable Jira instance and queue", JiraPublisher.class, "/templates/notification/publisher/jira.peb", MediaType.APPLICATION_JSON, true); diff --git a/src/main/resources/templates/notification/publisher/scheduled_console.peb b/src/main/resources/templates/notification/publisher/scheduled_console.peb new file mode 100644 index 0000000000..a3ba5fcba5 --- /dev/null +++ b/src/main/resources/templates/notification/publisher/scheduled_console.peb @@ -0,0 +1,10 @@ +-------------------------------------------------------------------------------- +Notification + -- timestamp: {{ timestamp }} + -- level: {{ notification.level }} + -- scope: {{ notification.scope }} + -- group: {{ notification.group }} + -- title: {{ notification.title }}{% if subject.overview.newVulnerabilitiesCount > 0 %} + -- details:{% for entry in subject.summary.affectedProjectSummaries %}{% if entry.value.newVulnerabilitiesBySeverity|length > 0 %} + -- project: {{ entry.key.name }} {% if entry.key.version %}[{{ entry.key.version }}]{% endif %}{% for newVulnEntry in entry.value.newVulnerabilitiesBySeverity %}{% if newVulnEntry.value > 0 %} + -- {{ newVulnEntry.key }}: {{ newVulnEntry.value }}{% endif %}{% endfor %}{% endif %}{% endfor %}{% endif %} diff --git a/src/main/resources/templates/notification/publisher/scheduled_email.peb b/src/main/resources/templates/notification/publisher/scheduled_email.peb deleted file mode 100644 index b5c3df931a..0000000000 --- a/src/main/resources/templates/notification/publisher/scheduled_email.peb +++ /dev/null @@ -1,44 +0,0 @@ -{{ notification.title }} - -{{ notification.content }} - -=================================================================================================== - -{% if notification.group == "NEW_VULNERABILITY" %} -{% for entry in subject.newProjectVulnerabilitiesBySeverity %} -Project-Name: {{ entry.key.name }} -Project-URL: {{ baseUrl }}/projects/{{ entry.key.uuid }} -{% for severityEntry in entry.value %} - - {{ severityEntry.key }}: {{ severityEntry.value|length }} new vulnerabilities -{% endfor %} -{% endfor %} - ---------------------------------------------------------------------------------------------------- - -Details to all {{ subject.newVulnerabilitiesTotal|length }} new vulnerabilities: -{% for entry in subject.newProjectVulnerabilities %} -------------------------------------------------- -Project-Name: {{ entry.key.name }} -{% for vuln in entry.value %} - -ID: {{ vuln.vulnId }} -Severity: {{ vuln.severity }} -Description: {{ vuln.description }} -Source: {{ vuln.source }} -{% endfor %} -{% endfor %} -{% elseif notification.group == "POLICY_VIOLATION" %} - -New Policy Violations: {{ subject.newPolicyViolationsTotal|length }} -PolicyViolations: -{% for projectEntry in subject.newProjectPolicyViolations %} -{{ projectEntry.key.name }} ({{ projectEntry.key.uuid }}) -{% for policyEntry in projectEntry.value %} -- [{{ policyEntry.type }}] {{ policyEntry.timestamp }} {{ policyEntry.policyCondition }} -{% endfor %} -{% endfor %} -{% endif %} - -=================================================================================================== - -{{ timestamp }} \ No newline at end of file From 30eb2b215b77edbd9a49fcef14c3c47af7c49ab1 Mon Sep 17 00:00:00 2001 From: Max Schiller Date: Mon, 10 Jun 2024 15:29:11 +0200 Subject: [PATCH 58/98] hide details part in mail if no new vulnerabilities were found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Schiller Signed-off-by: Marlon Gäthje --- .../notification/publisher/scheduled_email_summary.peb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb index dda04ffdf8..9d7f4af0ab 100644 --- a/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb +++ b/src/main/resources/templates/notification/publisher/scheduled_email_summary.peb @@ -62,7 +62,7 @@
- +

Summary per project in this rule

@@ -105,7 +105,7 @@ {% endfor %}
-
+ {% if subject.overview.newVulnerabilitiesCount > 0 %}

New vulnerabilities per project

@@ -183,7 +183,7 @@ - {% endif %}{% endfor %}{% endif %} + {% endif %}{% endfor %}{% endif %}{% endif %}