diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java index 1c2be049..18cae943 100644 --- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java +++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java @@ -20,6 +20,7 @@ * #L% */ +import com.contrastsecurity.sdk.internal.Nullable; import java.time.Instant; /** @@ -40,6 +41,10 @@ public interface CodeArtifact { /** @return filename */ String filename(); + /** @return metadata filename */ + @Nullable + String metadata(); + /** @return time at which the code artifact was uploaded to Contrast Scan */ Instant createdTime(); } diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java index 597f7ded..e5393365 100644 --- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java +++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java @@ -38,7 +38,7 @@ interface CodeArtifactClient { /** - * Transfers a file from the file system to Contrast Scan to create a new code artifact for + * Transfers an artifact from the file system to Contrast Scan to create a new code artifact for * analysis. * * @param projectId ID of the project to which the code artifact belongs @@ -51,4 +51,22 @@ interface CodeArtifactClient { * @throws ServerResponseException when Contrast API returns a response that cannot be understood */ CodeArtifactInner upload(String projectId, Path file) throws IOException; + + /** + * Transfers artifact and prescan metadata from the file system to Contrast Scan to create a new + * code artifact for analysis. + * + *
Prescan metadata will allow the scanner to produce more detailed finding reports.
+ *
+ * @param projectId ID of the project to which the code artifact belongs
+ * @param file the file to upload
+ * @param metadata the prescan metadata to upload with the file artifact.
+ * @return new {@link CodeArtifactInner} from Contrast API
+ * @throws IOException when an IO error occurs while making the request to the Contrast API
+ * @throws UnauthorizedException when Contrast rejects the credentials used to send the request
+ * @throws ResourceNotFoundException when the requested resource does not exist
+ * @throws HttpResponseException when Contrast rejects this request with an error code
+ * @throws ServerResponseException when Contrast API returns a response that cannot be understood
+ */
+ CodeArtifactInner upload(String projectId, Path file, Path metadata) throws IOException;
}
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
index 27ed847b..ec2e6ff2 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
@@ -54,6 +54,17 @@ final class CodeArtifactClientImpl implements CodeArtifactClient {
@Override
public CodeArtifactInner upload(final String projectId, final Path file) throws IOException {
+ return sendRequest(projectId, file, null);
+ }
+
+ @Override
+ public CodeArtifactInner upload(final String projectId, final Path file, final Path metadata)
+ throws IOException {
+ return sendRequest(projectId, file, Objects.requireNonNull(metadata));
+ }
+
+ private CodeArtifactInner sendRequest(
+ final String projectId, final Path file, final Path metadata) throws IOException {
final String uri =
contrast.getRestApiURL()
+ new URIBuilder()
@@ -66,9 +77,9 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
"code-artifacts")
.toURIString();
final String boundary = "ContrastFormBoundary" + ThreadLocalRandom.current().nextLong();
- final String header =
- "--"
- + boundary
+ final String boundaryMarker = CRLF + "--" + boundary;
+ final String filenameSection =
+ boundaryMarker
+ CRLF
+ "Content-Disposition: form-data; name=\"filename\"; filename=\""
+ file.getFileName().toString()
@@ -80,8 +91,28 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
+ "Content-Transfer-Encoding: binary"
+ CRLF
+ CRLF;
- final String footer = CRLF + "--" + boundary + "--" + CRLF;
- final long contentLength = header.length() + Files.size(file) + footer.length();
+ final String metadataSection =
+ metadata != null
+ ? boundaryMarker
+ + CRLF
+ + "Content-Disposition: form-data; name=\"metadata\"; filename=\""
+ + metadata.getFileName().toString()
+ + '"'
+ + CRLF
+ + "Content-Type: "
+ + determineMime(metadata)
+ + CRLF
+ + "Content-Transfer-Encoding: binary"
+ + CRLF
+ + CRLF
+ : "";
+
+ final String footer = boundaryMarker + "--" + CRLF;
+ long contentLength = filenameSection.length() + Files.size(file);
+ if (metadata != null) {
+ contentLength += metadataSection.length() + Files.size(metadata);
+ }
+ contentLength += footer.length();
final HttpURLConnection connection = contrast.makeConnection(uri, "POST");
connection.setDoOutput(true);
@@ -91,9 +122,14 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
try (OutputStream os = connection.getOutputStream();
PrintWriter writer =
new PrintWriter(new OutputStreamWriter(os, StandardCharsets.US_ASCII), true)) {
- writer.append(header).flush();
+ writer.append(filenameSection).flush();
Files.copy(file, os);
os.flush();
+ if (metadata != null) {
+ writer.append(metadataSection).flush();
+ Files.copy(metadata, os);
+ os.flush();
+ }
writer.append(footer).flush();
}
final int code = connection.getResponseCode();
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
index 5514b898..41830120 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
@@ -52,6 +52,11 @@ public String filename() {
return inner.filename();
}
+ @Override
+ public String metadata() {
+ return inner.metadata();
+ }
+
@Override
public Instant createdTime() {
return inner.createdTime();
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
index 9ceebb09..99689490 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
@@ -20,6 +20,7 @@
* #L%
*/
+import com.contrastsecurity.sdk.internal.Nullable;
import com.google.auto.value.AutoValue;
import java.time.Instant;
@@ -44,6 +45,10 @@ static Builder builder() {
/** @return filename */
abstract String filename();
+ /** @return metadata filename */
+ @Nullable
+ abstract String metadata();
+
/** @return time at which the code artifact was uploaded to Contrast Scan */
abstract Instant createdTime();
@@ -63,6 +68,9 @@ abstract static class Builder {
/** @see CodeArtifactInner#filename() */
abstract Builder filename(String value);
+ /** @see CodeArtifactInner#metadata() */
+ abstract Builder metadata(String value);
+
/** @see CodeArtifactInner#createdTime() */
abstract Builder createdTime(Instant value);
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
index ea91267e..1c962926 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
@@ -62,6 +62,23 @@ interface Factory {
*/
CodeArtifact upload(Path file, String name) throws IOException;
+ /**
+ * Transfers a file from the file system to Contrast Scan to create a new code artifact for static
+ * analysis.
+ *
+ * @param file the code artifact to upload
+ * @param name the name of the code artifact
+ * @param metadata the path of the prescan data file to upload
+ * @param metaname the name of the prescan data file
+ * @return new {@link CodeArtifact} from Contrast
+ * @throws IOException when an IO error occurs while making the request to the Contrast API
+ * @throws UnauthorizedException when Contrast rejects the credentials used to send the request
+ * @throws ResourceNotFoundException when the requested resource does not exist
+ * @throws HttpResponseException when Contrast rejects this request with an error code
+ * @throws ServerResponseException when Contrast API returns a response that cannot be understood
+ */
+ CodeArtifact upload(Path file, String name, Path metadata, String metaname) throws IOException;
+
/**
* Transfers a file from the file system to Contrast Scan to create a new code artifact for static
* analysis.
@@ -75,4 +92,19 @@ interface Factory {
* @throws ServerResponseException when Contrast API returns a response that cannot be understood
*/
CodeArtifact upload(Path file) throws IOException;
+
+ /**
+ * Transfers a file from the file system to Contrast Scan to create a new code artifact for static
+ * analysis.
+ *
+ * @param file the code artifact to upload
+ * @param metadata the path of the prescan data file to upload
+ * @return new {@link CodeArtifact} from Contrast
+ * @throws IOException when an IO error occurs while making the request to the Contrast API
+ * @throws UnauthorizedException when Contrast rejects the credentials used to send the request
+ * @throws ResourceNotFoundException when the requested resource does not exist
+ * @throws HttpResponseException when Contrast rejects this request with an error code
+ * @throws ServerResponseException when Contrast API returns a response that cannot be understood
+ */
+ CodeArtifact upload(Path file, Path metadata) throws IOException;
}
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
index 99f9c596..23f9a48a 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
@@ -50,6 +50,14 @@ public CodeArtifacts create(final String projectId) {
this.projectId = projectId;
}
+ @Override
+ public CodeArtifact upload(
+ final Path file, final String name, final Path metadata, final String metaname)
+ throws IOException {
+ final CodeArtifactInner inner = client.upload(projectId, file, metadata);
+ return new CodeArtifactImpl(inner);
+ }
+
@Override
public CodeArtifact upload(final Path file, final String name) throws IOException {
final CodeArtifactInner inner = client.upload(projectId, file);
@@ -60,4 +68,9 @@ public CodeArtifact upload(final Path file, final String name) throws IOExceptio
public CodeArtifact upload(final Path file) throws IOException {
return upload(file, file.getFileName().toString());
}
+
+ @Override
+ public CodeArtifact upload(final Path file, final Path metadata) throws IOException {
+ return upload(file, file.getFileName().toString(), metadata, metadata.getFileName().toString());
+ }
}
diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactAssert.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactAssert.java
index 0132f38c..4badb971 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactAssert.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactAssert.java
@@ -49,6 +49,7 @@ public CodeArtifactAssert hasSameValuesAsInner(final CodeArtifactInner inner) {
Assertions.assertThat(actual.projectId()).isEqualTo(inner.projectId());
Assertions.assertThat(actual.organizationId()).isEqualTo(inner.organizationId());
Assertions.assertThat(actual.filename()).isEqualTo(inner.filename());
+ Assertions.assertThat(actual.metadata()).isEqualTo(inner.metadata());
Assertions.assertThat(actual.createdTime()).isEqualTo(inner.createdTime());
return this;
}
diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
index aecb93d3..269dd4bc 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
@@ -53,6 +53,23 @@ void upload(@TempDir final Path tmp) throws IOException {
assertThat(codeArtifact).hasSameValuesAsInner(inner);
}
+ @Test
+ void upload_with_metadata(@TempDir final Path tmp) throws IOException {
+ // GIVEN stubbed code artifacts client
+ final CodeArtifactClient client = mock(CodeArtifactClient.class);
+ final CodeArtifactInner inner = builder().metadata("prescan.json").build();
+ final Path file = tmp.resolve(inner.filename());
+ final Path meta = tmp.resolve(inner.metadata());
+ when(client.upload(inner.projectId(), file, meta)).thenReturn(inner);
+
+ // WHEN upload file,meta
+ final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
+ final CodeArtifact codeArtifact = codeArtifacts.upload(file, meta);
+
+ // THEN returns expected code artifact
+ assertThat(codeArtifact).hasSameValuesAsInner(inner);
+ }
+
@Test
void upload_custom_filename(@TempDir final Path tmp) throws IOException {
// GIVEN stubbed code artifacts client
@@ -69,6 +86,24 @@ void upload_custom_filename(@TempDir final Path tmp) throws IOException {
assertThat(codeArtifact).hasSameValuesAsInner(inner);
}
+ @Test
+ void upload_custom_metaname(@TempDir final Path tmp) throws IOException {
+ // GIVEN stubbed code artifacts client
+ final CodeArtifactClient client = mock(CodeArtifactClient.class);
+ final CodeArtifactInner inner = builder().metadata("prescan.json").build();
+ final Path file = tmp.resolve(inner.filename());
+ final Path meta = tmp.resolve("other-prescan.json");
+ when(client.upload(inner.projectId(), file, meta)).thenReturn(inner);
+
+ // WHEN upload file,meta
+ final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
+ final CodeArtifact codeArtifact =
+ codeArtifacts.upload(file, inner.filename(), meta, inner.metadata());
+
+ // THEN returns expected code artifact
+ assertThat(codeArtifact).hasSameValuesAsInner(inner);
+ }
+
@Test
void delegates_to_inner() {
final CodeArtifactInner inner = builder().build();
diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
index 393073d3..acec3197 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
@@ -36,12 +36,15 @@
import com.google.gson.Gson;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -53,6 +56,7 @@
final class CodeArtifactsPactTest {
private Path jar;
+ private Path metadataJson;
/**
* Creates a test jar for the test to upload as a code artifact
@@ -65,6 +69,12 @@ void before(@TempDir final Path tmp) throws IOException {
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
jos.putNextEntry(new ZipEntry("HelloWorld.class"));
}
+ metadataJson = tmp.resolve("prescan.json");
+ try (OutputStreamWriter os =
+ new OutputStreamWriter(
+ new FileOutputStream(metadataJson.toFile()), StandardCharsets.UTF_8)) {
+ os.write("{\"test\": \"data\" }");
+ }
}
/** Verifies the code artifact upload behavior. */
@@ -128,4 +138,80 @@ void upload_code_artifact(final MockServer server) throws IOException {
assertThat(codeArtifact).isEqualTo(expected);
}
}
+ /** Verifies the code artifact upload with metadata behavior. */
+ @Nested
+ final class UploadCodeArtifactWithMetadata {
+
+ @Disabled("https://github.com/pact-foundation/pact-jvm/issues/668")
+ @Pact(consumer = "contrast-sdk")
+ RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
+
+ final HashMap