Skip to content

Commit

Permalink
Add Notification For BOM_VALIDATION_FAILED
Browse files Browse the repository at this point in the history
If uploaded BOM is invalid, dispatches a notification with InvalidBomProblemDetails before
throwing the respective exception

Signed-off-by: Aravind Parappil <[email protected]>
  • Loading branch information
aravindparappil46 committed Jun 2, 2024
1 parent 133e5ba commit 43903e7
Show file tree
Hide file tree
Showing 17 changed files with 430 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 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;
}

}
30 changes: 27 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, Format.CYCLONEDX);

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


private static void dispatchBomValidationFailedNotification(final Project project, final String bom,
final InvalidBomProblemDetails problemDetails, 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, problemDetails, 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
42 changes: 42 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 All @@ -69,6 +70,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.dependencytrack.resources.v1.problems.InvalidBomProblemDetails;

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

Expand Down Expand Up @@ -305,6 +307,28 @@ public static JsonObject toJson(final Component component) {
return componentBuilder.build();
}

public static JsonObject toJson(final InvalidBomProblemDetails problemDetails) {
final JsonObjectBuilder builder = Json.createObjectBuilder();
final var errors = problemDetails.getErrors();

if (problemDetails.getType() != null) {
builder.add("type", problemDetails.getType().toString());
}
JsonUtil.add(builder, "status", problemDetails.getStatus().toString());
JsonUtil.add(builder, "title", problemDetails.getTitle());
JsonUtil.add(builder, "detail", problemDetails.getDetail());

if (errors != null && !errors.isEmpty()) {
final var commaSeparatedErrors = String.join(",", errors);
JsonUtil.add(builder, "errors", commaSeparatedErrors);
}

if (problemDetails.getInstance() != null) {
JsonUtil.add(builder, "instance", problemDetails.getInstance().toString());
}
return builder.build();
}

public static JsonObject toJson(final Vulnerability vulnerability) {
final JsonObjectBuilder vulnerabilityBuilder = Json.createObjectBuilder();
vulnerabilityBuilder.add("uuid", vulnerability.getUuid().toString());
Expand Down Expand Up @@ -480,6 +504,24 @@ public static JsonObject toJson(final BomProcessingFailed vo) {
return builder.build();
}

public static JsonObject toJson(final BomValidationFailed vo) {
final JsonObjectBuilder 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()
);
}
if (vo.getProblemDetails() != null) {
builder.add("problemDetails", toJson(vo.getProblemDetails()));
}
return builder.build();
}

public static JsonObject toJson(final VexConsumedOrProcessed vo) {
final JsonObjectBuilder builder = Json.createObjectBuilder();
if (vo.getProject() != null) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/templates/notification/publisher/email.peb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ 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 }}

--------------------------------------------------------------------------------

Expand Down
31 changes: 31 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,37 @@
"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": "Summary",
"value": "{{ subject.problemDetails.title.toString | escape(strategy='json') }} - {{ subject.problemDetails.detail.toString | escape(strategy='json') }}"
},
{
"name": "Errors",
"value": "{{ subject.problemDetails.errors.toString | escape(strategy='json') }}"
}
],
{% else %}
"facts": [
{
Expand Down
52 changes: 52 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,58 @@
{% 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") }} | {{ subject.problemDetails.title | escape(strategy="json") }}",
"type": "plain_text"
}
},
{
"type": "section",
"text": {
"text": "{{ subject.problemDetails.detail.toString | escape(strategy="json") }}",
"type": "plain_text"
}
},
{
"type": "section",
"text": {
"text": "{{ subject.problemDetails.errors.toString | escape(strategy="json") }}",
"type": "plain_text"
}
}
]
}
{% else %}
{
"blocks": [
Expand Down
Loading

0 comments on commit 43903e7

Please sign in to comment.