Skip to content

Commit

Permalink
Merge pull request #839 from DependencyTrack/port-Add-Notification-Fo…
Browse files Browse the repository at this point in the history
…r-BOM_VALIDATION_FAILED

Port : Add Notification For `BOM_VALIDATION_FAILED`
  • Loading branch information
nscuro authored Aug 13, 2024
2 parents b726569 + 8e66ff3 commit 1386be8
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.dependencytrack.parser.dependencytrack.NotificationModelConverter;
import org.dependencytrack.proto.notification.v1.BomConsumedOrProcessedSubject;
import org.dependencytrack.proto.notification.v1.BomProcessingFailedSubject;
import org.dependencytrack.proto.notification.v1.BomValidationFailedSubject;
import org.dependencytrack.proto.notification.v1.NewVulnerabilitySubject;
import org.dependencytrack.proto.notification.v1.NewVulnerableDependencySubject;
import org.dependencytrack.proto.notification.v1.Notification;
Expand Down Expand Up @@ -170,7 +171,7 @@ static KafkaEvent<String, String> convert(final EpssMirrorEvent ignored) {
private static Topic<String, Notification> extractDestinationTopic(final Notification notification) {
return switch (notification.getGroup()) {
case GROUP_ANALYZER -> KafkaTopics.NOTIFICATION_ANALYZER;
case GROUP_BOM_CONSUMED, GROUP_BOM_PROCESSED, GROUP_BOM_PROCESSING_FAILED -> KafkaTopics.NOTIFICATION_BOM;
case GROUP_BOM_CONSUMED, GROUP_BOM_PROCESSED, GROUP_BOM_PROCESSING_FAILED, GROUP_BOM_VALIDATION_FAILED -> KafkaTopics.NOTIFICATION_BOM;
case GROUP_CONFIGURATION -> KafkaTopics.NOTIFICATION_CONFIGURATION;
case GROUP_DATASOURCE_MIRRORING -> KafkaTopics.NOTIFICATION_DATASOURCE_MIRRORING;
case GROUP_FILE_SYSTEM -> KafkaTopics.NOTIFICATION_FILE_SYSTEM;
Expand Down Expand Up @@ -204,6 +205,11 @@ private static String extractEventKey(final Notification notification) throws In
final var subject = notification.getSubject().unpack(BomProcessingFailedSubject.class);
yield requireNonEmpty(subject.getProject().getUuid());
}
case GROUP_BOM_VALIDATION_FAILED -> {
requireSubjectOfTypeAnyOf(notification, List.of(BomValidationFailedSubject.class));
final var subject = notification.getSubject().unpack(BomValidationFailedSubject.class);
yield requireNonEmpty(subject.getProject().getUuid());
}
case GROUP_NEW_VULNERABILITY -> {
requireSubjectOfTypeAnyOf(notification, List.of(NewVulnerabilitySubject.class));
final var subject = notification.getSubject().unpack(NewVulnerabilitySubject.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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 @@ -39,6 +39,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
@@ -0,0 +1,48 @@
/*
* 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.Project;

import java.util.List;

public class BomValidationFailed {

private Project project;
private String bom;
private List<String> errors;

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

public Project getProject() {
return project;
}

public String getBom() {
return bom;
}

public List<String> getErrors() {
return errors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 @@ -42,6 +43,7 @@
import org.dependencytrack.proto.notification.v1.BackReference;
import org.dependencytrack.proto.notification.v1.BomConsumedOrProcessedSubject;
import org.dependencytrack.proto.notification.v1.BomProcessingFailedSubject;
import org.dependencytrack.proto.notification.v1.BomValidationFailedSubject;
import org.dependencytrack.proto.notification.v1.Component;
import org.dependencytrack.proto.notification.v1.Group;
import org.dependencytrack.proto.notification.v1.Level;
Expand Down Expand Up @@ -73,6 +75,7 @@
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_CONSUMED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSING_FAILED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_VALIDATION_FAILED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_CONFIGURATION;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_DATASOURCE_MIRRORING;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_FILE_SYSTEM;
Expand Down Expand Up @@ -161,6 +164,7 @@ private static Group convertGroup(final String group) {
case BOM_CONSUMED -> GROUP_BOM_CONSUMED;
case BOM_PROCESSED -> GROUP_BOM_PROCESSED;
case BOM_PROCESSING_FAILED -> GROUP_BOM_PROCESSING_FAILED;
case BOM_VALIDATION_FAILED -> GROUP_BOM_VALIDATION_FAILED;
case VEX_CONSUMED -> GROUP_VEX_CONSUMED;
case VEX_PROCESSED -> GROUP_VEX_PROCESSED;
case POLICY_VIOLATION -> GROUP_POLICY_VIOLATION;
Expand All @@ -184,6 +188,8 @@ private static Optional<Any> convert(final Object subject) {
return Optional.of(Any.pack(convert(bcop)));
} else if (subject instanceof final BomProcessingFailed bpf) {
return Optional.of(Any.pack(convert(bpf)));
} else if (subject instanceof final BomValidationFailed bvf) {
return Optional.of(Any.pack(convert(bvf)));
} else if (subject instanceof final VexConsumedOrProcessed vcop) {
return Optional.of(Any.pack(convert(vcop)));
} else if (subject instanceof final PolicyViolationIdentified pvi) {
Expand Down Expand Up @@ -278,6 +284,19 @@ private static BomProcessingFailedSubject convert(final BomProcessingFailed subj
return builder.build();
}

private static BomValidationFailedSubject convert(final BomValidationFailed subject) {

org.dependencytrack.proto.notification.v1.Bom.Builder bomBuilder = org.dependencytrack.proto.notification.v1.Bom.newBuilder();
Optional.ofNullable(subject.getBom()).ifPresent(bomBuilder::setContent);

final BomValidationFailedSubject.Builder builder = BomValidationFailedSubject.newBuilder()
.setProject(convert(subject.getProject()))
.setBom(bomBuilder.build());

Optional.ofNullable(subject.getErrors()).ifPresent(builder::addAllErrors);
return builder.build();
}

private static VexConsumedOrProcessedSubject convert(final VexConsumedOrProcessed subject) {
return VexConsumedOrProcessedSubject.newBuilder()
.setProject(convert(subject.getProject()))
Expand Down
25 changes: 23 additions & 2 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 Down Expand Up @@ -51,11 +53,16 @@
import org.cyclonedx.exception.GeneratorException;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.BomUploadEvent;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.WorkflowState;
import org.dependencytrack.model.WorkflowStatus;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.notification.NotificationConstants;
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 @@ -538,7 +545,7 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart
}

private File validateAndStoreBom(final byte[] bomBytes, final Project project) throws IOException {
validate(bomBytes);
validate(bomBytes, project);

// TODO: Store externally so other instances of the API server can pick it up.
// https://github.com/CycloneDX/cyclonedx-bom-repo-server
Expand All @@ -554,7 +561,7 @@ private File validateAndStoreBom(final byte[] bomBytes, final Project project) t
return tmpFile;
}

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

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

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

private static void dispatchBomValidationFailedNotification(Project project, String bom, List<String> errors) {
final KafkaEventDispatcher eventDispatcher = new KafkaEventDispatcher();
eventDispatcher.dispatchNotification(new Notification()
.scope(NotificationScope.PORTFOLIO)
.group(NotificationGroup.BOM_VALIDATION_FAILED)
.level(NotificationLevel.ERROR)
.title(NotificationConstants.Title.BOM_VALIDATION_FAILED)
.content("An error occurred while validating a BOM")
.subject(new BomValidationFailed(project, bom, errors)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,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 @@ -281,7 +281,7 @@ private Response process(QueryManager qm, Project project, List<FormDataBodyPart
}
try (InputStream in = bodyPartEntity.getInputStream()) {
final byte[] content = IOUtils.toByteArray(new BOMInputStream((in)));
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ enum Group {
GROUP_PROJECT_VULN_ANALYSIS_COMPLETE = 18;
GROUP_USER_CREATED = 19;
GROUP_USER_DELETED = 20;
GROUP_BOM_VALIDATION_FAILED = 21;

// Indexing service has been removed as of
// https://github.com/DependencyTrack/hyades/issues/661
Expand Down Expand Up @@ -80,6 +81,12 @@ message BomProcessingFailedSubject {
string token = 4;
}

message BomValidationFailedSubject {
Project project = 1;
Bom bom = 2;
repeated string errors = 3;
}

message Bom {
string content = 1;
string format = 2;
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/templates/notification/publisher/email.peb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ Project URL: {{ baseUrl }}/projects/{{ subject.project.uuid }}

Cause:
{{ subject.cause }}
{% elseif notification.group == "GROUP_BOM_VALIDATION_FAILED" %}
Project: {{ subject.project.name }}
Version: {{ subject.project.version }}
Description: {{ subject.project.description }}
Project URL: {{ baseUrl }}/projects/{{ subject.project.uuid }}

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

Errors:
{% for error in subject.errorsList %}
{{ error }}
{% endfor %}
{% elseif notification.group == "GROUP_VEX_CONSUMED" %}
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 | summarize | 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
Loading

0 comments on commit 1386be8

Please sign in to comment.