Skip to content
This repository was archived by the owner on Apr 11, 2025. It is now read-only.

Commit cf9638b

Browse files
committed
✨ JAVA-3297 Create Scan Project If Not Exists (#36)
Looks up the Contrast Scan project by name, and creates one if it does not exist. Default name is the Maven project name.
1 parent 7e1ba7e commit cf9638b

25 files changed

+1330
-84
lines changed

Diff for: scan.http

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# scan.http
2+
# HTTP file for experimenting with the scan API
3+
# To use this file with IntelliJ, create a file http-client.private.env.json with configuration for
4+
# connecting to your Contrast instance e.g.
5+
# {
6+
# "production": {
7+
# "url": "https://app.contrastsecurity.com/Contrast/api",
8+
# "apiKey": "<your-api-key>",
9+
# "authorization": "<your-authorization-header>",
10+
# "organizationId": "<your-organization-id>"
11+
# }
12+
# }
13+
14+
15+
### Create Scan Project
16+
POST {{ url }}/sast/organizations/{{ organizationId }}/projects
17+
API-Key: {{ apiKey }}
18+
Authorization: {{ authorization }}
19+
Content-Type: application/json
20+
21+
{
22+
"name": "spring-test-application",
23+
"language": "JAVA",
24+
"includeNamespaceFilters": [],
25+
"excludeNamespaceFilters": []
26+
}
27+
28+
> {% client.global.set("projectId", response.body.id); %}
29+
30+
31+
### DELETE Scan Project
32+
DELETE {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}
33+
API-Key: {{ apiKey }}
34+
Authorization: {{ authorization }}
35+
36+
37+
### Find Scan Project
38+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects?name=spring-test-application&archived=false&unique=true
39+
API-Key: {{ apiKey }}
40+
Authorization: {{ authorization }}
41+
Accept: application/json
42+
43+
> {% client.global.set("projectId", response.body.content[0].id); %}
44+
45+
46+
### Upload Code Artifact
47+
48+
POST {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/code-artifacts
49+
API-Key: {{ apiKey }}
50+
Authorization: {{ authorization }}
51+
Content-Type: multipart/form-data; boundary=WebAppBoundary
52+
53+
--WebAppBoundary
54+
Content-Disposition: form-data; name="filename"; filename="spring-test-application-0.0.1-SNAPSHOT.jar"
55+
Content-Type: application/java-archive
56+
57+
< ./target/test-classes/it/spring-boot/target/spring-test-application-0.0.1-SNAPSHOT.jar
58+
--WebAppBoundary--
59+
60+
> {% client.global.set("artifactId", response.body.id); %}
61+
62+
63+
### Start Scan
64+
65+
POST {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans
66+
API-Key: {{ apiKey }}
67+
Authorization: {{ authorization }}
68+
Content-Type: application/json
69+
70+
{
71+
"codeArtifactId": "{{ artifactId }}",
72+
"label": "scan.http"
73+
}
74+
75+
> {% client.global.set("scanId", response.body.id); %}
76+
77+
78+
### Cancel Scan
79+
80+
PUT {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}
81+
API-Key: {{ apiKey }}
82+
Authorization: {{ authorization }}
83+
Content-Type: application/json
84+
85+
{
86+
"label": "scan.http",
87+
"status": "CANCELLED"
88+
}
89+
90+
91+
### Get Scan
92+
93+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}
94+
API-Key: {{ apiKey }}
95+
Authorization: {{ authorization }}
96+
97+
98+
### Get Scan Results
99+
100+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/result-instances
101+
API-Key: {{ apiKey }}
102+
Authorization: {{ authorization }}
103+
104+
105+
### Get Scan Results (abreviated)
106+
107+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/result-instances/info
108+
API-Key: {{ apiKey }}
109+
Authorization: {{ authorization }}
110+
111+
112+
### Get Scan Results Summary
113+
GET {{ url }}/sast/organizations/{{ organizationId}}/projects/{{ projectId }}/scans/{{ scanId }}/summary
114+
API-Key: {{ apiKey }}
115+
Authorization: {{ authorization }}
116+
117+
118+
### Get Project Results Summary
119+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/summary
120+
API-Key: {{ apiKey }}
121+
Authorization: {{ authorization }}
122+
123+
124+
### Get Scan Results in SARIF
125+
GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/raw-output
126+
API-Key: {{ apiKey }}
127+
Authorization: {{ authorization }}

Diff for: src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java

+79-13
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
* #L%
2121
*/
2222

23+
import com.contrastsecurity.exceptions.UnauthorizedException;
2324
import com.contrastsecurity.maven.plugin.sdkx.ContrastScanSDK;
25+
import com.contrastsecurity.maven.plugin.sdkx.CreateProjectRequest;
2426
import com.contrastsecurity.maven.plugin.sdkx.DefaultContrastScanSDK;
27+
import com.contrastsecurity.maven.plugin.sdkx.Project;
2528
import com.contrastsecurity.maven.plugin.sdkx.ScanSummary;
2629
import com.contrastsecurity.maven.plugin.sdkx.scan.ArtifactScanner;
2730
import com.contrastsecurity.maven.plugin.sdkx.scan.ScanOperation;
@@ -33,6 +36,7 @@
3336
import java.nio.file.Files;
3437
import java.nio.file.Path;
3538
import java.time.Duration;
39+
import java.util.Collections;
3640
import java.util.concurrent.CompletableFuture;
3741
import java.util.concurrent.CompletionStage;
3842
import java.util.concurrent.ExecutionException;
@@ -61,15 +65,14 @@
6165
public final class ContrastScanMojo extends AbstractContrastMojo {
6266

6367
@Parameter(defaultValue = "${project}", readonly = true)
64-
private MavenProject project;
68+
private MavenProject mavenProject;
6569

6670
/**
6771
* Contrast Scan project unique ID to which the plugin runs new Scans. This will be replaced
6872
* imminently with a project name
6973
*/
70-
// TODO[JAVA-3297] replace this with "project"
71-
@Parameter(required = true)
72-
private String projectId;
74+
@Parameter(property = "project", defaultValue = "${project.name}")
75+
private String projectName;
7376

7477
/**
7578
* File path of the Java artifact to upload for scanning. By default, uses the path to this
@@ -113,13 +116,13 @@ public final class ContrastScanMojo extends AbstractContrastMojo {
113116
private ContrastSDK contrast;
114117

115118
/** visible for testing */
116-
String getProjectId() {
117-
return projectId;
119+
String getProjectName() {
120+
return projectName;
118121
}
119122

120123
/** visible for testing */
121-
void setProjectId(final String projectId) {
122-
this.projectId = projectId;
124+
void setProjectName(final String projectName) {
125+
this.projectName = projectName;
123126
}
124127

125128
/** visible for testing */
@@ -136,21 +139,27 @@ void setConsoleOutput(final boolean consoleOutput) {
136139
public void execute() throws MojoExecutionException, MojoFailureException {
137140
// initialize plugin
138141
initialize();
142+
final ContrastScanSDK contrastScan = new DefaultContrastScanSDK(contrast, getURL());
139143

140144
// check that file exists
141145
final Path file =
142-
artifactPath == null ? project.getArtifact().getFile().toPath() : artifactPath.toPath();
146+
artifactPath == null
147+
? mavenProject.getArtifact().getFile().toPath()
148+
: artifactPath.toPath();
143149
if (!Files.exists(file)) {
144150
throw new MojoExecutionException(
145151
file
146152
+ " does not exist. Make sure to bind the scan goal to a phase that will execute after the artifact to scan has been built");
147153
}
148154

149-
final ContrastScanSDK contrastScan = new DefaultContrastScanSDK(contrast, getURL());
155+
// get or create project
156+
final Project project = findOrCreateProject(contrastScan);
157+
158+
// start scan
150159
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
151160
final ArtifactScanner scanner =
152161
new ArtifactScanner(
153-
executor, contrastScan, getOrganizationId(), getProjectId(), POLL_SCAN_INTERVAL);
162+
executor, contrastScan, getOrganizationId(), project.getId(), POLL_SCAN_INTERVAL);
154163
try {
155164
getLog().info("Uploading " + file.getFileName() + " to Contrast Scan");
156165
final ScanOperation operation = scanner.scanArtifact(file, label);
@@ -161,7 +170,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
161170
}
162171

163172
// show link in build log
164-
final URL clickableScanURL = createClickableScanURL(operation.id());
173+
final URL clickableScanURL = createClickableScanURL(project.getId(), operation.id());
165174
getLog().info("Scan results will be available at " + clickableScanURL.toExternalForm());
166175

167176
// optionally wait for results, output summary to console, output sarif to file system
@@ -174,13 +183,70 @@ public void execute() throws MojoExecutionException, MojoFailureException {
174183
}
175184
}
176185

186+
/**
187+
* Finds a Scan project with the project name from the plugin configuration, or creates such a
188+
* "Java" project if one does not exist.
189+
*
190+
* <p>Note: the Scan API does not expose an endpoint for doing this atomically, so it is possible
191+
* that another process creates the project after having determined it to not-exist but before
192+
* attempting to create it.
193+
*
194+
* @param contrastScan client with which to make the requests
195+
* @return existing or new {@link Project}
196+
* @throws MojoExecutionException when fails to make request to the Scan API
197+
*/
198+
private Project findOrCreateProject(final ContrastScanSDK contrastScan)
199+
throws MojoExecutionException {
200+
final Project project;
201+
try {
202+
project = contrastScan.findProjectByName(getOrganizationId(), projectName);
203+
} catch (final IOException e) {
204+
throw new MojoExecutionException("Failed to retrieve project " + projectName, e);
205+
} catch (final UnauthorizedException e) {
206+
throw new MojoExecutionException(
207+
"Authentication failure while retrieving project "
208+
+ projectName
209+
+ " - verify Contrast connection configuration",
210+
e);
211+
}
212+
if (project != null) {
213+
getLog().debug("Found project with name " + projectName);
214+
if (project.isArchived()) {
215+
// TODO the behavior of tools like this plugin has yet to be defined with respect to
216+
// archived projects; however, the UI exposes no way to archive projects at the moment.
217+
// For now, simply log a warning to help debug this in the future should we encounter this
218+
// case
219+
getLog().warn("Project " + projectName + " is archived");
220+
}
221+
return project;
222+
}
223+
224+
// project does not exist, so create a new one
225+
getLog().debug("No project exists with name " + projectName + " - creating one");
226+
final CreateProjectRequest request =
227+
new CreateProjectRequest(
228+
projectName, "JAVA", Collections.emptyList(), Collections.emptyList());
229+
try {
230+
return contrastScan.createProject(getOrganizationId(), request);
231+
} catch (final IOException e) {
232+
throw new MojoExecutionException("Failed to create project " + projectName, e);
233+
} catch (final UnauthorizedException e) {
234+
throw new MojoExecutionException(
235+
"Authentication failure while creating project "
236+
+ projectName
237+
+ " - verify Contrast connection configuration",
238+
e);
239+
}
240+
}
241+
177242
/**
178243
* visible for testing
179244
*
180245
* @return Contrast browser application URL for users to click-through and see their scan results
181246
* @throws MojoExecutionException when the URL is malformed
182247
*/
183-
URL createClickableScanURL(final String scanId) throws MojoExecutionException {
248+
URL createClickableScanURL(final String projectId, final String scanId)
249+
throws MojoExecutionException {
184250
final String path =
185251
String.join(
186252
"/",

Diff for: src/main/java/com/contrastsecurity/maven/plugin/sdkx/ContrastAPIException.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222

2323
/** Indicates an error occurred while using the Contrast HTTP API */
24-
public class ContrastAPIException extends RuntimeException {
24+
public class ContrastAPIException extends ContrastException {
2525

2626
private final int status;
2727

@@ -47,4 +47,10 @@ public ContrastAPIException(final int status, final String message, final Except
4747
public int getStatus() {
4848
return status;
4949
}
50+
51+
@Override
52+
public String getMessage() {
53+
final String message = super.getMessage();
54+
return message == null ? String.valueOf(status) : status + " " + super.getMessage();
55+
}
5056
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.contrastsecurity.maven.plugin.sdkx;
2+
3+
/*-
4+
* #%L
5+
* Contrast Maven Plugin
6+
* %%
7+
* Copyright (C) 2021 Contrast Security, Inc.
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* #L%
21+
*/
22+
23+
/**
24+
* Generic {@link RuntimeException} thrown by Contrast code to indicate an unexpected error has
25+
* occurred.
26+
*/
27+
public class ContrastException extends RuntimeException {
28+
29+
/** @see RuntimeException#RuntimeException(String) */
30+
public ContrastException(final String message) {
31+
super(message);
32+
}
33+
34+
/** @see RuntimeException#RuntimeException(String, Throwable) */
35+
public ContrastException(final String message, final Throwable inner) {
36+
super(message, inner);
37+
}
38+
}

Diff for: src/main/java/com/contrastsecurity/maven/plugin/sdkx/ContrastScanSDK.java

+24
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@
3232
*/
3333
public interface ContrastScanSDK {
3434

35+
/**
36+
* Creates a new Scan project.
37+
*
38+
* @param organizationId unique ID for the user's organization
39+
* @param request new project request
40+
* @return the new {@link Project}
41+
* @throws IOException when an IO error occurs sending the request to the Contrast Scan API
42+
* @throws UnauthorizedException when Contrast rejects this request as unauthorized
43+
*/
44+
Project createProject(final String organizationId, final CreateProjectRequest request)
45+
throws IOException, UnauthorizedException;
46+
47+
/**
48+
* Scan project lookup.
49+
*
50+
* @param organizationId unique ID for the user's organization
51+
* @param projectName unique project name to find
52+
* @return the {@link Project}, or {@code null} if no such project is found
53+
* @throws IOException when an IO error occurs sending the query to the Contrast Scan API
54+
* @throws UnauthorizedException when Contrast rejects this request as unauthorized
55+
*/
56+
Project findProjectByName(final String organizationId, final String projectName)
57+
throws IOException, UnauthorizedException;
58+
3559
/**
3660
* Transfers a file from the file system to Contrast Scan to create a new code artifact for
3761
* analysis.

0 commit comments

Comments
 (0)