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\" }"); } }