Skip to content

Commit ca3ef44

Browse files
authored
Merge pull request #3983 from 2000rosser/issue-2924
Add option to test notification publisher
2 parents f4791bf + cda16bb commit ca3ef44

3 files changed

Lines changed: 194 additions & 5 deletions

File tree

src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
3535
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
3636
import io.swagger.v3.oas.annotations.tags.Tag;
37+
3738
import org.dependencytrack.auth.Permissions;
3839
import org.dependencytrack.model.ConfigPropertyConstants;
3940
import org.dependencytrack.model.NotificationPublisher;
41+
import org.dependencytrack.model.NotificationRule;
4042
import org.dependencytrack.model.validation.ValidUuid;
4143
import org.dependencytrack.notification.NotificationConstants;
4244
import org.dependencytrack.notification.NotificationGroup;
@@ -49,6 +51,7 @@
4951

5052
import jakarta.json.Json;
5153
import jakarta.json.JsonObject;
54+
import jakarta.json.JsonReader;
5255
import jakarta.validation.Validator;
5356
import jakarta.ws.rs.Consumes;
5457
import jakarta.ws.rs.DELETE;
@@ -62,6 +65,7 @@
6265
import jakarta.ws.rs.core.MediaType;
6366
import jakarta.ws.rs.core.Response;
6467
import java.io.IOException;
68+
import java.io.StringReader;
6569
import java.lang.reflect.InvocationTargetException;
6670
import java.util.List;
6771

@@ -322,4 +326,52 @@ public Response testSmtpPublisherConfig(@FormParam("destination") String destina
322326
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while sending test mail notification.").build();
323327
}
324328
}
329+
330+
@POST
331+
@Path("/test/{uuid}")
332+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
333+
@Produces(MediaType.APPLICATION_JSON)
334+
@Operation(
335+
summary = "Dispatches a rule notification test",
336+
description = "<p>Requires permission <strong>SYSTEM_CONFIGURATION</strong></p>"
337+
)
338+
@ApiResponses(value = {
339+
@ApiResponse(responseCode = "200", description = "Test notification dispatched successfully"),
340+
@ApiResponse(responseCode = "401", description = "Unauthorized")
341+
})
342+
@PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION)
343+
public Response testSlackPublisherConfig(
344+
@Parameter(description = "The UUID of the rule to test", schema = @Schema(type = "string", format = "uuid"), required = true)
345+
@PathParam("uuid") @ValidUuid String ruleUuid) throws Exception {
346+
try(QueryManager qm = new QueryManager()){
347+
NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid);
348+
NotificationPublisher notificationPublisher = rule.getPublisher();
349+
final Class<?> publisherClass = Class.forName(notificationPublisher.getPublisherClass());
350+
Publisher publisher = (Publisher) publisherClass.getDeclaredConstructor().newInstance();
351+
String publisherConfig = rule.getPublisherConfig();
352+
JsonReader jsonReader = Json.createReader(new StringReader(publisherConfig));
353+
JsonObject configObject = jsonReader.readObject();
354+
jsonReader.close();
355+
final JsonObject config = Json.createObjectBuilder()
356+
.add(Publisher.CONFIG_DESTINATION, configObject.getString("destination"))
357+
.add(Publisher.CONFIG_TEMPLATE_KEY, rule.getPublisher().getTemplate())
358+
.add(Publisher.CONFIG_TEMPLATE_MIME_TYPE_KEY, rule.getPublisher().getTemplateMimeType())
359+
.build();
360+
361+
for(NotificationGroup group : rule.getNotifyOn()){
362+
final Notification notification = new Notification()
363+
.scope(rule.getScope())
364+
.group(group.toString())
365+
.title(group)
366+
.content("Rule configuration test")
367+
.level(rule.getNotificationLevel())
368+
.subject(NotificationUtil.generateSubject(group.toString()));
369+
publisher.inform(PublishContext.from(notification), notification, config);
370+
}
371+
return Response.ok().build();
372+
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
373+
LOGGER.error(e.getMessage(), e);
374+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while sending the notification.").build();
375+
}
376+
}
325377
}

src/main/java/org/dependencytrack/util/NotificationUtil.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import alpine.notification.NotificationLevel;
2525
import org.apache.commons.io.FileUtils;
2626
import org.dependencytrack.model.Analysis;
27+
import org.dependencytrack.model.AnalysisState;
2728
import org.dependencytrack.model.Bom;
2829
import org.dependencytrack.model.Component;
2930
import org.dependencytrack.model.ComponentIdentity;
@@ -32,9 +33,12 @@
3233
import org.dependencytrack.model.NotificationPublisher;
3334
import org.dependencytrack.model.Policy;
3435
import org.dependencytrack.model.PolicyCondition;
36+
import org.dependencytrack.model.PolicyCondition.Operator;
3537
import org.dependencytrack.model.PolicyViolation;
3638
import org.dependencytrack.model.Project;
39+
import org.dependencytrack.model.Severity;
3740
import org.dependencytrack.model.Tag;
41+
import org.dependencytrack.model.Vex;
3842
import org.dependencytrack.model.ViolationAnalysis;
3943
import org.dependencytrack.model.ViolationAnalysisState;
4044
import org.dependencytrack.model.Vulnerability;
@@ -65,11 +69,14 @@
6569
import java.io.IOException;
6670
import java.net.URLDecoder;
6771
import java.nio.file.Path;
72+
import java.util.Date;
6873
import java.util.HashMap;
6974
import java.util.HashSet;
7075
import java.util.List;
7176
import java.util.Map;
7277
import java.util.Optional;
78+
import java.util.Set;
79+
import java.util.UUID;
7380

7481
import static java.nio.charset.StandardCharsets.UTF_8;
7582

@@ -634,4 +641,105 @@ public static String generateNotificationTitle(String messageType, Project proje
634641
}
635642
return messageType;
636643
}
644+
645+
public static Object generateSubject(String group) {
646+
final Project project = createProject();
647+
final Vulnerability vuln = createVulnerability();
648+
final Component component = createComponent(project);
649+
final Analysis analysis = createAnalysis(component, vuln);
650+
final PolicyViolation policyViolation = createPolicyViolation(component, project);
651+
652+
switch (group) {
653+
case "BOM_CONSUMED":
654+
return new BomConsumedOrProcessed(project, "bomContent", Bom.Format.CYCLONEDX, "1.5");
655+
case "BOM_PROCESSED":
656+
return new BomConsumedOrProcessed(project, "bomContent", Bom.Format.CYCLONEDX, "1.5");
657+
case "BOM_PROCESSING_FAILED":
658+
return new BomProcessingFailed(project, "bomContent", "cause", Bom.Format.CYCLONEDX, "1.5");
659+
case "BOM_VALIDATION_FAILED":
660+
return new BomValidationFailed(project, "bomContent", List.of("TEST"), Bom.Format.CYCLONEDX);
661+
case "VEX_CONSUMED":
662+
return new VexConsumedOrProcessed(project, "", Vex.Format.CYCLONEDX, "");
663+
case "VEX_PROCESSED":
664+
return new VexConsumedOrProcessed(project, "", Vex.Format.CYCLONEDX, "");
665+
case "NEW_VULNERABILITY":
666+
return new NewVulnerabilityIdentified(vuln, component, Set.of(project), VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS);
667+
case "NEW_VULNERABLE_DEPENDENCY":
668+
return new NewVulnerableDependency(component, List.of(vuln));
669+
case "POLICY_VIOLATION":
670+
return new PolicyViolationIdentified(policyViolation, component, project);
671+
case "PROJECT_CREATED":
672+
return NotificationUtil.toJson(project);
673+
case "PROJECT_AUDIT_CHANGE":
674+
return new AnalysisDecisionChange(vuln, component, project, analysis);
675+
default:
676+
return null;
677+
}
678+
}
679+
680+
private static Project createProject() {
681+
final Project project = new Project();
682+
project.setUuid(UUID.fromString("c9c9539a-e381-4b36-ac52-6a7ab83b2c95"));
683+
project.setName("projectName");
684+
project.setVersion("projectVersion");
685+
project.setPurl("pkg:maven/org.acme/projectName@projectVersion");
686+
return project;
687+
}
688+
689+
private static Vulnerability createVulnerability() {
690+
final Vulnerability vuln = new Vulnerability();
691+
vuln.setUuid(UUID.fromString("bccec5d5-ec21-4958-b3e8-22a7a866a05a"));
692+
vuln.setVulnId("INT-001");
693+
vuln.setSource(Vulnerability.Source.INTERNAL);
694+
vuln.setSeverity(Severity.MEDIUM);
695+
return vuln;
696+
}
697+
698+
private static Component createComponent(Project project) {
699+
final Component component = new Component();
700+
component.setProject(project);
701+
component.setUuid(UUID.fromString("94f87321-a5d1-4c2f-b2fe-95165debebc6"));
702+
component.setName("componentName");
703+
component.setVersion("componentVersion");
704+
return component;
705+
}
706+
707+
private static Analysis createAnalysis(Component component, Vulnerability vuln) {
708+
final Analysis analysis = new Analysis();
709+
analysis.setComponent(component);
710+
analysis.setVulnerability(vuln);
711+
analysis.setAnalysisState(AnalysisState.FALSE_POSITIVE);
712+
analysis.setSuppressed(true);
713+
return analysis;
714+
}
715+
716+
private static PolicyViolation createPolicyViolation(Component component, Project project) {
717+
final Policy policy = new Policy();
718+
policy.setId(1);
719+
policy.setName("test");
720+
policy.setOperator(Policy.Operator.ALL);
721+
policy.setProjects(List.of(project));
722+
policy.setUuid(UUID.randomUUID());
723+
policy.setViolationState(Policy.ViolationState.INFO);
724+
725+
final PolicyCondition condition = new PolicyCondition();
726+
condition.setId(1);
727+
condition.setUuid(UUID.randomUUID());
728+
condition.setOperator(Operator.NUMERIC_EQUAL);
729+
condition.setSubject(PolicyCondition.Subject.AGE);
730+
condition.setValue("1");
731+
condition.setPolicy(policy);
732+
733+
final PolicyViolation policyViolation = new PolicyViolation();
734+
policyViolation.setId(1);
735+
policyViolation.setPolicyCondition(condition);
736+
policyViolation.setComponent(component);
737+
policyViolation.setText("test");
738+
policyViolation.setType(PolicyViolation.Type.SECURITY);
739+
policyViolation.setAnalysis(new ViolationAnalysis());
740+
policyViolation.setUuid(UUID.randomUUID());
741+
policyViolation.setTimestamp(new Date(System.currentTimeMillis()));
742+
return policyViolation;
743+
}
744+
637745
}

src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,38 @@
1818
*/
1919
package org.dependencytrack.resources.v1;
2020

21-
import alpine.common.util.UuidUtil;
22-
import alpine.notification.NotificationLevel;
23-
import alpine.server.filters.ApiFilter;
24-
import alpine.server.filters.AuthenticationFilter;
21+
import java.util.HashSet;
22+
import java.util.Set;
23+
import java.util.UUID;
24+
2525
import org.dependencytrack.JerseyTestRule;
2626
import org.dependencytrack.ResourceTest;
2727
import org.dependencytrack.model.ConfigPropertyConstants;
2828
import org.dependencytrack.model.NotificationPublisher;
2929
import org.dependencytrack.model.NotificationRule;
30+
import org.dependencytrack.notification.NotificationGroup;
3031
import org.dependencytrack.notification.NotificationScope;
3132
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
3233
import org.dependencytrack.notification.publisher.Publisher;
3334
import org.dependencytrack.notification.publisher.SendMailPublisher;
35+
import org.dependencytrack.notification.publisher.SlackPublisher;
3436
import org.dependencytrack.persistence.DefaultObjectGenerator;
3537
import org.glassfish.jersey.server.ResourceConfig;
3638
import org.junit.Assert;
3739
import org.junit.Before;
3840
import org.junit.ClassRule;
3941
import org.junit.Test;
4042

43+
import alpine.common.util.UuidUtil;
44+
import alpine.notification.NotificationLevel;
45+
import alpine.server.filters.ApiFilter;
46+
import alpine.server.filters.AuthenticationFilter;
4147
import jakarta.json.JsonArray;
4248
import jakarta.json.JsonObject;
4349
import jakarta.ws.rs.client.Entity;
4450
import jakarta.ws.rs.core.Form;
4551
import jakarta.ws.rs.core.MediaType;
4652
import jakarta.ws.rs.core.Response;
47-
import java.util.UUID;
4853

4954
public class NotificationPublisherResourceTest extends ResourceTest {
5055

@@ -339,6 +344,30 @@ public void testSmtpPublisherConfigTest() {
339344
Assert.assertEquals(200, response.getStatus(), 0);
340345
}
341346

347+
@Test
348+
public void testNotificationRuleTest() {
349+
NotificationPublisher publisher = qm.createNotificationPublisher(
350+
"Example Publisher", "Publisher description",
351+
SlackPublisher.class, "template", "text/html",
352+
false);
353+
354+
NotificationRule rule = qm.createNotificationRule("Example Rule 1", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher);
355+
356+
Set<NotificationGroup> groups = new HashSet<>(Set.of(NotificationGroup.BOM_CONSUMED, NotificationGroup.BOM_PROCESSED, NotificationGroup.BOM_PROCESSING_FAILED,
357+
NotificationGroup.BOM_VALIDATION_FAILED, NotificationGroup.NEW_VULNERABILITY, NotificationGroup.NEW_VULNERABLE_DEPENDENCY,
358+
NotificationGroup.POLICY_VIOLATION, NotificationGroup.PROJECT_CREATED, NotificationGroup.PROJECT_AUDIT_CHANGE,
359+
NotificationGroup.VEX_CONSUMED, NotificationGroup.VEX_PROCESSED));
360+
rule.setNotifyOn(groups);
361+
362+
rule.setPublisherConfig("{\"destination\":\"https://example.com/webhook\"}");
363+
364+
Response sendMailResponse = jersey.target(V1_NOTIFICATION_PUBLISHER + "/test/" + rule.getUuid()).request()
365+
.header(X_API_KEY, apiKey)
366+
.post(Entity.entity("", MediaType.APPLICATION_FORM_URLENCODED_TYPE));
367+
368+
Assert.assertEquals(200, sendMailResponse.getStatus());
369+
}
370+
342371
@Test
343372
public void restoreDefaultTemplatesTest() {
344373
NotificationPublisher slackPublisher = qm.getDefaultNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherClass());

0 commit comments

Comments
 (0)