From 43903e724aecca610a7105f733eba94138ea3da6 Mon Sep 17 00:00:00 2001 From: Aravind Parappil Date: Sat, 1 Jun 2024 20:48:05 -0400 Subject: [PATCH] Add Notification For BOM_VALIDATION_FAILED If uploaded BOM is invalid, dispatches a notification with InvalidBomProblemDetails before throwing the respective exception Signed-off-by: Aravind Parappil --- docs/_docs/integrations/notifications.md | 1 + .../notification/NotificationConstants.java | 1 + .../notification/NotificationGroup.java | 1 + .../notification/NotificationRouter.java | 4 ++ .../notification/publisher/Publisher.java | 4 ++ .../notification/vo/BomValidationFailed.java | 55 +++++++++++++++++ .../resources/v1/BomResource.java | 30 ++++++++- .../resources/v1/VexResource.java | 4 +- .../util/NotificationUtil.java | 42 +++++++++++++ .../notification/publisher/email.peb | 5 ++ .../notification/publisher/msteams.peb | 31 ++++++++++ .../notification/publisher/slack.peb | 52 ++++++++++++++++ .../notification/NotificationRouterTest.java | 26 ++++++++ .../publisher/AbstractPublisherTest.java | 28 +++++++++ .../publisher/MsTeamsPublisherTest.java | 54 ++++++++++++++++ .../publisher/SendMailPublisherTest.java | 36 +++++++++++ .../publisher/SlackPublisherTest.java | 61 +++++++++++++++++++ 17 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/dependencytrack/notification/vo/BomValidationFailed.java diff --git a/docs/_docs/integrations/notifications.md b/docs/_docs/integrations/notifications.md index 5595de405a..8cdc82a919 100644 --- a/docs/_docs/integrations/notifications.md +++ b/docs/_docs/integrations/notifications.md @@ -53,6 +53,7 @@ multiple levels, while others can only ever have a single level. | 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 diff --git a/src/main/java/org/dependencytrack/notification/NotificationConstants.java b/src/main/java/org/dependencytrack/notification/NotificationConstants.java index 6897136f7c..83e78c5339 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationConstants.java +++ b/src/main/java/org/dependencytrack/notification/NotificationConstants.java @@ -58,6 +58,7 @@ public static class Title { public static final String BOM_CONSUMED = "Bill of Materials Consumed"; public static final String BOM_PROCESSED = "Bill of Materials Processed"; public static final String BOM_PROCESSING_FAILED = "Bill of Materials Processing Failed"; + public static final String BOM_VALIDATION_FAILED = "Bill of Materials Validation Failed"; public static final String VEX_CONSUMED = "Vulnerability Exploitability Exchange (VEX) Consumed"; public static final String VEX_PROCESSED = "Vulnerability Exploitability Exchange (VEX) Processed"; public static final String PROJECT_CREATED = "Project Added"; diff --git a/src/main/java/org/dependencytrack/notification/NotificationGroup.java b/src/main/java/org/dependencytrack/notification/NotificationGroup.java index 67db8803fb..64596886c5 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationGroup.java +++ b/src/main/java/org/dependencytrack/notification/NotificationGroup.java @@ -40,6 +40,7 @@ public enum NotificationGroup { BOM_CONSUMED, BOM_PROCESSED, BOM_PROCESSING_FAILED, + BOM_VALIDATION_FAILED, VEX_CONSUMED, VEX_PROCESSED, POLICY_VIOLATION, diff --git a/src/main/java/org/dependencytrack/notification/NotificationRouter.java b/src/main/java/org/dependencytrack/notification/NotificationRouter.java index 6fddbad41a..a5b6cf7aa5 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationRouter.java +++ b/src/main/java/org/dependencytrack/notification/NotificationRouter.java @@ -32,6 +32,7 @@ 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; @@ -186,6 +187,9 @@ List resolveRules(final PublishContext ctx, final Notification } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final BomProcessingFailed subject) { limitToProject(ctx, rules, result, notification, subject.getProject()); + } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) + && notification.getSubject() instanceof final BomValidationFailed subject) { + limitToProject(ctx, rules, result, notification, subject.getProject()); } else if (NotificationScope.PORTFOLIO.name().equals(notification.getScope()) && notification.getSubject() instanceof final VexConsumedOrProcessed subject) { limitToProject(ctx, rules, result, notification, subject.getProject()); diff --git a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java index 3793a3830b..50a319d92f 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/Publisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/Publisher.java @@ -30,6 +30,7 @@ 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; @@ -120,6 +121,9 @@ default String prepareTemplate(final Notification notification, final PebbleTemp } else if (notification.getSubject() instanceof final BomProcessingFailed subject) { context.put("subject", subject); context.put("subjectJson", NotificationUtil.toJson(subject)); + } else if (notification.getSubject() instanceof final BomValidationFailed subject) { + context.put("subject", subject); + context.put("subjectJson", NotificationUtil.toJson(subject)); } else if (notification.getSubject() instanceof final VexConsumedOrProcessed subject) { context.put("subject", subject); context.put("subjectJson", NotificationUtil.toJson(subject)); diff --git a/src/main/java/org/dependencytrack/notification/vo/BomValidationFailed.java b/src/main/java/org/dependencytrack/notification/vo/BomValidationFailed.java new file mode 100644 index 0000000000..0fb8cf20ac --- /dev/null +++ b/src/main/java/org/dependencytrack/notification/vo/BomValidationFailed.java @@ -0,0 +1,55 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.notification.vo; + +import org.dependencytrack.model.Bom; +import org.dependencytrack.model.Project; +import org.dependencytrack.resources.v1.problems.InvalidBomProblemDetails; + +public class BomValidationFailed { + + private Project project; + private String bom; + private InvalidBomProblemDetails problemDetails; + private Bom.Format format; + + public BomValidationFailed(final Project project, final String bom, final InvalidBomProblemDetails problemDetails, final Bom.Format format) { + this.project = project; + this.bom = bom; + this.problemDetails = problemDetails; + this.format = format; + } + + public Project getProject() { + return project; + } + + public String getBom() { + return bom; + } + + public InvalidBomProblemDetails getProblemDetails() { + return problemDetails; + } + + public Bom.Format getFormat() { + return format; + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index f429cf6e12..34df79d7ca 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -20,6 +20,8 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; +import alpine.notification.Notification; +import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; @@ -38,10 +40,16 @@ import org.cyclonedx.exception.GeneratorException; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.BomUploadEvent; +import org.dependencytrack.model.Bom; +import org.dependencytrack.model.Bom.Format; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.notification.NotificationConstants.Title; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.notification.vo.BomValidationFailed; import org.dependencytrack.parser.cyclonedx.CycloneDXExporter; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; import org.dependencytrack.parser.cyclonedx.InvalidBomException; @@ -461,7 +469,7 @@ private Response process(QueryManager qm, Project project, String encodedBomData final byte[] decoded = Base64.getDecoder().decode(encodedBomData); try (final ByteArrayInputStream bain = new ByteArrayInputStream(decoded)) { final byte[] content = IOUtils.toByteArray(BOMInputStream.builder().setInputStream(bain).get()); - validate(content); + validate(content, project); final BomUploadEvent bomUploadEvent = new BomUploadEvent(qm.getPersistenceManager().detachCopy(project), content); Event.dispatch(bomUploadEvent); return Response.ok(Collections.singletonMap("token", bomUploadEvent.getChainIdentifier())).build(); @@ -485,7 +493,7 @@ private Response process(QueryManager qm, Project project, List assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); } + @Test + public void testBomValidationFailedLimitedToProject() { + final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); + final Project projectB = qm.createProject("Project B", null, "1.0", null, null, null, true, false); + + final NotificationPublisher publisher = createSlackPublisher(); + + final NotificationRule rule = qm.createNotificationRule("Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setNotifyOn(Set.of(NotificationGroup.BOM_VALIDATION_FAILED)); + rule.setProjects(List.of(projectA)); + + final var notification = new Notification(); + notification.setScope(NotificationScope.PORTFOLIO.name()); + notification.setGroup(NotificationGroup.BOM_VALIDATION_FAILED.name()); + notification.setLevel(NotificationLevel.ERROR); + notification.setSubject(new BomValidationFailed(projectB, "", null, Bom.Format.CYCLONEDX)); + + final var router = new NotificationRouter(); + assertThat(router.resolveRules(PublishContext.from(notification), notification)).isEmpty(); + + notification.setSubject(new BomValidationFailed(projectA, "", null, Bom.Format.CYCLONEDX)); + assertThat(router.resolveRules(PublishContext.from(notification), notification)) + .satisfiesExactly(resolvedRule -> assertThat(resolvedRule.getName()).isEqualTo("Test Rule")); + } + @Test public void testVexConsumedOrProcessedLimitedToProject() { final Project projectA = qm.createProject("Project A", null, "1.0", null, null, null, true, false); diff --git a/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java index 5f638f5383..0efd766e26 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/AbstractPublisherTest.java @@ -37,7 +37,9 @@ 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.resources.v1.problems.InvalidBomProblemDetails; import org.dependencytrack.notification.vo.NewVulnerableDependency; import org.junit.Test; @@ -98,6 +100,23 @@ public void testInformWithBomProcessingFailedNotification() { .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); } + @Test + public void testInformWithBomValidationFailedNotification() { + final var subject = new BomValidationFailed(createProject(), "bomContent", createInvalidBomProblemDetails(), Bom.Format.CYCLONEDX); + + final var notification = new Notification() + .scope(NotificationScope.PORTFOLIO) + .group(NotificationGroup.BOM_VALIDATION_FAILED) + .title(NotificationConstants.Title.BOM_VALIDATION_FAILED) + .content("An error occurred during BOM Validation") + .level(NotificationLevel.ERROR) + .timestamp(LocalDateTime.ofEpochSecond(1234, 888, ZoneOffset.UTC)) + .subject(subject); + + assertThatNoException() + .isThrownBy(() -> publisherInstance.inform(PublishContext.from(notification), notification, createConfig())); + } + @Test // https://github.com/DependencyTrack/dependency-track/issues/3197 public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject() { final var subject = new BomProcessingFailed(createProject(), "bomContent", "cause", Bom.Format.CYCLONEDX, null); @@ -219,6 +238,15 @@ private static Project createProject() { return project; } + private static InvalidBomProblemDetails createInvalidBomProblemDetails() { + final var invalidBomProblemDetails = new InvalidBomProblemDetails(); + invalidBomProblemDetails.setTitle("The uploaded BOM is invalid"); + invalidBomProblemDetails.setDetail("Schema validation failed"); + invalidBomProblemDetails.setStatus(400); + invalidBomProblemDetails.setErrors(List.of("$.components[928].externalReferences[1].url: does not match the iri-reference pattern must be a valid RFC 3987 IRI-reference")); + return invalidBomProblemDetails; + } + private static Vulnerability createVulnerability() { final var alias = new org.dependencytrack.model.VulnerabilityAlias(); alias.setInternalId("INT-001"); diff --git a/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java index 7aaf2290d9..7f71322bc6 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/MsTeamsPublisherTest.java @@ -114,6 +114,60 @@ public void testInformWithBomProcessingFailedNotification() { """))); } + @Override + public void testInformWithBomValidationFailedNotification() { + super.testInformWithBomValidationFailedNotification(); + + verify(postRequestedFor(anyUrl()) + .withHeader("Content-Type", equalTo("application/json")) + .withRequestBody(equalToJson(""" + { + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "summary": "Bill of Materials Validation Failed", + "title": "Bill of Materials Validation Failed", + "sections": [ + { + "activityTitle": "Dependency-Track", + "activitySubtitle": "1970-01-01T00:20:34.000000888", + "activityImage": "https://raw.githubusercontent.com/DependencyTrack/branding/master/dt-logo-symbol-blue-background.png", + "facts": [ + { + "name": "Level", + "value": "ERROR" + }, + { + "name": "Scope", + "value": "PORTFOLIO" + }, + { + "name": "Group", + "value": "BOM_VALIDATION_FAILED" + }, + { + "name": "Project", + "value": "pkg:maven/org.acme/projectName@projectVersion" + }, + { + "name": "Project URL", + "value": "https://example.com/projects/c9c9539a-e381-4b36-ac52-6a7ab83b2c95" + }, + { + "name": "Summary", + "value": "The uploaded BOM is invalid - Schema validation failed" + }, + { + "name": "Errors", + "value": "[$.components[928].externalReferences[1].url: does not match the iri-reference pattern must be a valid RFC 3987 IRI-reference]" + } + ], + "text": "An error occurred during BOM Validation" + } + ] + } + """))); + } + @Override public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject() { super.testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject(); diff --git a/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java index eac32c4ef6..5808ee8734 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java @@ -177,6 +177,42 @@ public void testInformWithBomProcessingFailedNotification() { }); } + @Override + public void testInformWithBomValidationFailedNotification() { + super.testInformWithBomValidationFailedNotification(); + + assertThat(greenMail.getReceivedMessages()).satisfiesExactly(message -> { + assertThat(message.getSubject()).isEqualTo("[Dependency-Track] Bill of Materials Validation Failed"); + assertThat(message.getContent()).isInstanceOf(MimeMultipart.class); + final MimeMultipart content = (MimeMultipart) message.getContent(); + assertThat(content.getCount()).isEqualTo(1); + assertThat(content.getBodyPart(0)).isInstanceOf(MimeBodyPart.class); + assertThat((String) content.getBodyPart(0).getContent()).isEqualToIgnoringNewLines(""" + Bill of Materials Validation Failed + + -------------------------------------------------------------------------------- + + Project: projectName + Version: projectVersion + Description: projectDescription + Project URL: /projects/c9c9539a-e381-4b36-ac52-6a7ab83b2c95 + + -------------------------------------------------------------------------------- + + Cause: + + + -------------------------------------------------------------------------------- + + An error occurred during BOM Validation + + -------------------------------------------------------------------------------- + + 1970-01-01T00:20:34.000000888 + """); + }); + } + @Override public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject() { super.testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject(); diff --git a/src/test/java/org/dependencytrack/notification/publisher/SlackPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/SlackPublisherTest.java index 1e47eba589..003ee835e4 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/SlackPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/SlackPublisherTest.java @@ -128,6 +128,67 @@ public void testInformWithBomProcessingFailedNotification() { """))); } + @Override + public void testInformWithBomValidationFailedNotification() { + super.testInformWithBomValidationFailedNotification(); + + verify(postRequestedFor(anyUrl()) + .withHeader("Content-Type", equalTo("application/json")) + .withRequestBody(equalToJson(""" + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "BOM_VALIDATION_FAILED | pkg:maven/org.acme/projectName@projectVersion" + } + }, + { + "type": "context", + "elements": [ + { + "text": "*ERROR* | *PORTFOLIO*", + "type": "mrkdwn" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "text": "Bill of Materials Validation Failed", + "type": "plain_text" + } + }, + { + "type": "section", + "text": { + "text": "An error occurred during BOM Validation | The uploaded BOM is invalid", + "type": "plain_text" + } + }, + { + "type" : "section", + "text" : { + "text" : "Schema validation failed", + "type" : "plain_text" + } + }, + { + "type" : "section", + "text" : { + "text" : "[$.components[928].externalReferences[1].url: does not match the iri-reference pattern must be a valid RFC 3987 IRI-reference]", + "type" : "plain_text" + } + } + ] + } + """))); + } + @Override public void testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject() { super.testInformWithBomProcessingFailedNotificationAndNoSpecVersionInSubject();