diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index f9196cb94..da1dc8aa6 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -20,6 +20,7 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; +import alpine.notification.Notification; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; @@ -44,9 +45,13 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.notification.NotificationConstants; +import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.publisher.PublisherClass; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.NotificationUtil; @@ -267,4 +272,39 @@ public Response restoreDefaultTemplates() { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while restoring default notification publisher templates.").build(); } } + + @POST + @Path("/test/{uuid}") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Dispatches a rule notification test", + description = "

Requires permission SYSTEM_CONFIGURATION

" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Test notification dispatched successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION) + public Response testSlackPublisherConfig( + @Parameter(description = "The UUID of the rule to test", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String ruleUuid) { + try (QueryManager qm = new QueryManager()) { + NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid); + final KafkaEventDispatcher eventDispatcher = new KafkaEventDispatcher(); + for(NotificationGroup group : rule.getNotifyOn()){ + eventDispatcher.dispatchNotification(new Notification() + .scope(rule.getScope()) + .group(group.toString()) + .level(rule.getNotificationLevel()) + .title(NotificationConstants.Title.NOTIFICATION_TEST) + .subject(NotificationUtil.generateSubjectForTestRuleNotification(group)) + .content("Rule configuration test")); + } + return Response.ok().build(); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while sending the notification.").build(); + } + } } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index 629825157..faba2d1b8 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -26,6 +26,7 @@ import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisState; +import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; @@ -33,15 +34,25 @@ import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; import org.dependencytrack.model.Tag; +import org.dependencytrack.model.Vex; import org.dependencytrack.model.ViolationAnalysis; import org.dependencytrack.model.ViolationAnalysisState; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.vo.AnalysisDecisionChange; +import org.dependencytrack.notification.vo.BomConsumedOrProcessed; +import org.dependencytrack.notification.vo.BomProcessingFailed; +import org.dependencytrack.notification.vo.BomValidationFailed; +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 org.dependencytrack.persistence.QueryManager; @@ -54,8 +65,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import static java.nio.charset.StandardCharsets.UTF_8; @@ -437,4 +450,99 @@ public static class PolicyViolationNotificationProjection { public String analysisState; } + public static Object generateSubjectForTestRuleNotification(NotificationGroup group) { + final Project project = createProjectForTestRuleNotification(); + final Vulnerability vuln = createVulnerabilityForTestRuleNotification(); + final Component component = createComponentForTestRuleNotification(project); + final Analysis analysis = createAnalysisForTestRuleNotification(component, vuln); + final PolicyViolation policyViolation = createPolicyViolationForTestRuleNotification(component, project); + final UUID token = UUID.randomUUID(); + switch (group) { + case BOM_CONSUMED, BOM_PROCESSED: + return new BomConsumedOrProcessed(token, project, /* bom */ "(Omitted)", Bom.Format.CYCLONEDX, "1.5"); + case BOM_PROCESSING_FAILED: + return new BomProcessingFailed(token, project, /* bom */ "(Omitted)", "cause", Bom.Format.CYCLONEDX, "1.5"); + case BOM_VALIDATION_FAILED: + return new BomValidationFailed(project, /* bom */ "(Omitted)", List.of("TEST")); + case VEX_CONSUMED, VEX_PROCESSED: + return new VexConsumedOrProcessed(project, "", Vex.Format.CYCLONEDX, ""); + case NEW_VULNERABILITY: + return new NewVulnerabilityIdentified(vuln, component, VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS); + case NEW_VULNERABLE_DEPENDENCY: + return new NewVulnerableDependency(component, Set.of(vuln)); + case POLICY_VIOLATION: + return new PolicyViolationIdentified(policyViolation, component, project); + case PROJECT_CREATED: + return project; + case PROJECT_AUDIT_CHANGE: + return new AnalysisDecisionChange(vuln, component, project, analysis); + default: + return null; + } + } + + private static Project createProjectForTestRuleNotification() { + final Project project = new Project(); + project.setUuid(UUID.fromString("c9c9539a-e381-4b36-ac52-6a7ab83b2c95")); + project.setName("projectName"); + project.setVersion("projectVersion"); + project.setPurl("pkg:maven/org.acme/projectName@projectVersion"); + return project; + } + + private static Vulnerability createVulnerabilityForTestRuleNotification() { + final Vulnerability vuln = new Vulnerability(); + vuln.setUuid(UUID.fromString("bccec5d5-ec21-4958-b3e8-22a7a866a05a")); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setSeverity(Severity.MEDIUM); + return vuln; + } + + private static Component createComponentForTestRuleNotification(Project project) { + final Component component = new Component(); + component.setProject(project); + component.setUuid(UUID.fromString("94f87321-a5d1-4c2f-b2fe-95165debebc6")); + component.setName("componentName"); + component.setVersion("componentVersion"); + return component; + } + + private static Analysis createAnalysisForTestRuleNotification(Component component, Vulnerability vuln) { + final Analysis analysis = new Analysis(); + analysis.setComponent(component); + analysis.setVulnerability(vuln); + analysis.setAnalysisState(AnalysisState.FALSE_POSITIVE); + analysis.setSuppressed(true); + return analysis; + } + + private static PolicyViolation createPolicyViolationForTestRuleNotification(Component component, Project project) { + final Policy policy = new Policy(); + policy.setId(1); + policy.setName("test"); + policy.setOperator(Policy.Operator.ALL); + policy.setProjects(List.of(project)); + policy.setUuid(UUID.randomUUID()); + policy.setViolationState(Policy.ViolationState.INFO); + + final PolicyCondition condition = new PolicyCondition(); + condition.setId(1); + condition.setUuid(UUID.randomUUID()); + condition.setOperator(PolicyCondition.Operator.NUMERIC_EQUAL); + condition.setSubject(PolicyCondition.Subject.AGE); + condition.setValue("1"); + condition.setPolicy(policy); + + final PolicyViolation policyViolation = new PolicyViolation(); + policyViolation.setId(1); + policyViolation.setPolicyCondition(condition); + policyViolation.setComponent(component); + policyViolation.setText("test"); + policyViolation.setType(PolicyViolation.Type.SECURITY); + policyViolation.setAnalysis(new ViolationAnalysis()); + policyViolation.setUuid(UUID.randomUUID()); + policyViolation.setTimestamp(new Date(System.currentTimeMillis())); + return policyViolation; + } } diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index 54cbc79c7..610bd562a 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -22,11 +22,17 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.persistence.DefaultObjectGenerator; @@ -36,13 +42,11 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher; public class NotificationPublisherResourceTest extends ResourceTest { @@ -316,4 +320,28 @@ public void restoreDefaultTemplatesTest() { slackPublisher = qm.getDefaultNotificationPublisherByName(DefaultNotificationPublishers.SLACK.getPublisherName()); Assert.assertEquals(DefaultNotificationPublishers.SLACK.getPublisherName(), slackPublisher.getName()); } + + @Test + public void testNotificationRuleTest() { + NotificationPublisher slackPublisher = qm.getDefaultNotificationPublisherByName(DefaultNotificationPublishers.SLACK.getPublisherName()); + slackPublisher.setName(slackPublisher.getName()+" Test Rule"); + qm.persist(slackPublisher); + qm.detach(NotificationPublisher.class, slackPublisher.getId()); + + NotificationRule rule = qm.createNotificationRule("Example Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, slackPublisher); + + Set groups = new HashSet<>(Set.of(NotificationGroup.BOM_CONSUMED, NotificationGroup.BOM_PROCESSED, NotificationGroup.BOM_PROCESSING_FAILED, + NotificationGroup.BOM_VALIDATION_FAILED, NotificationGroup.NEW_VULNERABILITY, NotificationGroup.NEW_VULNERABLE_DEPENDENCY, + NotificationGroup.POLICY_VIOLATION, NotificationGroup.PROJECT_CREATED, NotificationGroup.PROJECT_AUDIT_CHANGE, + NotificationGroup.VEX_CONSUMED, NotificationGroup.VEX_PROCESSED)); + rule.setNotifyOn(groups); + rule.setPublisherConfig("{\"destination\":\"https://example.com/webhook\"}"); + + Response response = jersey.target(V1_NOTIFICATION_PUBLISHER + "/test/" + rule.getUuid()).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity("", MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + Assert.assertEquals(200, response.getStatus()); + assertThat(kafkaMockProducer.history().size()).isEqualTo(11); + } }