From c532d3e39ca4764f3dbd4f712766522fddd513cb Mon Sep 17 00:00:00 2001
From: Shane Schisler <shane.schisler@contrastsecurity.com>
Date: Thu, 4 Nov 2021 10:51:08 -0400
Subject: [PATCH 1/4] Add metadata parameter to CodeArtifact api

The Scan API has an optional "metadata" parameter that can be given
which will aid in creating rich sarif reporting content.  This rich
sarif reporting content will be used by the github sarif viewer to
provided better integration support by annotating the actual code in the
repo view with the vulnerability flow and information.

The changeset here adds support for using this extra parameter when
calling the CodeArtifact Client.

Related Tickets:
UC-559
---
 .../sdk/scan/CodeArtifact.java                |  5 ++
 .../sdk/scan/CodeArtifactClient.java          |  4 +-
 .../sdk/scan/CodeArtifactClientImpl.java      | 43 +++++++--
 .../sdk/scan/CodeArtifactImpl.java            |  7 ++
 .../sdk/scan/CodeArtifactInner.java           |  8 ++
 .../sdk/scan/CodeArtifacts.java               |  3 +
 .../sdk/scan/CodeArtifactsImpl.java           | 15 +++-
 .../sdk/scan/CodeArtifactAssert.java          |  1 +
 .../sdk/scan/CodeArtifactsImplTest.java       |  4 +-
 .../sdk/scan/CodeArtifactsPactTest.java       | 88 ++++++++++++++++++-
 10 files changed, 165 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java
index 1c2be049..39c54f86 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();
 
+  @Nullable
+  /** @return metadata filename */
+  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..d209d9d3 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java
@@ -43,6 +43,8 @@ interface CodeArtifactClient {
    *
    * @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. Null may be given if
+   *     prescan data is not present.
    * @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
@@ -50,5 +52,5 @@ interface CodeArtifactClient {
    * @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) throws IOException;
+  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..f4182010 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
@@ -53,7 +53,8 @@ final class CodeArtifactClientImpl implements CodeArtifactClient {
   }
 
   @Override
-  public CodeArtifactInner upload(final String projectId, final Path file) throws IOException {
+  public CodeArtifactInner upload(final String projectId, final Path file, final Path metadata)
+      throws IOException {
     final String uri =
         contrast.getRestApiURL()
             + new URIBuilder()
@@ -66,9 +67,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 +81,31 @@ 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;
+    if (metadata != null) {
+      metadataSection =
+          boundaryMarker
+              + CRLF
+              + "Content-Disposition: form-data; name=\"metadata\"; filename=\""
+              + metadata.getFileName().toString()
+              + '"'
+              + CRLF
+              + "Content-Type: "
+              + determineMime(metadata)
+              + CRLF
+              + "Content-Transfer-Encoding: binary"
+              + CRLF
+              + CRLF;
+    } else {
+      metadataSection = "";
+    }
+
+    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 +115,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..a047221e 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
@@ -20,6 +20,7 @@
  * #L%
  */
 
+import com.contrastsecurity.sdk.internal.Nullable;
 import java.time.Instant;
 import java.util.Objects;
 
@@ -52,6 +53,12 @@ public String filename() {
     return inner.filename();
   }
 
+  @Override
+  @Nullable
+  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..71a010b3 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();
 
+  @Nullable
+  /** @return metadata filename */
+  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..1259fba4 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
@@ -62,6 +62,7 @@ interface Factory {
    */
   CodeArtifact upload(Path file, String name) throws IOException;
 
+  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 +76,6 @@ interface Factory {
    * @throws ServerResponseException when Contrast API returns a response that cannot be understood
    */
   CodeArtifact upload(Path file) throws IOException;
+
+  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..c86c99bd 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
@@ -50,9 +50,17 @@ 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);
+    final CodeArtifactInner inner = client.upload(projectId, file, null);
     return new CodeArtifactImpl(inner);
   }
 
@@ -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..369ca6a5 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
@@ -43,7 +43,7 @@ void upload(@TempDir final Path tmp) throws IOException {
     final CodeArtifactClient client = mock(CodeArtifactClient.class);
     final CodeArtifactInner inner = builder().build();
     final Path file = tmp.resolve(inner.filename());
-    when(client.upload(inner.projectId(), file)).thenReturn(inner);
+    when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
 
     // WHEN upload file
     final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
@@ -59,7 +59,7 @@ void upload_custom_filename(@TempDir final Path tmp) throws IOException {
     final CodeArtifactClient client = mock(CodeArtifactClient.class);
     final CodeArtifactInner inner = builder().build();
     final Path file = tmp.resolve("other-file.jar");
-    when(client.upload(inner.projectId(), file)).thenReturn(inner);
+    when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
 
     // WHEN upload file
     final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
index 393073d3..23fc5d42 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
@@ -36,12 +36,14 @@
 import com.google.gson.Gson;
 import java.io.FileOutputStream;
 import java.io.IOException;
+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 +55,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 +68,10 @@ 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 (FileOutputStream fos = new FileOutputStream(metadataJson.toFile())) {
+      fos.write("{\"test\": \"data\" }".getBytes(StandardCharsets.UTF_8));
+    }
   }
 
   /** Verifies the code artifact upload behavior. */
@@ -78,7 +85,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
       params.put("organizationId", "organization-id");
       return builder
           .given("Projects Exist", params)
-          .uponReceiving("upload new code artifact")
+          .uponReceiving("upload new code artifact with metadata")
           .method("POST")
           .pathFromProviderState(
               "/sast/organizations/${organizationId}/projects/${projectId}/code-artifacts",
@@ -115,7 +122,83 @@ void upload_code_artifact(final MockServer server) throws IOException {
               .build();
       final Gson gson = GsonFactory.create();
       CodeArtifactClient client = new CodeArtifactClientImpl(contrast, gson, "organization-id");
-      final CodeArtifactInner codeArtifact = client.upload("project-id", jar);
+      final CodeArtifactInner codeArtifact = client.upload("project-id", jar, null);
+
+      final CodeArtifactInner expected =
+          CodeArtifactInner.builder()
+              .id("code-artifact-id")
+              .projectId("project-id")
+              .organizationId("organization-id")
+              .filename(jar.getFileName().toString())
+              .createdTime(TestDataConstants.TIMESTAMP_EXAMPLE)
+              .build();
+      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<String, Object> params = new HashMap<>();
+      params.put("id", "project-id");
+      params.put("organizationId", "organization-id");
+      return builder
+          .given("Projects Exist", params)
+          .uponReceiving("upload new code artifact")
+          .method("POST")
+          .pathFromProviderState(
+              "/sast/organizations/${organizationId}/projects/${projectId}/code-artifacts",
+              "/sast/organizations/organization-id/projects/project-id/code-artifacts")
+          .withFileUpload(
+              "filename",
+              jar.getFileName().toString(),
+              "application/java-archive",
+              Files.readAllBytes(jar))
+          // BUG: https://github.com/pact-foundation/pact-jvm/issues/668. Unable to define a PACT
+          // request matcher that
+          // has multiple multipart sections.
+          // Consumer interface definition is:
+          // https://github.com/Contrast-Security-Inc/sast-api-documentation/blob/master/sast-code-artifacts.yaml#L83
+          .withFileUpload(
+              "metadata",
+              metadataJson.getFileName().toString(),
+              "application/json",
+              Files.readAllBytes(metadataJson))
+          .willRespondWith()
+          .status(201)
+          .body(
+              newJsonBody(
+                      o -> {
+                        o.stringType("id", "code-artifact-id");
+                        o.valueFromProviderState("projectId", "${projectId}", "project-id");
+                        o.valueFromProviderState(
+                            "organizationId", "${organizationId}", "organization-id");
+                        o.stringType("filename", jar.getFileName().toString());
+                        o.stringType("metadata", metadataJson.getFileName().toString());
+                        o.datetime(
+                            "createdTime",
+                            PactConstants.DATETIME_FORMAT,
+                            TestDataConstants.TIMESTAMP_EXAMPLE);
+                      })
+                  .build())
+          .toPact();
+    }
+
+    @Disabled("https://github.com/pact-foundation/pact-jvm/issues/668")
+    @Test
+    void upload_code_artifact_with_metadata(final MockServer server) throws IOException {
+      System.out.println("running metadata test");
+      final ContrastSDK contrast =
+          new ContrastSDK.Builder("test-user", "test-service-key", "test-api-key")
+              .withApiUrl(server.getUrl())
+              .build();
+      final Gson gson = GsonFactory.create();
+      CodeArtifactClient client = new CodeArtifactClientImpl(contrast, gson, "organization-id");
+      final CodeArtifactInner codeArtifact = client.upload("project-id", jar, metadataJson);
 
       final CodeArtifactInner expected =
           CodeArtifactInner.builder()
@@ -123,6 +206,7 @@ void upload_code_artifact(final MockServer server) throws IOException {
               .projectId("project-id")
               .organizationId("organization-id")
               .filename(jar.getFileName().toString())
+              .metadata(metadataJson.getFileName().toString())
               .createdTime(TestDataConstants.TIMESTAMP_EXAMPLE)
               .build();
       assertThat(codeArtifact).isEqualTo(expected);

From 1e28f20caec8589767a40c05565e592cc8e8c13e Mon Sep 17 00:00:00 2001
From: Shane Schisler <shane.schisler@contrastsecurity.com>
Date: Wed, 17 Nov 2021 10:09:21 -0500
Subject: [PATCH 2/4] fix formatting and interface from PR comments

---
 .../sdk/scan/CodeArtifact.java                |  2 +-
 .../sdk/scan/CodeArtifactClient.java          | 22 +++++++++--
 .../sdk/scan/CodeArtifactClientImpl.java      | 10 +++++
 .../sdk/scan/CodeArtifactImpl.java            |  2 -
 .../sdk/scan/CodeArtifactInner.java           |  2 +-
 .../sdk/scan/CodeArtifacts.java               | 29 ++++++++++++++
 .../sdk/scan/CodeArtifactsImpl.java           |  2 +-
 .../sdk/scan/CodeArtifactsImplTest.java       | 39 ++++++++++++++++++-
 .../sdk/scan/CodeArtifactsPactTest.java       |  5 +--
 9 files changed, 100 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java
index 39c54f86..18cae943 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java
@@ -41,8 +41,8 @@ public interface CodeArtifact {
   /** @return filename */
   String filename();
 
-  @Nullable
   /** @return metadata filename */
+  @Nullable
   String metadata();
 
   /** @return time at which the code artifact was uploaded to Contrast Scan */
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java
index d209d9d3..e5393365 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java
@@ -38,13 +38,29 @@
 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
    * @param file the file to upload
-   * @param metadata the prescan metadata to upload with the file artifact. Null may be given if
-   *     prescan data is not present.
+   * @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) throws IOException;
+
+  /**
+   * Transfers artifact and prescan metadata from the file system to Contrast Scan to create a new
+   * code artifact for analysis.
+   *
+   * <p>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
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
index f4182010..f62bc656 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
@@ -52,9 +52,19 @@ final class CodeArtifactClientImpl implements CodeArtifactClient {
     this.organizationId = Objects.requireNonNull(organizationId);
   }
 
+  @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()
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
index a047221e..41830120 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java
@@ -20,7 +20,6 @@
  * #L%
  */
 
-import com.contrastsecurity.sdk.internal.Nullable;
 import java.time.Instant;
 import java.util.Objects;
 
@@ -54,7 +53,6 @@ public String filename() {
   }
 
   @Override
-  @Nullable
   public String metadata() {
     return inner.metadata();
   }
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
index 71a010b3..99689490 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java
@@ -45,8 +45,8 @@ static Builder builder() {
   /** @return filename */
   abstract String filename();
 
-  @Nullable
   /** @return metadata filename */
+  @Nullable
   abstract String metadata();
 
   /** @return time at which the code artifact was uploaded to Contrast Scan */
diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
index 1259fba4..1c962926 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java
@@ -62,7 +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.
@@ -77,5 +93,18 @@ interface Factory {
    */
   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 c86c99bd..23f9a48a 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java
@@ -60,7 +60,7 @@ public CodeArtifact upload(
 
   @Override
   public CodeArtifact upload(final Path file, final String name) throws IOException {
-    final CodeArtifactInner inner = client.upload(projectId, file, null);
+    final CodeArtifactInner inner = client.upload(projectId, file);
     return new CodeArtifactImpl(inner);
   }
 
diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
index 369ca6a5..269dd4bc 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java
@@ -43,7 +43,7 @@ void upload(@TempDir final Path tmp) throws IOException {
     final CodeArtifactClient client = mock(CodeArtifactClient.class);
     final CodeArtifactInner inner = builder().build();
     final Path file = tmp.resolve(inner.filename());
-    when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
+    when(client.upload(inner.projectId(), file)).thenReturn(inner);
 
     // WHEN upload file
     final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
@@ -53,13 +53,30 @@ 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
     final CodeArtifactClient client = mock(CodeArtifactClient.class);
     final CodeArtifactInner inner = builder().build();
     final Path file = tmp.resolve("other-file.jar");
-    when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
+    when(client.upload(inner.projectId(), file)).thenReturn(inner);
 
     // WHEN upload file
     final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
@@ -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 23fc5d42..4287cd2b 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
@@ -85,7 +85,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
       params.put("organizationId", "organization-id");
       return builder
           .given("Projects Exist", params)
-          .uponReceiving("upload new code artifact with metadata")
+          .uponReceiving("upload new code artifact")
           .method("POST")
           .pathFromProviderState(
               "/sast/organizations/${organizationId}/projects/${projectId}/code-artifacts",
@@ -122,7 +122,7 @@ void upload_code_artifact(final MockServer server) throws IOException {
               .build();
       final Gson gson = GsonFactory.create();
       CodeArtifactClient client = new CodeArtifactClientImpl(contrast, gson, "organization-id");
-      final CodeArtifactInner codeArtifact = client.upload("project-id", jar, null);
+      final CodeArtifactInner codeArtifact = client.upload("project-id", jar);
 
       final CodeArtifactInner expected =
           CodeArtifactInner.builder()
@@ -191,7 +191,6 @@ RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
     @Disabled("https://github.com/pact-foundation/pact-jvm/issues/668")
     @Test
     void upload_code_artifact_with_metadata(final MockServer server) throws IOException {
-      System.out.println("running metadata test");
       final ContrastSDK contrast =
           new ContrastSDK.Builder("test-user", "test-service-key", "test-api-key")
               .withApiUrl(server.getUrl())

From f376c92a1146ff0b025067bd10421e7f2b929554 Mon Sep 17 00:00:00 2001
From: Shane Schisler <shane.schisler@contrastsecurity.com>
Date: Wed, 17 Nov 2021 11:14:20 -0500
Subject: [PATCH 3/4] formatting

---
 .../sdk/scan/CodeArtifactClientImpl.java      | 33 +++++++++----------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
index f62bc656..ec2e6ff2 100644
--- a/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
+++ b/src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java
@@ -91,24 +91,21 @@ private CodeArtifactInner sendRequest(
             + "Content-Transfer-Encoding: binary"
             + CRLF
             + CRLF;
-    final String metadataSection;
-    if (metadata != null) {
-      metadataSection =
-          boundaryMarker
-              + CRLF
-              + "Content-Disposition: form-data; name=\"metadata\"; filename=\""
-              + metadata.getFileName().toString()
-              + '"'
-              + CRLF
-              + "Content-Type: "
-              + determineMime(metadata)
-              + CRLF
-              + "Content-Transfer-Encoding: binary"
-              + CRLF
-              + CRLF;
-    } else {
-      metadataSection = "";
-    }
+    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);

From 08db808185a975ebcf4e02acb6c16d4caebc5389 Mon Sep 17 00:00:00 2001
From: Shane Schisler <shane.schisler@contrastsecurity.com>
Date: Wed, 17 Nov 2021 11:51:41 -0500
Subject: [PATCH 4/4] fix stream pattern in test

---
 .../contrastsecurity/sdk/scan/CodeArtifactsPactTest.java   | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
index 4287cd2b..acec3197 100644
--- a/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
+++ b/src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java
@@ -36,6 +36,7 @@
 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;
@@ -69,8 +70,10 @@ void before(@TempDir final Path tmp) throws IOException {
       jos.putNextEntry(new ZipEntry("HelloWorld.class"));
     }
     metadataJson = tmp.resolve("prescan.json");
-    try (FileOutputStream fos = new FileOutputStream(metadataJson.toFile())) {
-      fos.write("{\"test\": \"data\" }".getBytes(StandardCharsets.UTF_8));
+    try (OutputStreamWriter os =
+        new OutputStreamWriter(
+            new FileOutputStream(metadataJson.toFile()), StandardCharsets.UTF_8)) {
+      os.write("{\"test\": \"data\" }");
     }
   }