diff --git a/docs/_docs/integrations/notifications.md b/docs/_docs/integrations/notifications.md
index 28c94ed297..b7ac21f48f 100644
--- a/docs/_docs/integrations/notifications.md
+++ b/docs/_docs/integrations/notifications.md
@@ -61,16 +61,18 @@ multiple levels, while others can only ever have a single level.
A notification publisher is a Dependency-Track concept allowing users to describe the structure of a notification (i.e. MIME type, template) and how to send a notification (i.e. publisher class).
The following notification publishers are included by default :
-| Publisher | Description |
-|------------|-----------------------------------------------------|
-| Slack | Publishes notifications to Slack channels |
-| Teams | Publishes notifications to Microsoft Teams channels |
-| Mattermost | Publishes notifications to Mattermost channels |
-| WebEx | Publishes notifications to Cisco WebEx channels |
-| Webhook | Publishes notifications to a configurable endpoint |
-| Email | Sends notifications to an email address |
-| Console | Displays notifications on the system console |
-| Jira | Publishes notifications to Jira |
+| Publisher | Description |
+| ----------------- | ---------------------------------------------------------------------------------------------- |
+| Slack | Publishes notifications to Slack channels |
+| Teams | Publishes notifications to Microsoft Teams channels |
+| Mattermost | Publishes notifications to Mattermost channels |
+| WebEx | Publishes notifications to Cisco WebEx channels |
+| Webhook | Publishes notifications to a configurable endpoint |
+| Email | Sends notifications to an email address |
+| Console | Displays notifications on the system console |
+| Jira | Publishes notifications to Jira |
+| Scheduled Email | Sends a summary of all subscribed events since last notification to an email address |
+| Scheduled Console | Displays a slim summary of all subscribed events since last notification to the system console |
### Templating
@@ -96,7 +98,7 @@ The template context is enhanced with the following variables :
> subject will be present at all times. Some fields are optional since the underlying fields in the datamodel are optional.
> The section below will describe the portfolio notifications in JSON format.
-#### NEW_VULNERABILITY
+#### NEW_VULNERABILITY (per event)
This type of notification will always contain:
* 1 component
* 1 vulnerability
@@ -160,6 +162,96 @@ This type of notification will always contain:
> The `cwe` field is deprecated and will be removed in a later version. Please use `cwes` instead.
+#### NEW_VULNERABILITY (scheduled summary)
+
+```json
+{
+ "notification": {
+ "level": "INFORMATIONAL",
+ "scope": "PORTFOLIO",
+ "group": "NEW_VULNERABILITY",
+ "timestamp": "2024-05-16T23:26:22.961",
+ "title": "123 new Vulnerabilities in 45 components in Scheduled Rule 'ABC'",
+ "content": "Find below a summary of new vulnerabilities since 2024-05-16T00:00:00Z in Scheduled Notification Rule 'ABC'.",
+ "subject": {
+ "overview": {
+ "affectedProjectsCount": 7,
+ "newVulnerabilitiesCount": 123,
+ "affectedComponentsCount": 45,
+ "suppressedNewVulnerabilitiesCount": 0,
+ "newVulnerabilitiesBySeverity": {
+ "CRITICAL": 13,
+ "HIGH": 24,
+ "MEDIUM": 56,
+ "LOW": 10,
+ "INFO": 17,
+ "UNASSIGNED": 3
+ }
+ },
+ "summary": {
+ "projectSummaries": [
+ {
+ "project": {
+ "uuid": "6fb1820f-5280-4577-ac51-40124aabe307",
+ "name": "Acme Example",
+ "version": "1.0.0"
+ },
+ "summary": {
+ "newVulnerabilitiesBySeverity": {
+ "CRITICAL": 3,
+ "HIGH": 4,
+ "LOW": 2,
+ "INFO": 7
+ },
+ "totalProjectVulnerabilitiesBySeverity": {
+ "CRITICAL": 35,
+ "HIGH": 57,
+ "MEDIUM": 13,
+ "LOW": 105,
+ "INFO": 23,
+ "UNASSIGNED": 13
+ },
+ "suppressedNewVulnerabilitiesBySeverity": {
+ "HIGH": 2,
+ "LOW": 5,
+ "INFO": 1
+ }
+ }
+ }
+ ]
+ },
+ "details": {
+ "projectDetails": [
+ {
+ "project": {
+ "uuid": "6fb1820f-5280-4577-ac51-40124aabe307",
+ "name": "Acme Example",
+ "version": "1.0.0"
+ },
+ "findings": [
+ {
+ "componentUuid": "4d0da61c-b462-4895-b296-da0b4bb34744",
+ "componentName": "axis",
+ "componentVersion": "1.4",
+ "componentGroup": "apache",
+ "vulnerabilitySource": "NVD",
+ "vulnerabilityId": "CVE-2012-5784",
+ "vulnerabilitySeverity": "MEDIUM",
+ "analyzer": "OSSINDEX_ANALYZER",
+ "attributionReferenceUrl": "https://ossindex.sonatype.org/vulnerability/CVE-2012-5784",
+ "attributedOn": "2024-05-16T12:34:39Z",
+ "analysisState": "IN_TRIAGE",
+ "suppressed": false
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
#### NEW_VULNERABLE_DEPENDENCY
This type of notification will always contain:
* 1 project
@@ -324,7 +416,7 @@ This type of notification will always contain:
}
```
-#### POLICY_VIOLATION
+#### POLICY_VIOLATION (per event)
```json
{
@@ -368,6 +460,93 @@ This type of notification will always contain:
}
```
+#### POLICY_VIOLATION (scheduled summary)
+
+```json
+{
+ "notification": {
+ "level": "INFORMATIONAL",
+ "scope": "PORTFOLIO",
+ "group": "POLICY_VIOLATION",
+ "timestamp": "2022-05-12T23:07:59.611303",
+ "title": "2 new Policy Violations in 2 components in Scheduled Rule 'Policy Guard'",
+ "content": "Find below a summary of new policy violations since 2022-05-12T00:00:00Z in Scheduled Notification Rule 'Policy Guard'.",
+ "subject": {
+ "overview": {
+ "affectedProjectsCount": 1,
+ "newViolationsCount": 2,
+ "affectedComponentsCount": 2,
+ "suppressedNewViolationsCount": 0,
+ "newViolationsByRiskType": {
+ "LICENSE": 0,
+ "SECURITY": 0,
+ "OPERATIONAL": 2
+ }
+ },
+ "summary": {
+ "affectedProjectSummaries": [
+ {
+ "project": {
+ "uuid": "7a36e5c0-9f09-42dd-b401-360da56c2abe",
+ "name": "Acme Example",
+ "version": "1.0.0"
+ },
+ "summary": {
+ "newViolationsByRiskType": {
+ "OPERATIONAL": 2
+ },
+ "totalProjectViolationsByRiskType": {
+ "LICENSE": 5,
+ "OPERATIONAL": 2
+ },
+ "suppressedNewViolationsByRiskType": {
+ }
+ }
+ }
+ ]
+ }
+ "details": {
+ "projectDetails": [
+ {
+ "project": {
+ "uuid": "7a36e5c0-9f09-42dd-b401-360da56c2abe",
+ "name": "Acme Example",
+ "version": "1.0.0"
+ },
+ "violations": [
+ {
+ "component": {
+ "uuid": "4e04c695-9acd-46fc-9bf6-ed23d7eb551e",
+ "group": "apache",
+ "name": "axis",
+ "version": "1.4"
+ },
+ "violation": {
+ "uuid": "c82fcb50-029a-4636-a657-96242b20680e",
+ "type": "OPERATIONAL",
+ "timestamp": "2022-05-12T20:34:46Z",
+ "policyCondition": {
+ "uuid": "8e5c0a5b-71fb-45c5-afac-6c6a99742cbe",
+ "subject": "COORDINATES",
+ "operator": "MATCHES",
+ "value": "{\"group\":\"apache\",\"name\":\"axis\",\"version\":\"*\"}",
+ "policy": {
+ "uuid": "6d4c7398-689a-4ec7-b5c5-9abb6b5393e9",
+ "name": "Banned Components",
+ "violationState": "FAIL"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
#### USER_CREATED
```json
diff --git a/pom.xml b/pom.xml
index 2c325d6b23..ed45be832d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,6 +100,7 @@
1.4.21.0.19.1.0
+ 3.0.22.1.02.18.02.18.0
@@ -114,17 +115,20 @@
3.2.24.28.32.2.0
+ 2.2.252.1.221.19.01.20.32.35.27.0.0
+ 4.13.21.1.12.1.14.5.145.42.0.161.323
+ 1.4.012.8.1.jre118.2.0
@@ -216,6 +220,12 @@
provided
+
+ jakarta.validation
+ jakarta.validation-api
+ ${lib.jakarta-validation.version}
+
+
com.github.package-urlpackageurl-java
@@ -411,6 +421,12 @@
${lib.org-kohsuke-github-api.version}
+
+ com.asahaf.javacron
+ javacron
+ ${lib.com-asahaf-javacron.version}
+
+
junit
diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java
index db989d7489..5482da61cb 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_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/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java
index 13867227b2..0a8475813a 100644
--- a/src/main/java/org/dependencytrack/model/Finding.java
+++ b/src/main/java/org/dependencytrack/model/Finding.java
@@ -104,6 +104,105 @@ 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 (:includeSuppressed = :true OR "ANALYSIS"."SUPPRESSED" IS NULL OR "ANALYSIS"."SUPPRESSED" = :false)
+ AND "FINDINGATTRIBUTION"."ATTRIBUTED_ON" BETWEEN ? AND ?
+ """;
+
+ // language=SQL
+ public static final String QUERY_ALL_FINDINGS_SINCE = """
+ 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"
+ , "VULNERABILITY"."PUBLISHED"
+ , "PROJECT"."UUID"
+ , "PROJECT"."NAME"
+ , "PROJECT"."VERSION"
+ 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"
+ INNER JOIN "PROJECT"
+ ON "COMPONENT"."PROJECT_ID" = "PROJECT"."ID"
+ WHERE "FINDINGATTRIBUTION"."ATTRIBUTED_ON" BETWEEN :fromDateTime AND :toDateTime
+ AND (:includeSuppressed = :true OR "ANALYSIS"."SUPPRESSED" IS NULL OR "ANALYSIS"."SUPPRESSED" = :false)
+ """;
+
// language=SQL
public static final String QUERY_ALL_FINDINGS = """
SELECT "COMPONENT"."UUID"
@@ -154,7 +253,7 @@ public class Finding implements Serializable {
ON "COMPONENT"."PROJECT_ID" = "PROJECT"."ID"
""";
- private final UUID project;
+ private final UUID projectUuid;
private final Map component = new LinkedHashMap<>();
private final Map vulnerability = new LinkedHashMap<>();
private final Map analysis = new LinkedHashMap<>();
@@ -167,7 +266,7 @@ public class Finding implements Serializable {
* @param o An array of values specific to an individual row returned from {@link #QUERY} or {@link #QUERY_ALL_FINDINGS}
*/
public Finding(UUID project, Object... o) {
- this.project = project;
+ this.projectUuid = project;
optValue(component, "uuid", o[0]);
optValue(component, "name", o[1]);
optValue(component, "group", o[2]);
@@ -222,6 +321,10 @@ public Finding(UUID project, Object... o) {
}
}
+ public UUID getProjectUuid() {
+ return projectUuid;
+ }
+
public Map getComponent() {
return component;
}
@@ -276,7 +379,7 @@ static List getCwes(final Object value) {
}
public String getMatrix() {
- return project.toString() + ":" + component.get("uuid") + ":" + vulnerability.get("uuid");
+ return projectUuid.toString() + ":" + component.get("uuid") + ":" + vulnerability.get("uuid");
}
public void addVulnerabilityAliases(List aliases) {
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/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java
index 9fdad4c536..5cdf76bfac 100644
--- a/src/main/java/org/dependencytrack/model/NotificationRule.java
+++ b/src/main/java/org/dependencytrack/model/NotificationRule.java
@@ -22,6 +22,7 @@
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;
@@ -61,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/PublishTrigger.java b/src/main/java/org/dependencytrack/model/PublishTrigger.java
new file mode 100644
index 0000000000..9e402e63ae
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/PublishTrigger.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+public enum PublishTrigger {
+ ALL,
+ EVENT,
+ SCHEDULE,
+}
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..e3ac5e3f81
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/Rule.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+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
new file mode 100644
index 0000000000..cca4238b20
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/ScheduledNotificationRule.java
@@ -0,0 +1,349 @@
+/*
+ * 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.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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+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.NotPersistent;
+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 jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+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;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+
+/**
+ * Defines a Model class for scheduled notification configurations.
+ */
+@PersistenceCapable
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ScheduledNotificationRule implements Rule, Serializable {
+ private static final long serialVersionUID = 3390485832822256096L;
+
+ @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;
+
+ /*
+ * For standard notifications, this property is used to determine all
+ * notification rules with a level equal to or greater than the specified
+ * notification level.
+ * Only notification rules with the correct rule level with then be published.
+ *
+ * For scheduled notifications, this property is unnecessary because they're
+ * published on-demand or by cron triggers instead through the internal
+ * notification service, so no notification level will be provided for filtering.
+ */
+ @JsonIgnore
+ @NotPersistent
+ 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")
+ @JsonDeserialize(using = TrimmedStringDeserializer.class)
+ private String cronConfig;
+
+ @Persistent(defaultFetchGroup = "true")
+ @Column(name = "LAST_EXECUTION_TIME")
+ @JsonSerialize(using = Iso8601ZonedDateTimeSerializer.class)
+ private ZonedDateTime lastExecutionTime;
+
+ @Persistent
+ @Column(name = "PUBLISH_ONLY_WITH_UPDATES")
+ 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> affectedProjectViolations) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationOverview.java b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationOverview.java
new file mode 100644
index 0000000000..ef951566c3
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationOverview.java
@@ -0,0 +1,35 @@
+/*
+ * 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.policyviolations;
+
+import java.util.Map;
+
+import org.dependencytrack.model.PolicyViolation;
+
+/*
+ * Part of the ScheduledPolicyViolationsIdentified Template Models.
+ * Contains several key metrics to provide an overview of the policy violations identified.
+ */
+public record PolicyViolationOverview(
+ Integer affectedProjectsCount,
+ Integer newViolationsCount,
+ Map newViolationsByRiskType,
+ Integer affectedComponentsCount,
+ Integer suppressedNewViolationsCount) {
+}
diff --git a/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummary.java b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummary.java
new file mode 100644
index 0000000000..582726885f
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummary.java
@@ -0,0 +1,30 @@
+/*
+ * 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.policyviolations;
+
+import java.util.Map;
+
+import org.dependencytrack.model.Project;
+
+/*
+ * Part of the ScheduledPolicyViolationsIdentified Template Models.
+ * Contains packed summarized informations about the identified policy violations, grouped by the affected projects.
+ */
+public record PolicyViolationSummary(Map affectedProjectSummaries) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummaryInfo.java b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummaryInfo.java
new file mode 100644
index 0000000000..aa90c6a8f3
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/policyviolations/PolicyViolationSummaryInfo.java
@@ -0,0 +1,33 @@
+/*
+ * 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.policyviolations;
+
+import java.util.Map;
+
+import org.dependencytrack.model.PolicyViolation;
+
+/*
+ * Part of the ScheduledPolicyViolationsIdentified Template Models.
+ * Contains detailed information about the amount of the identified policy violations grouped by their type.
+ */
+public record PolicyViolationSummaryInfo(
+ Map newViolationsByRiskType,
+ Map totalProjectViolationsByRiskType,
+ Map suppressedNewViolationsByRiskType) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetails.java b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetails.java
new file mode 100644
index 0000000000..70a5156851
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetails.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.scheduled.vulnerabilities;
+
+import java.util.List;
+import java.util.Map;
+
+import org.dependencytrack.model.Project;
+
+/*
+ * Part of the ScheduledNewVulnerabilitiesIdentified Template Models.
+ * Contains packed summarized informations about the new vulnerabilities identified, grouped by the affected projects.
+ */
+public record VulnerabilityDetails(Map> affectedProjectFindings) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetailsInfo.java b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetailsInfo.java
new file mode 100644
index 0000000000..06aabe7d00
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityDetailsInfo.java
@@ -0,0 +1,60 @@
+/*
+ * 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.vulnerabilities;
+
+import java.util.Date;
+import org.dependencytrack.model.Finding;
+import org.dependencytrack.util.ScheduledUtil;
+
+/*
+ * Part of the ScheduledPolicyViolationsIdentified Template Models.
+ * Contains detailed information about the vulnerability/finding it represents.
+ * This class helps to extract the relevant data from the Finding object, to make it easier to use in the template.
+ */
+public record VulnerabilityDetailsInfo(
+ String componentUuid,
+ String componentName,
+ String componentVersion,
+ String componentGroup,
+ String vulnerabilitySource,
+ String vulnerabilityId,
+ String vulnerabilitySeverity,
+ String analyzer,
+ String attributionReferenceUrl,
+ String attributedOn,
+ String analysisState,
+ Boolean suppressed) {
+ public VulnerabilityDetailsInfo(Finding finding) {
+ this(
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getComponent().get("uuid")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getComponent().get("name")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getComponent().get("version")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getComponent().get("group")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getVulnerability().get("source")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getVulnerability().get("vulnId")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getVulnerability().get("severity")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getAttribution().get("analyzerIdentity")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getAttribution().get("referenceUrl")),
+ ScheduledUtil.getDateOrUnknownIfNull((Date) finding.getAttribution().get("attributedOn")),
+ ScheduledUtil.getValueOrEmptyIfNull(finding.getAnalysis().get("state")),
+ finding.getAnalysis().get("isSuppressed") instanceof Boolean
+ ? (Boolean) finding.getAnalysis().get("isSuppressed")
+ : false);
+ }
+}
diff --git a/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityOverview.java b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityOverview.java
new file mode 100644
index 0000000000..813428f5bd
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilityOverview.java
@@ -0,0 +1,34 @@
+/*
+ * 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.vulnerabilities;
+
+import java.util.Map;
+import org.dependencytrack.model.Severity;
+
+/*
+ * Part of the ScheduledNewVulnerabilitiesIdentified Template Models.
+ * Contains several key metrics to provide an overview of the new vulnerabilities identified.
+ */
+public record VulnerabilityOverview(
+ Integer affectedProjectsCount,
+ Integer newVulnerabilitiesCount,
+ Map newVulnerabilitiesBySeverity,
+ Integer affectedComponentsCount,
+ Integer suppressedNewVulnerabilitiesCount) {
+}
diff --git a/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummary.java b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummary.java
new file mode 100644
index 0000000000..5b7ffcfbec
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummary.java
@@ -0,0 +1,30 @@
+/*
+ * 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.vulnerabilities;
+
+import java.util.Map;
+
+import org.dependencytrack.model.Project;
+
+/*
+ * Part of the ScheduledNewVulnerabilitiesIdentified Template Models.
+ * Contains packed summarized informations about the identified new vulnerabilities, grouped by the affected projects.
+ */
+public record VulnerabilitySummary(Map affectedProjectSummaries) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummaryInfo.java b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummaryInfo.java
new file mode 100644
index 0000000000..abc9719108
--- /dev/null
+++ b/src/main/java/org/dependencytrack/model/scheduled/vulnerabilities/VulnerabilitySummaryInfo.java
@@ -0,0 +1,33 @@
+/*
+ * 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.vulnerabilities;
+
+import java.util.Map;
+
+import org.dependencytrack.model.Severity;
+
+/*
+ * Part of the ScheduledNewVulnerabilitiesIdentified Template Models.
+ * Contains detailed information about the amount of the identified new vulnerabilities grouped by their severity.
+ */
+public record VulnerabilitySummaryInfo(
+ Map newVulnerabilitiesBySeverity,
+ Map totalProjectVulnerabilitiesBySeverity,
+ Map suppressedNewVulnerabilitiesBySeverity) {
+}
diff --git a/src/main/java/org/dependencytrack/notification/NotificationConstants.java b/src/main/java/org/dependencytrack/notification/NotificationConstants.java
index 83e78c5339..f3f9165407 100644
--- a/src/main/java/org/dependencytrack/notification/NotificationConstants.java
+++ b/src/main/java/org/dependencytrack/notification/NotificationConstants.java
@@ -39,6 +39,7 @@ public static class Title {
public static final String REPO_ERROR = "Repository Error";
public static final String ANALYZER_ERROR = "Analyzer Error";
public static final String INTEGRATION_ERROR = "Integration Error";
+ public static final String NEW_POLICY_VIOLATION = "New Policy Violation Identified";
public static final String NEW_VULNERABILITY = "New Vulnerability Identified";
public static final String NEW_VULNERABLE_DEPENDENCY = "Vulnerable Dependency Introduced";
public static final String ANALYSIS_DECISION_EXPLOITABLE = "Analysis Decision: Exploitable";
diff --git a/src/main/java/org/dependencytrack/notification/ScheduledNotificationFactory.java b/src/main/java/org/dependencytrack/notification/ScheduledNotificationFactory.java
new file mode 100644
index 0000000000..a7c7d98a93
--- /dev/null
+++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationFactory.java
@@ -0,0 +1,281 @@
+/*
+ * 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.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.dependencytrack.model.Finding;
+import org.dependencytrack.model.PolicyViolation;
+import org.dependencytrack.model.Project;
+import org.dependencytrack.model.ScheduledNotificationRule;
+import org.dependencytrack.model.Severity;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationDetails;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationOverview;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationSummary;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationSummaryInfo;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilityDetails;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilityDetailsInfo;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilityOverview;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilitySummary;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilitySummaryInfo;
+import org.dependencytrack.notification.vo.ScheduledNewVulnerabilitiesIdentified;
+import org.dependencytrack.notification.vo.ScheduledPolicyViolationsIdentified;
+import org.dependencytrack.persistence.QueryManager;
+
+public class ScheduledNotificationFactory {
+ public static ScheduledNewVulnerabilitiesIdentified CreateScheduledVulnerabilitySubject(ScheduledNotificationRule rule, ZonedDateTime lastExecution) {
+ Map> affectedProjectFindings = new LinkedHashMap<>();
+
+ try (var qm = new QueryManager()) {
+ var findings = qm.getAllFindingsSince(true, lastExecution.withZoneSameInstant(ZoneOffset.UTC));
+
+ for (Finding finding : findings) {
+ var findingProject = qm.getProject(finding.getProjectUuid().toString());
+
+ if (!checkIfProjectShallBeProcessed(findingProject, rule))
+ continue;
+
+ var entry = affectedProjectFindings.get(findingProject);
+ if (entry == null) {
+ ArrayList initial = new ArrayList();
+ initial.add(finding);
+ affectedProjectFindings.put(findingProject, initial);
+ } else {
+ entry.add(finding);
+ }
+ }
+ }
+
+ var overview = createVulnerabilityOverview(affectedProjectFindings);
+ var summary = createVulnerabilitySummary(affectedProjectFindings);
+ var details = createVulnerabilityDetails(affectedProjectFindings);
+
+ return new ScheduledNewVulnerabilitiesIdentified(overview, summary, details);
+ }
+
+ public static ScheduledPolicyViolationsIdentified CreateScheduledPolicyViolationSubject(ScheduledNotificationRule rule, ZonedDateTime lastExecution) {
+ Map> affectedProjectViolations = new LinkedHashMap<>();
+
+ try (var qm = new QueryManager()){
+ var violations = qm.getAllPolicyViolationsSince(true, lastExecution);
+
+ for (PolicyViolation violation : violations) {
+ if (!checkIfProjectShallBeProcessed(violation.getProject(), rule))
+ continue;
+
+ var entry = affectedProjectViolations.get(violation.getProject());
+ if (entry == null) {
+ ArrayList initial = new ArrayList();
+ initial.add(violation);
+ affectedProjectViolations.put(violation.getProject(), initial);
+ } else {
+ entry.add(violation);
+ }
+ }
+ }
+
+ var overview = createPolicyOverview(affectedProjectViolations);
+ var summary = createPolicySummary(affectedProjectViolations);
+ var details = createPolicyDetails(affectedProjectViolations);
+
+ return new ScheduledPolicyViolationsIdentified(overview, summary, details);
+ }
+
+ private static boolean checkIfProjectShallBeProcessed(Project project, ScheduledNotificationRule rule) {
+ if (rule.getProjects() == null || rule.getProjects().isEmpty()) {
+ return true;
+ }
+ for (final Project ruleProject : rule.getProjects()) {
+ var projectIsMatch = project.getUuid().equals(ruleProject.getUuid());
+ var considerChildren = Boolean.TRUE.equals(rule.isNotifyChildren()
+ && checkIfChildrenAreAffected(ruleProject, project.getUuid()));
+ if (projectIsMatch || considerChildren) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean checkIfChildrenAreAffected(Project parent, UUID uuid) {
+ boolean isChild = false;
+ if (parent.getChildren() == null || parent.getChildren().isEmpty()) {
+ return false;
+ }
+ for (Project child : parent.getChildren()) {
+ final boolean isChildActive = child.isActive() == null || child.isActive();
+ if ((child.getUuid().equals(uuid) && isChildActive) || isChild) {
+ return true;
+ }
+ isChild = checkIfChildrenAreAffected(child, uuid);
+ }
+ return isChild;
+ }
+
+ private static VulnerabilityOverview createVulnerabilityOverview(Map> affectedProjectFindings) {
+ Integer affectedProjectsCount;
+ Integer newVulnerabilitiesCount = 0;
+ Map newVulnerabilitiesBySeverity = new EnumMap<>(Severity.class);
+ Integer affectedComponentsCount = 0;
+ Integer suppressedNewVulnerabilitiesCount = 0;
+
+ for (Severity severity : Severity.values()) {
+ newVulnerabilitiesBySeverity.put(severity, 0);
+ }
+
+ try (var qm = new QueryManager()) {
+ for (var findings : affectedProjectFindings.values()) {
+ for (Finding finding : findings) {
+ if(finding.getComponent() != null)
+ affectedComponentsCount++;
+
+ if (finding.getAnalysis().get("isSuppressed") instanceof Boolean suppressed) {
+ if (suppressed) {
+ suppressedNewVulnerabilitiesCount++;
+ } else {
+ newVulnerabilitiesCount++;
+ newVulnerabilitiesBySeverity.merge(
+ Enum.valueOf(Severity.class, finding.getVulnerability().get("severity").toString()),
+ 1, Integer::sum);
+ }
+ }
+ }
+ }
+ }
+
+ affectedProjectsCount = affectedProjectFindings.size();
+
+ return new VulnerabilityOverview(affectedProjectsCount, newVulnerabilitiesCount, newVulnerabilitiesBySeverity, affectedComponentsCount, suppressedNewVulnerabilitiesCount);
+ }
+
+ private static VulnerabilitySummary createVulnerabilitySummary(Map> affectedProjectFindings) {
+ Map projectSummaryInfoMap = new LinkedHashMap<>();
+ try (var qm = new QueryManager()) {
+ for (var entry : affectedProjectFindings.entrySet()) {
+ var totalProjectFindings = qm.getFindings(entry.getKey());
+ projectSummaryInfoMap.put(entry.getKey(), createVulnerabilitySummaryInfo(entry.getValue(), totalProjectFindings));
+
+ }
+ }
+ return new VulnerabilitySummary(projectSummaryInfoMap);
+ }
+
+ private static VulnerabilitySummaryInfo createVulnerabilitySummaryInfo(List newFindings, List totalProjectFindings){
+ Map newVulnerabilitiesBySeverity = new EnumMap<>(Severity.class);
+ Map totalProjectVulnerabilitiesBySeverity = new EnumMap<>(Severity.class);
+ Map suppressedNewVulnerabilitiesBySeverity = new EnumMap<>(Severity.class);
+
+ try (var qm = new QueryManager()) {
+ for (Finding finding : newFindings) {
+ if (finding.getAnalysis().get("isSuppressed") instanceof Boolean suppressed) {
+ var severity = Enum.valueOf(Severity.class, finding.getVulnerability().get("severity").toString());
+ if (suppressed) {
+ suppressedNewVulnerabilitiesBySeverity.merge(severity, 1, Integer::sum);
+ } else {
+ newVulnerabilitiesBySeverity.merge(severity, 1, Integer::sum);
+ }
+ }
+ }
+ for (Finding finding : totalProjectFindings) {
+ var severity = Enum.valueOf(Severity.class, finding.getVulnerability().get("severity").toString());
+ totalProjectVulnerabilitiesBySeverity.merge(severity, 1, Integer::sum);
+ }
+ }
+ return new VulnerabilitySummaryInfo(newVulnerabilitiesBySeverity, totalProjectVulnerabilitiesBySeverity, suppressedNewVulnerabilitiesBySeverity);
+ }
+
+ private static VulnerabilityDetails createVulnerabilityDetails(Map> affectedProjectFindings) {
+ Map> projectDetailsInfoMap = new LinkedHashMap<>();
+ for (var entry : affectedProjectFindings.entrySet()) {
+ projectDetailsInfoMap.put(entry.getKey(), entry.getValue().stream().map(f -> createVulnerabilityDetailsInfo(f)).toList());
+ }
+ return new VulnerabilityDetails(projectDetailsInfoMap);
+ }
+
+ private static VulnerabilityDetailsInfo createVulnerabilityDetailsInfo(Finding finding) {
+ return new VulnerabilityDetailsInfo(finding);
+ }
+
+ private static PolicyViolationOverview createPolicyOverview(Map> affectedProjectViolations) {
+ var affectedComponentsCount = 0;
+ var affectedProjectsCount = 0;
+ var newViolationsCount = 0;
+ var suppressedNewViolationsCount = 0;
+ Map newViolationsByRiskType = new EnumMap<>(PolicyViolation.Type.class);
+
+ for (PolicyViolation.Type riskType : PolicyViolation.Type.values()) {
+ newViolationsByRiskType.put(riskType, 0);
+ }
+
+ try (var qm = new QueryManager()) {
+ for (var violations : affectedProjectViolations.values()) {
+ affectedProjectsCount++;
+ for (PolicyViolation violation : violations) {
+ if(violation.getComponent() != null)
+ affectedComponentsCount++;
+
+ var analysis = violation.getAnalysis();
+ if (analysis != null && analysis.isSuppressed()) {
+ suppressedNewViolationsCount++;
+ } else {
+ newViolationsCount++;
+ newViolationsByRiskType.merge(violation.getType(), 1, Integer::sum);
+ }
+ }
+ }
+ }
+
+ return new PolicyViolationOverview(affectedProjectsCount, newViolationsCount, newViolationsByRiskType, affectedComponentsCount, suppressedNewViolationsCount);
+ }
+
+ private static PolicyViolationSummary createPolicySummary(Map> affectedProjectViolations) {
+ Map affectedProjectSummaries = new LinkedHashMap<>();
+ for (var entry : affectedProjectViolations.entrySet()) {
+ affectedProjectSummaries.put(entry.getKey(), createPolicySummaryInfo(entry.getValue()));
+ }
+ return new PolicyViolationSummary(affectedProjectSummaries);
+ }
+
+ private static PolicyViolationSummaryInfo createPolicySummaryInfo(List violations) {
+ Map newViolationsByRiskType = new EnumMap<>(PolicyViolation.Type.class);
+ Map totalProjectViolationsByRiskType = new EnumMap<>(PolicyViolation.Type.class);
+ Map suppressedNewViolationsByRiskType = new EnumMap<>(PolicyViolation.Type.class);
+
+ for (PolicyViolation violation : violations) {
+ var analysis = violation.getAnalysis();
+ if (analysis != null && analysis.isSuppressed()) {
+ suppressedNewViolationsByRiskType.merge(violation.getType(), 1, Integer::sum);
+ } else {
+ newViolationsByRiskType.merge(violation.getType(), 1, Integer::sum);
+ }
+ totalProjectViolationsByRiskType.merge(violation.getType(), 1, Integer::sum);
+ }
+
+ return new PolicyViolationSummaryInfo(newViolationsByRiskType, totalProjectViolationsByRiskType, suppressedNewViolationsByRiskType);
+ }
+
+ private static PolicyViolationDetails createPolicyDetails(Map> affectedProjectViolations) {
+ return new PolicyViolationDetails(affectedProjectViolations);
+ }
+}
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..75755dd570
--- /dev/null
+++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskInitializer.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;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.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;
+
+/*
+ * Initializes the scheduled notification task service.
+ * It schedules the task executions for each enabled scheduled notification rule on startup, based on their cron configuration.
+ */
+public class ScheduledNotificationTaskInitializer implements ServletContextListener {
+ private static final Logger LOGGER = Logger.getLogger(ScheduledNotificationTaskInitializer.class);
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ LOGGER.info("Initializing scheduled notification task service");
+ try (var qm = new QueryManager()) {
+ var scheduledRulesList = qm.getScheduledNotificationRules().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) {
+ LOGGER.info("Shutting down scheduled notification task service");
+ 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..59fd184aa1
--- /dev/null
+++ b/src/main/java/org/dependencytrack/notification/ScheduledNotificationTaskManager.java
@@ -0,0 +1,75 @@
+/*
+ * 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.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;
+
+/*
+ * Holds all scheduled notification tasks to be able to cancel them if needed.
+ * This may be necessary if a rule is deleted or disabled.
+ */
+public final class ScheduledNotificationTaskManager {
+ 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), actionAfterTaskCompletion);
+
+ var future = scheduledExecutor.schedule(
+ futureTask,
+ customDelay,
+ TimeUnit.MILLISECONDS);
+ SCHEDULED_NOTIFY_TASKS.put(ruleUuid, future);
+ }
+
+ public static void cancelActiveRuleTask(UUID ruleUuid) {
+ if (SCHEDULED_NOTIFY_TASKS.containsKey(ruleUuid)) {
+ SCHEDULED_NOTIFY_TASKS.get(ruleUuid).cancel(true);
+ SCHEDULED_NOTIFY_TASKS.remove(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/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java
index 3c5ee7c262..218a3a75c9 100644
--- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java
+++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java
@@ -25,8 +25,10 @@ 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),
+ 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),
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 extends Publisher> 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 extends Publisher> 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/notification/publisher/PublishContext.java b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java
index e60a1e9287..3e26f86113 100644
--- a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java
+++ b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java
@@ -21,12 +21,15 @@
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;
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;
@@ -101,6 +104,10 @@ 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.summary().affectedProjectSummaries().keySet().stream().map(Project::convert).toList());
+ } else if (notification.getSubject() instanceof final ScheduledPolicyViolationsIdentified subject) {
+ notificationSubjects.put(SUBJECT_PROJECTS, subject.summary().affectedProjectSummaries().keySet().stream().map(Project::convert).toList());
}
return new PublishContext(notification.getGroup(), Optional.ofNullable(notification.getLevel()).map(Enum::name).orElse(null),
@@ -114,7 +121,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());
}
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/publisher/SendMailPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java
index c83c38fb65..e318f70f8c 100644
--- a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java
+++ b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java
@@ -60,6 +60,8 @@ public class SendMailPublisher implements Publisher {
private static final Logger LOGGER = Logger.getLogger(SendMailPublisher.class);
private static final PebbleEngine ENGINE = new PebbleEngine.Builder().newLineTrimming(false).build();
+ private static final String TEXT_HTML_UTF8 = "text/html; charset=UTF-8";
+ private static final String TEXT_PLAIN_UTF8 = "text/plain; charset=UTF-8";
public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) {
if (config == null) {
@@ -147,7 +149,7 @@ private void sendNotification(final PublishContext ctx, Notification notificatio
.to(destinations)
.subject(emailSubjectPrefix + " " + notification.getTitle())
.body(mimeType == MediaType.TEXT_HTML ? StringEscapeUtils.escapeHtml4(unescapedContent): unescapedContent)
- .bodyMimeType(mimeType)
+ .bodyMimeType(mimeType == MediaType.TEXT_HTML ? TEXT_HTML_UTF8 : TEXT_PLAIN_UTF8)
.host(smtpHostname)
.port(smtpPort)
.username(smtpUser)
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..2adea78718
--- /dev/null
+++ b/src/main/java/org/dependencytrack/notification/vo/ScheduledNewVulnerabilitiesIdentified.java
@@ -0,0 +1,34 @@
+/*
+ * 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 org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilityDetails;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilityOverview;
+import org.dependencytrack.model.scheduled.vulnerabilities.VulnerabilitySummary;
+
+/**
+ * Main part of the ScheduledNewVulnerabilitiesIdentified Template Models.
+ * Contains the separate parts used in the template to display the new
+ * vulnerabilities identified since the last notification.
+ */
+public record ScheduledNewVulnerabilitiesIdentified(
+ VulnerabilityOverview overview,
+ VulnerabilitySummary summary,
+ VulnerabilityDetails details) {
+}
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..2932f5beb5
--- /dev/null
+++ b/src/main/java/org/dependencytrack/notification/vo/ScheduledPolicyViolationsIdentified.java
@@ -0,0 +1,34 @@
+/*
+ * 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 org.dependencytrack.model.scheduled.policyviolations.PolicyViolationDetails;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationOverview;
+import org.dependencytrack.model.scheduled.policyviolations.PolicyViolationSummary;
+
+/**
+ * Main part of the ScheduledPolicyViolationsIdentified Template Models.
+ * Contains the separate parts used in the template to display the new policy
+ * violations identified since the last notification.
+ */
+public record ScheduledPolicyViolationsIdentified(
+ PolicyViolationOverview overview,
+ PolicyViolationSummary summary,
+ PolicyViolationDetails details) {
+}
\ No newline at end of file
diff --git a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java
index d56394f2d1..53134c5504 100644
--- a/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java
+++ b/src/main/java/org/dependencytrack/persistence/FindingsQueryManager.java
@@ -37,11 +37,15 @@
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;
import java.util.Map;
import java.util.Objects;
+import java.util.UUID;
import java.util.stream.Collectors;
public class FindingsQueryManager extends QueryManager implements IQueryManager {
@@ -256,9 +260,8 @@ 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);
+ return getFindings(project, false, null);
}
/**
@@ -267,22 +270,126 @@ public List getFindings(Project project) {
* @param includeSuppressed determines if suppressed vulnerabilities should be included or not
* @return a List of Finding objects
*/
- @SuppressWarnings("unchecked")
public List getFindings(Project project, boolean includeSuppressed) {
- final Query