Skip to content

Commit 98a8bfd

Browse files
committed
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
1 parent 7c661e4 commit 98a8bfd

10 files changed

+165
-13
lines changed

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifact.java

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* #L%
2121
*/
2222

23+
import com.contrastsecurity.sdk.internal.Nullable;
2324
import java.time.Instant;
2425

2526
/**
@@ -40,6 +41,10 @@ public interface CodeArtifact {
4041
/** @return filename */
4142
String filename();
4243

44+
@Nullable
45+
/** @return metadata filename */
46+
String metadata();
47+
4348
/** @return time at which the code artifact was uploaded to Contrast Scan */
4449
Instant createdTime();
4550
}

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClient.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ interface CodeArtifactClient {
5050
* @throws HttpResponseException when Contrast rejects this request with an error code
5151
* @throws ServerResponseException when Contrast API returns a response that cannot be understood
5252
*/
53-
CodeArtifactInner upload(String projectId, Path file) throws IOException;
53+
CodeArtifactInner upload(String projectId, Path file, Path metadata) throws IOException;
5454
}

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactClientImpl.java

+38-7
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ final class CodeArtifactClientImpl implements CodeArtifactClient {
5353
}
5454

5555
@Override
56-
public CodeArtifactInner upload(final String projectId, final Path file) throws IOException {
56+
public CodeArtifactInner upload(final String projectId, final Path file, final Path metadata)
57+
throws IOException {
5758
final String uri =
5859
contrast.getRestApiURL()
5960
+ new URIBuilder()
@@ -66,9 +67,9 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
6667
"code-artifacts")
6768
.toURIString();
6869
final String boundary = "ContrastFormBoundary" + ThreadLocalRandom.current().nextLong();
69-
final String header =
70-
"--"
71-
+ boundary
70+
final String boundaryMarker = CRLF + "--" + boundary;
71+
final String filenameSection =
72+
boundaryMarker
7273
+ CRLF
7374
+ "Content-Disposition: form-data; name=\"filename\"; filename=\""
7475
+ file.getFileName().toString()
@@ -80,8 +81,31 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
8081
+ "Content-Transfer-Encoding: binary"
8182
+ CRLF
8283
+ CRLF;
83-
final String footer = CRLF + "--" + boundary + "--" + CRLF;
84-
final long contentLength = header.length() + Files.size(file) + footer.length();
84+
final String metadataSection;
85+
if (metadata != null) {
86+
metadataSection =
87+
boundaryMarker
88+
+ CRLF
89+
+ "Content-Disposition: form-data; name=\"metadata\"; filename=\""
90+
+ metadata.getFileName().toString()
91+
+ '"'
92+
+ CRLF
93+
+ "Content-Type: "
94+
+ determineMime(metadata)
95+
+ CRLF
96+
+ "Content-Transfer-Encoding: binary"
97+
+ CRLF
98+
+ CRLF;
99+
} else {
100+
metadataSection = "";
101+
}
102+
103+
final String footer = boundaryMarker + "--" + CRLF;
104+
long contentLength = filenameSection.length() + Files.size(file);
105+
if (metadata != null) {
106+
contentLength += metadataSection.length() + Files.size(metadata);
107+
}
108+
contentLength += footer.length();
85109

86110
final HttpURLConnection connection = contrast.makeConnection(uri, "POST");
87111
connection.setDoOutput(true);
@@ -91,9 +115,16 @@ public CodeArtifactInner upload(final String projectId, final Path file) throws
91115
try (OutputStream os = connection.getOutputStream();
92116
PrintWriter writer =
93117
new PrintWriter(new OutputStreamWriter(os, StandardCharsets.US_ASCII), true)) {
94-
writer.append(header).flush();
118+
writer.append(filenameSection).flush();
95119
Files.copy(file, os);
96120
os.flush();
121+
System.out.println("wrote fileHdr: " + filenameSection);
122+
if (metadata != null) {
123+
writer.append(metadataSection).flush();
124+
Files.copy(metadata, os);
125+
os.flush();
126+
System.out.println("wrote mdHdr: " + metadataSection);
127+
}
97128
writer.append(footer).flush();
98129
}
99130
final int code = connection.getResponseCode();

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactImpl.java

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* #L%
2121
*/
2222

23+
import com.contrastsecurity.sdk.internal.Nullable;
2324
import java.time.Instant;
2425
import java.util.Objects;
2526

@@ -52,6 +53,12 @@ public String filename() {
5253
return inner.filename();
5354
}
5455

56+
@Override
57+
@Nullable
58+
public String metadata() {
59+
return inner.metadata();
60+
}
61+
5562
@Override
5663
public Instant createdTime() {
5764
return inner.createdTime();

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactInner.java

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* #L%
2121
*/
2222

23+
import com.contrastsecurity.sdk.internal.Nullable;
2324
import com.google.auto.value.AutoValue;
2425
import java.time.Instant;
2526

@@ -44,6 +45,10 @@ static Builder builder() {
4445
/** @return filename */
4546
abstract String filename();
4647

48+
@Nullable
49+
/** @return metadata filename */
50+
abstract String metadata();
51+
4752
/** @return time at which the code artifact was uploaded to Contrast Scan */
4853
abstract Instant createdTime();
4954

@@ -63,6 +68,9 @@ abstract static class Builder {
6368
/** @see CodeArtifactInner#filename() */
6469
abstract Builder filename(String value);
6570

71+
/** @see CodeArtifactInner#metadata() */
72+
abstract Builder metadata(String value);
73+
6674
/** @see CodeArtifactInner#createdTime() */
6775
abstract Builder createdTime(Instant value);
6876

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifacts.java

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ interface Factory {
6262
*/
6363
CodeArtifact upload(Path file, String name) throws IOException;
6464

65+
CodeArtifact upload(Path file, String name, Path metadata, String metaname) throws IOException;
6566
/**
6667
* Transfers a file from the file system to Contrast Scan to create a new code artifact for static
6768
* analysis.
@@ -75,4 +76,6 @@ interface Factory {
7576
* @throws ServerResponseException when Contrast API returns a response that cannot be understood
7677
*/
7778
CodeArtifact upload(Path file) throws IOException;
79+
80+
CodeArtifact upload(Path file, Path metadata) throws IOException;
7881
}

src/main/java/com/contrastsecurity/sdk/scan/CodeArtifactsImpl.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,27 @@ public CodeArtifacts create(final String projectId) {
5050
this.projectId = projectId;
5151
}
5252

53+
@Override
54+
public CodeArtifact upload(
55+
final Path file, final String name, final Path metadata, final String metaname)
56+
throws IOException {
57+
final CodeArtifactInner inner = client.upload(projectId, file, metadata);
58+
return new CodeArtifactImpl(inner);
59+
}
60+
5361
@Override
5462
public CodeArtifact upload(final Path file, final String name) throws IOException {
55-
final CodeArtifactInner inner = client.upload(projectId, file);
63+
final CodeArtifactInner inner = client.upload(projectId, file, null);
5664
return new CodeArtifactImpl(inner);
5765
}
5866

5967
@Override
6068
public CodeArtifact upload(final Path file) throws IOException {
6169
return upload(file, file.getFileName().toString());
6270
}
71+
72+
@Override
73+
public CodeArtifact upload(final Path file, final Path metadata) throws IOException {
74+
return upload(file, file.getFileName().toString(), metadata, metadata.getFileName().toString());
75+
}
6376
}

src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactAssert.java

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public CodeArtifactAssert hasSameValuesAsInner(final CodeArtifactInner inner) {
4949
Assertions.assertThat(actual.projectId()).isEqualTo(inner.projectId());
5050
Assertions.assertThat(actual.organizationId()).isEqualTo(inner.organizationId());
5151
Assertions.assertThat(actual.filename()).isEqualTo(inner.filename());
52+
Assertions.assertThat(actual.metadata()).isEqualTo(inner.metadata());
5253
Assertions.assertThat(actual.createdTime()).isEqualTo(inner.createdTime());
5354
return this;
5455
}

src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsImplTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ void upload(@TempDir final Path tmp) throws IOException {
4343
final CodeArtifactClient client = mock(CodeArtifactClient.class);
4444
final CodeArtifactInner inner = builder().build();
4545
final Path file = tmp.resolve(inner.filename());
46-
when(client.upload(inner.projectId(), file)).thenReturn(inner);
46+
when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
4747

4848
// WHEN upload file
4949
final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());
@@ -59,7 +59,7 @@ void upload_custom_filename(@TempDir final Path tmp) throws IOException {
5959
final CodeArtifactClient client = mock(CodeArtifactClient.class);
6060
final CodeArtifactInner inner = builder().build();
6161
final Path file = tmp.resolve("other-file.jar");
62-
when(client.upload(inner.projectId(), file)).thenReturn(inner);
62+
when(client.upload(inner.projectId(), file, null)).thenReturn(inner);
6363

6464
// WHEN upload file
6565
final CodeArtifacts codeArtifacts = new CodeArtifactsImpl(client, inner.projectId());

src/test/java/com/contrastsecurity/sdk/scan/CodeArtifactsPactTest.java

+86-2
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@
3636
import com.google.gson.Gson;
3737
import java.io.FileOutputStream;
3838
import java.io.IOException;
39+
import java.nio.charset.StandardCharsets;
3940
import java.nio.file.Files;
4041
import java.nio.file.Path;
4142
import java.util.HashMap;
4243
import java.util.jar.JarOutputStream;
4344
import java.util.zip.ZipEntry;
4445
import org.junit.jupiter.api.BeforeEach;
46+
import org.junit.jupiter.api.Disabled;
4547
import org.junit.jupiter.api.Nested;
4648
import org.junit.jupiter.api.Test;
4749
import org.junit.jupiter.api.extension.ExtendWith;
@@ -53,6 +55,7 @@
5355
final class CodeArtifactsPactTest {
5456

5557
private Path jar;
58+
private Path metadataJson;
5659

5760
/**
5861
* 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 {
6568
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
6669
jos.putNextEntry(new ZipEntry("HelloWorld.class"));
6770
}
71+
metadataJson = tmp.resolve("prescan.json");
72+
try (FileOutputStream fos = new FileOutputStream(metadataJson.toFile())) {
73+
fos.write("{\"test\": \"data\" }".getBytes(StandardCharsets.UTF_8));
74+
}
6875
}
6976

7077
/** Verifies the code artifact upload behavior. */
@@ -78,7 +85,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
7885
params.put("organizationId", "organization-id");
7986
return builder
8087
.given("Projects Exist", params)
81-
.uponReceiving("upload new code artifact")
88+
.uponReceiving("upload new code artifact with metadata")
8289
.method("POST")
8390
.pathFromProviderState(
8491
"/sast/organizations/${organizationId}/projects/${projectId}/code-artifacts",
@@ -115,14 +122,91 @@ void upload_code_artifact(final MockServer server) throws IOException {
115122
.build();
116123
final Gson gson = GsonFactory.create();
117124
CodeArtifactClient client = new CodeArtifactClientImpl(contrast, gson, "organization-id");
118-
final CodeArtifactInner codeArtifact = client.upload("project-id", jar);
125+
final CodeArtifactInner codeArtifact = client.upload("project-id", jar, null);
126+
127+
final CodeArtifactInner expected =
128+
CodeArtifactInner.builder()
129+
.id("code-artifact-id")
130+
.projectId("project-id")
131+
.organizationId("organization-id")
132+
.filename(jar.getFileName().toString())
133+
.createdTime(TestDataConstants.TIMESTAMP_EXAMPLE)
134+
.build();
135+
assertThat(codeArtifact).isEqualTo(expected);
136+
}
137+
}
138+
/** Verifies the code artifact upload with metadata behavior. */
139+
@Nested
140+
final class UploadCodeArtifactWithMetadata {
141+
142+
@Disabled("https://github.com/pact-foundation/pact-jvm/issues/668")
143+
@Pact(consumer = "contrast-sdk")
144+
RequestResponsePact pact(final PactDslWithProvider builder) throws IOException {
145+
146+
final HashMap<String, Object> params = new HashMap<>();
147+
params.put("id", "project-id");
148+
params.put("organizationId", "organization-id");
149+
return builder
150+
.given("Projects Exist", params)
151+
.uponReceiving("upload new code artifact")
152+
.method("POST")
153+
.pathFromProviderState(
154+
"/sast/organizations/${organizationId}/projects/${projectId}/code-artifacts",
155+
"/sast/organizations/organization-id/projects/project-id/code-artifacts")
156+
.withFileUpload(
157+
"filename",
158+
jar.getFileName().toString(),
159+
"application/java-archive",
160+
Files.readAllBytes(jar))
161+
// BUG: https://github.com/pact-foundation/pact-jvm/issues/668. Unable to define a PACT
162+
// request matcher that
163+
// has multiple multipart sections.
164+
// Consumer interface definition is:
165+
// https://github.com/Contrast-Security-Inc/sast-api-documentation/blob/master/sast-code-artifacts.yaml#L83
166+
.withFileUpload(
167+
"metadata",
168+
metadataJson.getFileName().toString(),
169+
"application/json",
170+
Files.readAllBytes(metadataJson))
171+
.willRespondWith()
172+
.status(201)
173+
.body(
174+
newJsonBody(
175+
o -> {
176+
o.stringType("id", "code-artifact-id");
177+
o.valueFromProviderState("projectId", "${projectId}", "project-id");
178+
o.valueFromProviderState(
179+
"organizationId", "${organizationId}", "organization-id");
180+
o.stringType("filename", jar.getFileName().toString());
181+
o.stringType("metadata", metadataJson.getFileName().toString());
182+
o.datetime(
183+
"createdTime",
184+
PactConstants.DATETIME_FORMAT,
185+
TestDataConstants.TIMESTAMP_EXAMPLE);
186+
})
187+
.build())
188+
.toPact();
189+
}
190+
191+
@Disabled("https://github.com/pact-foundation/pact-jvm/issues/668")
192+
@Test
193+
void upload_code_artifact_with_metadata(final MockServer server) throws IOException {
194+
System.out.println("running metadata test");
195+
final ContrastSDK contrast =
196+
new ContrastSDK.Builder("test-user", "test-service-key", "test-api-key")
197+
.withApiUrl(server.getUrl())
198+
.build();
199+
final Gson gson = GsonFactory.create();
200+
CodeArtifactClient client = new CodeArtifactClientImpl(contrast, gson, "organization-id");
201+
final CodeArtifactInner codeArtifact = client.upload("project-id", jar, metadataJson);
119202

120203
final CodeArtifactInner expected =
121204
CodeArtifactInner.builder()
122205
.id("code-artifact-id")
123206
.projectId("project-id")
124207
.organizationId("organization-id")
125208
.filename(jar.getFileName().toString())
209+
.metadata(metadataJson.getFileName().toString())
126210
.createdTime(TestDataConstants.TIMESTAMP_EXAMPLE)
127211
.build();
128212
assertThat(codeArtifact).isEqualTo(expected);

0 commit comments

Comments
 (0)