Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,19 @@ From Dependency-Track server 4.12.0 onwards you can set `isLatest` and `projectT
| parentVersion | false | | ${project.parent.version} |
| isLatest | false | | true |
| projectTags[].name | false | | <name>tag1</name> |
| uploadWithPut | false | false | false |

The `isLatest` option sets the flag on the project to indicate that it is the latest version.

The `projectTags` option allows for tags to be added to a project. This adds project tags only, and doesn't reconcile
the tags on the remote server, so if they are removed from the list or modified, they will need to be removed or
modified on the server to reflect the new state.

When `uploadWithPut` is set to `true` the PUT API will be used to upload the BOM to Dependency Track,
which was the standard behavior of this plugin up to and including version 1.10.2.
The new default is to use the POST API, which uses a multipart request body. This API is less restrained on the maximum SBOM size,
and it plays better with WebApplication Firewalls.

Example:

```xml
Expand Down
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,18 @@
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>3.14.5</version>
<classifier>standalone</classifier>
</dependency>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-objectmapper-jackson</artifactId>
<version>3.14.5</version>
</dependency>
<dependency>
<!-- unirest imports httpclient which includes commons-logging 1.2.0 which does not recognize slf4j, which maven uses -->
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
Expand Down
7 changes: 7 additions & 0 deletions src/it/put-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This test project will upload the SBOM to Dependency Track using the PUT API,
which was the default up to version 1.10.2.

The POST BOM API uses a multipart/form-data body instead of JSON. This API is
less limited on the BOM size, and also plays nicer with Web Application Firewalls.

https://github.com/pmckeown/dependency-track-maven-plugin/issues/454
37 changes: 37 additions & 0 deletions src/it/put-upload/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.pmckeown.dtmp.tests</groupId>
<artifactId>common-parent-project</artifactId>
<version>1.0.0</version>
<relativePath>../common-parent-project</relativePath>
</parent>
<artifactId>put-upload</artifactId>
<version>[email protected]@</version>

<dependencies>
<dependency>
<groupId>io.github.pmckeown</groupId>
<artifactId>dependency-track-maven-plugin</artifactId>
<version>${dependency-track-maven-plugin.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.github.pmckeown</groupId>
<artifactId>dependency-track-maven-plugin</artifactId>
<configuration>
<uploadWithPut>true</uploadWithPut>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.github.pmckeown.dependencytrack;

import static io.github.pmckeown.dependencytrack.Constants.VERSION;
import static io.github.pmckeown.dependencytrack.ObjectMapperFactory.relaxedObjectMapper;
import static kong.unirest.HeaderNames.ACCEPT;
import static kong.unirest.HeaderNames.ACCEPT_ENCODING;
import static kong.unirest.HeaderNames.USER_AGENT;

import io.github.pmckeown.util.Logger;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -179,6 +181,10 @@ private void configureUnirest() {
if (unirestConfiguration.compareAndSet(false, true)) {
Unirest.config()
.setObjectMapper(new JacksonObjectMapper(relaxedObjectMapper()))
.setDefaultHeader(
USER_AGENT,
"dependency-track-maven-plugin/" + VERSION
+ " (+https://github.com/pmckeown/dependency-track-maven-plugin)")
.setDefaultHeader(ACCEPT_ENCODING, "gzip, deflate")
.setDefaultHeader(ACCEPT, "application/json")
.verifySsl(verifySsl);
Expand Down
26 changes: 23 additions & 3 deletions src/main/java/io/github/pmckeown/dependencytrack/Constants.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package io.github.pmckeown.dependencytrack;

import java.io.InputStream;
import java.util.Properties;

public final class Constants {

private Constants() {
// Hiding implicit constructor
}
public static final String VERSION;

public static final String DELIMITER = "========================================================================";

Expand All @@ -25,4 +26,23 @@ private Constants() {
public static final String FINDINGS_AUDITED = "Findings Audited";
public static final String FIRST_OCCURRENCE = "First Occurrence";
public static final String LAST_OCCURRENCE = "Last Occurrence";

static {
String version = "unknown";
try (InputStream is = Constants.class.getResourceAsStream(
"/META-INF/maven/io.github.pmckeown/dependency-track-maven-plugin/pom.properties")) {
if (is != null) {
Properties properties = new Properties();
properties.load(is);
version = properties.getProperty("version", version);
}
} catch (Exception e) {
// ignore
}
VERSION = version;
}

private Constants() {
// Hiding implicit constructor
}
}
111 changes: 95 additions & 16 deletions src/main/java/io/github/pmckeown/dependencytrack/upload/BomClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@
import static kong.unirest.HeaderNames.CONTENT_TYPE;
import static kong.unirest.Unirest.get;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.util.StdConverter;
import io.github.pmckeown.dependencytrack.CommonConfig;
import io.github.pmckeown.dependencytrack.Response;
import io.github.pmckeown.dependencytrack.project.ProjectTag;
import io.github.pmckeown.util.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import kong.unirest.ContentType;
import kong.unirest.GenericType;
import kong.unirest.HttpRequest;
import kong.unirest.HttpResponse;
import kong.unirest.MultipartBody;
import kong.unirest.RequestBodyEntity;
import kong.unirest.Unirest;

Expand All @@ -34,19 +52,20 @@ class BomClient {
}

/**
* Upload a BOM to the Dependency-Track server. The BOM is processed asynchronously after the
* upload is completed and the response returned. The response contains a token that can be used
* later to query if the bom that the token relates to has been completely processed.
* Upload a BOM to the Dependency-Track server. The BOM is processed asynchronously after the upload is completed and the response returned. The response
* contains a token that can be used later to query if the bom that the token relates to has been completely processed.
*
* @param bom the request object containing the project details and the Base64 encoded bom.xml
* @return a response containing a token to later determine if processing the supplied BOM is
* completed
* @param bom the request object containing the project details and a reference to the bom.xml
* @param uploadWithPut if true the PUT API will be used instead of the POST API
* @return a response containing a token to later determine if processing the supplied BOM is completed
*/
Response<UploadBomResponse> uploadBom(UploadBomRequest bom) {
RequestBodyEntity requestBodyEntity = Unirest.put(commonConfig.getDependencyTrackBaseUrl() + V1_BOM)
.header(CONTENT_TYPE, "application/json")
.header("X-Api-Key", commonConfig.getApiKey())
.body(bom);
Response<UploadBomResponse> uploadBom(UploadBomRequest bom, boolean uploadWithPut) {
HttpRequest<?> requestBodyEntity;
if (uploadWithPut) {
requestBodyEntity = putUploadRequest(bom);
} else {
requestBodyEntity = postUploadRequest(bom);
}
HttpResponse<UploadBomResponse> httpResponse =
requestBodyEntity.asObject(new GenericType<UploadBomResponse>() {});

Expand All @@ -63,14 +82,42 @@ Response<UploadBomResponse> uploadBom(UploadBomRequest bom) {
return new Response<>(httpResponse.getStatus(), httpResponse.getStatusText(), httpResponse.isSuccess(), body);
}

private RequestBodyEntity putUploadRequest(UploadBomRequest bom) {
return Unirest.put(commonConfig.getDependencyTrackBaseUrl() + V1_BOM)
.header(CONTENT_TYPE, "application/json")
.header("X-Api-Key", commonConfig.getApiKey())
.body(bom);
}

private MultipartBody postUploadRequest(UploadBomRequest bom) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(UploadBomRequest.class, UploadBomRequestPostMixin.class);
Map<String, Object> requestFields = objectMapper.convertValue(bom, new TypeReference<Map<String, Object>>() {});

MultipartBody request = Unirest.post(commonConfig.getDependencyTrackBaseUrl() + V1_BOM)
.header("X-Api-Key", commonConfig.getApiKey())
.fields(requestFields);

if (bom.getBom().isFileReference()) {
request.field("bom", bom.getBom().getFile(), ContentType.APPLICATION_OCTET_STREAM.toString());
} else {
try {
InputStream inputStream = bom.getBom().getInputStream();
request.field("bom", inputStream, ContentType.APPLICATION_OCTET_STREAM, "bom.xml");
} catch (IOException e) {
logger.debug("Opening an input stream to the BOM reference failed. %s", e.getMessage());
throw new IllegalStateException("Failure reading BOM source", e);
}
}
return request;
}

/**
* Query the server with a processing token to see if the BOM that it related to has been
* completely processed.
* Query the server with a processing token to see if the BOM that it related to has been completely processed.
*
* @param token The token that was returned from an Upload BOM call
* @return a response containing a processing flag. If the flag is true, processing has not yet
* completed. If the flag is false, processing is either completed or the token supplied was
* invalid.
* @return a response containing a processing flag. If the flag is true, processing has not yet completed. If the flag is false, processing is either
* completed or the token supplied was invalid.
*/
Response<BomProcessingResponse> isBomBeingProcessed(String token) {
final HttpResponse<BomProcessingResponse> httpResponse = get(commonConfig.getDependencyTrackBaseUrl()
Expand All @@ -88,4 +135,36 @@ Response<BomProcessingResponse> isBomBeingProcessed(String token) {

return new Response<>(httpResponse.getStatus(), httpResponse.getStatusText(), httpResponse.isSuccess(), body);
}

/**
* Jackson mix-in to create payload for the POST API.
*/
@JsonInclude(Include.NON_NULL)
static class UploadBomRequestPostMixin {
@JsonProperty("isLatest")
Boolean getIsLatest() {
return Boolean.FALSE;
}

@JsonSerialize(converter = TagListConverter.class)
List<ProjectTag> getProjectTags() {
return Collections.emptyList();
}

@JsonIgnore(/* bom field will be manually added to the request */ )
BomReference getBom() {
return null;
}
}

static class TagListConverter extends StdConverter<List<ProjectTag>, String> {
@Override
public String convert(List<ProjectTag> value) {
if (value == null) {
return null;
} else {
return value.stream().map(ProjectTag::getName).collect(Collectors.joining(","));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.github.pmckeown.dependencytrack.upload;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class BomReference {
private final File file;
private final String string;

public BomReference(File bomFile) {
if (bomFile == null) {
throw new IllegalArgumentException("bom file cannot be null");
}
this.file = bomFile;
this.string = null;
}

public BomReference(String bom) {
if (bom == null) {
throw new IllegalArgumentException("bom cannot be null");
}
this.string = bom;
this.file = null;
}

public boolean isFileReference() {
return file != null;
}

public File getFile() {
return file;
}

/**
* Create new input stream for the BOM.
*
* @return InputStream A new input stream to the BOM.
* @throws IOException Throw when the input stream could not be created.
*/
public InputStream getInputStream() throws IOException {
if (isFileReference()) {
return new FileInputStream(file);
} else {
return ReaderInputStream.builder()
// The SBOM is either XML or JSON, so it should be read as UTF-8
.setCharset(StandardCharsets.UTF_8)
.setReader(new StringReader(string))
.get();
}
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
}

/**
* Jackson serializer which serializes the BOM as a base64 encoded value.
*/
public static class Base64Serializer extends JsonSerializer<BomReference> {
@Override
public void serialize(BomReference value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
try (InputStream is = value.getInputStream()) {
gen.writeBinary(is, -1);
}
}
}
}
Loading
Loading