Skip to content

Commit

Permalink
Merge pull request DependencyTrack#3796 from aravindparappil46/featur…
Browse files Browse the repository at this point in the history
…e/3778-notif-for-validation-failed

Add Notification For `BOM_VALIDATION_FAILED`
  • Loading branch information
nscuro authored Jun 2, 2024
2 parents 133e5ba + 451aaa6 commit f785fc5
Show file tree
Hide file tree
Showing 20 changed files with 449 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/_docs/integrations/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public enum NotificationGroup {
BOM_CONSUMED,
BOM_PROCESSED,
BOM_PROCESSING_FAILED,
BOM_VALIDATION_FAILED,
VEX_CONSUMED,
VEX_PROCESSED,
POLICY_VIOLATION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -186,6 +187,9 @@ List<NotificationRule> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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 java.util.List;
import org.dependencytrack.model.Bom;
import org.dependencytrack.model.Project;

public class BomValidationFailed {

private Project project;
private String bom;
private List<String> errors;
private Bom.Format format;

public BomValidationFailed(final Project project, final String bom, final List<String> errors, final Bom.Format format) {
this.project = project;
this.bom = bom;
this.errors = errors;
this.format = format;
}

public Project getProject() {
return project;
}

public String getBom() {
return bom;
}

public List<String> getErrors() {
return errors;
}

public Bom.Format getFormat() {
return format;
}

}
29 changes: 26 additions & 3 deletions src/main/java/org/dependencytrack/resources/v1/BomResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -485,7 +493,7 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart
}
try (InputStream in = bodyPartEntity.getInputStream()) {
final byte[] content = IOUtils.toByteArray(BOMInputStream.builder().setInputStream(in).get());
validate(content);
validate(content, project);
// todo: make option to combine all the bom data so components are reconciled in a single pass.
// todo: https://github.com/DependencyTrack/dependency-track/issues/130
final BomUploadEvent bomUploadEvent = new BomUploadEvent(qm.getPersistenceManager().detachCopy(project), content);
Expand All @@ -506,7 +514,7 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart
return Response.ok().build();
}

static void validate(final byte[] bomBytes) {
static void validate(final byte[] bomBytes, final Project project) {
try (QueryManager qm = new QueryManager()) {
if (!qm.isEnabled(ConfigPropertyConstants.BOM_VALIDATION_ENABLED)) {
return;
Expand All @@ -529,6 +537,10 @@ static void validate(final byte[] bomBytes) {
.entity(problemDetails)
.build();

final var bomEncoded = Base64.getEncoder()
.encodeToString(bomBytes);
dispatchBomValidationFailedNotification(project, bomEncoded, problemDetails.getErrors(), Format.CYCLONEDX);

throw new WebApplicationException(response);
} catch (RuntimeException e) {
LOGGER.error("Failed to validate BOM", e);
Expand All @@ -537,4 +549,15 @@ static void validate(final byte[] bomBytes) {
}
}


private static void dispatchBomValidationFailedNotification(final Project project, final String bom, final List<String> errors, final Bom.Format bomFormat) {
Notification.dispatch(new Notification()
.scope(NotificationScope.PORTFOLIO)
.group(NotificationGroup.BOM_VALIDATION_FAILED)
.level(NotificationLevel.ERROR)
.title(Title.BOM_VALIDATION_FAILED)
.content("An error occurred during BOM Validation")
.subject(new BomValidationFailed(project, bom, errors, bomFormat)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ private Response process(QueryManager qm, Project project, String encodedVexData
return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build();
}
final byte[] decoded = Base64.getDecoder().decode(encodedVexData);
BomResource.validate(decoded);
BomResource.validate(decoded, project);
final VexUploadEvent vexUploadEvent = new VexUploadEvent(project.getUuid(), decoded);
Event.dispatch(vexUploadEvent);
return Response.ok(Collections.singletonMap("token", vexUploadEvent.getChainIdentifier())).build();
Expand All @@ -282,7 +282,7 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart
}
try (InputStream in = bodyPartEntity.getInputStream()) {
final byte[] content = IOUtils.toByteArray(BOMInputStream.builder().setInputStream(in).get());
BomResource.validate(content);
BomResource.validate(content, project);
final VexUploadEvent vexUploadEvent = new VexUploadEvent(project.getUuid(), content);
Event.dispatch(vexUploadEvent);
return Response.ok(Collections.singletonMap("token", vexUploadEvent.getChainIdentifier())).build();
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/dependencytrack/util/NotificationUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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;
Expand Down Expand Up @@ -480,6 +481,26 @@ public static JsonObject toJson(final BomProcessingFailed vo) {
return builder.build();
}

public static JsonObject toJson(final BomValidationFailed vo) {
final var builder = Json.createObjectBuilder();
if (vo.getProject() != null) {
builder.add("project", toJson(vo.getProject()));
}
if (vo.getBom() != null) {
builder.add("bom", Json.createObjectBuilder()
.add("content", Optional.ofNullable(vo.getBom()).orElse("Unknown"))
.add("format", Optional.ofNullable(vo.getFormat()).map(Bom.Format::getFormatShortName).orElse("Unknown"))
.build()
);
}
final var errors = vo.getErrors();
if (errors != null && !errors.isEmpty()) {
final var commaSeparatedErrors = String.join(",", errors);
JsonUtil.add(builder, "errors", commaSeparatedErrors);
}
return builder.build();
}

public static JsonObject toJson(final VexConsumedOrProcessed vo) {
final JsonObjectBuilder builder = Json.createObjectBuilder();
if (vo.getProject() != null) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/templates/notification/publisher/email.peb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ Project: {{ subject.project.name }}
Version: {{ subject.project.version }}
Description: {{ subject.project.description }}
Project URL: {{ baseUrl }}/projects/{{ subject.project.uuid }}
{% elseif notification.group == "BOM_VALIDATION_FAILED" %}
Project: {{ subject.project.name }}
Version: {{ subject.project.version }}
Description: {{ subject.project.description }}
Project URL: {{ baseUrl }}/projects/{{ subject.project.uuid }}
Errors: {{ subject.errors }}
{% elseif notification.group == "BOM_PROCESSED" %}
Project: {{ subject.project.name }}
Version: {{ subject.project.version }}
Expand Down
27 changes: 27 additions & 0 deletions src/main/resources/templates/notification/publisher/msteams.peb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@
"value": "{{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy='json') }}"
}
],
{% elseif notification.group == "BOM_VALIDATION_FAILED" %}
"facts": [
{
"name": "Level",
"value": "{{ notification.level | escape(strategy="json") }}"
},
{
"name": "Scope",
"value": "{{ notification.scope | escape(strategy="json") }}"
},
{
"name": "Group",
"value": "{{ notification.group | escape(strategy="json") }}"
},
{
"name": "Project",
"value": "{{ subject.project.toString | escape(strategy="json") }}"
},
{
"name": "Project URL",
"value": "{{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy='json') }}"
},
{
"name": "Errors",
"value": "{{ subject.errors.toString | escape(strategy='json') }}"
}
],
{% else %}
"facts": [
{
Expand Down
45 changes: 45 additions & 0 deletions src/main/resources/templates/notification/publisher/slack.peb
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,51 @@
{% endif %}
]
}
{% elseif notification.group == "BOM_VALIDATION_FAILED" %}
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "{{ notification.group | escape(strategy="json") }} | {{ subject.project.toString | escape(strategy="json") }}"
}
},
{
"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": "plain_text"
}
},
{
"type": "section",
"text": {
"text": "{{ notification.content | escape(strategy="json") }}",
"type": "plain_text"
}
},
{
"type": "section",
"text": {
"text": "{{ subject.errors.toString | escape(strategy="json") }}",
"type": "plain_text"
}
}
]
}
{% else %}
{
"blocks": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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;
Expand Down Expand Up @@ -430,6 +431,31 @@ public void testBomProcessingFailedLimitedToProject() {
.satisfiesExactly(resolvedRule -> 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);
Expand Down
Loading

0 comments on commit f785fc5

Please sign in to comment.