Skip to content

Commit

Permalink
Add metadata parameter to CodeArtifact api
Browse files Browse the repository at this point in the history
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
  • Loading branch information
seschis committed Nov 8, 2021
1 parent 7c661e4 commit 98a8bfd
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 13 deletions.
5 changes: 5 additions & 0 deletions src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* #L%
*/

import com.contrastsecurity.sdk.internal.Nullable;
import java.time.Instant;

/**
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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);
Expand All @@ -91,9 +115,16 @@ 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();
System.out.println("wrote fileHdr: " + filenameSection);
if (metadata != null) {
writer.append(metadataSection).flush();
Files.copy(metadata, os);
os.flush();
System.out.println("wrote mdHdr: " + metadataSection);
}
writer.append(footer).flush();
}
final int code = connection.getResponseCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* #L%
*/

import com.contrastsecurity.sdk.internal.Nullable;
import java.time.Instant;
import java.util.Objects;

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* #L%
*/

import com.contrastsecurity.sdk.internal.Nullable;
import com.google.auto.value.AutoValue;
import java.time.Instant;

Expand All @@ -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();

Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,27 @@ 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);
}

@Override
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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. */
Expand All @@ -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",
Expand Down Expand Up @@ -115,14 +122,91 @@ 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()
.id("code-artifact-id")
.projectId("project-id")
.organizationId("organization-id")
.filename(jar.getFileName().toString())
.metadata(metadataJson.getFileName().toString())
.createdTime(TestDataConstants.TIMESTAMP_EXAMPLE)
.build();
assertThat(codeArtifact).isEqualTo(expected);
Expand Down

0 comments on commit 98a8bfd

Please sign in to comment.