Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port : Add Notification For BOM_VALIDATION_FAILED #839

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading