From f8402c980f9fd0ec4194d81bf6ac45fde00c6c99 Mon Sep 17 00:00:00 2001 From: Enora Germond Date: Wed, 12 Apr 2023 17:28:07 +0200 Subject: [PATCH] Feat: Add notification when severity of a vulnerability changes Signed-off-by: Enora Germond Signed-off-by: 4naesthetic <37602498+4naesthetic@users.noreply.github.com> --- docs/_docs/integrations/notifications.md | 85 +++++++++++++---- .../event/EventSubsystemInitializer.java | 2 + .../event/VulnerabilityUpdateEvent.java | 42 ++++++++ .../model/VulnerabilityUpdateDiff.java | 33 +++++++ .../notification/NotificationConstants.java | 1 + .../notification/NotificationGroup.java | 1 + .../notification/NotificationRouter.java | 19 ++++ .../notification/publisher/Publisher.java | 4 + .../notification/vo/VulnerabilityUpdate.java | 50 ++++++++++ .../VulnerabilityQueryManager.java | 5 + .../tasks/VulnerabilityUpdateTask.java | 47 +++++++++ .../util/NotificationUtil.java | 62 ++++++++++++ .../notification/publisher/cswebex.peb | 2 +- .../notification/publisher/email.peb | 23 ++++- .../templates/notification/publisher/jira.peb | 4 +- .../notification/publisher/mattermost.peb | 2 +- .../notification/publisher/msteams.peb | 23 +++++ .../notification/publisher/slack.peb | 95 +++++++++++++++++++ .../event/VulnerabilityUpdateEventTest.java | 39 ++++++++ .../NotificationConstantsTest.java | 1 + .../notification/NotificationGroupTest.java | 1 + .../notification/NotificationRouterTest.java | 35 +++++++ .../vo/VulnerabilityUpdateTest.java | 30 ++++++ 23 files changed, 584 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/dependencytrack/event/VulnerabilityUpdateEvent.java create mode 100644 src/main/java/org/dependencytrack/model/VulnerabilityUpdateDiff.java create mode 100644 src/main/java/org/dependencytrack/notification/vo/VulnerabilityUpdate.java create mode 100644 src/main/java/org/dependencytrack/tasks/VulnerabilityUpdateTask.java create mode 100644 src/test/java/org/dependencytrack/event/VulnerabilityUpdateEventTest.java create mode 100644 src/test/java/org/dependencytrack/notification/vo/VulnerabilityUpdateTest.java diff --git a/docs/_docs/integrations/notifications.md b/docs/_docs/integrations/notifications.md index 8cdc82a919..51c78546a8 100644 --- a/docs/_docs/integrations/notifications.md +++ b/docs/_docs/integrations/notifications.md @@ -37,24 +37,25 @@ Notification levels behave identical to logging levels: 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 | -| SYSTEM | USER_CREATED | INFORMATIONAL | Notifications generated as a result of a user creation | -| SYSTEM | USER_DELETED | INFORMATIONAL | Notifications generated as a result of a user deletion | -| 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 | BOM_VALIDATION_FAILED | ERROR | Notifications generated whenever an invalid BOM is uploaded | -| PORTFOLIO | POLICY_VIOLATION | INFORMATIONAL | Notifications generated whenever a policy violation is identified | +| 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 | +| SYSTEM | USER_CREATED | INFORMATIONAL | Notifications generated as a result of a user creation | +| SYSTEM | USER_DELETED | INFORMATIONAL | Notifications generated as a result of a user deletion | +| PORTFOLIO | NEW_VULNERABILITY | INFORMATIONAL | Notifications generated whenever a new vulnerability is identified | +| PORTFOLIO | VULNERABILITY_UPDATED | INFORMATIONAL | Notifications generated if the severity of a vulnerability changes after it has been created | +| 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 | BOM_VALIDATION_FAILED | ERROR | Notifications generated whenever an invalid BOM is uploaded | +| PORTFOLIO | POLICY_VIOLATION | INFORMATIONAL | Notifications generated whenever a policy violation is identified | ## Configuring Publishers @@ -160,6 +161,54 @@ This type of notification will always contain: > The `cwe` field is deprecated and will be removed in a later version. Please use `cwes` instead. + +#### VULNERABILITY_UPDATED +This type of notification will always contain: +* 1 component +* 1 vulnerability +* 1 or more affected projects + +```json +{ + "notification": { + "level": "INFORMATIONAL", + "scope": "PORTFOLIO", + "group": "VULNERABILITY_UPDATED", + "timestamp": "2018-08-27T23:26:22.961", + "title": "Change in Severity of a Vulnerability on Project: [Acme Example]", + "content": "The vulnerability CVE-2012-5784 on component axis has changed severity from LOW to MEDIUM", + "subject": { + "vulnerability": { + "uuid": "941a93f5-e06b-4304-84de-4d788eeb4969", + "old": { + "severity": "MEDIUM" + }, + "new": { + "severity": "HIGH" + } + }, + "component": { + "uuid": "4d5cd8df-cff7-4212-a038-91ae4ab79396", + "group": "apache", + "name": "axis", + "version": "1.4", + "md5": "03dcfdd88502505cc5a805a128bfdd8d", + "sha1": "94a9ce681a42d0352b3ad22659f67835e560d107", + "sha256": "05aebb421d0615875b4bf03497e041fe861bf0556c3045d8dda47e29241ffdd3", + "purl": "pkg:maven/apache/axis@1.4" + }, + "affectedProjects": [ + { + "uuid": "6fb1820f-5280-4577-ac51-40124aabe307", + "name": "Acme Example", + "version": "1.0.0" + } + ] + } + } +} +``` + #### NEW_VULNERABLE_DEPENDENCY This type of notification will always contain: * 1 project diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index c4784dbea6..180ac2f945 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -46,6 +46,7 @@ import org.dependencytrack.tasks.VexUploadProcessingTask; import org.dependencytrack.tasks.VulnDbSyncTask; import org.dependencytrack.tasks.VulnerabilityAnalysisTask; +import org.dependencytrack.tasks.VulnerabilityUpdateTask; import org.dependencytrack.tasks.metrics.ComponentMetricsUpdateTask; import org.dependencytrack.tasks.metrics.PortfolioMetricsUpdateTask; import org.dependencytrack.tasks.metrics.ProjectMetricsUpdateTask; @@ -118,6 +119,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(ClearComponentAnalysisCacheEvent.class, ClearComponentAnalysisCacheTask.class); EVENT_SERVICE.subscribe(CallbackEvent.class, CallbackTask.class); EVENT_SERVICE.subscribe(NewVulnerableDependencyAnalysisEvent.class, NewVulnerableDependencyAnalysisTask.class); + EVENT_SERVICE.subscribe(VulnerabilityUpdateEvent.class, VulnerabilityUpdateTask.class); EVENT_SERVICE.subscribe(NistMirrorEvent.class, NistMirrorTask.class); EVENT_SERVICE.subscribe(NistApiMirrorEvent.class, NistApiMirrorTask.class); EVENT_SERVICE.subscribe(EpssMirrorEvent.class, EpssMirrorTask.class); diff --git a/src/main/java/org/dependencytrack/event/VulnerabilityUpdateEvent.java b/src/main/java/org/dependencytrack/event/VulnerabilityUpdateEvent.java new file mode 100644 index 0000000000..5cbc76991c --- /dev/null +++ b/src/main/java/org/dependencytrack/event/VulnerabilityUpdateEvent.java @@ -0,0 +1,42 @@ +/* + * 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.event; + +import alpine.event.framework.SingletonCapableEvent; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityUpdateDiff; + + +public class VulnerabilityUpdateEvent extends SingletonCapableEvent { + private final VulnerabilityUpdateDiff vulnerabilityUpdateDiff; + private final Vulnerability vulnerability; + + public VulnerabilityUpdateEvent(Vulnerability vulnerability, VulnerabilityUpdateDiff vulnerabilityUpdateDiff) { + this.vulnerability = vulnerability; + this.vulnerabilityUpdateDiff = vulnerabilityUpdateDiff; + } + + public Vulnerability getVulnerability() { + return vulnerability; + } + + public VulnerabilityUpdateDiff getVulnerabilityUpdateDiff() { + return vulnerabilityUpdateDiff; + } +} diff --git a/src/main/java/org/dependencytrack/model/VulnerabilityUpdateDiff.java b/src/main/java/org/dependencytrack/model/VulnerabilityUpdateDiff.java new file mode 100644 index 0000000000..27d7eb203e --- /dev/null +++ b/src/main/java/org/dependencytrack/model/VulnerabilityUpdateDiff.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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +public class VulnerabilityUpdateDiff { + private Severity oldSeverity; + private Severity newSeverity; + + public VulnerabilityUpdateDiff(Severity oldSeverity, Severity newSeverity) { + this.oldSeverity = oldSeverity; + this.newSeverity = newSeverity; + } + + public Severity getOldSeverity() { return oldSeverity; } + + public Severity getNewSeverity() { return newSeverity; } +} diff --git a/src/main/java/org/dependencytrack/notification/NotificationConstants.java b/src/main/java/org/dependencytrack/notification/NotificationConstants.java index 83e78c5339..4b8797bb75 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationConstants.java +++ b/src/main/java/org/dependencytrack/notification/NotificationConstants.java @@ -40,6 +40,7 @@ public static class Title { public static final String ANALYZER_ERROR = "Analyzer Error"; public static final String INTEGRATION_ERROR = "Integration Error"; public static final String NEW_VULNERABILITY = "New Vulnerability Identified"; + public static final String VULNERABILITY_UPDATED = "Change in Severity of a Vulnerability"; public static final String NEW_VULNERABLE_DEPENDENCY = "Vulnerable Dependency Introduced"; public static final String ANALYSIS_DECISION_EXPLOITABLE = "Analysis Decision: Exploitable"; public static final String ANALYSIS_DECISION_IN_TRIAGE = "Analysis Decision: In Triage"; diff --git a/src/main/java/org/dependencytrack/notification/NotificationGroup.java b/src/main/java/org/dependencytrack/notification/NotificationGroup.java index 64596886c5..188b21759b 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationGroup.java +++ b/src/main/java/org/dependencytrack/notification/NotificationGroup.java @@ -32,6 +32,7 @@ public enum NotificationGroup { // Portfolio Groups NEW_VULNERABILITY, NEW_VULNERABLE_DEPENDENCY, + VULNERABILITY_UPDATED, //NEW_OUTDATED_COMPONENT, //FIXED_VULNERABILITY, //FIXED_OUTDATED, diff --git a/src/main/java/org/dependencytrack/notification/NotificationRouter.java b/src/main/java/org/dependencytrack/notification/NotificationRouter.java index a5b6cf7aa5..311b56bedb 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationRouter.java +++ b/src/main/java/org/dependencytrack/notification/NotificationRouter.java @@ -38,6 +38,7 @@ import org.dependencytrack.notification.vo.PolicyViolationIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; +import org.dependencytrack.notification.vo.VulnerabilityUpdate; import org.dependencytrack.persistence.QueryManager; import jakarta.json.Json; @@ -178,6 +179,24 @@ List resolveRules(final PublishContext ctx, final Notification } } } + } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) + && notification.getSubject() instanceof final VulnerabilityUpdate subject) { + for (final NotificationRule rule: result) { + if (rule.getNotifyOn().contains(NotificationGroup.valueOf(notification.getGroup()))) { + if (rule.getProjects() != null && rule.getProjects().size() > 0 + && subject.getComponent() != null) { + if (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); + } + } + } + } else { + rules.add(rule); + } + } + } } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final NewVulnerableDependency subject) { limitToProject(ctx, rules, result, notification, subject.getComponent().getProject()); diff --git a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java index 50a319d92f..cd4efbc11b 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java @@ -36,6 +36,7 @@ import org.dependencytrack.notification.vo.PolicyViolationIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; +import org.dependencytrack.notification.vo.VulnerabilityUpdate; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.NotificationUtil; @@ -106,6 +107,9 @@ default String prepareTemplate(final Notification notification, final PebbleTemp if (notification.getSubject() instanceof final NewVulnerabilityIdentified subject) { context.put("subject", subject); context.put("subjectJson", NotificationUtil.toJson(subject)); + } else if (notification.getSubject() instanceof final VulnerabilityUpdate subject) { + context.put("subject", subject); + context.put("subjectJson", NotificationUtil.toJson(subject)); } else if (notification.getSubject() instanceof final NewVulnerableDependency subject) { context.put("subject", subject); context.put("subjectJson", NotificationUtil.toJson(subject)); diff --git a/src/main/java/org/dependencytrack/notification/vo/VulnerabilityUpdate.java b/src/main/java/org/dependencytrack/notification/vo/VulnerabilityUpdate.java new file mode 100644 index 0000000000..7f7fb1abc1 --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/vo/VulnerabilityUpdate.java @@ -0,0 +1,50 @@ +/* + * 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.vo; + +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityUpdateDiff; + +import java.util.Set; + +public class VulnerabilityUpdate { + private final Vulnerability vulnerability; + private final VulnerabilityUpdateDiff vulnerabilityUpdateDiff; + private final Component component; + private final Set affectedProjects; + + public VulnerabilityUpdate(final Vulnerability vulnerability, final VulnerabilityUpdateDiff vulnerabilityUpdateDiff, final Component component, final Set affectedProjects) { + this.vulnerability = vulnerability; + this.vulnerabilityUpdateDiff = vulnerabilityUpdateDiff; + this.component = component; + this.affectedProjects = affectedProjects; + } + + public Vulnerability getVulnerability() { + return vulnerability; + } + + public VulnerabilityUpdateDiff getVulnerabilityUpdateDiff(){ return vulnerabilityUpdateDiff; } + + public Component getComponent() { return component; } + + public Set getAffectedProjects() { return affectedProjects; } +} diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 4594e539d8..008d3de9ea 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -23,6 +23,7 @@ import alpine.resources.AlpineRequest; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.event.IndexEvent; +import org.dependencytrack.event.VulnerabilityUpdateEvent; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.Component; @@ -31,6 +32,7 @@ import org.dependencytrack.model.VulnIdAndSource; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; +import org.dependencytrack.model.VulnerabilityUpdateDiff; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.resources.v1.vo.AffectedProject; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; @@ -95,6 +97,9 @@ public Vulnerability updateVulnerability(Vulnerability transientVulnerability, b vulnerability = getVulnerabilityByVulnId(transientVulnerability.getSource(), transientVulnerability.getVulnId()); } if (vulnerability != null) { + if (vulnerability.getSeverity() != transientVulnerability.getSeverity()) { + Event.dispatch(new VulnerabilityUpdateEvent(vulnerability, new VulnerabilityUpdateDiff(vulnerability.getSeverity(), transientVulnerability.getSeverity()))); + } vulnerability.setCreated(transientVulnerability.getCreated()); vulnerability.setPublished(transientVulnerability.getPublished()); vulnerability.setUpdated(transientVulnerability.getUpdated()); diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityUpdateTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityUpdateTask.java new file mode 100644 index 0000000000..4d1f73b142 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/VulnerabilityUpdateTask.java @@ -0,0 +1,47 @@ +/* + * 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.tasks; + +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; +import org.dependencytrack.event.VulnerabilityUpdateEvent; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityUpdateDiff; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.NotificationUtil; + +public class VulnerabilityUpdateTask implements Subscriber { + + private static final Logger LOGGER = Logger.getLogger(VulnerabilityUpdateTask.class); + + @Override + public void inform(Event e) { + if (e instanceof VulnerabilityUpdateEvent event){ + try (final var qm = new QueryManager()) { + Vulnerability vulnerability = event.getVulnerability(); + VulnerabilityUpdateDiff diff = event.getVulnerabilityUpdateDiff(); + + NotificationUtil.analyzeNotificationCriteria(qm, vulnerability, diff); + } catch (Exception ex) { + LOGGER.error("An unknown error occurred while analyzing notification criteria for vulnerability change", ex); + } + } + } +} diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index 4eb50a6320..d9280e5ea1 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -39,6 +39,7 @@ import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAnalysisLevel; +import org.dependencytrack.model.VulnerabilityUpdateDiff; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; @@ -52,6 +53,7 @@ import org.dependencytrack.notification.vo.PolicyViolationIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; +import org.dependencytrack.notification.vo.VulnerabilityUpdate; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.persistence.QueryManager; @@ -108,6 +110,29 @@ public static void analyzeNotificationCriteria(QueryManager qm, Vulnerability vu } } + public static void analyzeNotificationCriteria(QueryManager qm, Vulnerability vulnerability, VulnerabilityUpdateDiff vulnerabilityUpdateDiff) { + final Map affectedProjects = new HashMap<>(); + List components = vulnerability.getComponents(); + if (components != null) { + for (final Component c : components) { + if (!affectedProjects.containsKey(c.getProject().getId())) { + affectedProjects.put(c.getProject().getId(), qm.detach(Project.class, c.getProject().getId())); + } + } + + for (final Component c : components) { + Notification.dispatch(new Notification() + .scope(NotificationScope.PORTFOLIO) + .group(NotificationGroup.VULNERABILITY_UPDATED) + .title(generateNotificationTitle(NotificationConstants.Title.VULNERABILITY_UPDATED, qm.detach(Project.class, c.getProject().getId()))) + .level(NotificationLevel.INFORMATIONAL) + .content(generateNotificationContent(vulnerability, vulnerabilityUpdateDiff, c)) + .subject(new VulnerabilityUpdate(vulnerability, vulnerabilityUpdateDiff, c, new HashSet<>(affectedProjects.values()))) + ); + } + } + } + public static void analyzeNotificationCriteria(final QueryManager qm, Component component) { List vulnerabilities = qm.getAllVulnerabilities(component, false); if (vulnerabilities != null && !vulnerabilities.isEmpty()) { @@ -398,6 +423,39 @@ public static JsonObject toJson(final NewVulnerabilityIdentified vo) { return builder.build(); } + public static JsonObject toJson(final VulnerabilityUpdate vo) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + if (vo.getVulnerability().getUuid() != null) { + Map vulnerabilityMap = new HashMap<>(); + vulnerabilityMap.put("uuid", vo.getVulnerability().getUuid().toString()); + + if (vo.getVulnerabilityUpdateDiff() != null) { + Map oldVulnerabilityMap = new HashMap<>(); + oldVulnerabilityMap.put("severity", vo.getVulnerabilityUpdateDiff().getOldSeverity().toString()); + vulnerabilityMap.put("old", oldVulnerabilityMap); + + Map newVulnerabilityMap = new HashMap<>(); + newVulnerabilityMap.put("severity", vo.getVulnerabilityUpdateDiff().getNewSeverity().toString()); + vulnerabilityMap.put("new", newVulnerabilityMap); + } + + JsonObject vulnerabilityJson = Json.createObjectBuilder(vulnerabilityMap).build(); + builder.add("vulnerability", vulnerabilityJson); + } + + if (vo.getAffectedProjects() != null && vo.getAffectedProjects().size() > 0) { + final JsonArrayBuilder projectsBuilder = Json.createArrayBuilder(); + for (final Project project: vo.getAffectedProjects()) { + projectsBuilder.add(toJson(project)); + } + builder.add("affectedProjects", projectsBuilder.build()); + } + if (vo.getComponent() != null) { + builder.add("component", toJson(vo.getComponent())); + } + return builder.build(); + } + public static JsonObject toJson(final NewVulnerableDependency vo) { final JsonObjectBuilder builder = Json.createObjectBuilder(); if (vo.getComponent().getProject() != null) { @@ -600,6 +658,10 @@ private static String generateNotificationContent(final Vulnerability vulnerabil return content; } + private static String generateNotificationContent(final Vulnerability vulnerability, final VulnerabilityUpdateDiff vulnerabilityUpdateDiff, final Component component){ + return "The vulnerability " + vulnerability.getVulnId() + " on component " + component.getName() + " has changed severity from " + vulnerabilityUpdateDiff.getOldSeverity() + " to " + vulnerabilityUpdateDiff.getNewSeverity(); + } + private static String generateNotificationContent(final PolicyViolation policyViolation) { return "A " + policyViolation.getType().name().toLowerCase() + " policy violation occurred"; } diff --git a/src/main/resources/templates/notification/publisher/cswebex.peb b/src/main/resources/templates/notification/publisher/cswebex.peb index c458226e27..83840ff32a 100644 --- a/src/main/resources/templates/notification/publisher/cswebex.peb +++ b/src/main/resources/templates/notification/publisher/cswebex.peb @@ -1 +1 @@ -{"markdown":"**{{ notification.title | escape(strategy="json") }}**{% if notification.group == "NEW_VULNERABILITY" %}\n**VulnID:** {{ subject.vulnerability.vulnId | escape(strategy="json") }}\n**Severity:** {{ subject.vulnerability.severity | escape(strategy="json") }}\n**Source:** {{ subject.vulnerability.source | escape(strategy="json") }}\n**Component:** {{ subject.component.toString | escape(strategy="json") }}\n**Actions:**\n[View Vulnerability]({{ baseUrl }}/vulnerability/?source={{ subject.vulnerability.source | escape(strategy="json") }}&vulnId={{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}\n**Project:** {{ subject.dependency.project.toString | escape(strategy="json") }}\n**Component:** {{ subject.dependency.component.toString | escape(strategy="json") }}\n**Actions:**\n[View Project]({{ baseUrl }}/projects/?uuid={{ subject.dependency.project.uuid | escape(strategy="json") }}){% endif %}\n[View Component]({{ baseUrl }}/component/?uuid={{ subject.dependency.component.uuid | escape(strategy="json") }})\n**Description:** {{ notification.content | escape(strategy="json") }}"} +{"markdown":"**{{ notification.title | escape(strategy="json") }}**{% if notification.group == "NEW_VULNERABILITY" %}\n**VulnID:** {{ subject.vulnerability.vulnId | escape(strategy="json") }}\n**Severity:** {{ subject.vulnerability.severity | escape(strategy="json") }}\n**Source:** {{ subject.vulnerability.source | escape(strategy="json") }}\n**Component:** {{ subject.component.toString | escape(strategy="json") }}\n**Actions:**\n[View Vulnerability]({{ baseUrl }}/vulnerability/?source={{ subject.vulnerability.source | escape(strategy="json") }}&vulnId={{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "VULNERABILITY_UPDATED" %}\n**VulnID:** {{ subject.vulnerability.vulnId | escape(strategy="json") }}\n**OldSeverity:** {{ subject.vulnerabilityUpdateDiff.oldSeverity | escape(strategy="json") }}\n**NewSeverity:** {{ subject.vulnerabilityUpdateDiff.newSeverity | escape(strategy="json") }}\n**Source:** {{ subject.vulnerability.source | escape(strategy="json") }}\n**Component:** {{ subject.component.toString | escape(strategy="json") }}\n**Actions:**\n[View Vulnerability]({{ baseUrl }}/vulnerability/?source={{ subject.vulnerability.source | escape(strategy="json") }}&vulnId={{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}\n**Project:** {{ subject.dependency.project.toString | escape(strategy="json") }}\n**Component:** {{ subject.dependency.component.toString | escape(strategy="json") }}\n**Actions:**\n[View Project]({{ baseUrl }}/projects/?uuid={{ subject.dependency.project.uuid | escape(strategy="json") }}){% endif %}\n[View Component]({{ baseUrl }}/component/?uuid={{ subject.dependency.component.uuid | escape(strategy="json") }})\n**Description:** {{ notification.content | escape(strategy="json") }}"} diff --git a/src/main/resources/templates/notification/publisher/email.peb b/src/main/resources/templates/notification/publisher/email.peb index 2e09ce5fca..a4988bc991 100644 --- a/src/main/resources/templates/notification/publisher/email.peb +++ b/src/main/resources/templates/notification/publisher/email.peb @@ -19,7 +19,28 @@ Other affected projects: {% for affectedProject in notification.subject.affectedProjects %}{% if not (affectedProject.uuid == subject.component.project.uuid) %} Project: [{{ affectedProject.name }} : {{ affectedProject.version }}] Project URL: {{ baseUrl }}/projects/{{ affectedProject.uuid }} -{% endif %}{% endfor %}{% endif %}{% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %} +{% endif %}{% endfor %}{% endif %} +{% elseif notification.group == "VULNERABILITY_UPDATED" %} +Vulnerability ID: {{ subject.vulnerability.vulnId }} +Vulnerability URL: {{ baseUrl }}/vulnerability/?source={{ subject.vulnerability.source }}&vulnId={{ subject.vulnerability.vulnId }} +Old Severity: {{ subject.vulnerabilityUpdateDiff.oldSeverity }} +New Severity: {{ subject.vulnerabilityUpdateDiff.newSeverity }} +Source: {{ subject.vulnerability.source }} +Component: {{ subject.component.toString }} +Component URL: {{ baseUrl }}/component/?uuid={{ subject.component.uuid }} +Project: {{ subject.component.project.name }} +Version: {{ subject.component.project.version }} +Description: {{ subject.component.project.description }} +Project URL: {{ baseUrl }}/projects/{{ subject.component.project.uuid }} +{% if notification.subject.affectedProjects|length > 1%} +-------------------------------------------------------------------------------- + +Other affected projects: +{% for affectedProject in notification.subject.affectedProjects %}{% if not (affectedProject.uuid == subject.component.project.uuid) %} +Project: [{{ affectedProject.name }} : {{ affectedProject.version }}] +Project URL: {{ baseUrl }}/projects/{{ affectedProject.uuid }} +{% endif %}{% endfor %}{% endif %} +{% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %} Project: {{ subject.component.project.toString }} Project URL: {{ baseUrl }}/projects/{{ subject.component.project.uuid }} Component: {{ subject.component.toString }} diff --git a/src/main/resources/templates/notification/publisher/jira.peb b/src/main/resources/templates/notification/publisher/jira.peb index 996abe94c7..3ee31dc749 100644 --- a/src/main/resources/templates/notification/publisher/jira.peb +++ b/src/main/resources/templates/notification/publisher/jira.peb @@ -6,9 +6,11 @@ "issuetype": { "name": "{{ jiraTicketType }}" }, - "summary": "[Dependency-Track] [{{ notification.group | escape(strategy="json") }}] {% if notification.group == "NEW_VULNERABILITY" %}[{{ subject.vulnerability.severity }}] New {{ subject.vulnerability.severity | lower }} vulnerability identified: {{ subject.vulnerability.vulnId }}{% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}Vulnerable dependency introduced on project {{ subject.component.project.name | escape(strategy="json") }}{% else %}{{ notification.title | escape(strategy="json") }}{% endif %}", + "summary": "[Dependency-Track] [{{ notification.group | escape(strategy="json") }}] {% if notification.group == "NEW_VULNERABILITY" %}[{{ subject.vulnerability.severity }}] New {{ subject.vulnerability.severity | lower }} vulnerability identified: {{ subject.vulnerability.vulnId }}{% elseif notification.group == "VULNERABILITY_UPDATED" %}[{{ subject.vulnerabilityUpdateDiff.newSeverity }}] Change in the severity {{ subject.vulnerabilityUpdateDiff.oldSeverity }} to {{ subject.vulnerabilityUpdateDiff.newSeverity }} of a vulnerability: {{ subject.vulnerability.vulnId }}{% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}Vulnerable dependency introduced on project {{ subject.component.project.name | escape(strategy="json") }}{% else %}{{ notification.title | escape(strategy="json") }}{% endif %}", {% if notification.group == "NEW_VULNERABILITY" %} "description": "A new vulnerability has been identified on your project(s).\n\\\\\n\\\\\n*Vulnerability description*\n{code:none|bgColor=white|borderStyle=none}{{ subject.vulnerability.description | escape(strategy="json") }}{code}\n\n*VulnID*\n{{ subject.vulnerability.vulnId }}\n\n*Severity*\n{{ subject.vulnerability.severity | lower | capitalize }}\n\n*Component*\n[{{ subject.component | escape(strategy="json") }}|{{ baseUrl }}/components/{{ subject.component.uuid }}]\n\n*Affected project(s)*\n{% for project in subject.affectedProjects %}- [{{ project.name | escape(strategy="json") }} ({{ project.version | escape(strategy="json") }})|{{ baseUrl }}/projects/{{ project.uuid }}]\n{% endfor %}" + {% elseif notification.group == "VULNERABILITY_UPDATED" %} + "description": "A vulnerability has been updated on your project(s).\n\\\\\n\\\\\n*Vulnerability description*\n{code:none|bgColor=white|borderStyle=none}{{ subject.vulnerability.description | escape(strategy="json") }}{code}\n\n*VulnID*\n{{ subject.vulnerability.vulnId }}\n\n*OldSeverity*\n{{ subject.vulnerabilityUpdateDiff.oldSeverity | lower | capitalize }}\n\n*NewSeverity*\n{{ subject.vulnerabilityUpdateDiff.newSeverity | lower | capitalize }}\n\n*Component*\n[{{ subject.component | escape(strategy="json") }}|{{ baseUrl }}/components/{{ subject.component.uuid }}]\n\n*Affected project(s)*\n{% for project in subject.affectedProjects %}- [{{ project.name | escape(strategy="json") }} ({{ project.version | escape(strategy="json") }})|{{ baseUrl }}/projects/{{ project.uuid }}]\n{% endfor %}" {% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %} "description": "A component which contains one or more vulnerabilities has been added to your project.\n\\\\\n\\\\\n*Project*\n[{{ subject.component.project | escape(strategy="json") }}|{{ baseUrl }}/projects/{{ subject.component.project.uuid }}]\n\n*Component*\n[{{ subject.component | escape(strategy="json") }}|{{ baseUrl }}/components/{{ subject.component.uuid }}]\n\n*Vulnerabilities*\n{% for vulnerability in subject.vulnerabilities %}- {{ vulnerability.vulnId }} ({{ vulnerability.severity | lower | capitalize }})\n{% endfor %}" {% else %} diff --git a/src/main/resources/templates/notification/publisher/mattermost.peb b/src/main/resources/templates/notification/publisher/mattermost.peb index 2a01512163..8061fb61fb 100644 --- a/src/main/resources/templates/notification/publisher/mattermost.peb +++ b/src/main/resources/templates/notification/publisher/mattermost.peb @@ -1,5 +1,5 @@ { "username": "Dependency Track", "icon_url": "https://raw.githubusercontent.com/DependencyTrack/branding/master/dt-logo-symbol-blue-background.png", - "text": "#### {{ notification.title | escape(strategy="json") }}\n{{ notification.content | escape(strategy="json") }}\n{% if notification.group == "NEW_VULNERABILITY" %}**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Vulnerability**: {{ subject.vulnerability.vulnId | escape(strategy="json") }}, {{ subject.vulnerability.severity | escape(strategy="json") }}\n[View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}) - [View Vulnerability]({{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}){% elseif notification.group == "PROJECT_AUDIT_CHANGE" or notification.group == "GLOBAL_AUDIT_CHANGE" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Vulnerability**: {{ subject.vulnerability.vulnId | escape(strategy="json") }}, {{ subject.vulnerability.severity | escape(strategy="json") }}\n**Analysis**: {{ subject.analysis.analysisState | escape(strategy="json") }}, suppressed: {{ subject.analysis.suppressed | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}) - [View Vulnerability]({{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "BOM_CONSUMED" or notification.group == "BOM_PROCESSED" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}){% elseif notification.group == "POLICY_VIOLATION" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Policy**: {{ subject.policyViolation.policyCondition.policy.violationState | escape(strategy="json") }}, {{ subject.policyViolation.policyCondition.policy.name | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}){% endif %}" + "text": "#### {{ notification.title | escape(strategy="json") }}\n{{ notification.content | escape(strategy="json") }}\n{% if notification.group == "NEW_VULNERABILITY" %}**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Vulnerability**: {{ subject.vulnerability.vulnId | escape(strategy="json") }}, {{ subject.vulnerability.severity | escape(strategy="json") }}\n[View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}) - [View Vulnerability]({{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "VULNERABILITY_UPDATED" %}**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Vulnerability**: {{ subject.vulnerability.vulnId | escape(strategy="json") }}, Old severity: {{ subject.vulnerabilityUpdateDiff.oldSeverity | escape(strategy="json") }}, New severity: {{ subject.vulnerabilityUpdateDiff.newSeverity | escape(strategy="json") }}\n[View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}) - [View Vulnerability]({{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}){% elseif notification.group == "PROJECT_AUDIT_CHANGE" or notification.group == "GLOBAL_AUDIT_CHANGE" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Vulnerability**: {{ subject.vulnerability.vulnId | escape(strategy="json") }}, {{ subject.vulnerability.severity | escape(strategy="json") }}\n**Analysis**: {{ subject.analysis.analysisState | escape(strategy="json") }}, suppressed: {{ subject.analysis.suppressed | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}) - [View Vulnerability]({{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}){% elseif notification.group == "BOM_CONSUMED" or notification.group == "BOM_PROCESSED" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}){% elseif notification.group == "POLICY_VIOLATION" %}**Project**: {{ subject.project.toString | escape(strategy="json") }}\n**Component**: {{ subject.component.toString | escape(strategy="json") }}\n**Policy**: {{ subject.policyViolation.policyCondition.policy.violationState | escape(strategy="json") }}, {{ subject.policyViolation.policyCondition.policy.name | escape(strategy="json") }}\n[View Project]({{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy="json") }}) - [View Component]({{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}){% endif %}" } diff --git a/src/main/resources/templates/notification/publisher/msteams.peb b/src/main/resources/templates/notification/publisher/msteams.peb index c91c8f691b..d91480e96d 100644 --- a/src/main/resources/templates/notification/publisher/msteams.peb +++ b/src/main/resources/templates/notification/publisher/msteams.peb @@ -27,6 +27,29 @@ "value": "{{ subject.component.toString | escape(strategy="json") }}" } ], + {% elseif notification.group == "VULNERABILITY_UPDATED" %} + "facts": [ + { + "name": "VulnID", + "value": "{{ subject.vulnerability.vulnId | escape(strategy="json") }}" + }, + { + "name": "Old Severity", + "value": "{{ subject.vulnerabilityUpdateDiff.oldSeverity | escape(strategy="json") }}" + }, + { + "name": "New Severity", + "value": "{{ subject.vulnerabilityUpdateDiff.newSeverity | escape(strategy="json") }}" + }, + { + "name": "Source", + "value": "{{ subject.vulnerability.source | escape(strategy="json") }}" + }, + { + "name": "Component", + "value": "{{ subject.component.toString | escape(strategy="json") }}" + } + ], {% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %} "facts": [ { diff --git a/src/main/resources/templates/notification/publisher/slack.peb b/src/main/resources/templates/notification/publisher/slack.peb index beb0de3eb9..c651736ca1 100644 --- a/src/main/resources/templates/notification/publisher/slack.peb +++ b/src/main/resources/templates/notification/publisher/slack.peb @@ -87,6 +87,101 @@ {% endif %} ] } +{% elseif notification.group == "VULNERABILITY_UPDATED" %} +{ + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "Vulnerability Updated" + } + }, + { + "type": "context", + "elements": [ + { + "text": "*{{ notification.level | escape(strategy="json") }}* | *{{ notification.scope | escape(strategy="json") }}*", + "type": "mrkdwn" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "text": "{{ notification.title | escape(strategy="json") }}", + "type": "mrkdwn" + }, + "fields": [ + { + "type": "mrkdwn", + "text": "*VulnID*" + }, + { + "type": "plain_text", + "text": "{{ subject.vulnerability.vulnId | escape(strategy="json") }}" + }, + { + "type": "mrkdwn", + "text": "*Old Severity*" + }, + { + "type": "plain_text", + "text": "{{ subject.vulnerabilityUpdateDiff.oldSeverity | escape(strategy="json") }}" + },{ + "type": "mrkdwn", + "text": "*New Severity*" + }, + { + "type": "plain_text", + "text": "{{ subject.vulnerabilityUpdateDiff.newSeverity | escape(strategy="json") }}" + }, + { + "type": "mrkdwn", + "text": "*Source*" + }, + { + "type": "plain_text", + "text": "{{ subject.vulnerability.source | escape(strategy="json") }}" + }, + { + "type": "mrkdwn", + "text": "*Component*" + }, + { + "type": "plain_text", + "text": "{{ subject.component.toString | escape(strategy="json") }}" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Vulnerability" + }, + "action_id": "actionId-1", + "url": "{{ baseUrl }}/vulnerabilities/{{ subject.vulnerability.source | escape(strategy="json") }}/{{ subject.vulnerability.vulnId | escape(strategy="json") }}" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Component" + }, + "action_id": "actionId-2", + "url": "{{ baseUrl }}/components/{{ subject.component.uuid | escape(strategy="json") }}" + } + ] + } + ] +} {% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %} { "blocks": [ diff --git a/src/test/java/org/dependencytrack/event/VulnerabilityUpdateEventTest.java b/src/test/java/org/dependencytrack/event/VulnerabilityUpdateEventTest.java new file mode 100644 index 0000000000..54601dca1b --- /dev/null +++ b/src/test/java/org/dependencytrack/event/VulnerabilityUpdateEventTest.java @@ -0,0 +1,39 @@ +/* + * 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.event; + +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityUpdateDiff; +import org.junit.Assert; +import org.junit.Test; + + +public class VulnerabilityUpdateEventTest { + + @Test + public void testEvent() { + Vulnerability vulnerability = new Vulnerability(); + VulnerabilityUpdateDiff vulnerabilityUpdateDiff = new VulnerabilityUpdateDiff(Severity.UNASSIGNED, Severity.HIGH); + VulnerabilityUpdateEvent event = new VulnerabilityUpdateEvent(vulnerability, vulnerabilityUpdateDiff); + Assert.assertNotNull(event.getVulnerability()); + Assert.assertEquals(Severity.UNASSIGNED, event.getVulnerabilityUpdateDiff().getOldSeverity()); + Assert.assertEquals(Severity.HIGH, event.getVulnerabilityUpdateDiff().getNewSeverity()); + } +} diff --git a/src/test/java/org/dependencytrack/notification/NotificationConstantsTest.java b/src/test/java/org/dependencytrack/notification/NotificationConstantsTest.java index 48f7252e67..361a14546e 100644 --- a/src/test/java/org/dependencytrack/notification/NotificationConstantsTest.java +++ b/src/test/java/org/dependencytrack/notification/NotificationConstantsTest.java @@ -38,6 +38,7 @@ public void testConstants() { Assert.assertEquals("Repository Error", NotificationConstants.Title.REPO_ERROR); Assert.assertEquals("Integration Error", NotificationConstants.Title.INTEGRATION_ERROR); Assert.assertEquals("New Vulnerability Identified", NotificationConstants.Title.NEW_VULNERABILITY); + Assert.assertEquals("Change in Severity of a Vulnerability", NotificationConstants.Title.VULNERABILITY_UPDATED); Assert.assertEquals("Vulnerable Dependency Introduced", NotificationConstants.Title.NEW_VULNERABLE_DEPENDENCY); Assert.assertEquals("Analysis Decision: Exploitable", NotificationConstants.Title.ANALYSIS_DECISION_EXPLOITABLE); Assert.assertEquals("Analysis Decision: In Triage", NotificationConstants.Title.ANALYSIS_DECISION_IN_TRIAGE); diff --git a/src/test/java/org/dependencytrack/notification/NotificationGroupTest.java b/src/test/java/org/dependencytrack/notification/NotificationGroupTest.java index d10d5f0dff..ca32eeb1af 100644 --- a/src/test/java/org/dependencytrack/notification/NotificationGroupTest.java +++ b/src/test/java/org/dependencytrack/notification/NotificationGroupTest.java @@ -34,6 +34,7 @@ public void testEnums() { Assert.assertEquals("INDEXING_SERVICE", NotificationGroup.INDEXING_SERVICE.name()); // Portfolio Groups Assert.assertEquals("NEW_VULNERABILITY", NotificationGroup.NEW_VULNERABILITY.name()); + Assert.assertEquals("VULNERABILITY_UPDATED", NotificationGroup.VULNERABILITY_UPDATED.name()); Assert.assertEquals("NEW_VULNERABLE_DEPENDENCY", NotificationGroup.NEW_VULNERABLE_DEPENDENCY.name()); //Assert.assertEquals("NEW_OUTDATED_COMPONENT", NotificationGroup.NEW_OUTDATED_COMPONENT.name()); //Assert.assertEquals("FIXED_VULNERABILITY", NotificationGroup.FIXED_VULNERABILITY.name()); diff --git a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java index 944b59bcf3..261497ec61 100644 --- a/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java +++ b/src/test/java/org/dependencytrack/notification/NotificationRouterTest.java @@ -41,6 +41,7 @@ import org.dependencytrack.notification.vo.PolicyViolationIdentified; import org.dependencytrack.notification.vo.VexConsumedOrProcessed; import org.dependencytrack.notification.vo.ViolationAnalysisDecisionChange; +import org.dependencytrack.notification.vo.VulnerabilityUpdate; import org.junit.Assert; import org.junit.Test; @@ -347,6 +348,40 @@ public void testNewVulnerabilityIdentifiedLimitedToProject() { .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } + @Test + public void testVulnerabilityUpdateLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("Component A"); + componentA = qm.createComponent(componentA, false); + + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("Component B"); + componentB = qm.createComponent(componentB, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.VULNERABILITY_UPDATED)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.VULNERABILITY_UPDATED.name()); + notification.setLevel(NotificationLevel.INFORMATIONAL); + notification.setSubject(new VulnerabilityUpdate(null, null, componentB, Set.of())); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(notification)).isEmpty(); + + notification.setSubject(new VulnerabilityUpdate(null, null, componentA, Set.of())); + assertThat(router.resolveRules(notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + @Test public void testNewVulnerableDependencyLimitedToProject() { final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); diff --git a/src/test/java/org/dependencytrack/notification/vo/VulnerabilityUpdateTest.java b/src/test/java/org/dependencytrack/notification/vo/VulnerabilityUpdateTest.java new file mode 100644 index 0000000000..9d9b440f07 --- /dev/null +++ b/src/test/java/org/dependencytrack/notification/vo/VulnerabilityUpdateTest.java @@ -0,0 +1,30 @@ +package org.dependencytrack.notification.vo; + +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityUpdateDiff; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class VulnerabilityUpdateTest { + + @Test + public void testVo() { + Vulnerability vuln = new Vulnerability(); + VulnerabilityUpdateDiff vulnDiff = new VulnerabilityUpdateDiff(Severity.UNASSIGNED, Severity.LOW); + Component component = new Component(); + Set affectedProjects = new HashSet<>(); + VulnerabilityUpdate vo = new VulnerabilityUpdate(vuln, vulnDiff, component, affectedProjects); + Assert.assertEquals(vuln, vo.getVulnerability()); + Assert.assertEquals(vulnDiff, vo.getVulnerabilityUpdateDiff()); + Assert.assertEquals(component, vo.getComponent()); + Assert.assertEquals(affectedProjects, vo.getAffectedProjects()); + } +}