├── 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 | *
36 |  *   @ContrastAPIStub
37 |  *   @Test
38 |  *   public void test(final ContrastAPI contrast) { ... }
39 |  * 
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 | *

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 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 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 | *
  1. The {@code appVersion} as configured in the plugin properties. 130 | *
  2. If your build is running in TravisCI, Contrast will use {@code 131 | * appName-$TRAVIS_BUILD_NUMBER}. 132 | *
  3. If your build is running in CircleCI, Contrast will use {@code 133 | * appName-$CIRCLE_BUILD_NUM}. 134 | *
  4. 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 | *

This is useful for tests to initialize the {@link ContrastSDK} without running the whole 387 | * {@link #execute()} method 388 | * 389 | * @throws IllegalStateException when has already been initialized 390 | * @throws MojoFailureException when cannot connect to Contrast 391 | */ 392 | private synchronized void initialize() throws MojoFailureException { 393 | if (contrast != null) { 394 | throw new IllegalStateException("Already initialized"); 395 | } 396 | contrast = connectToContrast(); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.contrastsecurity 8 | contrast-maven-plugin 9 | 2.13.3-SNAPSHOT 10 | maven-plugin 11 | 12 | Contrast Maven Plugin 13 | 14 | Maven plugin for including Contrast security analysis in Java web applications 15 | 16 | https://docs.contrastsecurity.com/en/maven.html 17 | 18 | 19 | Contrast Security, Inc. 20 | https://contrastsecurity.com 21 | 22 | 23 | 24 | 25 | Contrast Security 26 | support@contrastsecurity.com 27 | 28 | 29 | 30 | 31 | 32 | Apache License, Version 2.0 33 | https://www.apache.org/licenses/LICENSE-2.0.txt 34 | repo 35 | A business-friendly OSS license 36 | 37 | 38 | 39 | 40 | 41 | ossrh 42 | https://oss.sonatype.org/content/repositories/snapshots 43 | 44 | 45 | ossrh 46 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 47 | 48 | 49 | github 50 | scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git 51 | 52 | 53 | 54 | 55 | scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git 56 | 57 | scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git 58 | 59 | scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git 60 | 61 | HEAD 62 | 63 | 64 | 65 | UTF-8 66 | ${project.build.directory}/test-repository 67 | 5.11.0 68 | 5.11.0 69 | 1.6.3 70 | 71 | 72 | 73 | 74 | org.apache.maven 75 | maven-plugin-api 76 | 3.5.3 77 | 78 | 79 | org.apache.maven.plugin-tools 80 | maven-plugin-annotations 81 | 3.6.1 82 | provided 83 | 84 | 85 | com.contrastsecurity 86 | contrast-sdk-java 87 | 3.3 88 | 89 | 90 | org.apache.maven 91 | maven-project 92 | 2.2.1 93 | 94 | 95 | org.apache.maven 96 | maven-settings 97 | 2.2.1 98 | 99 | 100 | org.apache.maven 101 | maven-model 102 | 3.5.3 103 | 104 | 105 | org.codehaus.plexus 106 | plexus-utils 107 | 3.4.2 108 | 109 | 110 | 111 | com.google.auto.value 112 | auto-value-annotations 113 | ${auto-value.version} 114 | provided 115 | 116 | 117 | 118 | 119 | org.junit.jupiter 120 | junit-jupiter-api 121 | ${junit-jupiter.version} 122 | test 123 | 124 | 125 | org.junit.jupiter 126 | junit-jupiter-params 127 | ${junit-jupiter.version} 128 | test 129 | 130 | 131 | org.junit.jupiter 132 | junit-jupiter-engine 133 | ${junit-jupiter.version} 134 | test 135 | 136 | 137 | org.mockito 138 | mockito-core 139 | 5.13.0 140 | test 141 | 142 | 143 | 144 | org.junit.vintage 145 | junit-vintage-engine 146 | ${junit-vintage-engine.version} 147 | test 148 | 149 | 150 | junit 151 | junit 152 | 4.13.2 153 | test 154 | 155 | 156 | com.github.stefanbirkner 157 | system-rules 158 | 1.17.0 159 | test 160 | 161 | 162 | org.apache.maven.shared 163 | maven-verifier 164 | 1.2 165 | test 166 | 167 | 168 | org.assertj 169 | assertj-core 170 | 3.20.2 171 | test 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | src/test/resources/it 181 | true 182 | ${project.build.directory}/test-classes/it 183 | 184 | **/pom.xml 185 | 186 | 187 | 188 | 189 | 190 | 191 | maven-compiler-plugin 192 | 193 | 8 194 | 195 | 196 | com.google.auto.value 197 | auto-value 198 | ${auto-value.version} 199 | 200 | 201 | 202 | 203 | 204 | org.codehaus.mojo 205 | license-maven-plugin 206 | 207 | 208 | 209 | update-generated-sources 210 | 211 | update-file-header 212 | 213 | process-test-sources 214 | 215 | ${project.build.directory}/generated-sources 216 | 217 | 218 | 219 | 220 | 221 | maven-plugin-plugin 222 | 223 | contrast 224 | true 225 | 226 | 227 | 228 | maven-source-plugin 229 | 230 | 231 | attach-sources 232 | package 233 | 234 | jar-no-fork 235 | 236 | 237 | 238 | 239 | 240 | maven-javadoc-plugin 241 | 242 | 11 243 | false 244 | 245 | 246 | 247 | attach-javadocs 248 | package 249 | 250 | jar 251 | 252 | 253 | 254 | 255 | 256 | maven-release-plugin 257 | 258 | release 259 | 260 | 261 | 262 | maven-deploy-plugin 263 | 264 | 265 | deploy 266 | deploy 267 | 268 | deploy 269 | 270 | 271 | 272 | 273 | 274 | maven-site-plugin 275 | 276 | 277 | true 278 | 279 | 280 | 281 | stage-site 282 | site-deploy 283 | 284 | stage 285 | 286 | 287 | 288 | 289 | 290 | org.codehaus.mojo 291 | templating-maven-plugin 292 | 293 | 294 | filtering-java-templates 295 | 296 | filter-sources 297 | 298 | 299 | 300 | 301 | 302 | maven-scm-publish-plugin 303 | 3.1.0 304 | 305 | gh-pages 306 | ${env.GITHUB_ACTOR} 307 | ${env.GITHUB_TOKEN} 308 | 309 | 310 | 311 | site-deploy 312 | site-deploy 313 | 314 | publish-scm 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | maven-clean-plugin 325 | 3.1.0 326 | 327 | 328 | maven-source-plugin 329 | 3.2.1 330 | 331 | 332 | maven-resources-plugin 333 | 3.2.0 334 | 335 | 336 | maven-compiler-plugin 337 | 3.8.1 338 | 339 | 340 | maven-surefire-plugin 341 | 3.5.0 342 | 343 | 344 | 11 345 | 346 | 347 | 348 | 349 | maven-failsafe-plugin 350 | 3.5.0 351 | 352 | 353 | 354 | junit:junit 355 | org.junit.vintage:junit-vintage-engine 356 | 357 | 358 | 359 | 360 | 361 | maven-jar-plugin 362 | 3.2.0 363 | 364 | 365 | maven-plugin-plugin 366 | 3.6.1 367 | 368 | 369 | maven-javadoc-plugin 370 | 3.3.0 371 | 372 | 373 | maven-install-plugin 374 | 2.5.2 375 | 376 | 377 | maven-deploy-plugin 378 | 2.8.2 379 | 380 | 381 | maven-site-plugin 382 | 3.9.1 383 | 384 | 385 | maven-gpg-plugin 386 | 3.0.1 387 | 388 | 389 | maven-project-info-reports-plugin 390 | 3.1.2 391 | 392 | 393 | maven-release-plugin 394 | 2.5.3 395 | 396 | 397 | maven-scm-plugin 398 | 1.11.2 399 | 400 | 401 | org.codehaus.mojo 402 | templating-maven-plugin 403 | 1.0.0 404 | 405 | 406 | com.diffplug.spotless 407 | spotless-maven-plugin 408 | 2.43.0 409 | 410 | 411 | 412 | 413 | pom.xml 414 | README.md 415 | .github/workflows/**/*.yml 416 | src/**/*.java 417 | src/**/*.xml 418 | src/**/*.properties 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 1.17.0 427 | 428 | 429 | 430 | 431 | 432 | 433 | org.codehaus.mojo 434 | license-maven-plugin 435 | 2.0.0 436 | 437 | apache_v2 438 | 2021 439 | 440 | **/*.json 441 | 442 | 443 | 444 | 445 | org.sonatype.plugins 446 | nexus-staging-maven-plugin 447 | 1.6.8 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | maven-plugin-plugin 456 | 457 | 458 | 459 | 460 | 461 | developer 462 | 463 | true 464 | 465 | 466 | 467 | 468 | org.codehaus.mojo 469 | license-maven-plugin 470 | 471 | 472 | 473 | update-file-header 474 | 475 | process-test-sources 476 | 477 | 478 | 479 | 480 | com.diffplug.spotless 481 | spotless-maven-plugin 482 | 483 | 484 | 485 | apply 486 | 487 | process-test-sources 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | ci 496 | 497 | 498 | env.CI 499 | 500 | 501 | 502 | 503 | 504 | org.codehaus.mojo 505 | license-maven-plugin 506 | 507 | 508 | 509 | check-file-header 510 | 511 | process-test-sources 512 | 513 | true 514 | true 515 | 516 | 517 | 518 | 519 | 520 | com.diffplug.spotless 521 | spotless-maven-plugin 522 | 523 | 524 | 525 | check 526 | 527 | process-test-sources 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | end-to-end-test 536 | 537 | 538 | 539 | ${env.CI} 540 | true 541 | 542 | 543 | 548 | 549 | 550 | 551 | maven-dependency-plugin 552 | 553 | 554 | install-to-test-repository 555 | pre-integration-test 556 | 557 | copy 558 | 559 | 560 | 561 | 562 | ${project.groupId} 563 | ${project.artifactId} 564 | ${project.version} 565 | 566 | 567 | ${project.groupId} 568 | ${project.artifactId} 569 | ${project.version} 570 | pom 571 | 572 | 573 | 574 | ${contrast.test-repository}/com/contrastsecurity/${project.artifactId}/${project.version} 575 | 576 | 577 | 578 | 579 | retrieve-agent-for-testing 580 | pre-integration-test 581 | 582 | copy 583 | 584 | 585 | 586 | 587 | com.contrastsecurity 588 | contrast-agent 589 | 590 | 3.8.5.20387 591 | 592 | 593 | true 594 | 595 | ${project.build.testOutputDirectory} 596 | 597 | 598 | 599 | 600 | analyze-dependencies 601 | test 602 | 603 | analyze-only 604 | 605 | 606 | true 607 | 608 | true 609 | 610 | 611 | 612 | 613 | 614 | maven-failsafe-plugin 615 | 616 | 617 | jdk-11 618 | 619 | integration-test 620 | verify 621 | 622 | 623 | 624 | 11 625 | 626 | 627 | 628 | 629 | jdk-17 630 | 631 | integration-test 632 | verify 633 | 634 | 635 | 636 | 17 637 | 638 | 639 | 640 | 641 | jdk-21 642 | 643 | integration-test 644 | verify 645 | 646 | 647 | 648 | 21 649 | 650 | 651 | 652 | 653 | 654 | 655 | ${contrast.test-repository} 656 | ${env.CONTRAST__API__URL} 657 | ${env.CONTRAST__API__USER_NAME} 658 | ${env.CONTRAST__API__API_KEY} 659 | ${env.CONTRAST__API__SERVICE_KEY} 660 | 661 | ${env.CONTRAST__API__ORGANIZATION_ID} 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | release 671 | 672 | 673 | 674 | org.sonatype.plugins 675 | nexus-staging-maven-plugin 676 | true 677 | 678 | ossrh 679 | https://oss.sonatype.org/ 680 | true 681 | 682 | 683 | 684 | maven-source-plugin 685 | 686 | 687 | attach-sources 688 | 689 | jar-no-fork 690 | 691 | 692 | 693 | 694 | 695 | maven-javadoc-plugin 696 | 697 | 698 | attach-javadocs 699 | 700 | jar 701 | 702 | 703 | 704 | 705 | 706 | maven-gpg-plugin 707 | 708 | 709 | sign-artifacts 710 | verify 711 | 712 | sign 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | --------------------------------------------------------------------------------