├── unset-contrast.sh
├── src
├── site
│ ├── resources
│ │ └── images
│ │ │ └── contrast-logo.png
│ ├── markdown
│ │ ├── index.md
│ │ ├── troubleshooting
│ │ │ └── artifact-not-set.md
│ │ ├── examples
│ │ │ └── multi-module-projects.md
│ │ └── usage.md
│ └── site.xml
├── main
│ ├── java-templates
│ │ └── com
│ │ │ └── contrastsecurity
│ │ │ └── maven
│ │ │ └── plugin
│ │ │ └── Version.java
│ └── java
│ │ └── com
│ │ └── contrastsecurity
│ │ └── maven
│ │ └── plugin
│ │ ├── AbstractAssessMojo.java
│ │ ├── AbstractContrastMojo.java
│ │ ├── ContrastVerifyMojo.java
│ │ ├── ContrastInstallAgentMojo.java
│ │ └── ContrastScanMojo.java
└── test
│ ├── resources
│ └── it
│ │ ├── spring-boot
│ │ ├── src
│ │ │ └── main
│ │ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── contrastsecurity
│ │ │ │ └── test
│ │ │ │ └── Application.java
│ │ └── pom.xml
│ │ └── parent-pom
│ │ └── pom.xml
│ └── java
│ └── com
│ └── contrastsecurity
│ └── maven
│ └── plugin
│ ├── it
│ ├── stub
│ │ ├── ContrastAPI.java
│ │ ├── ExternalContrastAPI.java
│ │ ├── ContrastAPIStub.java
│ │ ├── package-info.java
│ │ ├── ConnectionParameters.java
│ │ └── ContrastAPIStubExtension.java
│ ├── ContrastInstallAgentMojoIT.java
│ ├── Verifiers.java
│ └── ContrastScanMojoIT.java
│ ├── AbstractContrastMojoTest.java
│ ├── Resources.java
│ ├── FakeScanSummary.java
│ ├── ContrastVerifyMojoTest.java
│ ├── ContrastScanMojoTest.java
│ └── ContrastInstallAgentMojoTest.java
├── LICENSE
├── .mvn
└── wrapper
│ ├── maven-wrapper.properties
│ └── MavenWrapperDownloader.java
├── .github
└── workflows
│ ├── build.yml
│ └── publish.yml
├── CHANGELOG.md
├── README.md
├── scan.http
├── .gitignore
├── mvnw.cmd
├── mvnw
└── pom.xml
/unset-contrast.sh:
--------------------------------------------------------------------------------
1 | unset CONTRAST__API__URL CONTRAST__API__USER_NAME CONTRAST__API__API_KEY CONTRAST__API__SERVICE_KEY CONTRAST__API__ORGANIZATION_ID
2 |
--------------------------------------------------------------------------------
/src/site/resources/images/contrast-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-maven-plugin/main/src/site/resources/images/contrast-logo.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 Contrast Security, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 |
--------------------------------------------------------------------------------
/src/site/markdown/index.md:
--------------------------------------------------------------------------------
1 | ## Contrast Maven Plugin
2 |
3 | The Contrast Maven Plugin helps users include one or more Contrast Security analysis features in
4 | their Java web application Maven projects.
5 |
6 | ### Goals Overview
7 |
8 | * [contrast:install](install-mojo.html) includes the Contrast Java agent in integration testing to
9 | provide Contrast Assess runtime security analysis.
10 | * [contrast:verify](verify-mojo.html) verifies that none of the vulnerabilities found by Contrast
11 | Assess during integration testing violate the project's security policy (fails the build when
12 | violations are detected).
13 | * [contrast:scan](scan-mojo.html) analyzes the Maven project's artifact with Contrast Scan to find
14 | vulnerabilities using static analysis.
15 |
16 |
17 | ### Usage
18 |
19 | General instructions for how to use the Contrast Maven Plugin may be found on
20 | the [usage page](usage.html).
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip
18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
19 |
--------------------------------------------------------------------------------
/src/main/java-templates/com/contrastsecurity/maven/plugin/Version.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | /** Constants that describe this artifact. */
24 | public final class Version {
25 |
26 | /** Version of this contrast-maven-plugin */
27 | public static final String VERSION = "${project.version}";
28 |
29 | /** static members only */
30 | private Version() {}
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | changelog:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: dangoslen/changelog-enforcer@v3
10 | build:
11 | name: Verify
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - uses: actions/setup-java@v4
17 | with:
18 | java-version: 11
19 | distribution: temurin
20 |
21 | - uses: actions/setup-java@v4
22 | with:
23 | java-version: 17
24 | distribution: temurin
25 |
26 | - uses: actions/setup-java@v4
27 | with:
28 | java-version: 21
29 | distribution: temurin
30 |
31 | - name: Maven Verify
32 | env:
33 | CONTRAST__API__URL: ${{ secrets.CONTRAST__API__URL }}
34 | CONTRAST__API__USER_NAME: ${{ secrets.CONTRAST__API__USER_NAME }}
35 | CONTRAST__API__API_KEY: ${{ secrets.CONTRAST__API__API_KEY }}
36 | CONTRAST__API__SERVICE_KEY: ${{ secrets.CONTRAST__API__SERVICE_KEY }}
37 | CONTRAST__API__ORGANIZATION_ID: ${{ secrets.CONTRAST__API__ORGANIZATION_ID }}
38 | run: ./mvnw verify
39 |
--------------------------------------------------------------------------------
/src/test/resources/it/spring-boot/src/main/java/com/contrastsecurity/test/Application.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.test;
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 | import org.springframework.boot.SpringApplication;
24 | import org.springframework.boot.autoconfigure.SpringBootApplication;
25 |
26 | @SpringBootApplication
27 | public final class Application {
28 | public static void main(final String[] args) {
29 | SpringApplication.run(Application.class, args);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPI.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it.stub;
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 | /** Describes a test instance of Contrast API to which tests may requests */
24 | public interface ContrastAPI {
25 |
26 | /** starts the Contrast API instance */
27 | void start();
28 |
29 | /**
30 | * @return connection configuration necessary for making requests to this Contrast API instance
31 | */
32 | ConnectionParameters connection();
33 |
34 | /** stops the Contrast API instance */
35 | void stop();
36 | }
37 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
6 |
7 | ## [2.13.2] - 2022-01-24
8 | ### Changed
9 | - `install` and `verify` goals no longer require the `serverName` configuration parameter. The `serverName` configuration parameter can add stability to the `verify` goal for very active builds, but it is not strictly necessary nor desirable for most use cases.
10 |
11 | ## [2.13.1] - 2021-08-31
12 | ### Added
13 | - Contrast Scan support
14 |
15 | ### Removed
16 | - `profile` configuration which the Contrast server has not supported since before 3.7.7.
17 | - support for JRE 1.7. Requires minimum JRE 1.8
18 |
19 |
20 | ## [2.12] - 2021-03-09
21 | ### Changed
22 | - Tested with JDK 1.8, 11, and 15
23 | - Targets JRE 1.7
24 | - Maven version > 3.6.1 (Released April 2019) is required to build the plugin
25 |
26 |
27 | ## [2.0] - 2018-05-15
28 | ### Added
29 | - Vulnerabilities now reconciled using an app version instead of a timestamp
30 | - App version can be generated using `$TRAVIS_BUILD_NUMBER` or `$CIRCLE_BUILD_NUM`
31 | - Source packaging changed to `com.contrastsecurity.maven.plugin`
--------------------------------------------------------------------------------
/src/site/markdown/troubleshooting/artifact-not-set.md:
--------------------------------------------------------------------------------
1 | ## Troubleshooting: Artifact Not Set
2 |
3 | This error occurs when there is no project artifact available for the `scan` goal to analyze. This
4 | typically indicates that the `scan` goal has been:
5 |
6 | 1. included in a module that does not produce an artifact (e.g. a module of type `pom`).
7 | 2. configured to run before the project's artifact has been built.
8 |
9 |
10 | ### Only Include in Modules that Produce Artifacts
11 |
12 | The `scan` goal should only be included in modules that produce a build artifact (e.g. a module that
13 | produces a `jar` or `war` file).
14 |
15 | When configuring a [multi-module](https://maven.apache.org/guides/mini/guide-multiple-modules.html)
16 | build, users may erroneously include the `scan` goal in the build of a parent pom, and parent poms
17 | do not produce build artifacts. In a multi-module project, verify that the `scan` goal is only
18 | included in projects that produce a `war` or `jar` artifact. Reference
19 | the [multi-module example](../examples/multi-module-projects.html).
20 |
21 |
22 | ### Configure Scan to Run After the Build Produces an Artifact
23 |
24 | Maven typically generates an artifact during the `package` phase. By default, the `scan` goal runs
25 | after the `package` phase during the `verify` phase.
26 |
27 | You may have overridden the plugin's default phase so that the `scan` goal runs during an earlier
28 | phase before the artifact has been built (e.g. the `test` phase). In this case, the `scan` goal will
29 | not be able to find an artifact to scan. Make sure to attach the scan goal to a later phase (such as
30 | the default `verify` phase).
31 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ExternalContrastAPI.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it.stub;
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 | import java.util.Objects;
24 |
25 | /**
26 | * {@link ContrastAPI} implementation that represents an external system. Methods that affect the
27 | * system such as {@code start()} and {@code stop()} are no-ops.
28 | */
29 | final class ExternalContrastAPI implements ContrastAPI {
30 |
31 | private final ConnectionParameters connection;
32 |
33 | /**
34 | * @param connection the connection parameters constant to provide to users
35 | */
36 | public ExternalContrastAPI(final ConnectionParameters connection) {
37 | this.connection = Objects.requireNonNull(connection);
38 | }
39 |
40 | /** nop */
41 | @Override
42 | public void start() {}
43 |
44 | @Override
45 | public ConnectionParameters connection() {
46 | return connection;
47 | }
48 |
49 | /** nop */
50 | @Override
51 | public void stop() {}
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStub.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it.stub;
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 | import java.lang.annotation.Documented;
24 | import java.lang.annotation.ElementType;
25 | import java.lang.annotation.Retention;
26 | import java.lang.annotation.RetentionPolicy;
27 | import java.lang.annotation.Target;
28 | import org.junit.jupiter.api.extension.ExtendWith;
29 |
30 | /**
31 | * Provides a JUnit test with a {@link ContrastAPI} stub for testing. Starts the {@code ContrastAPI}
32 | * instance before starting the test, and handles gracefully terminating the Contrast API instance
33 | * at the conclusion of the test.
34 | *
35 | *
40 | */
41 | @Documented
42 | @Target(ElementType.TYPE)
43 | @Retention(RetentionPolicy.RUNTIME)
44 | @ExtendWith(ContrastAPIStubExtension.class)
45 | public @interface ContrastAPIStub {}
46 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/AbstractContrastMojoTest.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import static org.assertj.core.api.Assertions.assertThat;
24 |
25 | import com.contrastsecurity.sdk.UserAgentProduct;
26 | import org.junit.jupiter.api.Test;
27 |
28 | /** Unit tests for {@link AbstractContrastMojo}. */
29 | final class AbstractContrastMojoTest {
30 |
31 | @Test
32 | void creates_user_agent_product_with_expected_values() {
33 | // GIVEN some AbstractContrastMojo with the mavenVersion property injected
34 | final AbstractContrastMojo mojo =
35 | new AbstractContrastMojo() {
36 | @Override
37 | public void execute() {}
38 | };
39 | mojo.setMavenVersion("3.8.1");
40 |
41 | // WHEN build User-Agent product
42 | final UserAgentProduct ua = mojo.getUserAgentProduct();
43 |
44 | // THEN has expected values
45 | assertThat(ua.name()).isEqualTo("contrast-maven-plugin");
46 | assertThat(ua.version()).matches("\\d+\\.\\d+(\\.\\d+)?(-SNAPSHOT)?");
47 | assertThat(ua.comment()).isEqualTo("Apache Maven 3.8.1");
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * JUnit extension for stubbing the Contrast API for integration testing. Before a test, the
3 | * extension starts a new web server that simulates the subset of the Contrast API that the plugin
4 | * needs. At the conclusion of the test, the extension terminates the web server.
5 | *
6 | *
Some tests may be compatible with an external Contrast API system (that has already been
7 | * configured to be in the right state) instead of a stub. In this case, test authors can configure
8 | * this extension (using standard JUnit configuration) to provide connection parameters to the
9 | * external system instead of starting a stub system.
10 | *
11 | *
Set the following configuration parameters to configure the extension to use an external
12 | * Contrast API system instead of starting a stub:
13 | *
14 | *
15 | *
{@code contrast.api.url}
16 | *
{@code contrast.api.user_name}
17 | *
{@code contrast.api.api_key}
18 | *
{@code contrast.api.service_key}
19 | *
{@code contrast.api.organization}
20 | *
21 | */
22 | package com.contrastsecurity.maven.plugin.it.stub;
23 |
24 | /*-
25 | * #%L
26 | * Contrast Maven Plugin
27 | * %%
28 | * Copyright (C) 2021 Contrast Security, Inc.
29 | * %%
30 | * Licensed under the Apache License, Version 2.0 (the "License");
31 | * you may not use this file except in compliance with the License.
32 | * You may obtain a copy of the License at
33 | *
34 | * http://www.apache.org/licenses/LICENSE-2.0
35 | *
36 | * Unless required by applicable law or agreed to in writing, software
37 | * distributed under the License is distributed on an "AS IS" BASIS,
38 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
39 | * See the License for the specific language governing permissions and
40 | * limitations under the License.
41 | * #L%
42 | */
43 |
--------------------------------------------------------------------------------
/src/site/site.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
25 |
26 | org.apache.maven.skins
27 | maven-fluido-skin
28 | 1.9
29 |
30 |
31 |
32 |
33 | Contrast-Security-OSS/contrast-maven-plugin
34 | right
35 | green
36 |
37 |
38 |
39 |
40 | images/contrast-logo.png
41 | /
42 |
43 |
44 |
49 |
52 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | publish:
8 | permissions:
9 | contents: write
10 | environment: Maven Central
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Set up JDK 11
16 | uses: actions/setup-java@v2
17 | with:
18 | java-version: 11
19 | distribution: temurin
20 | server-id: ossrh
21 | server-username: OSSRH_USERNAME
22 | server-password: OSSRH_PASSWORD
23 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
24 | gpg-passphrase: GPG_PASSPHRASE
25 |
26 | - name: Cache Maven Wrapper
27 | uses: actions/cache@v2
28 | with:
29 | path: ./.mvn/wrapper/maven-wrapper.jar
30 | key: ${{ runner.os }}-maven-wrapper-${{ hashFiles('./.mvn/wrapper/maven-wrapper.properties') }}
31 | restore-keys: ${{ runner.os }}-maven-wrapper
32 |
33 | - name: Cache Maven Repository
34 | uses: actions/cache@v2
35 | with:
36 | path: ~/.m2/repository
37 | key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }}
38 | restore-keys: ${{ runner.os }}-m2-repository
39 |
40 | # See https://github.com/actions/checkout/issues/13
41 | - name: Configure Git User
42 | run: |
43 | git config --global user.name 'github-actions[bot]'
44 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
45 |
46 | - name: Maven Release (dry-run)
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
50 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
51 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
52 | run: |
53 | ./mvnw -DdryRun=true --batch-mode release:prepare release:perform -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN
54 |
55 | - name: Maven Release
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
59 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
60 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
61 | run: |
62 | ./mvnw --batch-mode release:prepare release:perform -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN
63 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/Resources.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import java.io.File;
24 | import java.io.InputStream;
25 | import java.net.URISyntaxException;
26 | import java.net.URL;
27 | import java.nio.file.Path;
28 | import java.nio.file.Paths;
29 |
30 | /** static utilities for retrieving test resources */
31 | public final class Resources {
32 |
33 | /**
34 | * Retrieves the given test resources as a {@link File}. Fails if the resource does not exist
35 | *
36 | * @param name resource name
37 | * @return {@link File} which refers to the resource
38 | * @throws NullPointerException when resource does not exist
39 | */
40 | public static Path file(final String name) {
41 | final URL resource = Resources.class.getResource(name);
42 | if (resource == null) {
43 | throw new NullPointerException(name + " resource not found");
44 | }
45 | try {
46 | return Paths.get(resource.toURI());
47 | } catch (final URISyntaxException e) {
48 | throw new AssertionError("This should never happen", e);
49 | }
50 | }
51 |
52 | /**
53 | * Retrieves the given test resource as an {@link InputStream}. Fails if the resource does not
54 | * exist
55 | *
56 | * @param name resource name
57 | * @return {@link InputStream} for reading the resource
58 | * @throws NullPointerException when resource does not exist
59 | */
60 | public static InputStream stream(final String name) {
61 | final InputStream stream = Resources.class.getResourceAsStream(name);
62 | if (stream == null) {
63 | throw new NullPointerException(name + " resource not found");
64 | }
65 | return stream;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/test/resources/it/parent-pom/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
25 | 4.0.0
26 |
27 | com.contrastsecurity.test
28 | test-parent-pom
29 | 0.0.1-SNAPSHOT
30 | pom
31 |
32 | Parent POM module that mistakenly includes the scan goal in its build
33 |
34 |
35 | UTF-8
36 | UTF-8
37 |
38 |
39 |
40 |
41 |
42 | @project.groupId@
43 | @project.artifactId@
44 | @project.version@
45 |
46 |
47 |
48 | scan
49 |
50 |
51 | 10000
52 |
53 |
54 |
55 |
56 | ${contrast.api.user_name}
57 | ${contrast.api.organization_id}
58 | ${contrast.api.api_key}
59 | ${contrast.api.service_key}
60 | ${contrast.api.url}
61 | spring-test-application
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/FakeScanSummary.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import com.contrastsecurity.sdk.scan.ScanSummary;
24 | import com.google.auto.value.AutoValue;
25 | import java.time.Duration;
26 | import java.time.Instant;
27 |
28 | /** Fake implementation of {@link ScanSummary} for testing. */
29 | @AutoValue
30 | abstract class FakeScanSummary implements ScanSummary {
31 |
32 | /** new {@link Builder} */
33 | static Builder builder() {
34 | return new AutoValue_FakeScanSummary.Builder();
35 | }
36 |
37 | /** Builder for {@link ScanSummary}. */
38 | @AutoValue.Builder
39 | abstract static class Builder {
40 |
41 | /**
42 | * @see ScanSummary#id()
43 | */
44 | abstract Builder id(String value);
45 |
46 | /**
47 | * @see ScanSummary#scanId()
48 | */
49 | abstract Builder scanId(String value);
50 |
51 | /**
52 | * @see ScanSummary#projectId()
53 | */
54 | abstract Builder projectId(String value);
55 |
56 | /**
57 | * @see ScanSummary#organizationId()
58 | */
59 | abstract Builder organizationId(String value);
60 |
61 | /**
62 | * @see ScanSummary#duration()
63 | */
64 | abstract Builder duration(Duration value);
65 |
66 | /**
67 | * @see ScanSummary#totalResults()
68 | */
69 | abstract Builder totalResults(int value);
70 |
71 | /**
72 | * @see ScanSummary#totalNewResults() ()
73 | */
74 | abstract Builder totalNewResults(int value);
75 |
76 | /**
77 | * @see ScanSummary#totalFixedResults() ()
78 | */
79 | abstract Builder totalFixedResults(int value);
80 |
81 | /**
82 | * @see ScanSummary#createdDate()
83 | */
84 | abstract Builder createdDate(Instant value);
85 |
86 | /**
87 | * @return new {@link ScanSummary}
88 | */
89 | abstract FakeScanSummary build();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/site/markdown/examples/multi-module-projects.md:
--------------------------------------------------------------------------------
1 | ## Multi-Module Projects
2 |
3 | Best practices for a multi-module project would have users configure the contrast-maven-plugin with
4 | Contrast connection configuration in the parent POM's `` section. Doing so allows
5 | this configuration to be reused in each child module that uses the contrast-maven-plugin.
6 |
7 | ```xml
8 |
9 |
12 | 4.0.0
13 | com.contrastsecurity
14 | multi-module-example-parent
15 | 0.0.1
16 | pom
17 |
18 |
19 |
20 |
21 |
22 | com.contrastsecurity
23 | contrast-maven-plugin
24 |
25 |
26 | ${env.CONTRAST__API__URL}
27 | ${env.CONTRAST__API__USER_NAME}
28 | ${env.CONTRAST__API__API_KEY}
29 | ${env.CONTRAST__API__SERVICE_KEY}
30 | ${env.CONTRAST__API__ORGANIZATION_ID}
31 |
32 |
33 |
34 |
35 |
36 |
37 | ```
38 |
39 | In child modules that use contrast-maven-plugin's goals, users may simply include the goal in
40 | their `` section with any module specific configuration:
41 |
42 | ```xml
43 |
44 |
47 | 4.0.0
48 | com.contrastsecurity
49 | multi-module-example-child
50 | 0.0.1
51 | war
52 |
53 |
54 |
55 |
56 | com.contrastsecurity
57 | contrast-maven-plugin
58 |
59 |
60 |
61 | scan
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ```
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastInstallAgentMojoIT.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it;
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 | import com.contrastsecurity.maven.plugin.it.stub.ContrastAPI;
24 | import com.contrastsecurity.maven.plugin.it.stub.ContrastAPIStub;
25 | import java.io.IOException;
26 | import org.apache.maven.it.VerificationException;
27 | import org.apache.maven.it.Verifier;
28 | import org.junit.jupiter.api.Test;
29 |
30 | /**
31 | * Functional test for the "install" goal.
32 | *
33 | *
Verifies that the agent downloads a Contrast Java agent from the Contrast API. This test lacks
34 | * the following verifications for this goal:
35 | *
36 | *
37 | *
Does not verify that the plugin configures the maven-surefire-plugin's argLine property to
38 | * include the agent
39 | *
Does not verify that the plugin configures the spring-boot-maven-plugin's
40 | * "run.jvmArguments" property to include the agent
41 | *
Does not verify that the agent returned by the Contrast API is preconfigured to report to
42 | * the user's Contrast organization
43 | *
44 | *
45 | * We accept the risk that the aforementioned features are not verified in this test, because we
46 | * plan to make breaking changes to this goal imminently. This functional test serves mainly as a
47 | * guiding example for future functional tests.
48 | */
49 | @ContrastAPIStub
50 | final class ContrastInstallAgentMojoIT {
51 |
52 | @Test
53 | public void test(final ContrastAPI contrast) throws IOException, VerificationException {
54 | // GIVEN a spring-boot project that uses the plugin
55 | final Verifier verifier = Verifiers.springBoot(contrast.connection());
56 |
57 | // WHEN execute the "verify" goal
58 | verifier.executeGoal("verify");
59 |
60 | // THEN the plugin retrieves a Contrast Java agent from the Contrast API without errors
61 | verifier.verifyErrorFreeLog();
62 | verifier.assertFilePresent("target/contrast.jar");
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Contrast Maven Plugin
2 |
3 | ### This repository has been archived
4 |
5 | | The Contrast Maven Plugin source code has been moved to the [Contrast-SDK-Java repository](https://github.com/Contrast-Security-OSS/contrast-sdk-java) |
6 | |----------------------------------------------------------------------------------------------------------------------------------------------------------|
7 | #
8 |
9 |
10 | Maven plugin for including Contrast security analysis in Java web applications
11 |
12 | See [usage](https://contrastsecurity.dev/contrast-maven-plugin/usage.html) to get started
13 |
14 | Available on [Maven Central](https://search.maven.org/search?q=a:contrast-maven-plugin)
15 |
16 |
17 | ## Building
18 |
19 | Requires JDK 21 to build.
20 |
21 | Tests require JDK 11 and 17 to be set-up in [Maven
22 | Toolchains](https://maven.apache.org/guides/mini/guide-using-toolchains.html)
23 | and requires that Maven be on the `PATH`.
24 |
25 | Use `./mvnw verify` to build and test changes to the project
26 |
27 |
28 | ### Formatting
29 |
30 | To avoid distracting white space changes in pull requests and wasteful bickering
31 | about format preferences, Contrast uses the google-java-format opinionated Java
32 | code formatter to automatically format all code to a common specification.
33 |
34 | Developers are expected to configure their editors to automatically apply this
35 | format (plugins exist for both IDEA and Eclipse). Alternatively, developers can
36 | apply the formatting before committing changes using the Maven plugin:
37 |
38 | ```shell
39 | ./mvnw spotless:apply
40 | ```
41 |
42 |
43 | ### End-to-End Testing
44 |
45 | By default, the integration tests simulate the Contrast API using a stub web server. Developers can
46 | change this behavior to test with an actual Contrast instance instead of the stub.
47 |
48 | First, configure your environment with standard Contrast connection environment variables.
49 |
50 | ```shell
51 | export CONTRAST__API__URL=https://app.contrastsecurity.com/Contrast/api
52 | export CONTRAST__API__USER_NAME=
53 | export CONTRAST__API__API_KEY=
54 | export CONTRAST__API__SERVICE_KEY=
55 | export CONTRAST__API__ORGANIZATION_ID=
56 | ```
57 |
58 | You may find it useful to store the environment variable configuration in a file so that you can
59 | easily include it in your environment
60 |
61 | ```shell
62 | source ~/contrast.env
63 | ```
64 |
65 | Having configured the environment, you can run the integration tests with the `end-to-end-test`
66 | profile active:
67 |
68 | ```shell
69 | ./mvnw -Pend-to-end-test verify
70 | ```
71 |
72 | When you are finished testing, you may want to remove the variables from the
73 | environment. In a POSIX shell, the script `unset-contrast.env` can take care of
74 | this:
75 |
76 | ```shell
77 | source unset-contrast.env
78 | ```
79 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/Verifiers.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it;
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 | import com.contrastsecurity.maven.plugin.it.stub.ConnectionParameters;
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.util.Objects;
27 | import java.util.Properties;
28 | import org.apache.maven.it.VerificationException;
29 | import org.apache.maven.it.Verifier;
30 | import org.apache.maven.it.util.ResourceExtractor;
31 |
32 | /**
33 | * Reusable static factory methods for creating new Maven Verifier instances from well-known sample
34 | * projects
35 | */
36 | final class Verifiers {
37 |
38 | /**
39 | * @return new {@link Verifier} for the /it/spring-boot sample Maven project
40 | */
41 | static Verifier springBoot(final ConnectionParameters connection)
42 | throws IOException, VerificationException {
43 | final String path = "/it/spring-boot";
44 | return verifier(connection, path);
45 | }
46 |
47 | /**
48 | * @return new {@link Verifier} for the /it/parent-pom sample Maven project
49 | */
50 | static Verifier parentPOM(final ConnectionParameters connection)
51 | throws IOException, VerificationException {
52 | final String path = "/it/parent-pom";
53 | return verifier(connection, path);
54 | }
55 |
56 | private static Verifier verifier(final ConnectionParameters connection, final String path)
57 | throws IOException, VerificationException {
58 | final File projectDir = ResourceExtractor.simpleExtractResources(Verifiers.class, path);
59 | final Verifier verifier = new Verifier(projectDir.getAbsolutePath());
60 | final String testRepository =
61 | Objects.requireNonNull(
62 | System.getProperty("contrast.test-repository"),
63 | "required system property contrast.test-repository must be set to a directory containing a maven repository with the contrast-maven-plugin installed");
64 | verifier.setLocalRepo(testRepository);
65 | // AND the user provides common agent configuration connection parameters for connecting to
66 | // Contrast
67 | final Properties connectionProperties = connection.toProperties();
68 | verifier.setSystemProperties(connectionProperties);
69 | return verifier;
70 | }
71 |
72 | /** static members only */
73 | private Verifiers() {}
74 | }
75 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastScanMojoIT.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it;
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 | import com.contrastsecurity.maven.plugin.it.stub.ContrastAPI;
24 | import com.contrastsecurity.maven.plugin.it.stub.ContrastAPIStub;
25 | import java.io.IOException;
26 | import java.util.Arrays;
27 | import org.apache.maven.it.VerificationException;
28 | import org.apache.maven.it.Verifier;
29 | import org.junit.jupiter.api.Test;
30 |
31 | /** Functional test for the "scan" goal */
32 | @ContrastAPIStub
33 | final class ContrastScanMojoIT {
34 |
35 | @Test
36 | void scan_submits_artifact_for_scanning(final ContrastAPI contrast)
37 | throws VerificationException, IOException {
38 | // GIVEN a spring-boot project that uses the plugin
39 | final Verifier verifier = Verifiers.springBoot(contrast.connection());
40 |
41 | // WHEN execute the "verify" goal
42 | verifier.setCliOptions(Arrays.asList("--activate-profiles", "scan"));
43 | verifier.executeGoal("verify");
44 |
45 | // THEN plugin submits the spring-boot application artifact for scanning
46 | verifier.verifyErrorFreeLog();
47 | verifier.verifyTextInLog(
48 | "Uploading spring-test-application-0.0.1-SNAPSHOT.jar to Contrast Scan");
49 | verifier.verifyTextInLog("Starting scan with label 0.0.1-SNAPSHOT");
50 | verifier.verifyTextInLog("Scan results will be available at http");
51 | verifier.verifyTextInLog("Waiting for scan results");
52 | verifier.verifyTextInLog("Scan completed");
53 | verifier.assertFilePresent("./target/contrast-scan-reports/contrast-scan-results.sarif.json");
54 | }
55 |
56 | @Test
57 | void fails_when_no_artifact_detected(final ContrastAPI contrast)
58 | throws VerificationException, IOException {
59 | // GIVEN a POM project that uses the plugin
60 | final Verifier verifier = Verifiers.parentPOM(contrast.connection());
61 |
62 | // WHEN execute the "verify" goal
63 | try {
64 | verifier.executeGoal("verify");
65 | } catch (VerificationException ignored) {
66 | }
67 |
68 | // THEN plugin fails because there is no artifact to scan
69 | verifier.verifyTextInLog(
70 | "Project's artifact file has not been set - see https://contrastsecurity.dev/contrast-maven-plugin/troubleshooting/artifact-not-set.html");
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ConnectionParameters.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it.stub;
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 | import com.google.auto.value.AutoValue;
24 | import java.util.Properties;
25 |
26 | /**
27 | * Value type that holds all of the connection configuration that a consumer needs to access
28 | * authenticated Contrast API endpoints.
29 | */
30 | @AutoValue
31 | public abstract class ConnectionParameters {
32 |
33 | /**
34 | * @return new {@link Builder}
35 | */
36 | public static Builder builder() {
37 | return new AutoValue_ConnectionParameters.Builder();
38 | }
39 |
40 | /**
41 | * @return Contrast API URL e.g. https://app.contrastsecurity.com/Contrast/api
42 | */
43 | public abstract String url();
44 |
45 | /**
46 | * @return Contrast API username
47 | */
48 | public abstract String username();
49 |
50 | /**
51 | * @return Contrast API Key
52 | */
53 | public abstract String apiKey();
54 |
55 | /**
56 | * @return Contrast API service key
57 | */
58 | public abstract String serviceKey();
59 |
60 | /**
61 | * @return Contrast organization ID
62 | */
63 | public abstract String organizationID();
64 |
65 | /**
66 | * @return new {@link Properties} object populated with the connection configuration with standard
68 | * Contrast Java system property names
69 | */
70 | public final Properties toProperties() {
71 | final Properties properties = new Properties();
72 | properties.setProperty("contrast.api.url", url());
73 | properties.setProperty("contrast.api.user_name", username());
74 | properties.setProperty("contrast.api.api_key", apiKey());
75 | properties.setProperty("contrast.api.service_key", serviceKey());
76 | properties.setProperty("contrast.api.organization_id", organizationID());
77 | return properties;
78 | }
79 |
80 | @AutoValue.Builder
81 | public abstract static class Builder {
82 |
83 | public abstract Builder url(String value);
84 |
85 | public abstract Builder username(String value);
86 |
87 | public abstract Builder apiKey(String value);
88 |
89 | public abstract Builder serviceKey(String value);
90 |
91 | public abstract Builder organizationID(String value);
92 |
93 | public abstract ConnectionParameters build();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/com/contrastsecurity/maven/plugin/AbstractAssessMojo.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import org.apache.maven.plugin.MojoFailureException;
24 | import org.apache.maven.plugins.annotations.Parameter;
25 |
26 | /** Abstract mojo for mojos that report Assess data to a Contrast instrumented application */
27 | abstract class AbstractAssessMojo extends AbstractContrastMojo {
28 |
29 | /**
30 | * As near as I can tell, there doesn't appear to be any way to share data between Mojo phases.
31 | * However, we need to compute the appVersion in the install phase and then use the
32 | * computedAppVersion in the verify phase. Setting the field to static is the only way I found for
33 | * it to work.
34 | *
35 | * @deprecated [JG] the Maven solution for the aforementioned issue is to save the information to
36 | * the file system between goals, so we will deprecate this static field in favor of that
37 | * approach
38 | */
39 | @Deprecated static String computedAppVersion;
40 |
41 | /**
42 | * Override the reported application name.
43 | *
44 | *
On Java systems where multiple, distinct applications may be served by a single process,
45 | * this configuration causes the agent to report all discovered applications as one application
46 | * with the given name.
47 | */
48 | @Parameter(property = "appName")
49 | private String appName;
50 |
51 | /**
52 | * ID of the application as seen in Contrast. Either the {@code appId} or {@code appName} is
53 | * required. If both are specified, Contrast uses the {@code appId} and ignores the {@code
54 | * appName}.
55 | *
56 | * @since 2.5
57 | */
58 | @Parameter(property = "appId")
59 | private String appId;
60 |
61 | /** Overrides the reported server name */
62 | @Parameter(property = "serverName")
63 | private String serverName;
64 |
65 | void verifyAppIdOrNameNotNull() throws MojoFailureException {
66 | if (appId == null && appName == null) {
67 | throw new MojoFailureException(
68 | "Please specify appId or appName in the plugin configuration.");
69 | }
70 | }
71 |
72 | String getAppName() {
73 | return appName;
74 | }
75 |
76 | /** For testing. Maven will set the field directly */
77 | void setAppName(String appName) {
78 | this.appName = appName;
79 | }
80 |
81 | String getAppId() {
82 | return appId;
83 | }
84 |
85 | /** For testing. Maven will set the field directly */
86 | void setAppId(String appId) {
87 | this.appId = appId;
88 | }
89 |
90 | String getServerName() {
91 | return serverName;
92 | }
93 |
94 | /** For testing. Maven will set the field directly */
95 | void setServerName(String serverName) {
96 | this.serverName = serverName;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojoTest.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import static org.junit.Assert.assertEquals;
24 | import static org.junit.Assert.assertNotNull;
25 | import static org.junit.Assert.assertNull;
26 | import static org.junit.Assert.assertTrue;
27 |
28 | import com.contrastsecurity.http.RuleSeverity;
29 | import com.contrastsecurity.http.TraceFilterForm;
30 | import java.util.ArrayList;
31 | import java.util.List;
32 | import org.junit.Before;
33 | import org.junit.Test;
34 |
35 | public class ContrastVerifyMojoTest {
36 |
37 | private ContrastVerifyMojo verifyContrastMavenPluginMojo;
38 |
39 | @Before
40 | public void setUp() {
41 | verifyContrastMavenPluginMojo = new ContrastVerifyMojo();
42 | verifyContrastMavenPluginMojo.minSeverity = "Medium";
43 | }
44 |
45 | @Test
46 | public void testGetTraceFilterFormServerIdsNull() {
47 | TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null);
48 | assertNull(traceFilterForm.getServerIds());
49 | }
50 |
51 | @Test
52 | public void testGetTraceFilterForm() {
53 | List serverIds = new ArrayList<>();
54 | long server1 = 123L;
55 | long server2 = 456L;
56 | long server3 = 789L;
57 |
58 | serverIds.add(server1);
59 | serverIds.add(server2);
60 | serverIds.add(server3);
61 |
62 | TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(serverIds);
63 | assertNotNull(traceFilterForm.getServerIds());
64 | assertEquals(3, traceFilterForm.getServerIds().size());
65 | assertEquals((Long) server1, traceFilterForm.getServerIds().get(0));
66 | assertEquals((Long) server2, traceFilterForm.getServerIds().get(1));
67 | assertEquals((Long) server3, traceFilterForm.getServerIds().get(2));
68 | }
69 |
70 | @Test
71 | public void testGetTraceFilterFormSeverities() {
72 | verifyContrastMavenPluginMojo.minSeverity = "Note";
73 | TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null);
74 |
75 | assertEquals(5, traceFilterForm.getSeverities().size());
76 | assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.NOTE));
77 | assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.LOW));
78 | assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.MEDIUM));
79 | assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.HIGH));
80 | assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.CRITICAL));
81 | }
82 |
83 | @Test
84 | public void testGetTraceFilterFormAppVersionTags() {
85 | String appVersion = "WebGoat-1";
86 |
87 | AbstractAssessMojo.computedAppVersion = appVersion;
88 | TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null);
89 |
90 | assertEquals(1, traceFilterForm.getAppVersionTags().size());
91 | assertEquals(appVersion, traceFilterForm.getAppVersionTags().get(0));
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/test/resources/it/spring-boot/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
25 | 4.0.0
26 |
27 | com.contrastsecurity.test
28 | spring-test-application
29 | 0.0.1-SNAPSHOT
30 | jar
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-parent
35 | 2.5.1
36 |
37 |
38 |
39 |
40 | UTF-8
41 | UTF-8
42 |
43 |
44 |
45 |
46 | org.springframework.boot
47 | spring-boot-starter-web
48 |
49 |
50 |
51 |
52 |
53 |
54 | org.springframework.boot
55 | spring-boot-maven-plugin
56 |
57 |
58 | @project.groupId@
59 | @project.artifactId@
60 | @project.version@
61 |
62 |
63 | install-contrast
64 |
65 | install
66 |
67 |
68 |
69 |
70 | ${contrast.api.user_name}
71 | ${contrast.api.organization_id}
72 | ${contrast.api.api_key}
73 | ${contrast.api.service_key}
74 | ${contrast.api.url}
75 | spring-test-application
76 |
77 |
78 |
79 |
80 |
81 |
82 | scan
83 |
84 |
85 |
86 | @project.groupId@
87 | @project.artifactId@
88 | @project.version@
89 |
90 |
91 |
92 | scan
93 |
94 |
95 | 10000
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/scan.http:
--------------------------------------------------------------------------------
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": "",
9 | # "authorization": "",
10 | # "organizationId": ""
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 }}
--------------------------------------------------------------------------------
/src/site/markdown/usage.md:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | The Contrast Maven Plugin helps users include one or more Contrast Security analysis features in
4 | their Maven projects. All such analysis must connect to Contrast, and therefore each plugin goal
5 | requires Contrast connection parameters. These connection parameters may be externalized to
6 | environment variables:
7 |
8 | ```xml
9 |
10 |
11 |
12 | com.contrastsecurity
13 | contrast-maven-plugin
14 |
15 | ${env.CONTRAST__API__USER_NAME}
16 | ${env.CONTRAST__API__API_KEY}
17 | ${env.CONTRAST__API__SERVICE_KEY}
18 | ${env.CONTRAST__API__ORGANIZATION_ID}
19 |
20 |
21 |
22 |
23 | ```
24 |
25 | ### Contrast Assess
26 |
27 | [Contrast Assess](https://docs.contrastsecurity.com/en/assess.html) is an application security
28 | testing tool that combines Static (SAST), Dynamic (DAST), and Interactive Application Security
29 | Testing (IAST) approaches, to provide highly accurate and continuous information on security
30 | vulnerabilities in your applications.
31 |
32 | To add Contrast Assess analysis in your Java web application testing, you must attach Contrast Java
33 | agent to your application. The `install` goal retrieves the Contrast Java agent and stores it
34 | in `${project.build.directory}/contrast.jar`.
35 |
36 | The `install` goal automatically detects and configures
37 | the [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html)
38 | , [Maven Failsafe Plugin](https://maven.apache.org/surefire/maven-failsafe-plugin/integration-test-mojo.html)
39 | ,
40 | and [Spring Boot Maven Plugin](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/)
41 | to include the Contrast Java agent in their `test`, `integration-test`, and `run` goals
42 | respectively.
43 |
44 | Users may include the `verify` goal to fail their Maven build if Contrast Assess detects
45 | vulnerabilities during the project's integration testing phase that violate the project's security
46 | policies.
47 |
48 | The following example includes the Contrast Java agent in the project's integration testing and
49 | fails the build if any "Medium" vulnerabilities are detected during testing
50 |
51 | ```xml
52 |
53 | assess
54 |
55 |
56 |
57 | com.contrastsecurity
58 | contrast-maven-plugin
59 |
60 |
61 |
62 | install
63 |
64 |
65 |
66 |
67 | verify
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | ```
76 |
77 | To run the integration tests with security analysis and verification, execute the `verify` lifecycle
78 | phase with this profile activated
79 |
80 | ```shell
81 | mvn verify -Passess
82 | ```
83 |
84 |
85 | ### Contrast Scan
86 |
87 | Contrast Scan is a static application security testing (SAST) tool that makes it easy for you to
88 | find and remediate vulnerabilities in your Java web applications.
89 |
90 | Use the `scan` goal to find vulnerabilities in your Java web application Maven project using
91 | Contrast Scan. The goal uploads your application package to Contrast Scan for analysis.
92 |
93 | The following example includes the `scan` goal in a `scan` profile
94 |
95 | ```xml
96 |
97 | scan
98 |
99 |
100 |
101 | com.contrastsecurity
102 | contrast-maven-plugin
103 |
104 |
105 |
106 | scan
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | ```
115 |
116 | To run the scan, execute Maven with the scan profile activated
117 |
118 | ```shell
119 | mvn verify -Pscan
120 | ```
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import java.net.*;
17 | import java.io.*;
18 | import java.nio.channels.*;
19 | import java.util.Properties;
20 |
21 | public class MavenWrapperDownloader {
22 |
23 | private static final String WRAPPER_VERSION = "0.5.6";
24 | /**
25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
26 | */
27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
29 |
30 | /**
31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
32 | * use instead of the default one.
33 | */
34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
35 | ".mvn/wrapper/maven-wrapper.properties";
36 |
37 | /**
38 | * Path where the maven-wrapper.jar will be saved to.
39 | */
40 | private static final String MAVEN_WRAPPER_JAR_PATH =
41 | ".mvn/wrapper/maven-wrapper.jar";
42 |
43 | /**
44 | * Name of the property which should be used to override the default download url for the wrapper.
45 | */
46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
47 |
48 | public static void main(String args[]) {
49 | System.out.println("- Downloader started");
50 | File baseDirectory = new File(args[0]);
51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
52 |
53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
54 | // wrapperUrl parameter.
55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
56 | String url = DEFAULT_DOWNLOAD_URL;
57 | if(mavenWrapperPropertyFile.exists()) {
58 | FileInputStream mavenWrapperPropertyFileInputStream = null;
59 | try {
60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
61 | Properties mavenWrapperProperties = new Properties();
62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
64 | } catch (IOException e) {
65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
66 | } finally {
67 | try {
68 | if(mavenWrapperPropertyFileInputStream != null) {
69 | mavenWrapperPropertyFileInputStream.close();
70 | }
71 | } catch (IOException e) {
72 | // Ignore ...
73 | }
74 | }
75 | }
76 | System.out.println("- Downloading from: " + url);
77 |
78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
79 | if(!outputFile.getParentFile().exists()) {
80 | if(!outputFile.getParentFile().mkdirs()) {
81 | System.out.println(
82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
83 | }
84 | }
85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
86 | try {
87 | downloadFileFromURL(url, outputFile);
88 | System.out.println("Done");
89 | System.exit(0);
90 | } catch (Throwable e) {
91 | System.out.println("- Error downloading");
92 | e.printStackTrace();
93 | System.exit(1);
94 | }
95 | }
96 |
97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
99 | String username = System.getenv("MVNW_USERNAME");
100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
101 | Authenticator.setDefault(new Authenticator() {
102 | @Override
103 | protected PasswordAuthentication getPasswordAuthentication() {
104 | return new PasswordAuthentication(username, password);
105 | }
106 | });
107 | }
108 | URL website = new URL(urlString);
109 | ReadableByteChannel rbc;
110 | rbc = Channels.newChannel(website.openStream());
111 | FileOutputStream fos = new FileOutputStream(destination);
112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
113 | fos.close();
114 | rbc.close();
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project
2 | *.private.env.json
3 | /.idea
4 | *.iml
5 |
6 | # Maven
7 |
8 | target/
9 | pom.xml.tag
10 | pom.xml.releaseBackup
11 | pom.xml.versionsBackup
12 | pom.xml.next
13 | release.properties
14 | dependency-reduced-pom.xml
15 | buildNumber.properties
16 | .mvn/timing.properties
17 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar
18 | .mvn/wrapper/maven-wrapper.jar
19 |
20 |
21 | # Java
22 |
23 | # Compiled class file
24 | *.class
25 |
26 | # Log file
27 | *.log
28 |
29 | # BlueJ files
30 | *.ctxt
31 |
32 | # Mobile Tools for Java (J2ME)
33 | .mtj.tmp/
34 |
35 | # Package Files #
36 | *.jar
37 | *.war
38 | *.nar
39 | *.ear
40 | *.zip
41 | *.tar.gz
42 | *.rar
43 |
44 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
45 | hs_err_pid*
46 |
47 |
48 | # JetBrains
49 |
50 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
51 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
52 |
53 | # User-specific stuff
54 | .idea/**/workspace.xml
55 | .idea/**/tasks.xml
56 | .idea/**/usage.statistics.xml
57 | .idea/**/dictionaries
58 | .idea/**/shelf
59 |
60 | # Generated files
61 | .idea/**/contentModel.xml
62 |
63 | # Sensitive or high-churn files
64 | .idea/**/dataSources/
65 | .idea/**/dataSources.ids
66 | .idea/**/dataSources.local.xml
67 | .idea/**/sqlDataSources.xml
68 | .idea/**/dynamic.xml
69 | .idea/**/uiDesigner.xml
70 | .idea/**/dbnavigator.xml
71 |
72 | # Gradle
73 | .idea/**/gradle.xml
74 | .idea/**/libraries
75 |
76 | # Gradle and Maven with auto-import
77 | # When using Gradle or Maven with auto-import, you should exclude module files,
78 | # since they will be recreated, and may cause churn. Uncomment if using
79 | # auto-import.
80 | # .idea/artifacts
81 | # .idea/compiler.xml
82 | # .idea/jarRepositories.xml
83 | # .idea/modules.xml
84 | # .idea/*.iml
85 | # .idea/modules
86 | # *.iml
87 | # *.ipr
88 |
89 | # CMake
90 | cmake-build-*/
91 |
92 | # Mongo Explorer plugin
93 | .idea/**/mongoSettings.xml
94 |
95 | # File-based project format
96 | *.iws
97 |
98 | # IntelliJ
99 | out/
100 |
101 | # mpeltonen/sbt-idea plugin
102 | .idea_modules/
103 |
104 | # JIRA plugin
105 | atlassian-ide-plugin.xml
106 |
107 | # Cursive Clojure plugin
108 | .idea/replstate.xml
109 |
110 | # Crashlytics plugin (for Android Studio and IntelliJ)
111 | com_crashlytics_export_strings.xml
112 | crashlytics.properties
113 | crashlytics-build.properties
114 | fabric.properties
115 |
116 | # Editor-based Rest Client
117 | .idea/httpRequests
118 |
119 | # Android studio 3.1+ serialized cache file
120 | .idea/caches/build_file_checksums.ser
121 |
122 |
123 | # macOS
124 |
125 | # General
126 | .DS_Store
127 | .AppleDouble
128 | .LSOverride
129 |
130 | # Icon must end with two \r
131 | Icon
132 |
133 | # Thumbnails
134 | ._*
135 |
136 | # Files that might appear in the root of a volume
137 | .DocumentRevisions-V100
138 | .fseventsd
139 | .Spotlight-V100
140 | .TemporaryItems
141 | .Trashes
142 | .VolumeIcon.icns
143 | .com.apple.timemachine.donotpresent
144 |
145 | # Directories potentially created on remote AFP share
146 | .AppleDB
147 | .AppleDesktop
148 | Network Trash Folder
149 | Temporary Items
150 | .apdisk
151 |
152 |
153 | # Windows
154 |
155 | # Windows thumbnail cache files
156 | Thumbs.db
157 | Thumbs.db:encryptable
158 | ehthumbs.db
159 | ehthumbs_vista.db
160 |
161 | # Dump file
162 | *.stackdump
163 |
164 | # Folder config file
165 | [Dd]esktop.ini
166 |
167 | # Recycle Bin used on file shares
168 | $RECYCLE.BIN/
169 |
170 | # Windows Installer files
171 | *.cab
172 | *.msi
173 | *.msix
174 | *.msm
175 | *.msp
176 |
177 | # Windows shortcuts
178 | *.lnk
179 |
180 |
181 | # Linux
182 |
183 | *~
184 |
185 | # temporary files which can be created if a process still has a handle open of a deleted file
186 | .fuse_hidden*
187 |
188 | # KDE directory preferences
189 | .directory
190 |
191 | # Linux trash folder which might appear on any partition or disk
192 | .Trash-*
193 |
194 | # .nfs files are created when an open file is removed but is still being accessed
195 | .nfs*
196 |
197 |
198 | # Eclipse
199 |
200 | .metadata
201 | bin/
202 | tmp/
203 | *.tmp
204 | *.bak
205 | *.swp
206 | *~.nib
207 | local.properties
208 | .settings/
209 | .loadpath
210 | .recommenders
211 |
212 | # External tool builders
213 | .externalToolBuilders/
214 |
215 | # Locally stored "Eclipse launch configurations"
216 | *.launch
217 |
218 | # PyDev specific (Python IDE for Eclipse)
219 | *.pydevproject
220 |
221 | # CDT-specific (C/C++ Development Tooling)
222 | .cproject
223 |
224 | # CDT- autotools
225 | .autotools
226 |
227 | # Java annotation processor (APT)
228 | .factorypath
229 |
230 | # PDT-specific (PHP Development Tools)
231 | .buildpath
232 |
233 | # sbteclipse plugin
234 | .target
235 |
236 | # Tern plugin
237 | .tern-project
238 |
239 | # TeXlipse plugin
240 | .texlipse
241 |
242 | # STS (Spring Tool Suite)
243 | .springBeans
244 |
245 | # Code Recommenders
246 | .recommenders/
247 |
248 | # Annotation Processing
249 | .apt_generated/
250 | .apt_generated_test/
251 |
252 | # Scala IDE specific (Scala & Java development for Eclipse)
253 | .cache-main
254 | .scala_dependencies
255 | .worksheet
256 |
257 | # Uncomment this line if you wish to ignore the project description file.
258 | # Typically, this file would be tracked if it contains build/dependency configurations:
259 | .project
260 |
261 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStubExtension.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin.it.stub;
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 | import java.util.Optional;
24 | import org.junit.jupiter.api.extension.AfterAllCallback;
25 | import org.junit.jupiter.api.extension.BeforeAllCallback;
26 | import org.junit.jupiter.api.extension.ExtensionContext;
27 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
28 | import org.junit.jupiter.api.extension.ParameterContext;
29 | import org.junit.jupiter.api.extension.ParameterResolutionException;
30 | import org.junit.jupiter.api.extension.ParameterResolver;
31 |
32 | /**
33 | * JUnit 5 test extension that provides test authors with a stubbed instance of the Contrast API.
34 | */
35 | public final class ContrastAPIStubExtension
36 | implements ParameterResolver, BeforeAllCallback, AfterAllCallback {
37 |
38 | /**
39 | * Starts the {@link ContrastAPI}
40 | *
41 | * @param context JUnit context
42 | */
43 | @Override
44 | public void beforeAll(final ExtensionContext context) {
45 | final ContrastAPI contrast = getContrastAPI(context);
46 | contrast.start();
47 | context.publishReportEntry("Contrast stub started " + contrast.connection().url());
48 | }
49 |
50 | /**
51 | * Stops the {@link ContrastAPI}
52 | *
53 | * @param context JUnit context
54 | */
55 | @Override
56 | public void afterAll(final ExtensionContext context) {
57 | final ContrastAPI contrast = getContrastAPI(context);
58 | contrast.stop();
59 | }
60 |
61 | /**
62 | * @return true if the parameter is of type {@link ContrastAPI}
63 | */
64 | @Override
65 | public boolean supportsParameter(
66 | final ParameterContext parameterContext, final ExtensionContext extensionContext)
67 | throws ParameterResolutionException {
68 | return parameterContext.getParameter().getType() == ContrastAPI.class;
69 | }
70 |
71 | /**
72 | * @return the {@link ContrastAPI} in the current test context
73 | */
74 | @Override
75 | public Object resolveParameter(
76 | final ParameterContext parameterContext, final ExtensionContext extensionContext)
77 | throws ParameterResolutionException {
78 | return getContrastAPI(extensionContext);
79 | }
80 |
81 | /**
82 | * @return new or existing {@link ContrastAPI} in the current test context
83 | */
84 | private static ContrastAPI getContrastAPI(final ExtensionContext context) {
85 | return context
86 | .getStore(NAMESPACE)
87 | .getOrComputeIfAbsent(
88 | "server", ignored -> createFromConfiguration(context), ContrastAPI.class);
89 | }
90 |
91 | /**
92 | * @param context the current JUnit {@link ExtensionContext}
93 | * @return {@link ExternalContrastAPI} if Contrast connection properties are provided, otherwise
94 | * fails. We leave open the possibility that we will once again provide a stubbed API e.g. one
95 | * that uses Pactflow hosted stubs.
96 | */
97 | private static ContrastAPI createFromConfiguration(final ExtensionContext context) {
98 | // gather configuration parameters from the current context
99 | final Optional url = context.getConfigurationParameter("contrast.api.url");
100 | final Optional username = context.getConfigurationParameter("contrast.api.user_name");
101 | final Optional apiKey = context.getConfigurationParameter("contrast.api.api_key");
102 | final Optional serviceKey =
103 | context.getConfigurationParameter("contrast.api.service_key");
104 | final Optional organization =
105 | context.getConfigurationParameter("contrast.api.organization_id");
106 |
107 | // if all connection parameters are present, then use end-to-end testing mode
108 | if (url.isPresent()
109 | && username.isPresent()
110 | && apiKey.isPresent()
111 | && serviceKey.isPresent()
112 | && organization.isPresent()) {
113 | context.publishReportEntry(
114 | "end-to-end testing enabled: using provided Contrast API connection instead of the stub");
115 | final ConnectionParameters connection =
116 | ConnectionParameters.builder()
117 | .url(url.get())
118 | .username(username.get())
119 | .apiKey(apiKey.get())
120 | .serviceKey(serviceKey.get())
121 | .organizationID(organization.get())
122 | .build();
123 | return new ExternalContrastAPI(connection);
124 | }
125 | throw new IllegalArgumentException(
126 | "Context lacks required configuration for connecting to an external Contrast instance");
127 | }
128 |
129 | private static final Namespace NAMESPACE = Namespace.create(ContrastAPIStubExtension.class);
130 | }
131 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/ContrastScanMojoTest.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import static org.assertj.core.api.Assertions.assertThat;
24 | import static org.mockito.Mockito.mock;
25 | import static org.mockito.Mockito.times;
26 | import static org.mockito.Mockito.verify;
27 |
28 | import com.contrastsecurity.sdk.scan.ScanSummary;
29 | import java.time.Duration;
30 | import java.time.Instant;
31 | import java.util.Arrays;
32 | import java.util.List;
33 | import java.util.function.Consumer;
34 | import org.apache.maven.plugin.MojoFailureException;
35 | import org.junit.jupiter.api.BeforeEach;
36 | import org.junit.jupiter.api.Test;
37 | import org.junit.jupiter.params.ParameterizedTest;
38 | import org.junit.jupiter.params.provider.ValueSource;
39 | import org.mockito.ArgumentCaptor;
40 |
41 | /** Unit tests for {@link ContrastScanMojo} */
42 | final class ContrastScanMojoTest {
43 |
44 | private ContrastScanMojo mojo;
45 |
46 | @BeforeEach
47 | void before() {
48 | mojo = new ContrastScanMojo();
49 | mojo.setOrganizationId("organization-id");
50 | mojo.setProjectName("project-id");
51 | }
52 |
53 | /**
54 | * Contrast MOJOs tolerate a variety of HTTP paths in the URL configuration. Regardless of the
55 | * path that the user has configured, the {@link ContrastScanMojo#createClickableScanURL} method
56 | * should generate the same URL
57 | */
58 | @ValueSource(
59 | strings = {
60 | "https://app.contrastsecurity.com/",
61 | "https://app.contrastsecurity.com/Contrast",
62 | "https://app.contrastsecurity.com/Contrast/api",
63 | "https://app.contrastsecurity.com/Contrast/api/"
64 | })
65 | @ParameterizedTest
66 | void it_generates_clickable_url(final String url) throws MojoFailureException {
67 | // GIVEN a scan mojo with known URL, organization ID, and project ID
68 | mojo.setURL(url);
69 | mojo.setOrganizationId("organization-id");
70 |
71 | // WHEN generate URL for the user to click-through to display the scan in their browser
72 | final String clickableScanURL =
73 | mojo.createClickableScanURL("project-id", "scan-id").toExternalForm();
74 |
75 | // THEN outputs expected URL
76 | assertThat(clickableScanURL)
77 | .isEqualTo(
78 | "https://app.contrastsecurity.com/Contrast/static/ng/index.html#/organization-id/scans/project-id/scans/scan-id");
79 | }
80 |
81 | @Test
82 | void it_prints_summary_to_console() {
83 | // GIVEN the plugin is configured to output scan results to the console
84 | mojo.setConsoleOutput(true);
85 |
86 | // WHEN print summary to console
87 | final int totalResults = 10;
88 | final int totalNewResults = 8;
89 | final int totalFixedResults = 1;
90 | final ScanSummary summary =
91 | FakeScanSummary.builder()
92 | .id("summary-id")
93 | .organizationId("organization-id")
94 | .projectId("project-id")
95 | .scanId("scan-id")
96 | .createdDate(Instant.now())
97 | .totalResults(totalResults)
98 | .totalNewResults(totalNewResults)
99 | .totalFixedResults(totalFixedResults)
100 | .duration(Duration.ofMillis(0))
101 | .build();
102 | @SuppressWarnings("unchecked")
103 | final Consumer console = mock(Consumer.class);
104 | mojo.writeSummaryToConsole(summary, console);
105 |
106 | // THEN prints expected lines
107 | final List expected =
108 | Arrays.asList(
109 | "Scan completed",
110 | "New Results\t" + totalNewResults,
111 | "Fixed Results\t" + totalFixedResults,
112 | "Total Results\t" + totalResults);
113 | final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class);
114 | verify(console, times(4)).accept(captor.capture());
115 | assertThat(captor.getAllValues()).hasSameElementsAs(expected);
116 | }
117 |
118 | @Test
119 | void it_may_be_configured_to_omit_summary_from_console() {
120 | // GIVEN the plugin is configured to omit scan results from the console
121 | mojo.setConsoleOutput(false);
122 |
123 | // WHEN print summary to console
124 | final ScanSummary summary =
125 | FakeScanSummary.builder()
126 | .id("summary-id")
127 | .organizationId("organization-id")
128 | .projectId("project-id")
129 | .scanId("scan-id")
130 | .createdDate(Instant.now())
131 | .totalResults(10)
132 | .totalNewResults(8)
133 | .totalFixedResults(1)
134 | .duration(Duration.ofMillis(0))
135 | .build();
136 | @SuppressWarnings("unchecked")
137 | final Consumer console = mock(Consumer.class);
138 | mojo.writeSummaryToConsole(summary, console);
139 |
140 | // THEN only prints "completed" line
141 | verify(console).accept("Scan completed");
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
124 |
125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% ^
162 | %JVM_CONFIG_MAVEN_PROPS% ^
163 | %MAVEN_OPTS% ^
164 | %MAVEN_DEBUG_OPTS% ^
165 | -classpath %WRAPPER_JAR% ^
166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
168 | if ERRORLEVEL 1 goto error
169 | goto end
170 |
171 | :error
172 | set ERROR_CODE=1
173 |
174 | :end
175 | @endlocal & set ERROR_CODE=%ERROR_CODE%
176 |
177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
181 | :skipRcPost
182 |
183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
185 |
186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
187 |
188 | cmd /C exit /B %ERROR_CODE%
189 |
--------------------------------------------------------------------------------
/src/main/java/com/contrastsecurity/maven/plugin/AbstractContrastMojo.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import com.contrastsecurity.sdk.ContrastSDK;
24 | import com.contrastsecurity.sdk.UserAgentProduct;
25 | import java.net.Authenticator;
26 | import java.net.InetSocketAddress;
27 | import java.net.PasswordAuthentication;
28 | import java.net.Proxy;
29 | import org.apache.maven.plugin.AbstractMojo;
30 | import org.apache.maven.plugin.MojoFailureException;
31 | import org.apache.maven.plugins.annotations.Parameter;
32 | import org.apache.maven.settings.Settings;
33 |
34 | /**
35 | * Abstract mojo for mojos that need to connect to Contrast. Handles authentication, organization
36 | * selection (multi-tenancy), and proxy configuration.
37 | *
38 | *
Extensions of this class use the {@link #connectToContrast()} to obtain an instance of the
39 | * {@link ContrastSDK} with which they can make requests to Contrast.
40 | */
41 | abstract class AbstractContrastMojo extends AbstractMojo {
42 |
43 | @Parameter(defaultValue = "${settings}", readonly = true)
44 | private Settings settings;
45 |
46 | @Parameter(defaultValue = "${maven.version}", readonly = true)
47 | private String mavenVersion;
48 |
49 | /**
50 | * User name for communicating with Contrast. Agent users lack permissions required by this
51 | * plugin. Find your personal
52 | * keys
53 | */
54 | @Parameter(alias = "username", required = true)
55 | private String userName;
56 |
57 | /**
58 | * API Key for communicating with Contrast. Find your personal keys
60 | */
61 | @Parameter(property = "apiKey", required = true)
62 | private String apiKey;
63 |
64 | /**
65 | * Service Key for communicating with Contrast. Find your personal keys
67 | */
68 | @Parameter(property = "serviceKey", required = true)
69 | private String serviceKey;
70 |
71 | /** Contrast API URL */
72 | @Parameter(alias = "apiUrl", defaultValue = "https://app.contrastsecurity.com/Contrast/api")
73 | private String url;
74 |
75 | /**
76 | * Unique ID for the Contrast Organization to which the plugin reports results. Find your Organization ID
78 | */
79 | // TODO[JG] must this be required? If a user is only in one org, we can look it up using the
80 | // endpoint /ng/profile/organizations
81 | @Parameter(alias = "orgUuid", required = true)
82 | private String organizationId;
83 |
84 | /**
85 | * When true, will override Maven's proxy settings with Contrast Maven plugin specific proxy
86 | * configuration
87 | *
88 | * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of
89 | * standard Maven proxy configuration
90 | * @since 2.8
91 | */
92 | @Deprecated
93 | @Parameter(property = "useProxy", defaultValue = "false")
94 | private boolean useProxy;
95 |
96 | /**
97 | * Proxy host used to communicate to Contrast when {@code useProxy} is true
98 | *
99 | * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of
100 | * standard Maven proxy configuration
101 | * @since 2.8
102 | */
103 | @Deprecated
104 | @Parameter(property = "proxyHost")
105 | private String proxyHost;
106 |
107 | /**
108 | * Proxy port used to communicate to Contrast when {@code useProxy} is true
109 | *
110 | * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of
111 | * standard Maven proxy configuration
112 | * @since 2.8
113 | */
114 | @Deprecated
115 | @Parameter(property = "proxyPort")
116 | private int proxyPort;
117 |
118 | String getMavenVersion() {
119 | return mavenVersion;
120 | }
121 |
122 | void setMavenVersion(final String mavenVersion) {
123 | this.mavenVersion = mavenVersion;
124 | }
125 |
126 | String getUserName() {
127 | return userName;
128 | }
129 |
130 | void setUserName(final String userName) {
131 | this.userName = userName;
132 | }
133 |
134 | String getApiKey() {
135 | return apiKey;
136 | }
137 |
138 | void setApiKey(final String apiKey) {
139 | this.apiKey = apiKey;
140 | }
141 |
142 | String getServiceKey() {
143 | return serviceKey;
144 | }
145 |
146 | void setServiceKey(final String serviceKey) {
147 | this.serviceKey = serviceKey;
148 | }
149 |
150 | String getURL() {
151 | return url;
152 | }
153 |
154 | void setURL(final String url) {
155 | this.url = url;
156 | }
157 |
158 | String getOrganizationId() {
159 | return organizationId;
160 | }
161 |
162 | void setOrganizationId(final String organizationId) {
163 | this.organizationId = organizationId;
164 | }
165 |
166 | /**
167 | * @return new ContrastSDK configured to connect with the authentication and proxy parameters
168 | * defined by this abstract mojo
169 | * @throws MojoFailureException when fails to connect to Contrast
170 | */
171 | ContrastSDK connectToContrast() throws MojoFailureException {
172 | final Proxy proxy = getProxy();
173 | final UserAgentProduct maven = getUserAgentProduct();
174 | try {
175 | return new ContrastSDK.Builder(userName, serviceKey, apiKey)
176 | .withApiUrl(url)
177 | .withProxy(proxy)
178 | .withUserAgentProduct(maven)
179 | .build();
180 | } catch (final IllegalArgumentException e) {
181 | throw new MojoFailureException(
182 | "\n\nWe couldn't connect to Contrast at this address [" + url + "]. The error is: ", e);
183 | }
184 | }
185 |
186 | /**
187 | * visible for testing
188 | *
189 | * @return {@link UserAgentProduct} for the contrast-maven-plugin
190 | */
191 | final UserAgentProduct getUserAgentProduct() {
192 | final String comment = "Apache Maven " + mavenVersion;
193 | return UserAgentProduct.of("contrast-maven-plugin", Version.VERSION, comment);
194 | }
195 |
196 | private Proxy getProxy() throws MojoFailureException {
197 | Proxy proxy = Proxy.NO_PROXY;
198 | final org.apache.maven.settings.Proxy proxySettings = settings.getActiveProxy();
199 | if (useProxy) {
200 | getLog().info(String.format("Using a proxy %s:%s", proxyHost, proxyPort));
201 | if (proxyHost != null && proxyPort != 0) {
202 | proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
203 | } else {
204 | throw new MojoFailureException(
205 | "When useProxy is true, proxyHost and proxyPort is required.");
206 | }
207 | } else if (proxySettings != null) {
208 | getLog()
209 | .info(
210 | String.format(
211 | "Using a proxy %s:%s", proxySettings.getHost(), proxySettings.getPort()));
212 | proxy =
213 | new Proxy(
214 | Proxy.Type.HTTP,
215 | new InetSocketAddress(proxySettings.getHost(), proxySettings.getPort()));
216 |
217 | if (proxySettings.getUsername() != null || proxySettings.getPassword() != null) {
218 | Authenticator.setDefault(
219 | new Authenticator() {
220 | @Override
221 | protected PasswordAuthentication getPasswordAuthentication() {
222 | if (getRequestorType() == RequestorType.PROXY
223 | && getRequestingHost().equalsIgnoreCase(proxySettings.getHost())
224 | && proxySettings.getPort() == getRequestingPort()) {
225 | return new PasswordAuthentication(
226 | proxySettings.getUsername(),
227 | proxySettings.getPassword() == null
228 | ? null
229 | : proxySettings.getPassword().toCharArray());
230 | } else {
231 | return null;
232 | }
233 | }
234 | });
235 | }
236 | }
237 |
238 | return proxy;
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/test/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojoTest.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import static org.junit.Assert.assertEquals;
24 |
25 | import java.text.SimpleDateFormat;
26 | import java.util.Date;
27 | import org.junit.Before;
28 | import org.junit.Rule;
29 | import org.junit.Test;
30 | import org.junit.contrib.java.lang.system.EnvironmentVariables;
31 |
32 | public class ContrastInstallAgentMojoTest {
33 |
34 | private ContrastInstallAgentMojo installMojo;
35 | private Date now;
36 |
37 | @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
38 |
39 | @Before
40 | public void setUp() {
41 | installMojo = new ContrastInstallAgentMojo();
42 | installMojo.setAppName("caddyshack");
43 | installMojo.setServerName("Bushwood");
44 | installMojo.contrastAgentLocation = "/usr/local/bin/contrast.jar";
45 |
46 | now = new Date();
47 | environmentVariables.clear(
48 | "TRAVIS_BUILD_NUMBER",
49 | "CIRCLE_BUILD_NUM",
50 | "GIT_BRANCH",
51 | "GIT_COMMITTER_NAME",
52 | "GIT_COMMIT",
53 | "GIT_URL",
54 | "BUILD_NUMBER");
55 | }
56 |
57 | @Test
58 | public void testGenerateAppVersion() {
59 | installMojo.appVersion = "mycustomversion";
60 | AbstractAssessMojo.computedAppVersion = null;
61 | assertEquals("mycustomversion", installMojo.computeAppVersion(now));
62 | }
63 |
64 | @Test
65 | public void testGenerateAppVersionNoAppVersion() {
66 | installMojo.appVersion = null;
67 | AbstractAssessMojo.computedAppVersion = null;
68 | String expectedVersion = new SimpleDateFormat("yyyyMMddHHmmss").format(now);
69 | assertEquals("caddyshack-" + expectedVersion, installMojo.computeAppVersion(now));
70 | assertEquals("caddyshack-" + expectedVersion, installMojo.computeAppVersion(now));
71 | }
72 |
73 | @Test
74 | public void testGenerateAppVersionTravis() {
75 | installMojo.appVersion = null;
76 | AbstractAssessMojo.computedAppVersion = null;
77 | environmentVariables.set("TRAVIS_BUILD_NUMBER", "19");
78 | assertEquals("caddyshack-19", installMojo.computeAppVersion(now));
79 | assertEquals("caddyshack-19", installMojo.computeAppVersion(now));
80 | }
81 |
82 | @Test
83 | public void testGenerateAppVersionCircle() {
84 | installMojo.appVersion = null;
85 | AbstractAssessMojo.computedAppVersion = null;
86 | environmentVariables.set("TRAVIS_BUILD_NUMBER", "circle");
87 | assertEquals("caddyshack-circle", installMojo.computeAppVersion(now));
88 | assertEquals("caddyshack-circle", installMojo.computeAppVersion(now));
89 | }
90 |
91 | @Test
92 | public void testGenerateAppVersionAppId() {
93 | String appName = "WebGoat";
94 | String appId = "12345";
95 | String travisBuildNumber = "travis";
96 |
97 | installMojo.appVersion = null;
98 | AbstractAssessMojo.computedAppVersion = null;
99 | environmentVariables.set("TRAVIS_BUILD_NUMBER", travisBuildNumber);
100 | installMojo.setAppId(appId);
101 | installMojo.applicationName = appName;
102 |
103 | assertEquals(appName + "-" + travisBuildNumber, installMojo.computeAppVersion(now));
104 | }
105 |
106 | @Test
107 | public void testGenerateSessionMetadata() {
108 | environmentVariables.set("GIT_BRANCH", "develop");
109 | assertEquals("branchName=develop", installMojo.computeSessionMetadata());
110 |
111 | environmentVariables.set("GIT_COMMITTER_NAME", "boh");
112 | assertEquals("branchName=develop,committer=boh", installMojo.computeSessionMetadata());
113 |
114 | environmentVariables.set("GIT_COMMIT", "deadbeef");
115 | assertEquals(
116 | "branchName=develop,commitHash=deadbeef,committer=boh",
117 | installMojo.computeSessionMetadata());
118 |
119 | environmentVariables.set(
120 | "GIT_URL", "https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git");
121 | assertEquals(
122 | "branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git",
123 | installMojo.computeSessionMetadata());
124 |
125 | environmentVariables.set("BUILD_NUMBER", "123");
126 | assertEquals(
127 | "buildNumber=123,branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git",
128 | installMojo.computeSessionMetadata());
129 |
130 | environmentVariables.clear("BUILD_NUMBER");
131 | environmentVariables.set("CIRCLE_BUILD_NUM", "12345");
132 | assertEquals(
133 | "buildNumber=12345,branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git",
134 | installMojo.computeSessionMetadata());
135 |
136 | environmentVariables.clear("CIRCLE_BUILD_NUM");
137 | environmentVariables.set("TRAVIS_BUILD_NUMBER", "54321");
138 | assertEquals(
139 | "branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git,buildNumber=54321",
140 | installMojo.computeSessionMetadata());
141 | }
142 |
143 | @Test
144 | public void testBuildArgLine() {
145 | AbstractAssessMojo.computedAppVersion = "caddyshack-2";
146 | String currentArgLine = "";
147 | String expectedArgLine =
148 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack";
149 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
150 |
151 | installMojo.setServerName(null); // no server name
152 | currentArgLine = "";
153 | expectedArgLine =
154 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack";
155 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
156 |
157 | installMojo.setServerName("Bushwood");
158 | installMojo.serverPath = "/home/tomcat/app/";
159 | currentArgLine = "";
160 | expectedArgLine =
161 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack -Dcontrast.path=/home/tomcat/app/";
162 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
163 |
164 | installMojo.standalone = true;
165 | expectedArgLine =
166 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.standalone.appname=caddyshack -Dcontrast.path=/home/tomcat/app/";
167 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
168 |
169 | environmentVariables.set("BUILD_NUMBER", "123");
170 | environmentVariables.set("GIT_COMMITTER_NAME", "boh");
171 | expectedArgLine =
172 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.application.session_metadata='buildNumber=123,committer=boh' -Dcontrast.standalone.appname=caddyshack -Dcontrast.path=/home/tomcat/app/";
173 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
174 | }
175 |
176 | @Test
177 | public void testBuildArgNull() {
178 | AbstractAssessMojo.computedAppVersion = "caddyshack-2";
179 | String currentArgLine = null;
180 | String expectedArgLine =
181 | "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack";
182 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
183 | }
184 |
185 | @Test
186 | public void testBuildArgLineAppend() {
187 | AbstractAssessMojo.computedAppVersion = "caddyshack-2";
188 | String currentArgLine = "-Xmx1024m";
189 | String expectedArgLine =
190 | "-Xmx1024m -javaagent:/usr/local/bin/contrast.jar -Dcontrast.server=Bushwood -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack";
191 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
192 | }
193 |
194 | @Test
195 | public void testBuildArgLineSkip() {
196 | installMojo.skipArgLine = true;
197 | String currentArgLine = "-Xmx1024m";
198 | String expectedArgLine = "-Xmx1024m";
199 | assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine));
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojo.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import com.contrastsecurity.exceptions.UnauthorizedException;
24 | import com.contrastsecurity.http.RuleSeverity;
25 | import com.contrastsecurity.http.ServerFilterForm;
26 | import com.contrastsecurity.http.TraceFilterForm;
27 | import com.contrastsecurity.models.Application;
28 | import com.contrastsecurity.models.Applications;
29 | import com.contrastsecurity.models.Server;
30 | import com.contrastsecurity.models.Servers;
31 | import com.contrastsecurity.models.Trace;
32 | import com.contrastsecurity.models.Traces;
33 | import com.contrastsecurity.sdk.ContrastSDK;
34 | import java.io.IOException;
35 | import java.net.URLEncoder;
36 | import java.util.ArrayList;
37 | import java.util.Arrays;
38 | import java.util.Collections;
39 | import java.util.EnumSet;
40 | import java.util.List;
41 | import org.apache.maven.plugin.MojoFailureException;
42 | import org.apache.maven.plugins.annotations.LifecyclePhase;
43 | import org.apache.maven.plugins.annotations.Mojo;
44 | import org.apache.maven.plugins.annotations.Parameter;
45 |
46 | /**
47 | * Verifies that none of the vulnerabilities found by Contrast Assess during integration testing
48 | * violate the project's security policy (fails the build when violations are detected).
49 | */
50 | @Mojo(name = "verify", requiresOnline = true, defaultPhase = LifecyclePhase.VERIFY)
51 | public final class ContrastVerifyMojo extends AbstractAssessMojo {
52 |
53 | /**
54 | * Verifies that no vulnerabilities were found at this or a higher severity level. Severity levels
55 | * include Note, Low, Medium, High, and Critical.
56 | */
57 | @Parameter(property = "minSeverity", defaultValue = "Medium")
58 | String minSeverity;
59 |
60 | public void execute() throws MojoFailureException {
61 | verifyAppIdOrNameNotNull();
62 | ContrastSDK contrast = connectToContrast();
63 |
64 | getLog().info("Successfully authenticated to Contrast.");
65 |
66 | getLog().info("Checking for new vulnerabilities for appVersion [" + computedAppVersion + "]");
67 |
68 | final String applicationId;
69 | if (getAppId() != null) {
70 | applicationId = getAppId();
71 | if (getAppName() != null) {
72 | getLog().info("Using 'appId' property; 'appName' property is ignored.");
73 | }
74 |
75 | } else {
76 | applicationId = getApplicationId(contrast, getAppName());
77 | }
78 |
79 | List serverIds = null;
80 |
81 | if (getServerName() != null) {
82 | serverIds = getServerId(contrast, applicationId);
83 | }
84 |
85 | TraceFilterForm form = getTraceFilterForm(serverIds);
86 |
87 | getLog().info("Sending vulnerability request to Contrast.");
88 |
89 | Traces traces;
90 |
91 | try {
92 | Thread.sleep(10000);
93 | } catch (InterruptedException e) {
94 | e.printStackTrace();
95 | }
96 |
97 | try {
98 | final String organizationID = getOrganizationId();
99 | traces = contrast.getTraces(organizationID, applicationId, form);
100 | } catch (IOException e) {
101 | throw new MojoFailureException("Unable to retrieve the traces.", e);
102 | } catch (UnauthorizedException e) {
103 | throw new MojoFailureException("Unable to connect to Contrast.", e);
104 | }
105 |
106 | if (traces != null && traces.getCount() > 0) {
107 | getLog().info(traces.getCount() + " new vulnerability(s) were found.");
108 |
109 | for (Trace trace : traces.getTraces()) {
110 | getLog().info(generateTraceReport(trace));
111 | }
112 |
113 | throw new MojoFailureException(
114 | "Your application is vulnerable. Please see the above report for new vulnerabilities.");
115 | } else {
116 | getLog().info("No new vulnerabilities were found.");
117 | }
118 |
119 | getLog().info("Finished verifying your application.");
120 | }
121 |
122 | TraceFilterForm getTraceFilterForm(List serverIds) {
123 | TraceFilterForm form = new TraceFilterForm();
124 | form.setSeverities(getSeverityList(minSeverity));
125 | form.setAppVersionTags(Collections.singletonList(computedAppVersion));
126 | if (serverIds != null) {
127 | form.setServerIds(serverIds);
128 | }
129 | return form;
130 | }
131 |
132 | /**
133 | * Retrieves the server id by server name
134 | *
135 | * @param sdk Contrast SDK object
136 | * @param applicationId application id to filter on
137 | * @return List id of the servers
138 | * @throws MojoFailureException
139 | */
140 | private List getServerId(ContrastSDK sdk, String applicationId)
141 | throws MojoFailureException {
142 | ServerFilterForm serverFilterForm = new ServerFilterForm();
143 | serverFilterForm.setApplicationIds(Arrays.asList(applicationId));
144 |
145 | Servers servers;
146 | List serverIds;
147 |
148 | final String organizationID = getOrganizationId();
149 | try {
150 | serverFilterForm.setQ(URLEncoder.encode(getServerName(), "UTF-8"));
151 | servers = sdk.getServersWithFilter(organizationID, serverFilterForm);
152 | } catch (IOException e) {
153 | throw new MojoFailureException("Unable to retrieve the servers.", e);
154 | } catch (UnauthorizedException e) {
155 | throw new MojoFailureException("Unable to connect to Contrast.", e);
156 | }
157 |
158 | if (!servers.getServers().isEmpty()) {
159 | serverIds = new ArrayList();
160 | for (Server server : servers.getServers()) {
161 | serverIds.add(server.getServerId());
162 | }
163 | } else {
164 | throw new MojoFailureException(
165 | "\n\nServer with name '"
166 | + getServerName()
167 | + "' not found. Make sure this server name appears in Contrast under the 'Servers' tab.\n");
168 | }
169 |
170 | return serverIds;
171 | }
172 |
173 | /**
174 | * Retrieves the application id by application name; else null
175 | *
176 | * @param sdk Contrast SDK object
177 | * @param applicationName application name to filter on
178 | * @return String of the application
179 | * @throws MojoFailureException
180 | */
181 | private String getApplicationId(ContrastSDK sdk, String applicationName)
182 | throws MojoFailureException {
183 |
184 | Applications applications;
185 |
186 | final String organizationID = getOrganizationId();
187 | try {
188 | applications = sdk.getApplications(organizationID);
189 | } catch (Exception e) {
190 | throw new MojoFailureException(
191 | "\n\nUnable to retrieve the application list from Contrast. Please check Contrast connection configuration\n",
192 | e);
193 | }
194 |
195 | for (Application application : applications.getApplications()) {
196 | if (applicationName.equals(application.getName())) {
197 | return application.getId();
198 | }
199 | }
200 |
201 | throw new MojoFailureException(
202 | "\n\nApplication with name '"
203 | + applicationName
204 | + "' not found. Make sure this server name appears in Contrast under the 'Applications' tab.\n");
205 | }
206 |
207 | /**
208 | * Creates a basic report for a Trace object
209 | *
210 | * @param trace Trace object
211 | * @return String report
212 | */
213 | private String generateTraceReport(Trace trace) {
214 | StringBuilder sb = new StringBuilder();
215 | sb.append("Trace: ");
216 | sb.append(
217 | trace
218 | .getTitle()
219 | .replaceAll("\\{\\{\\#unlicensed\\}\\}", "(")
220 | .replaceAll("\\{\\{\\/unlicensed\\}\\}", ")"));
221 | sb.append("\nTrace Uuid: ");
222 | sb.append(trace.getUuid());
223 | sb.append("\nTrace Severity: ");
224 | sb.append(trace.getSeverity());
225 | sb.append("\nTrace Likelihood: ");
226 | sb.append(trace.getLikelihood());
227 | sb.append("\n");
228 |
229 | return sb.toString();
230 | }
231 |
232 | /**
233 | * Returns the sublist of severities greater than or equal to the configured severity level
234 | *
235 | * @param severity include severity to filter with severity list with
236 | * @return list of severity strings
237 | */
238 | private static EnumSet getSeverityList(String severity) {
239 |
240 | List severityList = SEVERITIES.subList(SEVERITIES.indexOf(severity), SEVERITIES.size());
241 |
242 | List ruleSeverities = new ArrayList();
243 |
244 | for (String severityToAdd : severityList) {
245 | ruleSeverities.add(RuleSeverity.valueOf(severityToAdd.toUpperCase()));
246 | }
247 |
248 | return EnumSet.copyOf(ruleSeverities);
249 | }
250 |
251 | // Severity levels
252 | private static final List SEVERITIES =
253 | Arrays.asList("Note", "Low", "Medium", "High", "Critical");
254 | }
255 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------
/src/main/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojo.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import com.contrastsecurity.exceptions.UnauthorizedException;
24 | import com.contrastsecurity.models.AgentType;
25 | import com.contrastsecurity.models.Applications;
26 | import com.contrastsecurity.sdk.ContrastSDK;
27 | import java.io.IOException;
28 | import java.nio.file.Files;
29 | import java.nio.file.Path;
30 | import java.nio.file.Paths;
31 | import java.nio.file.StandardOpenOption;
32 | import java.text.SimpleDateFormat;
33 | import java.util.ArrayList;
34 | import java.util.Date;
35 | import java.util.List;
36 | import java.util.Map;
37 | import java.util.TreeMap;
38 | import org.apache.maven.model.Plugin;
39 | import org.apache.maven.plugin.MojoFailureException;
40 | import org.apache.maven.plugins.annotations.LifecyclePhase;
41 | import org.apache.maven.plugins.annotations.Mojo;
42 | import org.apache.maven.plugins.annotations.Parameter;
43 | import org.apache.maven.project.MavenProject;
44 | import org.codehaus.plexus.util.FileUtils;
45 |
46 | /**
47 | * Includes the Contrast Java agent in integration testing to provide Contrast Assess runtime
48 | * security analysis.
49 | */
50 | @Mojo(name = "install", defaultPhase = LifecyclePhase.PROCESS_SOURCES, requiresOnline = true)
51 | public final class ContrastInstallAgentMojo extends AbstractAssessMojo {
52 |
53 | @Parameter(defaultValue = "${project}", readonly = true)
54 | private MavenProject project;
55 |
56 | /**
57 | * When {@code true}, will not alter the Maven {@code argLine} property.
58 | *
59 | * @since 2.0
60 | */
61 | @Parameter(property = "skipArgLine")
62 | boolean skipArgLine;
63 |
64 | /**
65 | * When "true", will configure Contrast to treat this as a standalone application (e.g. one that
66 | * uses an embedded web server vs war packaging).
67 | *
68 | * @since 2.2
69 | */
70 | @Parameter(property = "standalone")
71 | boolean standalone;
72 |
73 | /**
74 | * Override the reported server environment {@see
75 | * https://docs.contrastsecurity.com/en/server-configuration.html}.
76 | *
77 | * @since 2.9
78 | */
79 | @Parameter(property = "environment")
80 | private String environment;
81 |
82 | /**
83 | * Override the reported server path. Default is the present working directory of the JVM process
84 | * Contrast is attached to.
85 | *
86 | *
In a multi-module build, the default value may lead Contrast to report a unique server per
87 | * module. Multi-module Maven builds can appear as different servers in the Contrast UI. If you
88 | * would like to discourage this behavior and would rather see all modules appear under the same
89 | * server in Contrast, use this property to set a common server path across modules.
90 | *
91 | * @since 2.1
92 | */
93 | @Parameter(property = "serverPath")
94 | String serverPath;
95 |
96 | /**
97 | * Path to an existing Contrast Java agent JAR. Specifying this configures the plugin to omit the
98 | * "retrieve Contrast JAR" step.
99 | */
100 | @Parameter(property = "jarPath")
101 | private String jarPath;
102 |
103 | /**
104 | * Define a set of key=value pairs (which conforms to RFC 2253) for specifying user-defined
105 | * metadata associated with the application. The set must be formatted as a comma-delimited list.
106 | * of {@code key=value} pairs.
107 | *
108 | *
Example - "business-unit=accounting, office=Baltimore"
109 | *
110 | * @since 2.9
111 | */
112 | @Parameter(property = "applicationSessionMetadata")
113 | private String applicationSessionMetadata;
114 |
115 | /**
116 | * Tags to apply to the Contrast application. Must be formatted as a comma-delimited list.
117 | *
118 | * @since 2.9
119 | */
120 | @Parameter(property = "applicationTags")
121 | private String applicationTags;
122 |
123 | /**
124 | * The {@code appVersion} metadata associated with Contrast analysis findings. Allows users to
125 | * compare vulnerabilities between applications versions, CI builds, etc. Contrast generates the
126 | * appVersion in the following order:
127 | *
128 | *
129 | *
The {@code appVersion} as configured in the plugin properties.
130 | *
If your build is running in TravisCI, Contrast will use {@code
131 | * appName-$TRAVIS_BUILD_NUMBER}.
132 | *
If your build is running in CircleCI, Contrast will use {@code
133 | * appName-$CIRCLE_BUILD_NUM}.
134 | *
If none of the above apply, Contrast will use a timestamp {@code appName-yyyyMMddHHmmss}
135 | * format.
136 | *
137 | */
138 | @Parameter(property = "appVersion")
139 | String appVersion;
140 |
141 | /** visible for testing */
142 | String contrastAgentLocation;
143 |
144 | String applicationName;
145 |
146 | static Map environmentToSessionMetadata = new TreeMap<>();
147 |
148 | static {
149 | // Jenkins git plugin environment variables
150 | environmentToSessionMetadata.put("GIT_BRANCH", "branchName");
151 | environmentToSessionMetadata.put("GIT_COMMITTER_NAME", "committer");
152 | environmentToSessionMetadata.put("GIT_COMMIT", "commitHash");
153 | environmentToSessionMetadata.put("GIT_URL", "repository");
154 | environmentToSessionMetadata.put("GIT_URL_1", "repository");
155 |
156 | // CI build number environment variables
157 | environmentToSessionMetadata.put("BUILD_NUMBER", "buildNumber");
158 | environmentToSessionMetadata.put("TRAVIS_BUILD_NUMBER", "buildNumber");
159 | environmentToSessionMetadata.put("CIRCLE_BUILD_NUM", "buildNumber");
160 | }
161 |
162 | public void execute() throws MojoFailureException {
163 | verifyAppIdOrNameNotNull();
164 | getLog().info("Attempting to connect to Contrast and install the Java agent.");
165 |
166 | ContrastSDK contrast = connectToContrast();
167 |
168 | Path agent = installJavaAgent(contrast);
169 | contrastAgentLocation = agent.toAbsolutePath().toString();
170 |
171 | getLog().info("Agent downloaded.");
172 |
173 | if (getAppId() != null) {
174 | applicationName = getAppName(contrast, getAppId());
175 |
176 | if (getAppName() != null) {
177 | getLog().info("Using 'appId' property; 'appName' property is ignored.");
178 | }
179 |
180 | } else {
181 | applicationName = getAppName();
182 | }
183 | project
184 | .getProperties()
185 | .setProperty(
186 | "argLine",
187 | buildArgLine(project.getProperties().getProperty("argLine"), applicationName));
188 |
189 | for (Plugin plugin : (List) project.getBuildPlugins()) {
190 | if ("org.springframework.boot".equals(plugin.getGroupId())
191 | && "spring-boot-maven-plugin".equals(plugin.getArtifactId())) {
192 | getLog().debug("Found the spring-boot-maven-plugin, with configuration:");
193 | String configuration = plugin.getConfiguration().toString();
194 | getLog().debug(configuration);
195 | if (configuration.contains("${argLine}")) {
196 | getLog().info("Skipping set of -Drun.jvmArguments as it references ${argLine}");
197 | } else {
198 | String jvmArguments =
199 | buildArgLine(
200 | project.getProperties().getProperty("run.jvmArguments"), applicationName);
201 | getLog().info(String.format("Setting -Drun.jvmArguments=%s", jvmArguments));
202 | project.getProperties().setProperty("run.jvmArguments", jvmArguments);
203 | }
204 |
205 | break;
206 | }
207 | }
208 | }
209 |
210 | private String getAppName(ContrastSDK contrastSDK, String applicationId)
211 | throws MojoFailureException {
212 | Applications applications;
213 | try {
214 | final String organizationID = getOrganizationId();
215 | applications = contrastSDK.getApplication(organizationID, applicationId);
216 | } catch (Exception e) {
217 | String logMessage;
218 | if (e.getMessage().contains("403")) {
219 | logMessage =
220 | "\n\n Unable to find the application on Contrast with the id [" + applicationId + "]\n";
221 | } else {
222 | logMessage =
223 | "\n\n Unable to retrieve the application list from Contrast. Please check Contrast connection configuration\n";
224 | }
225 | throw new MojoFailureException(logMessage, e);
226 | }
227 | if (applications.getApplication() == null) {
228 | throw new MojoFailureException(
229 | "\n\nApplication with id '"
230 | + applicationId
231 | + "' not found. Make sure this application appears in Contrast under the 'Applications' tab.\n");
232 | }
233 | return applications.getApplication().getName();
234 | }
235 |
236 | String computeAppVersion(Date currentDate) {
237 | if (computedAppVersion != null) {
238 | return computedAppVersion;
239 | }
240 |
241 | if (appVersion != null) {
242 | getLog().info("Using user-specified app version [" + appVersion + "]");
243 | computedAppVersion = appVersion;
244 | return computedAppVersion;
245 | }
246 |
247 | String travisBuildNumber = System.getenv("TRAVIS_BUILD_NUMBER");
248 | String circleBuildNum = System.getenv("CIRCLE_BUILD_NUM");
249 |
250 | final String appVersionQualifier;
251 | if (travisBuildNumber != null) {
252 | getLog()
253 | .info(
254 | "Build is running in TravisCI. We'll use TRAVIS_BUILD_NUMBER ["
255 | + travisBuildNumber
256 | + "]");
257 | appVersionQualifier = travisBuildNumber;
258 | } else if (circleBuildNum != null) {
259 | getLog()
260 | .info(
261 | "Build is running in CircleCI. We'll use CIRCLE_BUILD_NUM [" + circleBuildNum + "]");
262 | appVersionQualifier = circleBuildNum;
263 | } else {
264 | getLog().info("No CI build number detected, we'll use current timestamp.");
265 | appVersionQualifier = new SimpleDateFormat("yyyyMMddHHmmss").format(currentDate);
266 | }
267 | if (getAppId() != null) {
268 | computedAppVersion = applicationName + "-" + appVersionQualifier;
269 | } else {
270 | computedAppVersion = getAppName() + "-" + appVersionQualifier;
271 | }
272 |
273 | return computedAppVersion;
274 | }
275 |
276 | String computeSessionMetadata() {
277 | List metadata = new ArrayList<>();
278 |
279 | for (Map.Entry entry : environmentToSessionMetadata.entrySet()) {
280 | String environmentValue = System.getenv(entry.getKey());
281 |
282 | if (environmentValue != null) {
283 | metadata.add(String.format("%s=%s", entry.getValue(), environmentValue));
284 | }
285 | }
286 |
287 | return String.join(",", metadata);
288 | }
289 |
290 | String buildArgLine(String currentArgLine) {
291 | return buildArgLine(currentArgLine, getAppName());
292 | }
293 |
294 | String buildArgLine(String currentArgLine, String applicationName) {
295 |
296 | if (currentArgLine == null) {
297 | getLog().info("Current argLine is null");
298 | currentArgLine = "";
299 | } else {
300 | getLog().info("Current argLine is [" + currentArgLine + "]");
301 | }
302 |
303 | if (skipArgLine) {
304 | getLog().info("skipArgLine is set to false.");
305 | getLog()
306 | .info(
307 | "You will need to configure the Maven argLine property manually for the Contrast agent to work.");
308 | return currentArgLine;
309 | }
310 |
311 | getLog().info("Configuring argLine property.");
312 |
313 | computedAppVersion = computeAppVersion(new Date());
314 |
315 | StringBuilder argLineBuilder = new StringBuilder();
316 | argLineBuilder.append(currentArgLine);
317 | argLineBuilder.append(" -javaagent:").append(contrastAgentLocation);
318 | final String serverName = getServerName();
319 | if (serverName != null) {
320 | argLineBuilder.append(" -Dcontrast.server=").append(serverName);
321 | }
322 | if (environment != null) {
323 | argLineBuilder.append(" -Dcontrast.env=").append(environment);
324 | } else {
325 | argLineBuilder.append(" -Dcontrast.env=qa");
326 | }
327 | argLineBuilder.append(" -Dcontrast.override.appversion=").append(computedAppVersion);
328 | argLineBuilder.append(" -Dcontrast.reporting.period=").append("200");
329 |
330 | String sessionMetadata = computeSessionMetadata();
331 | if (!sessionMetadata.isEmpty()) {
332 | argLineBuilder
333 | .append(" -Dcontrast.application.session_metadata='")
334 | .append(sessionMetadata)
335 | .append("'");
336 | }
337 |
338 | if (standalone) {
339 | argLineBuilder.append(" -Dcontrast.standalone.appname=").append(applicationName);
340 | } else {
341 | argLineBuilder.append(" -Dcontrast.override.appname=").append(applicationName);
342 | }
343 |
344 | if (serverPath != null) {
345 | argLineBuilder.append(" -Dcontrast.path=").append(serverPath);
346 | }
347 |
348 | if (applicationSessionMetadata != null) {
349 | argLineBuilder
350 | .append(" -Dcontrast.application.session_metadata='")
351 | .append(applicationSessionMetadata)
352 | .append("'");
353 | }
354 |
355 | if (applicationTags != null) {
356 | argLineBuilder.append(" -Dcontrast.application.tags=").append(applicationTags);
357 | }
358 |
359 | String newArgLine = argLineBuilder.toString();
360 |
361 | getLog().info("Updated argLine is " + newArgLine);
362 | return newArgLine.trim();
363 | }
364 |
365 | Path installJavaAgent(ContrastSDK connection) throws MojoFailureException {
366 | if (jarPath != null) {
367 | getLog().info("Using configured jar path " + jarPath);
368 | final Path agent = Paths.get(jarPath);
369 | if (!Files.exists(agent)) {
370 | throw new MojoFailureException("Unable to load the local Java agent from " + jarPath);
371 | }
372 | getLog().info("Loaded the latest java agent from " + jarPath);
373 | return agent;
374 | }
375 |
376 | getLog().info("No jar path was configured. Downloading the latest contrast.jar...");
377 | final byte[] bytes;
378 | final String organizationID = getOrganizationId();
379 | try {
380 | bytes = connection.getAgent(AgentType.JAVA, organizationID);
381 | } catch (IOException e) {
382 | throw new MojoFailureException(
383 | "\n\nCouldn't download the Java agent from Contrast. Please check that all your credentials are correct. If everything is correct, please contact Contrast Support. The error is:",
384 | e);
385 | } catch (UnauthorizedException e) {
386 | throw new MojoFailureException(
387 | "\n\nWe contacted Contrast successfully but couldn't authorize with the credentials you provided. The error is:",
388 | e);
389 | }
390 | // Save the jar to the 'target' directory
391 | final Path target = Paths.get(project.getBuild().getDirectory());
392 | try {
393 | FileUtils.forceMkdir(target.toFile());
394 | } catch (final IOException e) {
395 | throw new MojoFailureException("Unable to create directory " + target, e);
396 | }
397 | final Path agent = target.resolve(AGENT_NAME);
398 | try {
399 | Files.write(agent, bytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
400 | } catch (final IOException e) {
401 | throw new MojoFailureException("Unable to save the latest java agent.", e);
402 | }
403 | getLog().info("Saved the latest java agent to " + agent.toAbsolutePath());
404 | return agent;
405 | }
406 |
407 | private static final String AGENT_NAME = "contrast.jar";
408 | }
409 |
--------------------------------------------------------------------------------
/src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java:
--------------------------------------------------------------------------------
1 | package com.contrastsecurity.maven.plugin;
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 | import com.contrastsecurity.exceptions.HttpResponseException;
24 | import com.contrastsecurity.exceptions.UnauthorizedException;
25 | import com.contrastsecurity.sdk.ContrastSDK;
26 | import com.contrastsecurity.sdk.scan.CodeArtifact;
27 | import com.contrastsecurity.sdk.scan.Project;
28 | import com.contrastsecurity.sdk.scan.Projects;
29 | import com.contrastsecurity.sdk.scan.Scan;
30 | import com.contrastsecurity.sdk.scan.ScanSummary;
31 | import java.io.File;
32 | import java.io.IOException;
33 | import java.io.UncheckedIOException;
34 | import java.net.MalformedURLException;
35 | import java.net.URL;
36 | import java.nio.file.Files;
37 | import java.nio.file.Path;
38 | import java.time.Duration;
39 | import java.util.Optional;
40 | import java.util.concurrent.CompletableFuture;
41 | import java.util.concurrent.CompletionStage;
42 | import java.util.concurrent.ExecutionException;
43 | import java.util.concurrent.Executors;
44 | import java.util.concurrent.ScheduledExecutorService;
45 | import java.util.concurrent.TimeUnit;
46 | import java.util.concurrent.TimeoutException;
47 | import java.util.function.Consumer;
48 | import org.apache.maven.artifact.Artifact;
49 | import org.apache.maven.plugin.MojoFailureException;
50 | import org.apache.maven.plugins.annotations.LifecyclePhase;
51 | import org.apache.maven.plugins.annotations.Mojo;
52 | import org.apache.maven.plugins.annotations.Parameter;
53 | import org.apache.maven.project.MavenProject;
54 |
55 | /**
56 | * Analyzes the Maven project's artifact with Contrast Scan to provide security insights
57 | *
58 | * @since 2.13.0
59 | */
60 | @Mojo(
61 | name = "scan",
62 | defaultPhase = LifecyclePhase.INTEGRATION_TEST,
63 | requiresOnline = true,
64 | threadSafe = true)
65 | public final class ContrastScanMojo extends AbstractContrastMojo {
66 |
67 | @Parameter(defaultValue = "${project}", readonly = true)
68 | private MavenProject mavenProject;
69 |
70 | /**
71 | * Contrast Scan project unique ID to which the plugin runs new Scans. This will be replaced
72 | * imminently with a project name
73 | */
74 | @Parameter(property = "project", defaultValue = "${project.name}")
75 | private String projectName;
76 |
77 | /**
78 | * File path of the Java artifact to upload for scanning. By default, uses the path to this
79 | * module's Maven artifact produced in the {@code package} phase.
80 | */
81 | @Parameter private File artifactPath;
82 |
83 | /** A label to distinguish this scan from others in your project */
84 | @Parameter(defaultValue = "${project.version}")
85 | private String label;
86 |
87 | /**
88 | * When {@code true}, will wait for and retrieve scan results before completing the goal.
89 | * Otherwise, will start a scan then complete the goal without waiting for Contrast Scan to
90 | * complete.
91 | */
92 | @Parameter(defaultValue = "" + true)
93 | private boolean waitForResults;
94 |
95 | /** When {@code true}, will output a summary of the scan results to the console (build log). */
96 | @Parameter(defaultValue = "" + true)
97 | private boolean consoleOutput;
98 |
99 | /**
100 | * File path to where the scan results (in SARIF)
101 | * will be written at the conclusion of the scan. Note: no results are written when {@link
102 | * #waitForResults} is {@code false}.
103 | */
104 | @Parameter(
105 | defaultValue =
106 | "${project.build.directory}/contrast-scan-reports/contrast-scan-results.sarif.json")
107 | private File outputPath;
108 |
109 | /**
110 | * Maximum time (in milliseconds) to wait for a Scan to complete. Scans that exceed this threshold
111 | * fail this goal.
112 | */
113 | @Parameter(defaultValue = "" + 5 * 60 * 1000)
114 | private long timeoutMs;
115 |
116 | private ContrastSDK contrast;
117 |
118 | /** visible for testing */
119 | String getProjectName() {
120 | return projectName;
121 | }
122 |
123 | /** visible for testing */
124 | void setProjectName(final String projectName) {
125 | this.projectName = projectName;
126 | }
127 |
128 | /** visible for testing */
129 | boolean isConsoleOutput() {
130 | return consoleOutput;
131 | }
132 |
133 | /** visible for testing */
134 | void setConsoleOutput(final boolean consoleOutput) {
135 | this.consoleOutput = consoleOutput;
136 | }
137 |
138 | @Override
139 | public void execute() throws MojoFailureException, MojoFailureException {
140 | // initialize plugin
141 | initialize();
142 | final Projects projects = contrast.scan(getOrganizationId()).projects();
143 |
144 | // check that file exists
145 | final Path file = artifactPath == null ? findProjectArtifactOrFail() : artifactPath.toPath();
146 | if (!Files.exists(file)) {
147 | throw new MojoFailureException(
148 | file
149 | + " does not exist. Make sure to bind the scan goal to a phase that will execute after the artifact to scan has been built");
150 | }
151 |
152 | // get or create project
153 | final Project project = findOrCreateProject(projects);
154 |
155 | // upload code artifact
156 | getLog().info("Uploading " + file.getFileName() + " to Contrast Scan");
157 | final CodeArtifact codeArtifact;
158 | try {
159 | codeArtifact = project.codeArtifacts().upload(file);
160 | } catch (final IOException | HttpResponseException e) {
161 | throw new MojoFailureException("Failed to upload code artifact to Contrast Scan", e);
162 | }
163 |
164 | // start scan
165 | getLog().info("Starting scan with label " + label);
166 | final Scan scan;
167 | try {
168 | scan =
169 | project.scans().define().withLabel(label).withExistingCodeArtifact(codeArtifact).create();
170 | } catch (final IOException | HttpResponseException e) {
171 | throw new MojoFailureException("Failed to start scan for code artifact " + codeArtifact, e);
172 | }
173 |
174 | // show link in build log
175 | final URL clickableScanURL = createClickableScanURL(project.id(), scan.id());
176 | getLog().info("Scan results will be available at " + clickableScanURL.toExternalForm());
177 |
178 | // optionally wait for results, output summary to console, output sarif to file system
179 | if (waitForResults) {
180 | getLog().info("Waiting for scan results");
181 | waitForResults(scan);
182 | }
183 | }
184 |
185 | /**
186 | * Inspects the {@link #mavenProject} to find the project's artifact, or fails if no such artifact
187 | * can be found. We may not find an artifact when the user has configured this goal to run before
188 | * the artifact is created, or if the project does not produce an artifact (e.g. a module of type
189 | * {@code pom}).
190 | *
191 | *
By default, some Maven plugins will skip their work instead of failing when inputs are not
192 | * found. For example, the {@code maven-surefire-plugin} default behavior will skip tests if no
193 | * test classes are found (and this may be overridden with configuration).
194 | *
195 | *
So, why does this plugin fail instead of simply skipping its work and logging a warning?
196 | * Because we want the user to avoid one particularly problematic configuration. Users can easily
197 | * mis-configure this plugin in a multi-module build by including it in the parent POM's build
198 | * plugins. In this case, all child modules will inherit this plugin in their builds, and the
199 | * build will scan not just the web application modules, but also the internal dependencies that
200 | * are components of their applications. Contrast Scan is intended to be used on their artifact
201 | * that represents the web application, and users should not scan the components of their web
202 | * application individually. We can detect this mis-configuration by failing when there is no
203 | * artifact, because the build will fail on the parent POM project (since it is of type {@code
204 | * pom}).
205 | *
206 | * @throws MojoFailureException when artifact does not exist
207 | */
208 | private Path findProjectArtifactOrFail() throws MojoFailureException {
209 | final Artifact artifact = mavenProject.getArtifact();
210 | final File file = artifact == null ? null : artifact.getFile();
211 | if (file == null) {
212 | throw new MojoFailureException(
213 | "Project's artifact file has not been set - see https://contrastsecurity.dev/contrast-maven-plugin/troubleshooting/artifact-not-set.html");
214 | }
215 | return file.toPath();
216 | }
217 |
218 | /**
219 | * Finds a Scan project with the project name from the plugin configuration, or creates such a
220 | * "Java" project if one does not exist.
221 | *
222 | *
Note: the Scan API does not expose an endpoint for doing this atomically, so it is possible
223 | * that another process creates the project after having determined it to not-exist but before
224 | * attempting to create it.
225 | *
226 | * @param projects project resource collection
227 | * @return existing or new {@link Project}
228 | * @throws MojoFailureException when fails to make request to the Scan API
229 | */
230 | private Project findOrCreateProject(Projects projects) throws MojoFailureException {
231 | final Optional optional;
232 | try {
233 | optional = projects.findByName(projectName);
234 | } catch (final IOException e) {
235 | throw new MojoFailureException("Failed to retrieve project " + projectName, e);
236 | } catch (final UnauthorizedException e) {
237 | throw new MojoFailureException(
238 | "Authentication failure while retrieving project "
239 | + projectName
240 | + " - verify Contrast connection configuration",
241 | e);
242 | }
243 | if (optional.isPresent()) {
244 | getLog().debug("Found project with name " + projectName);
245 | final Project project = optional.get();
246 | if (project.archived()) {
247 | // TODO the behavior of tools like this plugin has yet to be defined with respect to
248 | // archived projects; however, the UI exposes no way to archive projects at the moment.
249 | // For now, simply log a warning to help debug this in the future should we encounter this
250 | // case
251 | getLog().warn("Project " + projectName + " is archived");
252 | }
253 | return project;
254 | }
255 |
256 | getLog().debug("No project exists with name " + projectName + " - creating one");
257 | try {
258 | return projects.define().withName(projectName).withLanguage("JAVA").create();
259 | } catch (final IOException | HttpResponseException e) {
260 | throw new MojoFailureException("Failed to create project " + projectName, e);
261 | }
262 | }
263 |
264 | /**
265 | * visible for testing
266 | *
267 | * @return Contrast browser application URL for users to click-through and see their scan results
268 | * @throws MojoFailureException when the URL is malformed
269 | */
270 | URL createClickableScanURL(final String projectId, final String scanId)
271 | throws MojoFailureException {
272 | final String path =
273 | String.join(
274 | "/",
275 | "",
276 | "Contrast",
277 | "static",
278 | "ng",
279 | "index.html#",
280 | getOrganizationId(),
281 | "scans",
282 | projectId,
283 | "scans",
284 | scanId);
285 | try {
286 | final URL url = new URL(getURL());
287 | return new URL(url.getProtocol(), url.getHost(), url.getPort(), path);
288 | } catch (final MalformedURLException e) {
289 | throw new MojoFailureException(
290 | "Error building clickable Scan URL. Please contact support@contrastsecurity.com for help",
291 | e);
292 | }
293 | }
294 |
295 | /**
296 | * Waits for the scan to complete, writes results in SARIF to the file system, and optionally
297 | * displays a summary of the results in the console. Translates all errors that could occur while
298 | * retrieving results to one of {@code MojoFailureException} or {@code MojoFailureException}.
299 | *
300 | * @param scan the scan to wait for and retrieve the results of
301 | * @throws MojoFailureException when fails to retrieve scan results for unexpected reasons
302 | * @throws MojoFailureException when the wait for scan results operation times out
303 | */
304 | private void waitForResults(final Scan scan) throws MojoFailureException, MojoFailureException {
305 | final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
306 | try {
307 | final Path outputFile = outputPath.toPath();
308 | final Path reportsDirectory = outputFile.getParent();
309 | try {
310 | Files.createDirectories(reportsDirectory);
311 | } catch (final IOException e) {
312 | throw new MojoFailureException("Failed to create Contrast Scan reports directory", e);
313 | }
314 |
315 | final CompletionStage await = scan.await(scheduler);
316 | final CompletionStage save =
317 | await.thenCompose(
318 | completed ->
319 | CompletableFuture.runAsync(
320 | () -> {
321 | try {
322 | completed.saveSarif(outputFile);
323 | } catch (final IOException e) {
324 | throw new UncheckedIOException(e);
325 | }
326 | },
327 | scheduler));
328 | final CompletionStage output =
329 | await.thenAccept(
330 | completed -> {
331 | final ScanSummary summary;
332 | try {
333 | summary = completed.summary();
334 | } catch (final IOException e) {
335 | throw new UncheckedIOException(e);
336 | }
337 | writeSummaryToConsole(summary, line -> getLog().info(line));
338 | });
339 | CompletableFuture.allOf(save.toCompletableFuture(), output.toCompletableFuture())
340 | .get(timeoutMs, TimeUnit.MILLISECONDS);
341 | } catch (final ExecutionException e) {
342 | // try to unwrap the extraneous ExecutionException
343 | final Throwable cause = e.getCause();
344 | // ExecutionException should always have a cause, but its constructor does not enforce this,
345 | // so check if the cause is null
346 | final Throwable inner = cause == null ? e : cause;
347 | throw new MojoFailureException("Failed to retrieve Contrast Scan results", inner);
348 | } catch (final InterruptedException e) {
349 | Thread.currentThread().interrupt();
350 | throw new MojoFailureException("Interrupted while retrieving Contrast Scan results", e);
351 | } catch (final TimeoutException e) {
352 | final Duration duration = Duration.ofMillis(timeoutMs);
353 | final String durationString =
354 | duration.toMinutes() > 0
355 | ? duration.toMinutes() + " minutes"
356 | : (duration.toMillis() / 1000) + " seconds";
357 | throw new MojoFailureException(
358 | "Failed to retrieve Contrast Scan results in " + durationString, e);
359 | } finally {
360 | scheduler.shutdown();
361 | }
362 | }
363 |
364 | /**
365 | * visible for testing
366 | *
367 | * @param summary the scan summary to write to console
368 | * @param consoleLogger describes a console logger where each accepted string is printed to a new
369 | * line
370 | */
371 | void writeSummaryToConsole(final ScanSummary summary, final Consumer consoleLogger) {
372 | consoleLogger.accept("Scan completed");
373 | if (consoleOutput) {
374 | consoleLogger.accept("New Results\t" + summary.totalNewResults());
375 | consoleLogger.accept("Fixed Results\t" + summary.totalFixedResults());
376 | consoleLogger.accept("Total Results\t" + summary.totalResults());
377 | }
378 | }
379 |
380 | /**
381 | * Must be called after Maven has completed field injection.
382 | *
383 | *
I don't believe Maven has a post-injection callback that we bind to this method, so the
384 | * {@link #execute()} method calls this before continuing.
385 | *
386 | *