diff --git a/docs/_docs/integrations/notifications.md b/docs/_docs/integrations/notifications.md index 9172ca09d..c590391c4 100644 --- a/docs/_docs/integrations/notifications.md +++ b/docs/_docs/integrations/notifications.md @@ -9,34 +9,52 @@ Dependency-Track includes a robust and configurable notification framework capab to the presence of newly discovered vulnerabilities, previously known vulnerable components that are added to projects, as well as providing notifications on various system and error conditions. +## Scopes -Dependency-Track notifications come in two flavors: +Dependency-Track notifications come in two flavors (scopes): -| Scope | Description | -| ------|-------------| -| SYSTEM | Notifications on system-level informational and error conditions | +| Scope | Description | +|-----------|---------------------------------------------------------------------------------------| +| SYSTEM | Notifications on system-level informational and error conditions | | PORTFOLIO | Notifications on objects in the portfolio such as vulnerabilities and audit decisions | +## Levels -Each scope contains a set of notification groups that can be used to subscribe to. - -| Scope | Group | Description | -| ------|-------|-----------------------------------------------------------------------------------------------------------------------------------| -| SYSTEM | ANALYZER | Notifications generated as a result of interacting with an external source of vulnerability intelligence | -| SYSTEM | DATASOURCE_MIRRORING | Notifications generated when performing mirroring of one of the supported datasources such as the NVD | -| SYSTEM | INDEXING_SERVICE | Notifications generated as a result of performing maintenance on Dependency-Tracks internal index used for global searching | -| SYSTEM | FILE_SYSTEM | Notifications generated as a result of a file system operation. These are typically only generated on error conditions | -| SYSTEM | REPOSITORY | Notifications generated as a result of interacting with one of the supported repositories such as Maven Central, RubyGems, or NPM | -| PORTFOLIO | NEW_VULNERABILITY | Notifications generated whenever a new vulnerability is identified | -| PORTFOLIO | NEW_VULNERABLE_DEPENDENCY | Notifications generated as a result of a vulnerable component becoming a dependency of a project | -| PORTFOLIO | GLOBAL_AUDIT_CHANGE | Notifications generated whenever an analysis or suppression state has changed on a finding from a component (global) | -| PORTFOLIO | PROJECT_AUDIT_CHANGE | Notifications generated whenever an analysis or suppression state has changed on a finding from a project | -| PORTFOLIO | BOM_CONSUMED | Notifications generated whenever a supported BOM is ingested and identified | -| PORTFOLIO | BOM_PROCESSED | Notifications generated after a supported BOM is ingested, identified, and successfully processed | -| PORTFOLIO | BOM_PROCESSING_FAILED | Notifications generated whenever a BOM upload process fails | -| PORTFOLIO | POLICY_VIOLATION | Notifications generated whenever a policy violation is identified | +Notifications can have one of three possible levels: + +* INFORMATIONAL +* WARNING +* ERROR + +Notification levels behave identical to logging levels: + +* Configuring a rule for level INFORMATIONAL will match notifications of level INFORMATIONAL, WARNING, and ERROR +* Configuring a rule for level WARNING will match notifications of level WARNING and ERROR +* Configuring a rule for level ERROR will match notifications of level ERROR + +## Groups + +Each scope contains a set of notification groups that can be subscribed to. Some groups contain notifications of +multiple levels, while others can only ever have a single level. + +| Scope | Group | Level(s) | Description | +|-----------|---------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------| +| SYSTEM | ANALYZER | (Any) | Notifications generated as a result of interacting with an external source of vulnerability intelligence | +| SYSTEM | DATASOURCE_MIRRORING | (Any) | Notifications generated when performing mirroring of one of the supported datasources such as the NVD | +| SYSTEM | INDEXING_SERVICE | (Any) | Notifications generated as a result of performing maintenance on Dependency-Tracks internal index used for global searching | +| SYSTEM | FILE_SYSTEM | (Any) | Notifications generated as a result of a file system operation. These are typically only generated on error conditions | +| SYSTEM | REPOSITORY | (Any) | Notifications generated as a result of interacting with one of the supported repositories such as Maven Central, RubyGems, or NPM | +| PORTFOLIO | NEW_VULNERABILITY | INFORMATIONAL | Notifications generated whenever a new vulnerability is identified | +| PORTFOLIO | NEW_VULNERABLE_DEPENDENCY | INFORMATIONAL | Notifications generated as a result of a vulnerable component becoming a dependency of a project | +| PORTFOLIO | GLOBAL_AUDIT_CHANGE | INFORMATIONAL | Notifications generated whenever an analysis or suppression state has changed on a finding from a component (global) | +| PORTFOLIO | PROJECT_AUDIT_CHANGE | INFORMATIONAL | Notifications generated whenever an analysis or suppression state has changed on a finding from a project | +| PORTFOLIO | BOM_CONSUMED | INFORMATIONAL | Notifications generated whenever a supported BOM is ingested and identified | +| PORTFOLIO | BOM_PROCESSED | INFORMATIONAL | Notifications generated after a supported BOM is ingested, identified, and successfully processed | +| PORTFOLIO | BOM_PROCESSING_FAILED | ERROR | Notifications generated whenever a BOM upload process fails | +| PORTFOLIO | POLICY_VIOLATION | INFORMATIONAL | Notifications generated whenever a policy violation is identified | ## Configuring Publishers + 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 : @@ -59,7 +77,7 @@ The template context is enhanced with the following variables : | Variable | Type | Description | |------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------| | timestampEpochSecond | long | The notification timestamp | -| timestamp | string | The notification local date time in ISO 8601 format (i.e. uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS) | +| timestamp | string | The notification local date time in ISO 8601 format (i.e. uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS) | | notification.level | enum | One of INFORMATIONAL, WARNING, or ERROR | | notification.scope | string | The high-level type of notification. One of SYSTEM or PORTFOLIO | | notification.group | string | The specific type of notification | @@ -69,7 +87,7 @@ The template context is enhanced with the following variables : | notification.subject | Object | An optional object containing specifics of the notification | | baseUrl | string | Dependency Track base url | | subject | Specific | An optional object containing specifics of the notification. It is casted whereas notification.subject is a generic Object | -| subjectJson | javax.json.JsonObject | An optional JSON representation of the subject | +| subjectJson | javax.json.JsonObject | An optional JSON representation of the subject | > The format of the subject object will vary depending on the scope and group of notification. Not all fields in the > subject will be present at all times. Some fields are optional since the underlying fields in the datamodel are optional. @@ -419,4 +437,40 @@ by optionally limiting the projects. Expand the 'Limit To' button to reveal and With outbound webhooks, notifications and all of their relevant details can be delivered via HTTP to an endpoint configured through Dependency-Track's notification settings. -Notifications are sent via HTTP(S) POST and contain a JSON payload. The payload has the format described above in the templating section. \ No newline at end of file +Notifications are sent via HTTP(S) POST and contain a JSON payload. The payload has the format described above in the templating section. + +## Debugging missing notifications + +Missing notifications may be caused by a variety of issues: + +* Network outage between Dependency-Track and notification destination +* Faulty proxy configuration, causing Dependency-Track to be unable to reach the notification destination +* Misconfiguration of notification rules in Dependency-Track, causing the notification to not be sent +* Bug in Dependency-Track's notification routing mechanism, causing the notification to not be sent +* Syntactically invalid notification content, causing the destination system to fail upon parsing it + +Generally, when Dependency-Track *fails* to deliver a notification to the destination, it will emit log messages +with level `WARN` or `ERROR` about it. + +As of Dependency-Track v4.10, notification rules can additionally be configured to emit a log message with level `INFO` +when publishing *succeeded*. Other than for debugging missing notifications, enabling this may also be useful in cases +where notification volume needs to be audited or monitored. Note that this can cause a significant increase in log +output, depending on how busy the system is. + +Logs include high-level details about the notification itself, its subjects, as well as the matched rule. For example: + +``` +INFO [WebhookPublisher] Destination acknowledged reception of notification with status code 200 (PublishContext{notificationGroup=NEW_VULNERABILITY, notificationLevel=INFORMATIONAL, notificationScope=PORTFOLIO, notificationTimestamp=2023-11-20T19:14:43.427901Z, notificationSubjects={component=Component[uuid=9f608f76-382c-4e05-b05f-7f69f2f6f507, group=org.apache.commons, name=commons-compress, version=1.23.0], projects=[Project[uuid=79de8ff7-6929-4fa4-8bff-ddec2424cbd2, name=Acme App, version=1.2.3]], vulnerability=Vulnerability[id=GHSA-cgwf-w82q-5jrr, source=GITHUB]}, ruleName=Foo, ruleScope=PORTFOLIO, ruleLevel=INFORMATIONAL}) +``` + +For Webhook-based notifications (*Outbound Webhook*, *Slack*, *MS Teams*, *Mattermost*, *Cisco WebEx*, *JIRA*), +services like [Request Bin](https://pipedream.com/requestbin) can be used to manually verify that notifications are sent: + +* Create a (private) Request Bin at https://pipedream.com/requestbin +* Copy the generated endpoint URL to the *Destination* field of the notification rule +* Ensure the desired *Groups* are selected for the notification rule +* Perform an action that triggers any of the selected groups + * e.g. for group `BOM_PROCESSED`, upload a BOM +* Observe the Request Bin output for any incoming requests + +If requests make it to the Bin, the problem is not in Dependency-Track. diff --git a/docs/_posts/2023-xx-xx-v4.10.0.md b/docs/_posts/2023-xx-xx-v4.10.0.md index 0fd55e802..d94765d87 100644 --- a/docs/_posts/2023-xx-xx-v4.10.0.md +++ b/docs/_posts/2023-xx-xx-v4.10.0.md @@ -8,6 +8,7 @@ type: major * Add support for mirroring the NVD via its REST API - [apiserver/#3175] * Refer to the [NVD datasource documentation] for details * Improve efficiency of search index operations - [apiserver/#3116] +* Add option to emit log for successfully published notifications, and improve logging around notifications in general - [apiserver/#3211] **Fixes:** @@ -59,5 +60,6 @@ TBD [apiserver/#3117]: https://github.com/DependencyTrack/dependency-track/issues/3117 [apiserver/#3175]: https://github.com/DependencyTrack/dependency-track/pull/3175 [apiserver/#3209]: https://github.com/DependencyTrack/dependency-track/pull/3209 +[apiserver/#3211]: https://github.com/DependencyTrack/dependency-track/pull/3211 [NVD datasource documentation]: {{ site.baseurl }}{% link _docs/datasources/nvd.md %}#mirroring-via-nvd-rest-api \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/model/NotificationRule.java b/src/main/java/org/dependencytrack/model/NotificationRule.java index cfbe2aa06..731cd1e74 100644 --- a/src/main/java/org/dependencytrack/model/NotificationRule.java +++ b/src/main/java/org/dependencytrack/model/NotificationRule.java @@ -89,6 +89,18 @@ public class NotificationRule implements Serializable { @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 @@ -169,6 +181,14 @@ 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; diff --git a/src/main/java/org/dependencytrack/notification/NotificationRouter.java b/src/main/java/org/dependencytrack/notification/NotificationRouter.java index 5d9d6decf..033c7fea5 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationRouter.java +++ b/src/main/java/org/dependencytrack/notification/NotificationRouter.java @@ -26,6 +26,7 @@ import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Project; +import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.notification.vo.AnalysisDecisionChange; @@ -51,12 +52,18 @@ import java.util.UUID; import java.util.stream.Collectors; +import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_KEY; +import static org.dependencytrack.notification.publisher.Publisher.CONFIG_TEMPLATE_MIME_TYPE_KEY; + public class NotificationRouter implements Subscriber { private static final Logger LOGGER = Logger.getLogger(NotificationRouter.class); public void inform(final Notification notification) { - for (final NotificationRule rule: resolveRules(notification)) { + final PublishContext ctx = PublishContext.from(notification); + + for (final NotificationRule rule : resolveRules(ctx, notification)) { + final PublishContext ruleCtx = ctx.withRule(rule); // Not all publishers need configuration (i.e. ConsolePublisher) JsonObject config = Json.createObjectBuilder().build(); @@ -65,40 +72,39 @@ public void inform(final Notification notification) { 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", e); + LOGGER.error("An error occurred while preparing the configuration for the notification publisher (%s)".formatted(ruleCtx), e); } } try { NotificationPublisher notificationPublisher = rule.getPublisher(); final Class publisherClass = Class.forName(notificationPublisher.getPublisherClass()); if (Publisher.class.isAssignableFrom(publisherClass)) { - final Publisher publisher = (Publisher)publisherClass.getDeclaredConstructor().newInstance(); + final Publisher publisher = (Publisher) publisherClass.getDeclaredConstructor().newInstance(); JsonObject notificationPublisherConfig = Json.createObjectBuilder() - .add(Publisher.CONFIG_TEMPLATE_MIME_TYPE_KEY, notificationPublisher.getTemplateMimeType()) - .add(Publisher.CONFIG_TEMPLATE_KEY, notificationPublisher.getTemplate()) - .addAll(Json.createObjectBuilder(config)) - .build(); - if (publisherClass != SendMailPublisher.class || rule.getTeams().isEmpty() || rule.getTeams() == null){ - publisher.inform(restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig); + .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(notification, rule), notificationPublisherConfig); } else { - ((SendMailPublisher)publisher).inform(restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig, rule.getTeams()); + ((SendMailPublisher) publisher).inform(ruleCtx, restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig, rule.getTeams()); } - - } else { - LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName()); + 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", e); + } 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 occured during the publication of the notification", publisherException); + LOGGER.error("An error occurred during the publication of the notification (%s)".formatted(ruleCtx), publisherException); } } } - public Notification restrictNotificationToRuleProjects(Notification initialNotification, NotificationRule rule) { + public Notification restrictNotificationToRuleProjects(final Notification initialNotification, final NotificationRule rule) { Notification restrictedNotification = initialNotification; - if(canRestrictNotificationToRuleProjects(initialNotification, rule)) { + 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()); @@ -107,7 +113,7 @@ public Notification restrictNotificationToRuleProjects(Notification initialNotif restrictedNotification.setContent(initialNotification.getContent()); restrictedNotification.setTitle(initialNotification.getTitle()); restrictedNotification.setTimestamp(initialNotification.getTimestamp()); - if(initialNotification.getSubject() instanceof final NewVulnerabilityIdentified subject) { + 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); @@ -116,19 +122,19 @@ public Notification restrictNotificationToRuleProjects(Notification initialNotif return restrictedNotification; } - private boolean canRestrictNotificationToRuleProjects(Notification initialNotification, NotificationRule rule) { - return initialNotification.getSubject() instanceof NewVulnerabilityIdentified && - rule.getProjects() != null - && rule.getProjects().size() > 0; + private boolean canRestrictNotificationToRuleProjects(final Notification initialNotification, final NotificationRule rule) { + return initialNotification.getSubject() instanceof NewVulnerabilityIdentified + && rule.getProjects() != null + && !rule.getProjects().isEmpty(); } - List resolveRules(final Notification notification) { - // The notification rules to process for this specific notification + List resolveRules(final PublishContext ctx, final Notification notification) { final List rules = new ArrayList<>(); - if (notification == null || notification.getScope() == null || notification.getGroup() == null || notification.getLevel() == null) { + LOGGER.debug("Mandatory fields of notification are missing; Unable to resolve rules (%s)".formatted(ctx)); return rules; } + try (QueryManager qm = new QueryManager()) { final PersistenceManager pm = qm.getPersistenceManager(); final Query query = pm.newQuery(NotificationRule.class); @@ -149,6 +155,7 @@ List resolveRules(final Notification notification) { query.setParameters(NotificationScope.valueOf(notification.getScope())); final List result = query.executeList(); pm.detachCopyAll(result); + LOGGER.debug("Matched %d notification rules (%s)".formatted(result.size(), ctx)); if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final NewVulnerabilityIdentified subject) { @@ -156,10 +163,10 @@ List resolveRules(final Notification notification) { // of the notification down to those projects that the rule matches and which // also match project the component is included in. // NOTE: This logic is slightly different from what is implemented in limitToProject() - for (final NotificationRule rule: result) { + for (final NotificationRule rule : result) { if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { - if (rule.getProjects() != null && rule.getProjects().size() > 0 - && subject.getComponent() != null && subject.getComponent().getProject() != null) { + if (rule.getProjects() != null && !rule.getProjects().isEmpty() + && subject.getComponent() != null && subject.getComponent().getProject() != null) { for (final Project project : rule.getProjects()) { if (subject.getComponent().getProject().getUuid().equals(project.getUuid()) || (Boolean.TRUE.equals(rule.isNotifyChildren() && checkIfChildrenAreAffected(project, subject.getComponent().getProject().getUuid())))) { rules.add(rule); @@ -172,27 +179,27 @@ List resolveRules(final Notification notification) { } } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final NewVulnerableDependency subject) { - limitToProject(rules, result, notification, subject.getComponent().getProject()); + limitToProject(ctx, rules, result, notification, subject.getComponent().getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final BomConsumedOrProcessed subject) { - limitToProject(rules, result, notification, subject.getProject()); + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final BomProcessingFailed subject) { - limitToProject(rules, result, notification, subject.getProject()); + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final VexConsumedOrProcessed subject) { - limitToProject(rules, result, notification, subject.getProject()); + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final PolicyViolationIdentified subject) { - limitToProject(rules, result, notification, subject.getProject()); + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final AnalysisDecisionChange subject) { - limitToProject(rules, result, notification, subject.getProject()); + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final ViolationAnalysisDecisionChange subject) { - limitToProject(rules, result, notification, subject.getComponent().getProject()); + limitToProject(ctx, rules, result, notification, subject.getComponent().getProject()); } else { - for (final NotificationRule rule: result) { + for (final NotificationRule rule : result) { if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { rules.add(rule); } @@ -206,22 +213,42 @@ List resolveRules(final Notification notification) { * if the rule specified one or more projects as targets, reduce the execution * of the notification down to those projects that the rule matches and which * also match projects affected by the vulnerability. - * */ - private void limitToProject(final List applicableRules, final List rules, - final Notification notification, final Project limitToProject) { - for (final NotificationRule rule: rules) { + */ + private void limitToProject(final PublishContext ctx, final List applicableRules, + final List rules, final Notification notification, + final Project limitToProject) { + for (final NotificationRule rule : rules) { + final PublishContext ruleCtx = ctx.withRule(rule); if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { - if (rule.getProjects() != null && rule.getProjects().size() > 0) { + if (rule.getProjects() != null && !rule.getProjects().isEmpty()) { for (final Project project : rule.getProjects()) { - if (project.getUuid().equals(limitToProject.getUuid()) || (Boolean.TRUE.equals(rule.isNotifyChildren()) && checkIfChildrenAreAffected(project, limitToProject.getUuid()))) { + if (project.getUuid().equals(limitToProject.getUuid())) { + LOGGER.debug("Project %s is part of the \"limit to\" list of the rule; Rule is applicable (%s)" + .formatted(limitToProject.getUuid(), ruleCtx)); applicableRules.add(rule); + } else if (rule.isNotifyChildren()) { + final boolean isChildOfLimitToProject = checkIfChildrenAreAffected(project, limitToProject.getUuid()); + if (isChildOfLimitToProject) { + LOGGER.debug("Project %s is child of \"limit to\" project %s; Rule is applicable (%s)" + .formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx)); + applicableRules.add(rule); + } else { + LOGGER.debug("Project %s is not a child of \"limit to\" project %s; Rule is not applicable (%s)" + .formatted(limitToProject.getUuid(), project.getUuid(), ruleCtx)); + } + } else { + LOGGER.debug("Project %s is not part of the \"limit to\" list of the rule; Rule is not applicable (%s)" + .formatted(limitToProject.getUuid(), ruleCtx)); } } } else { + LOGGER.debug("Rule is not limited to projects; Rule is applicable (%s)".formatted(ruleCtx)); applicableRules.add(rule); } } } + LOGGER.debug("Applicable rules: %s (%s)" + .formatted(applicableRules.stream().map(NotificationRule::getName).collect(Collectors.joining(", ")), ctx)); } private boolean checkIfChildrenAreAffected(Project parent, UUID uuid) { diff --git a/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java index ab1b3b5fa..2ae0205b4 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java @@ -25,7 +25,6 @@ import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.dependencytrack.common.HttpClientPool; -import org.dependencytrack.exception.PublisherException; import org.dependencytrack.util.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,30 +33,43 @@ import java.io.IOException; public abstract class AbstractWebhookPublisher implements Publisher { - public void publish(final String publisherName, final PebbleTemplate template, final Notification notification, final JsonObject config) { + + public void publish(final PublishContext ctx, final PebbleTemplate template, final Notification notification, final JsonObject config) { final Logger logger = LoggerFactory.getLogger(getClass()); - logger.debug("Preparing to publish " + publisherName + " notification"); + if (config == null) { - logger.warn("No configuration found. Skipping notification."); + logger.warn("No publisher configuration found; Skipping notification (%s)".formatted(ctx)); return; } + final String destination = getDestinationUrl(config); - final String content = prepareTemplate(notification, template); - if (destination == null || content == null) { - logger.warn("A destination or template was not found. Skipping notification"); + if (destination == null) { + logger.warn("No destination configured; Skipping notification (%s)".formatted(ctx)); return; } - final String mimeType = getTemplateMimeType(config); - var request = new HttpPost(destination); - request.addHeader("content-type", mimeType); - request.addHeader("accept", mimeType); + final AuthCredentials credentials; try { credentials = getAuthCredentials(); - } catch (PublisherException e) { - logger.warn("An error occurred during the retrieval of credentials needed for notification publication. Skipping notification", e); + } catch (RuntimeException e) { + logger.warn(""" + An error occurred during the retrieval of credentials needed for notification \ + publication; Skipping notification (%s)""".formatted(ctx), e); return; } + + final String content; + try { + content = prepareTemplate(notification, template); + } catch (IOException | RuntimeException e) { + logger.error("Failed to prepare notification content (%s)".formatted(ctx), e); + return; + } + + final String mimeType = getTemplateMimeType(config); + var request = new HttpPost(destination); + request.addHeader("content-type", mimeType); + request.addHeader("accept", mimeType); if (credentials != null) { if(credentials.user() != null) { request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(credentials.user(), credentials.password())); @@ -69,23 +81,28 @@ public void publish(final String publisherName, final PebbleTemplate template, f try { request.setEntity(new StringEntity(content)); try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { - if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) { - logger.error("An error was encountered publishing notification to " + publisherName + - "with HTTP Status : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase() + - " Destination: " + destination + " Response: " + EntityUtils.toString(response.getEntity())); - logger.debug(content); + final int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode < 200 || statusCode >= 300) { + logger.warn("Destination responded with with status code %d, likely indicating a processing failure (%s)" + .formatted(statusCode, ctx)); + if (logger.isDebugEnabled()) { + logger.debug("Response headers: %s".formatted((Object[]) response.getAllHeaders())); + logger.debug("Response body: %s".formatted(EntityUtils.toString(response.getEntity()))); + } + } else if (ctx.shouldLogSuccess()) { + logger.info("Destination acknowledged reception of notification with status code %d (%s)" + .formatted(statusCode, ctx)); } } } catch (IOException ex) { - handleRequestException(logger, ex); + handleRequestException(ctx, logger, ex); } } protected String getDestinationUrl(final JsonObject config) { - return config.getString(CONFIG_DESTINATION); + return config.getString(CONFIG_DESTINATION, null); } - protected AuthCredentials getAuthCredentials() { return null; } @@ -93,7 +110,8 @@ protected AuthCredentials getAuthCredentials() { protected record AuthCredentials(String user, String password) { } - protected void handleRequestException(final Logger logger, final Exception e) { - logger.error("Request failure", e); + protected void handleRequestException(final PublishContext ctx, final Logger logger, final Exception e) { + logger.error("Failed to send notification request (%s)".formatted(ctx), e); } + } diff --git a/src/main/java/org/dependencytrack/notification/publisher/ConsolePublisher.java b/src/main/java/org/dependencytrack/notification/publisher/ConsolePublisher.java index 23f08f9f2..78b29126f 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/ConsolePublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/ConsolePublisher.java @@ -24,6 +24,7 @@ import io.pebbletemplates.pebble.PebbleEngine; import javax.json.JsonObject; +import java.io.IOException; import java.io.PrintStream; public class ConsolePublisher implements Publisher { @@ -31,10 +32,12 @@ public class ConsolePublisher implements Publisher { private static final Logger LOGGER = Logger.getLogger(ConsolePublisher.class); private static final PebbleEngine ENGINE = new PebbleEngine.Builder().newLineTrimming(false).build(); - public void inform(final Notification notification, final JsonObject config) { - final String content = prepareTemplate(notification, getTemplate(config)); - if (content == null) { - LOGGER.warn("A template was not found. Skipping notification"); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + final String content; + try { + content = prepareTemplate(notification, getTemplate(config)); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to prepare notification content (%s)".formatted(ctx), e); return; } final PrintStream ps; diff --git a/src/main/java/org/dependencytrack/notification/publisher/CsWebexPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/CsWebexPublisher.java index 9754bb534..f820c6494 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/CsWebexPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/CsWebexPublisher.java @@ -27,8 +27,8 @@ public class CsWebexPublisher extends AbstractWebhookPublisher implements Publis private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); - public void inform(final Notification notification, final JsonObject config) { - publish(DefaultNotificationPublishers.CS_WEBEX.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + publish(ctx, getTemplate(config), notification, config); } @Override diff --git a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java index 3f4b8c92d..c96e4fdec 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java +++ b/src/main/java/org/dependencytrack/notification/publisher/DefaultNotificationPublishers.java @@ -31,14 +31,14 @@ public enum DefaultNotificationPublishers { 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); - private String name; - private String description; - private Class publisherClass; - private String templateFile; - private String templateMimeType; - private boolean defaultPublisher; + private final String name; + private final String description; + private final Class publisherClass; + private final String templateFile; + private final String templateMimeType; + private final boolean defaultPublisher; - DefaultNotificationPublishers(final String name, final String description, final Class publisherClass, + DefaultNotificationPublishers(final String name, final String description, final Class publisherClass, final String templateFile, final String templateMimeType, final boolean defaultPublisher) { this.name = name; this.description = description; @@ -56,7 +56,7 @@ public String getPublisherDescription() { return description; } - public Class getPublisherClass() { + public Class getPublisherClass() { return publisherClass; } diff --git a/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java index 63a53d8b4..faad3d230 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/JiraPublisher.java @@ -40,16 +40,33 @@ * @since 4.7 */ public class JiraPublisher extends AbstractWebhookPublisher implements Publisher { + private static final Logger LOGGER = Logger.getLogger(JiraPublisher.class); private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); + private String jiraProjectKey; private String jiraTicketType; @Override - public void inform(final Notification notification, final JsonObject config) { - jiraTicketType = config.getString("jiraTicketType"); - jiraProjectKey = config.getString(CONFIG_DESTINATION); - publish(DefaultNotificationPublishers.JIRA.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + if (config == null) { + LOGGER.warn("No publisher configuration provided; Skipping notification (%s)".formatted(ctx)); + return; + } + + jiraTicketType = config.getString("jiraTicketType", null); + if (jiraTicketType == null) { + LOGGER.warn("No JIRA ticket type configured; Skipping notification (%s)".formatted(ctx)); + return; + } + + jiraProjectKey = config.getString(CONFIG_DESTINATION, null); + if (jiraProjectKey == null) { + LOGGER.warn("No JIRA project key configured; Skipping notification (%s)".formatted(ctx)); + return; + } + + publish(ctx, getTemplate(config), notification, config); } @Override @@ -68,7 +85,7 @@ public String getDestinationUrl(final JsonObject config) { } @Override - public AuthCredentials getAuthCredentials() { + protected AuthCredentials getAuthCredentials() { try (final QueryManager qm = new QueryManager()) { final ConfigProperty jiraUsernameProp = qm.getConfigProperty(JIRA_USERNAME.getGroupName(), JIRA_USERNAME.getPropertyName()); final String jiraUsername = (jiraUsernameProp == null) ? null : jiraUsernameProp.getPropertyValue(); diff --git a/src/main/java/org/dependencytrack/notification/publisher/MattermostPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/MattermostPublisher.java index 08ef4f444..32fa6f18d 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/MattermostPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/MattermostPublisher.java @@ -27,8 +27,8 @@ public class MattermostPublisher extends AbstractWebhookPublisher implements Pub private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); - public void inform(final Notification notification, final JsonObject config) { - publish(DefaultNotificationPublishers.MATTERMOST.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + publish(ctx, getTemplate(config), notification, config); } @Override diff --git a/src/main/java/org/dependencytrack/notification/publisher/MsTeamsPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/MsTeamsPublisher.java index 6c8b2204f..ec3361b92 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/MsTeamsPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/MsTeamsPublisher.java @@ -27,8 +27,8 @@ public class MsTeamsPublisher extends AbstractWebhookPublisher implements Publis private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); - public void inform(final Notification notification, final JsonObject config) { - publish(DefaultNotificationPublishers.MS_TEAMS.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + publish(ctx, getTemplate(config), notification, config); } @Override diff --git a/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java new file mode 100644 index 000000000..542a10ba3 --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/publisher/PublishContext.java @@ -0,0 +1,183 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.notification.publisher; + +import alpine.notification.Notification; +import com.google.common.base.MoreObjects; +import org.dependencytrack.model.NotificationRule; +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.VexConsumedOrProcessed; +import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; + +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Context information about a {@link Notification} being published. + * + * @param notificationGroup Group of the {@link Notification} being published + * @param notificationLevel Level of the {@link Notification} being published + * @param notificationScope Scope of the {@link Notification} being published + * @param notificationTimestamp UTC Timestamp in {@link DateTimeFormatter#ISO_DATE_TIME} of the {@link Notification} being published + * @param notificationSubjects Subject(s) of the {@link Notification} being published + * @param ruleName Name of the matched {@link NotificationRule} + * @param ruleScope Scope of the matched {@link NotificationRule} + * @param ruleLevel Level of the matched {@link NotificationRule} + * @param logSuccess Whether the publisher shall emit a log message upon successful publishing + * @since 4.10.0 + */ +public record PublishContext(String notificationGroup, String notificationLevel, String notificationScope, + String notificationTimestamp, Map notificationSubjects, + String ruleName, String ruleScope, String ruleLevel, Boolean logSuccess) { + + private static final String SUBJECT_COMPONENT = "component"; + private static final String SUBJECT_PROJECT = "project"; + private static final String SUBJECT_PROJECTS = "projects"; + private static final String SUBJECT_VULNERABILITY = "vulnerability"; + private static final String SUBJECT_VULNERABILITIES = "vulnerabilities"; + + public static PublishContext from(final Notification notification) { + if (notification == null) { + return null; + } + + final var notificationSubjects = new HashMap(); + if (notification.getSubject() instanceof final BomConsumedOrProcessed subject) { + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + } else if (notification.getSubject() instanceof final BomProcessingFailed subject) { + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + } else if (notification.getSubject() instanceof final NewVulnerabilityIdentified subject) { + notificationSubjects.put(SUBJECT_COMPONENT, Component.convert(subject.getComponent())); + if (subject.getAffectedProjects() != null) { + notificationSubjects.put(SUBJECT_PROJECTS, subject.getAffectedProjects().stream().map(Project::convert).toList()); + } else { + notificationSubjects.put(SUBJECT_PROJECTS, null); + } + notificationSubjects.put(SUBJECT_VULNERABILITY, Vulnerability.convert(subject.getVulnerability())); + } else if (notification.getSubject() instanceof final NewVulnerableDependency subject) { + notificationSubjects.put(SUBJECT_COMPONENT, Component.convert(subject.getComponent())); + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getComponent().getProject())); + if (subject.getVulnerabilities() != null) { + notificationSubjects.put(SUBJECT_VULNERABILITIES, subject.getVulnerabilities().stream().map(Vulnerability::convert).toList()); + } else { + notificationSubjects.put(SUBJECT_VULNERABILITIES, null); + } + } else if (notification.getSubject() instanceof final org.dependencytrack.model.Project subject) { + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject)); + } else if (notification.getSubject() instanceof final PolicyViolationIdentified subject) { + notificationSubjects.put(SUBJECT_COMPONENT, Component.convert(subject.getComponent())); + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + } else if (notification.getSubject() instanceof final ViolationAnalysisDecisionChange subject) { + notificationSubjects.put(SUBJECT_COMPONENT, Component.convert(subject.getComponent())); + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getComponent().getProject())); + } else if (notification.getSubject() instanceof final AnalysisDecisionChange subject) { + notificationSubjects.put(SUBJECT_COMPONENT, Component.convert(subject.getComponent())); + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + notificationSubjects.put(SUBJECT_VULNERABILITY, Vulnerability.convert(subject.getVulnerability())); + } else if (notification.getSubject() instanceof final VexConsumedOrProcessed subject) { + notificationSubjects.put(SUBJECT_PROJECT, Project.convert(subject.getProject())); + } + + return new PublishContext(notification.getGroup(), Optional.ofNullable(notification.getLevel()).map(Enum::name).orElse(null), + notification.getScope(), notification.getTimestamp().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE_TIME), notificationSubjects, + /* ruleName */ null, /* ruleScope */ null, /* ruleLevel */ null, /* logSuccess */ null); + } + + /** + * Enrich the {@link PublishContext} with additional information about the {@link NotificationRule} once known. + * + * @param rule The applicable {@link NotificationRule} + * @return This {@link PublishContext} + */ + public PublishContext withRule(final NotificationRule rule) { + return new PublishContext(this.notificationGroup, this.notificationLevel, this.notificationScope, this.notificationTimestamp, + this.notificationSubjects, rule.getName(), rule.getScope().name(), rule.getNotificationLevel().name(), rule.isLogSuccessfulPublish()); + } + + public boolean shouldLogSuccess() { + return logSuccess != null && logSuccess; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("notificationGroup", notificationGroup) + .add("notificationLevel", notificationLevel) + .add("notificationScope", notificationScope) + .add("notificationTimestamp", notificationTimestamp) + .add("notificationSubjects", notificationSubjects) + .add("ruleName", ruleName) + .add("ruleScope", ruleScope) + .add("ruleLevel", ruleLevel) + .omitNullValues() + .toString(); + } + + public record Component(String uuid, String group, String name, String version) { + + private static Component convert(final org.dependencytrack.model.Component notificationComponent) { + if (notificationComponent == null) { + return null; + } + return new Component( + Optional.ofNullable(notificationComponent.getUuid()).map(UUID::toString).orElse(null), + notificationComponent.getGroup(), + notificationComponent.getName(), + notificationComponent.getVersion() + ); + } + + } + + public record Project(String uuid, String name, String version) { + + private static Project convert(final org.dependencytrack.model.Project notificationProject) { + if (notificationProject == null) { + return null; + } + return new Project( + Optional.ofNullable(notificationProject.getUuid()).map(UUID::toString).orElse(null), + notificationProject.getName(), + notificationProject.getVersion() + ); + } + + } + + public record Vulnerability(String id, String source) { + + private static Vulnerability convert(final org.dependencytrack.model.Vulnerability notificationVuln) { + if (notificationVuln == null) { + return null; + } + return new Vulnerability(notificationVuln.getVulnId(), notificationVuln.getSource()); + } + + } + +} diff --git a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java index 69e15b411..34c06bc08 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.notification.publisher; -import alpine.common.logging.Logger; import alpine.common.util.UrlUtil; import alpine.model.ConfigProperty; import alpine.notification.Notification; @@ -54,7 +53,7 @@ public interface Publisher { String CONFIG_DESTINATION = "destination"; - void inform(Notification notification, JsonObject config); + void inform(final PublishContext ctx, final Notification notification, final JsonObject config); PebbleEngine getTemplateEngine(); @@ -78,8 +77,7 @@ default String getTemplateMimeType(JsonObject config) { default void enrichTemplateContext(final Map context) { } - default String prepareTemplate(final Notification notification, final PebbleTemplate template) { - + default String prepareTemplate(final Notification notification, final PebbleTemplate template) throws IOException { try (QueryManager qm = new QueryManager()) { final ConfigProperty baseUrlProperty = qm.getConfigProperty( ConfigPropertyConstants.GENERAL_BASE_URL.getGroupName(), @@ -132,9 +130,6 @@ default String prepareTemplate(final Notification notification, final PebbleTemp try (final Writer writer = new StringWriter()) { template.evaluate(writer, context); return writer.toString(); - } catch (IOException e) { - Logger.getLogger(this.getClass()).error("An error was encountered evaluating template", e); - return null; } } } diff --git a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java index 3794d947c..08f8dd637 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java @@ -19,8 +19,6 @@ package org.dependencytrack.notification.publisher; import alpine.common.logging.Logger; -import alpine.common.util.BooleanUtil; -import alpine.model.ConfigProperty; import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.OidcUser; @@ -28,12 +26,14 @@ import alpine.notification.Notification; 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 org.dependencytrack.persistence.QueryManager; import javax.json.JsonObject; import javax.json.JsonString; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -56,64 +56,106 @@ 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(); - public void inform(final Notification notification, final JsonObject config) { + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { if (config == null) { - LOGGER.warn("No configuration found. Skipping notification."); + LOGGER.warn("No configuration found; Skipping notification (%s)".formatted(ctx)); return; } final String[] destinations = parseDestination(config); - sendNotification(notification, config, destinations); + sendNotification(ctx, notification, config, destinations); } - public void inform(final Notification notification, final JsonObject config, List teams) { + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config, List teams) { if (config == null) { - LOGGER.warn("No configuration found. Skipping notification."); + LOGGER.warn("No configuration found. Skipping notification. (%s)".formatted(ctx)); return; } final String[] destinations = parseDestination(config, teams); - sendNotification(notification, config, destinations); + sendNotification(ctx, notification, config, destinations); } - private void sendNotification(Notification notification, JsonObject config, String[] destinations) { - PebbleTemplate template = getTemplate(config); - String mimeType = getTemplateMimeType(config); - final String content = prepareTemplate(notification, template); - if (destinations == null || content == null) { - LOGGER.warn("A destination or template was not found. Skipping notification"); + private void sendNotification(final PublishContext ctx, Notification notification, JsonObject config, String[] destinations) { + if (config == null) { + LOGGER.warn("No publisher configuration found; Skipping notification (%s)".formatted(ctx)); + return; + } + if (destinations == null) { + LOGGER.warn("No destination(s) provided; Skipping notification (%s)".formatted(ctx)); + return; + } + + final String content; + final String mimeType; + try { + final PebbleTemplate template = getTemplate(config); + mimeType = getTemplateMimeType(config); + content = prepareTemplate(notification, template); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to prepare notification content (%s)".formatted(ctx), e); return; } + + final boolean smtpEnabled; + final String smtpFrom; + final String smtpHostname; + final int smtpPort; + final String smtpUser; + final String encryptedSmtpPassword; + final boolean smtpSslTls; + final boolean smtpTrustCert; + try (QueryManager qm = new QueryManager()) { - final ConfigProperty smtpEnabled = qm.getConfigProperty(EMAIL_SMTP_ENABLED.getGroupName(), EMAIL_SMTP_ENABLED.getPropertyName()); - final ConfigProperty smtpFrom = qm.getConfigProperty(EMAIL_SMTP_FROM_ADDR.getGroupName(), EMAIL_SMTP_FROM_ADDR.getPropertyName()); - final ConfigProperty smtpHostname = qm.getConfigProperty(EMAIL_SMTP_SERVER_HOSTNAME.getGroupName(), EMAIL_SMTP_SERVER_HOSTNAME.getPropertyName()); - final ConfigProperty smtpPort = qm.getConfigProperty(EMAIL_SMTP_SERVER_PORT.getGroupName(), EMAIL_SMTP_SERVER_PORT.getPropertyName()); - final ConfigProperty smtpUser = qm.getConfigProperty(EMAIL_SMTP_USERNAME.getGroupName(), EMAIL_SMTP_USERNAME.getPropertyName()); - final ConfigProperty smtpPass = qm.getConfigProperty(EMAIL_SMTP_PASSWORD.getGroupName(), EMAIL_SMTP_PASSWORD.getPropertyName()); - final ConfigProperty smtpSslTls = qm.getConfigProperty(EMAIL_SMTP_SSLTLS.getGroupName(), EMAIL_SMTP_SSLTLS.getPropertyName()); - final ConfigProperty smtpTrustCert = qm.getConfigProperty(EMAIL_SMTP_TRUSTCERT.getGroupName(), EMAIL_SMTP_TRUSTCERT.getPropertyName()); - - if (!BooleanUtil.valueOf(smtpEnabled.getPropertyValue())) { - LOGGER.warn("SMTP is not enabled"); - return; // smtp is not enabled + smtpEnabled = qm.isEnabled(EMAIL_SMTP_ENABLED); + if (!smtpEnabled) { + LOGGER.warn("SMTP is not enabled; Skipping notification (%s)".formatted(ctx)); + return; } - final boolean smtpAuth = (smtpUser.getPropertyValue() != null && smtpPass.getPropertyValue() != null); - final String password = (smtpPass.getPropertyValue() != null) ? DataEncryption.decryptAsString(smtpPass.getPropertyValue()) : null; + + 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); + } catch (RuntimeException e) { + LOGGER.error("Failed to load SMTP configuration from datastore (%s)".formatted(ctx), e); + return; + } + + final boolean smtpAuth = (smtpUser != null && encryptedSmtpPassword != null); + final String decryptedSmtpPassword; + try { + decryptedSmtpPassword = (encryptedSmtpPassword != null) ? DataEncryption.decryptAsString(encryptedSmtpPassword) : null; + } catch (Exception e) { + LOGGER.error("Failed to decrypt SMTP password (%s)".formatted(ctx), e); + return; + } + + try { final SendMail sendMail = new SendMail() - .from(smtpFrom.getPropertyValue()) + .from(smtpFrom) .to(destinations) .subject("[Dependency-Track] " + notification.getTitle()) .body(content) .bodyMimeType(mimeType) - .host(smtpHostname.getPropertyValue()) - .port(Integer.valueOf(smtpPort.getPropertyValue())) - .username(smtpUser.getPropertyValue()) - .password(password) + .host(smtpHostname) + .port(smtpPort) + .username(smtpUser) + .password(decryptedSmtpPassword) .smtpauth(smtpAuth) - .useStartTLS(BooleanUtil.valueOf(smtpSslTls.getPropertyValue())) - .trustCert(Boolean.valueOf(smtpTrustCert.getPropertyValue())); + .useStartTLS(smtpSslTls) + .trustCert(smtpTrustCert); sendMail.send(); - } catch (Exception e) { - LOGGER.error("An error occurred sending output email notification", e); + } catch (SendMailException | RuntimeException e) { + LOGGER.error("Failed to send notification email via %s:%d (%s)" + .formatted(smtpHostname, smtpPort, ctx), e); + return; + } + + if (ctx.shouldLogSuccess()) { + LOGGER.info("Notification email sent successfully via %s:%d (%s)" + .formatted(smtpHostname, smtpPort, ctx)); } } diff --git a/src/main/java/org/dependencytrack/notification/publisher/SlackPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/SlackPublisher.java index 2bd5c6c81..707b17d64 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/SlackPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/SlackPublisher.java @@ -27,8 +27,8 @@ public class SlackPublisher extends AbstractWebhookPublisher implements Publishe private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); - public void inform(final Notification notification, final JsonObject config) { - publish(DefaultNotificationPublishers.SLACK.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + publish(ctx, getTemplate(config), notification, config); } @Override diff --git a/src/main/java/org/dependencytrack/notification/publisher/WebhookPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/WebhookPublisher.java index e64d6f2ed..fcfe7732f 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/WebhookPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/WebhookPublisher.java @@ -27,8 +27,8 @@ public class WebhookPublisher extends AbstractWebhookPublisher implements Publis private static final PebbleEngine ENGINE = new PebbleEngine.Builder().defaultEscapingStrategy("json").build(); - public void inform(final Notification notification, final JsonObject config) { - publish(DefaultNotificationPublishers.WEBHOOK.getPublisherName(), getTemplate(config), notification, config); + public void inform(final PublishContext ctx, final Notification notification, final JsonObject config) { + publish(ctx, getTemplate(config), notification, config); } @Override diff --git a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java index 1a6eab58d..ea94e985d 100644 --- a/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/NotificationQueryManager.java @@ -68,6 +68,7 @@ public NotificationRule createNotificationRule(String name, NotificationScope sc rule.setPublisher(publisher); rule.setEnabled(true); rule.setNotifyChildren(true); + rule.setLogSuccessfulPublish(false); return persist(rule); } @@ -81,6 +82,7 @@ public NotificationRule updateNotificationRule(NotificationRule transientRule) { 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()); @@ -133,7 +135,7 @@ public NotificationPublisher getNotificationPublisher(final String name) { * @param clazz The Class of the NotificationPublisher * @return a NotificationPublisher */ - public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { + public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { return getDefaultNotificationPublisher(clazz.getCanonicalName()); } @@ -155,7 +157,7 @@ private NotificationPublisher getDefaultNotificationPublisher(final String clazz * @return a NotificationPublisher */ public NotificationPublisher createNotificationPublisher(final String name, final String description, - final Class publisherClass, final String templateContent, + final Class publisherClass, final String templateContent, final String templateMimeType, final boolean defaultPublisher) { pm.currentTransaction().begin(); final NotificationPublisher publisher = new NotificationPublisher(); diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 6524a339c..e44d74ff4 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1156,12 +1156,12 @@ public NotificationPublisher getNotificationPublisher(final String name) { return getNotificationQueryManager().getNotificationPublisher(name); } - public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { + public NotificationPublisher getDefaultNotificationPublisher(final Class clazz) { return getNotificationQueryManager().getDefaultNotificationPublisher(clazz); } public NotificationPublisher createNotificationPublisher(final String name, final String description, - final Class publisherClass, final String templateContent, + final Class publisherClass, final String templateContent, final String templateMimeType, final boolean defaultPublisher) { return getNotificationQueryManager().createNotificationPublisher(name, description, publisherClass, templateContent, templateMimeType, defaultPublisher); } diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 76d725d5f..eaecceb6f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -24,13 +24,11 @@ import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; - -import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponses; import io.swagger.annotations.ApiParam; -import io.swagger.annotations.Api; - +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; @@ -39,6 +37,7 @@ import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.publisher.SendMailPublisher; import org.dependencytrack.persistence.QueryManager; @@ -47,8 +46,6 @@ import javax.json.Json; import javax.json.JsonObject; import javax.validation.Validator; - - import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -58,7 +55,6 @@ 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.io.IOException; @@ -295,7 +291,7 @@ public Response testSmtpPublisherConfig(@FormParam("destination") String destina .content("SMTP configuration test") .level(NotificationLevel.INFORMATIONAL); // Bypass Notification.dispatch() and go directly to the publisher itself - emailPublisher.inform(notification, config); + emailPublisher.inform(PublishContext.from(notification), notification, config); return Response.ok().build(); } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { LOGGER.error(e.getMessage(), e); diff --git a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java index 46167ee1d..1e1acc4b0 100644 --- a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java +++ b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java @@ -30,6 +30,7 @@ import org.dependencytrack.model.Vex; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; +import org.dependencytrack.notification.publisher.PublishContext; import org.dependencytrack.notification.publisher.Publisher; import org.dependencytrack.notification.vo.AnalysisDecisionChange; import org.dependencytrack.notification.vo.BomConsumedOrProcessed; @@ -57,7 +58,7 @@ public class NotificationRouterTest extends PersistenceCapableTest { public void testNullNotification() { Notification notification = null; NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(0, rules.size()); } @@ -65,7 +66,7 @@ public void testNullNotification() { public void testInvalidNotification() { Notification notification = new Notification(); NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(0, rules.size()); } @@ -76,7 +77,7 @@ public void testNoRules() { notification.setGroup(NotificationGroup.NEW_VULNERABILITY.name()); notification.setLevel(NotificationLevel.INFORMATIONAL); NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(0, rules.size()); } @@ -98,7 +99,7 @@ public void testValidMatchingRule() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(1, rules.size()); } @@ -127,7 +128,7 @@ public void testValidMatchingProjectLimitingRule() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(1, rules.size()); } @@ -157,7 +158,7 @@ public void testValidNonMatchingProjectLimitingRule() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(1, rules.size()); } @@ -244,7 +245,7 @@ public void testValidNonMatchingRule() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertEquals(0, rules.size()); } @@ -260,7 +261,7 @@ public void testRuleLevelEqual() { notification.setLevel(NotificationLevel.WARNING); // Rule level is equal final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).hasSize(1); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).hasSize(1); } @Test @@ -275,7 +276,7 @@ public void testRuleLevelBelow() { notification.setLevel(NotificationLevel.ERROR); // Rule level is lower final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).hasSize(1); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).hasSize(1); } @Test @@ -291,7 +292,7 @@ public void testRuleLevelAbove() { notification.setLevel(NotificationLevel.INFORMATIONAL); // Rule level is higher final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); } @Test @@ -308,7 +309,7 @@ public void testDisabledRule() { notification.setLevel(NotificationLevel.INFORMATIONAL); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); } @Test @@ -338,10 +339,10 @@ public void testNewVulnerabilityIdentifiedLimitedToProject() { notification.setSubject(new NewVulnerabilityIdentified(null, componentB, Set.of(), null)); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new NewVulnerabilityIdentified(null, componentA, Set.of(), null)); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -372,10 +373,10 @@ public void testNewVulnerableDependencyLimitedToProject() { notification.setSubject(new NewVulnerableDependency(componentB, null)); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new NewVulnerableDependency(componentA, null)); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -397,10 +398,10 @@ public void testBomConsumedOrProcessedLimitedToProject() { notification.setSubject(new BomConsumedOrProcessed(projectB, "", Bom.Format.CYCLONEDX, "")); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new BomConsumedOrProcessed(projectA, "", Bom.Format.CYCLONEDX, "")); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -422,10 +423,10 @@ public void testBomProcessingFailedLimitedToProject() { notification.setSubject(new BomProcessingFailed(projectB, "", null, Bom.Format.CYCLONEDX, "")); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new BomProcessingFailed(projectA, "", null, Bom.Format.CYCLONEDX, "")); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -447,10 +448,10 @@ public void testVexConsumedOrProcessedLimitedToProject() { notification.setSubject(new VexConsumedOrProcessed(projectB, "", Vex.Format.CYCLONEDX, "")); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new VexConsumedOrProcessed(projectA, "", Vex.Format.CYCLONEDX, "")); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -481,10 +482,10 @@ public void testPolicyViolationIdentifiedLimitedToProject() { notification.setSubject(new PolicyViolationIdentified(null, componentB, projectB)); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new PolicyViolationIdentified(null, componentA, projectA)); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -506,10 +507,10 @@ public void testAnalysisDecisionChangeLimitedToProject() { notification.setSubject(new AnalysisDecisionChange(null, null, projectB, null)); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new AnalysisDecisionChange(null, null, projectA, null)); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -540,10 +541,10 @@ public void testViolationAnalysisDecisionChangeLimitedToProject() { notification.setSubject(new ViolationAnalysisDecisionChange(null, componentB, null)); final var router = new NotificationRouter(); - assertThat(router.resolveRules(notification)).isEmpty(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); notification.setSubject(new ViolationAnalysisDecisionChange(null, componentA, null)); - assertThat(router.resolveRules(notification)) + assertThat(router.resolveRules(PublishContext.from(notification), notification)) .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } @@ -578,7 +579,7 @@ public void testAffectedChild() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertTrue(rule.isNotifyChildren()); Assert.assertEquals(1, rules.size()); } @@ -615,7 +616,7 @@ public void testAffectedChildNotifyChildrenDisabled() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertFalse(rule.isNotifyChildren()); Assert.assertEquals(0, rules.size()); } @@ -651,7 +652,7 @@ public void testAffectedInactiveChild() { notification.setSubject(subject); // Ok, let's test this NotificationRouter router = new NotificationRouter(); - List rules = router.resolveRules(notification); + List rules = router.resolveRules(PublishContext.from(notification), notification); Assert.assertTrue(rule.isNotifyChildren()); Assert.assertEquals(0, rules.size()); } @@ -672,7 +673,7 @@ private NotificationPublisher createMockPublisher() { return qm.createNotificationPublisher( MockPublisher.MOCK_PUBLISHER_NAME, MockPublisher.MOCK_PUBLISHER_DESCRIPTION, - (Class) NotificationRouterTest.MockPublisher.class, + NotificationRouterTest.MockPublisher.class, MockPublisher.MOCK_PUBLISHER_TEMPLATE_CONTENT, MockPublisher.MOCK_PUBLISHER_TEMPLATE_MIME_TYPE, true ); @@ -697,7 +698,7 @@ public MockPublisher() { } @Override - public void inform(Notification notification, JsonObject config) { + public void inform(final PublishContext ctx, Notification notification, JsonObject config) { MockPublisher.config = config; MockPublisher.notification = notification; } diff --git a/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java index c06d15b8e..52b8c87a3 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java @@ -76,7 +76,7 @@ public void testInformWithBomConsumedNotification() { .subject(subject); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } @Test @@ -93,7 +93,7 @@ public void testInformWithBomProcessingFailedNotification() { .subject(subject); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } @Test // https://github.com/DependencyTrack/dependency-track/issues/3197 @@ -110,7 +110,7 @@ public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubje .subject(subject); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } @Test @@ -124,7 +124,7 @@ public void testInformWithDataSourceMirroringNotification() { .timestamp(LocalDateTime.ofEpochSecond(66666, 666, ZoneOffset.UTC)); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } @Test @@ -146,7 +146,7 @@ public void testInformWithNewVulnerabilityNotification() { .subject(subject); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } @Test @@ -168,7 +168,7 @@ public void testInformWithProjectAuditChangeNotification() { .subject(subject); assertThatNoException() - .isThrownBy(() -> publisherInstance.inform(notification, createConfig())); + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } private static Component createComponent(final Project project) { diff --git a/src/test/java/org/dependencytrack/notification/publisher/ConsolePublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/ConsolePublisherTest.java index b0799565d..f1820e1c4 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/ConsolePublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/ConsolePublisherTest.java @@ -60,7 +60,7 @@ public void testOutputStream() throws IOException { notification.setTitle("Test Notification"); notification.setContent("This is only a test"); ConsolePublisher publisher = new ConsolePublisher(); - publisher.inform(notification, getConfig(DefaultNotificationPublishers.CONSOLE, "")); + publisher.inform(PublishContext.from(notification), notification, getConfig(DefaultNotificationPublishers.CONSOLE, "")); Assert.assertTrue(outContent.toString().contains(expectedResult(notification))); } @@ -73,7 +73,7 @@ public void testErrorStream() throws IOException { notification.setTitle("Test Notification"); notification.setContent("This is only a test"); ConsolePublisher publisher = new ConsolePublisher(); - publisher.inform(notification, getConfig(DefaultNotificationPublishers.CONSOLE, "")); + publisher.inform(PublishContext.from(notification), notification, getConfig(DefaultNotificationPublishers.CONSOLE, "")); Assert.assertTrue(errContent.toString().contains(expectedResult(notification))); } diff --git a/src/test/java/org/dependencytrack/notification/publisher/CsWebexPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/CsWebexPublisherTest.java index 9cb2b3887..c8aaeb1a8 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/CsWebexPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/CsWebexPublisherTest.java @@ -72,6 +72,6 @@ public void testPublish() throws IOException { notification.setTitle("Test Notification"); notification.setContent("This is only a test"); CsWebexPublisher publisher = new CsWebexPublisher(); - publisher.inform(notification, config); + publisher.inform(PublishContext.from(notification), notification, config); } } diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index d6f8fb2c3..cee9fc33d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -220,7 +220,7 @@ public void updateUnknownNotificationPublisherTest() { @Test public void updateExistingDefaultNotificationPublisherTest() { - NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher((Class) SendMailPublisher.class); + NotificationPublisher notificationPublisher = qm.getDefaultNotificationPublisher(SendMailPublisher.class); notificationPublisher.setName(notificationPublisher.getName() + " Updated"); Response response = target(V1_NOTIFICATION_PUBLISHER).request() .header(X_API_KEY, apiKey) @@ -235,7 +235,7 @@ public void updateExistingDefaultNotificationPublisherTest() { public void updateNotificationPublisherWithNameOfAnotherNotificationPublisherTest() { NotificationPublisher notificationPublisher = qm.createNotificationPublisher( "Example Publisher", "Publisher description", - (Class) SendMailPublisher.class, "template", "text/html", + SendMailPublisher.class, "template", "text/html", false ); notificationPublisher = qm.detach(NotificationPublisher.class, notificationPublisher.getId());