From 5f9dd49842599d26048770663c6aa9a6ae7da8cb Mon Sep 17 00:00:00 2001 From: Brian Phillips Date: Fri, 8 Nov 2024 10:38:27 -0500 Subject: [PATCH] JAVA-8427 migrate maven plugin to sdk repo --- .github/workflows/build-maven-plugin.yml | 38 + .github/workflows/build-sdk.yml | 2 +- .github/workflows/publish-maven-plugin.yml | 62 ++ .../{publish.yml => publish-sdk.yml} | 0 .gitignore | 1 + README.md | 77 +- gradle-plugin/.gitignore | 271 +++++++ maven-plugin/.gitignore | 260 +++++++ .../.mvn/wrapper/MavenWrapperDownloader.java | 117 +++ .../.mvn/wrapper/maven-wrapper.properties | 18 + maven-plugin/CHANGELOG.md | 31 + maven-plugin/LICENSE | 14 + maven-plugin/README.md | 71 ++ maven-plugin/mvnw | 316 ++++++++ maven-plugin/mvnw.cmd | 188 +++++ maven-plugin/pom.xml | 721 ++++++++++++++++++ maven-plugin/scan.http | 127 +++ .../maven/plugin/Version.java | 31 + .../maven/plugin/AbstractAssessMojo.java | 98 +++ .../maven/plugin/AbstractContrastMojo.java | 240 ++++++ .../plugin/ContrastInstallAgentMojo.java | 408 ++++++++++ .../maven/plugin/ContrastScanMojo.java | 398 ++++++++++ .../maven/plugin/ContrastVerifyMojo.java | 254 ++++++ .../examples/multi-module-projects.md | 69 ++ maven-plugin/src/site/markdown/index.md | 20 + .../troubleshooting/artifact-not-set.md | 30 + maven-plugin/src/site/markdown/usage.md | 120 +++ .../site/resources/images/contrast-logo.png | Bin 0 -> 12730 bytes maven-plugin/src/site/site.xml | 56 ++ .../plugin/AbstractContrastMojoTest.java | 49 ++ .../plugin/ContrastInstallAgentMojoTest.java | 201 +++++ .../maven/plugin/ContrastScanMojoTest.java | 143 ++++ .../maven/plugin/ContrastVerifyMojoTest.java | 93 +++ .../maven/plugin/FakeScanSummary.java | 91 +++ .../maven/plugin/Resources.java | 67 ++ .../plugin/it/ContrastInstallAgentMojoIT.java | 64 ++ .../maven/plugin/it/ContrastScanMojoIT.java | 72 ++ .../maven/plugin/it/Verifiers.java | 74 ++ .../plugin/it/stub/ConnectionParameters.java | 95 +++ .../maven/plugin/it/stub/ContrastAPI.java | 36 + .../maven/plugin/it/stub/ContrastAPIStub.java | 45 ++ .../it/stub/ContrastAPIStubExtension.java | 130 ++++ .../plugin/it/stub/ExternalContrastAPI.java | 52 ++ .../maven/plugin/it/stub/package-info.java | 42 + .../src/test/resources/it/parent-pom/pom.xml | 66 ++ .../src/test/resources/it/spring-boot/pom.xml | 104 +++ .../contrastsecurity/test/Application.java | 31 + maven-plugin/unset-contrast.sh | 1 + sdk/README.md | 80 ++ 49 files changed, 5501 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/build-maven-plugin.yml create mode 100644 .github/workflows/publish-maven-plugin.yml rename .github/workflows/{publish.yml => publish-sdk.yml} (100%) create mode 100644 gradle-plugin/.gitignore create mode 100644 maven-plugin/.gitignore create mode 100644 maven-plugin/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 maven-plugin/.mvn/wrapper/maven-wrapper.properties create mode 100644 maven-plugin/CHANGELOG.md create mode 100644 maven-plugin/LICENSE create mode 100644 maven-plugin/README.md create mode 100755 maven-plugin/mvnw create mode 100644 maven-plugin/mvnw.cmd create mode 100644 maven-plugin/pom.xml create mode 100644 maven-plugin/scan.http create mode 100644 maven-plugin/src/main/java-templates/com/contrastsecurity/maven/plugin/Version.java create mode 100644 maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractAssessMojo.java create mode 100644 maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractContrastMojo.java create mode 100644 maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojo.java create mode 100644 maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java create mode 100644 maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojo.java create mode 100644 maven-plugin/src/site/markdown/examples/multi-module-projects.md create mode 100644 maven-plugin/src/site/markdown/index.md create mode 100644 maven-plugin/src/site/markdown/troubleshooting/artifact-not-set.md create mode 100644 maven-plugin/src/site/markdown/usage.md create mode 100644 maven-plugin/src/site/resources/images/contrast-logo.png create mode 100644 maven-plugin/src/site/site.xml create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/AbstractContrastMojoTest.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojoTest.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastScanMojoTest.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojoTest.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/FakeScanSummary.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/Resources.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastInstallAgentMojoIT.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastScanMojoIT.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/Verifiers.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ConnectionParameters.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPI.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStub.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStubExtension.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ExternalContrastAPI.java create mode 100644 maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/package-info.java create mode 100644 maven-plugin/src/test/resources/it/parent-pom/pom.xml create mode 100644 maven-plugin/src/test/resources/it/spring-boot/pom.xml create mode 100644 maven-plugin/src/test/resources/it/spring-boot/src/main/java/com/contrastsecurity/test/Application.java create mode 100755 maven-plugin/unset-contrast.sh create mode 100644 sdk/README.md diff --git a/.github/workflows/build-maven-plugin.yml b/.github/workflows/build-maven-plugin.yml new file mode 100644 index 00000000..f91307cd --- /dev/null +++ b/.github/workflows/build-maven-plugin.yml @@ -0,0 +1,38 @@ +name: build + +on: [push] + +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - uses: dangoslen/changelog-enforcer@v3 + build: + name: Verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: 11 + distribution: temurin + + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + + - uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Maven Verify + env: + CONTRAST__API__URL: ${{ secrets.CONTRAST__API__URL }} + CONTRAST__API__USER_NAME: ${{ secrets.CONTRAST__API__USER_NAME }} + CONTRAST__API__API_KEY: ${{ secrets.CONTRAST__API__API_KEY }} + CONTRAST__API__SERVICE_KEY: ${{ secrets.CONTRAST__API__SERVICE_KEY }} + CONTRAST__API__ORGANIZATION_ID: ${{ secrets.CONTRAST__API__ORGANIZATION_ID }} + run: cd maven-plugin/ && ./mvnw --batch-mode -Pend-to-end-test verify diff --git a/.github/workflows/build-sdk.yml b/.github/workflows/build-sdk.yml index 60fe0f90..7c99404f 100644 --- a/.github/workflows/build-sdk.yml +++ b/.github/workflows/build-sdk.yml @@ -25,7 +25,7 @@ jobs: - name: Cache Maven Wrapper uses: actions/cache@v2 with: - path: ./.mvn/wrapper/maven-wrapper.jar + path: cd sdk/ ./.mvn/wrapper/maven-wrapper.jar key: ${{ runner.os }}-maven-wrapper-${{ hashFiles('./.mvn/wrapper/maven-wrapper.properties') }} restore-keys: ${{ runner.os }}-maven-wrapper diff --git a/.github/workflows/publish-maven-plugin.yml b/.github/workflows/publish-maven-plugin.yml new file mode 100644 index 00000000..f9caab13 --- /dev/null +++ b/.github/workflows/publish-maven-plugin.yml @@ -0,0 +1,62 @@ +name: publish + +on: + workflow_dispatch: + +jobs: + publish: + permissions: + contents: write + environment: Maven Central + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: temurin + server-id: ossrh + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Cache Maven Wrapper + uses: actions/cache@v2 + with: + path: cd maven-plugin/ && ./.mvn/wrapper/maven-wrapper.jar + key: ${{ runner.os }}-maven-wrapper-${{ hashFiles('./.mvn/wrapper/maven-wrapper.properties') }} + restore-keys: ${{ runner.os }}-maven-wrapper + + - name: Cache Maven Repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2-repository + + # See https://github.com/actions/checkout/issues/13 + - name: Configure Git User + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Maven Release (dry-run) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + cd maven-plugin/ && ./mvnw -DdryRun=true --batch-mode release:prepare release:perform -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN + + - name: Maven Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + cd maven-plugin/ && ./mvnw --batch-mode release:prepare release:perform -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN diff --git a/.github/workflows/publish.yml b/.github/workflows/publish-sdk.yml similarity index 100% rename from .github/workflows/publish.yml rename to .github/workflows/publish-sdk.yml diff --git a/.gitignore b/.gitignore index 50f49f6f..dc934cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Project +*.private.env.json /.idea *.iml diff --git a/README.md b/README.md index 9896e71a..63985f41 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,13 @@ -# Contrast Java SDK +# Contrast SDK Repo -[![javadoc](https://javadoc.io/badge2/com.contrastsecurity/contrast-sdk-java/javadoc.svg)](https://javadoc.io/doc/com.contrastsecurity/contrast-sdk-java) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.contrastsecurity/contrast-sdk-java/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.contrastsecurity/contrast-sdk-java) +Root repository for the Contrast SDK, Contrast Gradle Plugin, and Contrast Maven Plugin +Each sub-project is a standalone build, with their own maven/gradle builds. -This SDK gives you a quick start for programmatically accessing the [Contrast REST API](https://api.contrastsecurity.com/) using Java. +[SDK](sdk/README.md) -## Requirements +[Maven Plugin](maven-plugin/README.md) -* JDK 1.8 -* Contrast Account -## How to use this SDK - -1. Add the - [contrast-sdk-java](https://search.maven.org/artifact/com.contrastsecurity/contrast-sdk-java) - dependency from Maven Central to your project. -1. At a minimum, you will need to supply four basic connection parameters ([find them here](https://docs.contrastsecurity.com/en/personal-keys.html)): - * Username - * API Key - * Service Key - * Contrast REST API URL (e.g. https://app.contrastsecurity.com/Contrast/api) - - -## Example - -```java -ContrastSDK contrastSDK = new ContrastSDK.Builder("contrast_admin", "demo", "demo") - .withApiUrl("http://localhost:19080/Contrast/api") - .build(); - -String orgUuid = contrastSDK.getProfileDefaultOrganizations().getOrganization().getOrgUuid(); - -Applications apps = contrastSDK.getApplications(orgUuid); -for (Application app : apps.getApplications()) { - System.out.println(app.getName() + " (" + app.getCodeShorthand() + " LOC)"); -} -``` - -Sample output: -``` -Aneritx (48K LOC) -Default Web Site (0k LOC) -EnterpriseTPS (48K LOC) -Feynmann (48K LOC) -jhipster-sample (0k LOC) -JSPWiki (48K LOC) -Liferay (48K LOC) -OpenMRS (65K LOC) -OracleFS (48K LOC) -Security Test (< 1K LOC) -Ticketbook (2K LOC) -WebGoat (48K LOC) -WebGoat7 (106K LOC) -``` - - -## Building - -Requires JDK 11 to build - -Use `./mvnw verify` to build and test changes to the project - - -### Formatting - -To avoid distracting white space changes in pull requests and wasteful bickering -about format preferences, Contrast uses the google-java-format opinionated Java -code formatter to automatically format all code to a common specification. - -Developers are expected to configure their editors to automatically apply this -format (plugins exist for both IDEA and Eclipse). Alternatively, developers can -apply the formatting before committing changes using the Maven plugin: - -```shell -./mvnw spotless:apply -``` diff --git a/gradle-plugin/.gitignore b/gradle-plugin/.gitignore new file mode 100644 index 00000000..dc934cd0 --- /dev/null +++ b/gradle-plugin/.gitignore @@ -0,0 +1,271 @@ +# Project +*.private.env.json +/.idea +*.iml + +# Maven + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + + +# Java + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +# JetBrains + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + + +# macOS + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# Windows + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# Linux + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +# Eclipse + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.classpath + +# VSCode +.vscode/ + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +.project + +# need gradle wrapper, and jar files are usually ignored +!gradle-plugin/gradle/wrapper/gradle-wrapper.jar +# Gradle build directories +gradle-plugin/.gradle +gradle-plugin/build + diff --git a/maven-plugin/.gitignore b/maven-plugin/.gitignore new file mode 100644 index 00000000..f25d97d3 --- /dev/null +++ b/maven-plugin/.gitignore @@ -0,0 +1,260 @@ +# Project +*.private.env.json +/.idea +*.iml + +# Maven + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + + +# Java + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +# JetBrains + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + + +# macOS + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# Windows + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# Linux + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +# Eclipse + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +.project + diff --git a/maven-plugin/.mvn/wrapper/MavenWrapperDownloader.java b/maven-plugin/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..b901097f --- /dev/null +++ b/maven-plugin/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/maven-plugin/.mvn/wrapper/maven-wrapper.properties b/maven-plugin/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..db95c131 --- /dev/null +++ b/maven-plugin/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/maven-plugin/CHANGELOG.md b/maven-plugin/CHANGELOG.md new file mode 100644 index 00000000..0ecb611b --- /dev/null +++ b/maven-plugin/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) + +## [2.13.2] - 2022-01-24 +### Changed +- `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. + +## [2.13.1] - 2021-08-31 +### Added +- Contrast Scan support + +### Removed +- `profile` configuration which the Contrast server has not supported since before 3.7.7. +- support for JRE 1.7. Requires minimum JRE 1.8 + + +## [2.12] - 2021-03-09 +### Changed +- Tested with JDK 1.8, 11, and 15 +- Targets JRE 1.7 +- Maven version > 3.6.1 (Released April 2019) is required to build the plugin + + +## [2.0] - 2018-05-15 +### Added +- Vulnerabilities now reconciled using an app version instead of a timestamp +- App version can be generated using `$TRAVIS_BUILD_NUMBER` or `$CIRCLE_BUILD_NUM` +- Source packaging changed to `com.contrastsecurity.maven.plugin` \ No newline at end of file diff --git a/maven-plugin/LICENSE b/maven-plugin/LICENSE new file mode 100644 index 00000000..29b34d85 --- /dev/null +++ b/maven-plugin/LICENSE @@ -0,0 +1,14 @@ +Copyright 2021 Contrast Security, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/maven-plugin/README.md b/maven-plugin/README.md new file mode 100644 index 00000000..00380c85 --- /dev/null +++ b/maven-plugin/README.md @@ -0,0 +1,71 @@ +# Contrast Maven Plugin + +Maven plugin for including Contrast security analysis in Java web applications + +See [usage](https://contrastsecurity.dev/contrast-maven-plugin/usage.html) to get started + +Available on [Maven Central](https://search.maven.org/search?q=a:contrast-maven-plugin) + + +## Building + +Requires JDK 21 to build. + +Tests require JDK 11 and 17 to be set-up in [Maven +Toolchains](https://maven.apache.org/guides/mini/guide-using-toolchains.html) +and requires that Maven be on the `PATH`. + +Use `./mvnw verify` to build and test changes to the project + + +### Formatting + +To avoid distracting white space changes in pull requests and wasteful bickering +about format preferences, Contrast uses the google-java-format opinionated Java +code formatter to automatically format all code to a common specification. + +Developers are expected to configure their editors to automatically apply this +format (plugins exist for both IDEA and Eclipse). Alternatively, developers can +apply the formatting before committing changes using the Maven plugin: + +```shell +./mvnw spotless:apply +``` + + +### End-to-End Testing + +By default, the integration tests simulate the Contrast API using a stub web server. Developers can +change this behavior to test with an actual Contrast instance instead of the stub. + +First, configure your environment with standard Contrast connection environment variables. + +```shell +export CONTRAST__API__URL=https://app.contrastsecurity.com/Contrast/api +export CONTRAST__API__USER_NAME= +export CONTRAST__API__API_KEY= +export CONTRAST__API__SERVICE_KEY= +export CONTRAST__API__ORGANIZATION_ID= +``` + +You may find it useful to store the environment variable configuration in a file so that you can +easily include it in your environment + +```shell +source ~/contrast.env +``` + +Having configured the environment, you can run the integration tests with the `end-to-end-test` +profile active: + +```shell +./mvnw -Pend-to-end-test verify +``` + +When you are finished testing, you may want to remove the variables from the +environment. In a POSIX shell, the script `unset-contrast.env` can take care of +this: + +```shell +source unset-contrast.env +``` diff --git a/maven-plugin/mvnw b/maven-plugin/mvnw new file mode 100755 index 00000000..5643201c --- /dev/null +++ b/maven-plugin/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/maven-plugin/mvnw.cmd b/maven-plugin/mvnw.cmd new file mode 100644 index 00000000..23b7079a --- /dev/null +++ b/maven-plugin/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/maven-plugin/pom.xml b/maven-plugin/pom.xml new file mode 100644 index 00000000..2043322d --- /dev/null +++ b/maven-plugin/pom.xml @@ -0,0 +1,721 @@ + + + 4.0.0 + + com.contrastsecurity + contrast-maven-plugin + 2.13.3-SNAPSHOT + maven-plugin + + Contrast Maven Plugin + + Maven plugin for including Contrast security analysis in Java web applications + + https://docs.contrastsecurity.com/en/maven.html + + + Contrast Security, Inc. + https://contrastsecurity.com + + + + + Contrast Security + support@contrastsecurity.com + + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + github + scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git + + + + + scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git + + scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git + + scm:git:https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git + + HEAD + + + + UTF-8 + ${project.build.directory}/test-repository + 5.11.0 + 5.11.0 + 1.6.3 + + + + + org.apache.maven + maven-plugin-api + 3.5.3 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.1 + provided + + + com.contrastsecurity + contrast-sdk-java + 3.3 + + + org.apache.maven + maven-project + 2.2.1 + + + org.apache.maven + maven-settings + 2.2.1 + + + org.apache.maven + maven-model + 3.5.3 + + + org.codehaus.plexus + plexus-utils + 3.4.2 + + + + com.google.auto.value + auto-value-annotations + ${auto-value.version} + provided + + + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + 5.13.0 + test + + + + org.junit.vintage + junit-vintage-engine + ${junit-vintage-engine.version} + test + + + junit + junit + 4.13.2 + test + + + com.github.stefanbirkner + system-rules + 1.17.0 + test + + + org.apache.maven.shared + maven-verifier + 1.2 + test + + + org.assertj + assertj-core + 3.20.2 + test + + + + + + + + + src/test/resources/it + true + ${project.build.directory}/test-classes/it + + **/pom.xml + + + + + + + maven-compiler-plugin + + 8 + + + com.google.auto.value + auto-value + ${auto-value.version} + + + + + + org.codehaus.mojo + license-maven-plugin + + + + update-generated-sources + + update-file-header + + process-test-sources + + ${project.build.directory}/generated-sources + + + + + + maven-plugin-plugin + + contrast + true + + + + maven-source-plugin + + + attach-sources + package + + jar-no-fork + + + + + + maven-javadoc-plugin + + 11 + false + + + + attach-javadocs + package + + jar + + + + + + maven-release-plugin + + release + + + + maven-deploy-plugin + + + deploy + deploy + + deploy + + + + + + maven-site-plugin + + + true + + + + stage-site + site-deploy + + stage + + + + + + org.codehaus.mojo + templating-maven-plugin + + + filtering-java-templates + + filter-sources + + + + + + maven-scm-publish-plugin + 3.1.0 + + gh-pages + ${env.GITHUB_ACTOR} + ${env.GITHUB_TOKEN} + + + + site-deploy + site-deploy + + publish-scm + + + + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-source-plugin + 3.2.1 + + + maven-resources-plugin + 3.2.0 + + + maven-compiler-plugin + 3.8.1 + + + maven-surefire-plugin + 3.5.0 + + + 11 + + + + + maven-failsafe-plugin + 3.5.0 + + + + junit:junit + org.junit.vintage:junit-vintage-engine + + + + + + maven-jar-plugin + 3.2.0 + + + maven-plugin-plugin + 3.6.1 + + + maven-javadoc-plugin + 3.3.0 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-site-plugin + 3.9.1 + + + maven-gpg-plugin + 3.0.1 + + + maven-project-info-reports-plugin + 3.1.2 + + + maven-release-plugin + 2.5.3 + + + maven-scm-plugin + 1.11.2 + + + org.codehaus.mojo + templating-maven-plugin + 1.0.0 + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + pom.xml + README.md + .github/workflows/**/*.yml + src/**/*.java + src/**/*.xml + src/**/*.properties + + + + + + + + 1.17.0 + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + apache_v2 + 2021 + + **/*.json + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + + + + + + + + maven-plugin-plugin + + + + + + developer + + true + + + + + org.codehaus.mojo + license-maven-plugin + + + + update-file-header + + process-test-sources + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + apply + + process-test-sources + + + + + + + + ci + + + env.CI + + + + + + org.codehaus.mojo + license-maven-plugin + + + + check-file-header + + process-test-sources + + true + true + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + check + + process-test-sources + + + + + + + + end-to-end-test + + + + ${env.CI} + true + + + + + + + maven-dependency-plugin + + + install-to-test-repository + pre-integration-test + + copy + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + ${project.groupId} + ${project.artifactId} + ${project.version} + pom + + + + ${contrast.test-repository}/com/contrastsecurity/${project.artifactId}/${project.version} + + + + + retrieve-agent-for-testing + pre-integration-test + + copy + + + + + com.contrastsecurity + contrast-agent + + 3.8.5.20387 + + + true + + ${project.build.testOutputDirectory} + + + + + analyze-dependencies + test + + analyze-only + + + true + + true + + + + + + maven-failsafe-plugin + + + jdk-11 + + integration-test + verify + + + + 11 + + + + + jdk-17 + + integration-test + verify + + + + 17 + + + + + jdk-21 + + integration-test + verify + + + + 21 + + + + + + + ${contrast.test-repository} + ${env.CONTRAST__API__URL} + ${env.CONTRAST__API__USER_NAME} + ${env.CONTRAST__API__API_KEY} + ${env.CONTRAST__API__SERVICE_KEY} + + ${env.CONTRAST__API__ORGANIZATION_ID} + + + + + + + + + release + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + diff --git a/maven-plugin/scan.http b/maven-plugin/scan.http new file mode 100644 index 00000000..c6b58d30 --- /dev/null +++ b/maven-plugin/scan.http @@ -0,0 +1,127 @@ +# scan.http +# HTTP file for experimenting with the scan API +# To use this file with IntelliJ, create a file http-client.private.env.json with configuration for +# connecting to your Contrast instance e.g. +# { +# "production": { +# "url": "https://app.contrastsecurity.com/Contrast/api", +# "apiKey": "", +# "authorization": "", +# "organizationId": "" +# } +# } + + +### Create Scan Project +POST {{ url }}/sast/organizations/{{ organizationId }}/projects +API-Key: {{ apiKey }} +Authorization: {{ authorization }} +Content-Type: application/json + +{ + "name": "spring-test-application", + "language": "JAVA", + "includeNamespaceFilters": [], + "excludeNamespaceFilters": [] +} + +> {% client.global.set("projectId", response.body.id); %} + + +### DELETE Scan Project +DELETE {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }} +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Find Scan Project +GET {{ url }}/sast/organizations/{{ organizationId }}/projects?name=spring-test-application&archived=false&unique=true +API-Key: {{ apiKey }} +Authorization: {{ authorization }} +Accept: application/json + +> {% client.global.set("projectId", response.body.content[0].id); %} + + +### Upload Code Artifact + +POST {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/code-artifacts +API-Key: {{ apiKey }} +Authorization: {{ authorization }} +Content-Type: multipart/form-data; boundary=WebAppBoundary + +--WebAppBoundary +Content-Disposition: form-data; name="filename"; filename="spring-test-application-0.0.1-SNAPSHOT.jar" +Content-Type: application/java-archive + +< ./target/test-classes/it/spring-boot/target/spring-test-application-0.0.1-SNAPSHOT.jar +--WebAppBoundary-- + +> {% client.global.set("artifactId", response.body.id); %} + + +### Start Scan + +POST {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans +API-Key: {{ apiKey }} +Authorization: {{ authorization }} +Content-Type: application/json + +{ + "codeArtifactId": "{{ artifactId }}", + "label": "scan.http" +} + +> {% client.global.set("scanId", response.body.id); %} + + +### Cancel Scan + +PUT {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }} +API-Key: {{ apiKey }} +Authorization: {{ authorization }} +Content-Type: application/json + +{ + "label": "scan.http", + "status": "CANCELLED" +} + + +### Get Scan + +GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }} +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Get Scan Results + +GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/result-instances +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Get Scan Results (abreviated) + +GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/result-instances/info +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Get Scan Results Summary +GET {{ url }}/sast/organizations/{{ organizationId}}/projects/{{ projectId }}/scans/{{ scanId }}/summary +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Get Project Results Summary +GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/summary +API-Key: {{ apiKey }} +Authorization: {{ authorization }} + + +### Get Scan Results in SARIF +GET {{ url }}/sast/organizations/{{ organizationId }}/projects/{{ projectId }}/scans/{{ scanId }}/raw-output +API-Key: {{ apiKey }} +Authorization: {{ authorization }} \ No newline at end of file diff --git a/maven-plugin/src/main/java-templates/com/contrastsecurity/maven/plugin/Version.java b/maven-plugin/src/main/java-templates/com/contrastsecurity/maven/plugin/Version.java new file mode 100644 index 00000000..aedb5909 --- /dev/null +++ b/maven-plugin/src/main/java-templates/com/contrastsecurity/maven/plugin/Version.java @@ -0,0 +1,31 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** Constants that describe this artifact. */ +public final class Version { + + /** Version of this contrast-maven-plugin */ + public static final String VERSION = "${project.version}"; + + /** static members only */ + private Version() {} +} diff --git a/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractAssessMojo.java b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractAssessMojo.java new file mode 100644 index 00000000..c6fdab27 --- /dev/null +++ b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractAssessMojo.java @@ -0,0 +1,98 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Parameter; + +/** Abstract mojo for mojos that report Assess data to a Contrast instrumented application */ +abstract class AbstractAssessMojo extends AbstractContrastMojo { + + /** + * As near as I can tell, there doesn't appear to be any way to share data between Mojo phases. + * However, we need to compute the appVersion in the install phase and then use the + * computedAppVersion in the verify phase. Setting the field to static is the only way I found for + * it to work. + * + * @deprecated [JG] the Maven solution for the aforementioned issue is to save the information to + * the file system between goals, so we will deprecate this static field in favor of that + * approach + */ + @Deprecated static String computedAppVersion; + + /** + * Override the reported application name. + * + *

On Java systems where multiple, distinct applications may be served by a single process, + * this configuration causes the agent to report all discovered applications as one application + * with the given name. + */ + @Parameter(property = "appName") + private String appName; + + /** + * ID of the application as seen in Contrast. Either the {@code appId} or {@code appName} is + * required. If both are specified, Contrast uses the {@code appId} and ignores the {@code + * appName}. + * + * @since 2.5 + */ + @Parameter(property = "appId") + private String appId; + + /** Overrides the reported server name */ + @Parameter(property = "serverName") + private String serverName; + + void verifyAppIdOrNameNotNull() throws MojoFailureException { + if (appId == null && appName == null) { + throw new MojoFailureException( + "Please specify appId or appName in the plugin configuration."); + } + } + + String getAppName() { + return appName; + } + + /** For testing. Maven will set the field directly */ + void setAppName(String appName) { + this.appName = appName; + } + + String getAppId() { + return appId; + } + + /** For testing. Maven will set the field directly */ + void setAppId(String appId) { + this.appId = appId; + } + + String getServerName() { + return serverName; + } + + /** For testing. Maven will set the field directly */ + void setServerName(String serverName) { + this.serverName = serverName; + } +} diff --git a/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractContrastMojo.java b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractContrastMojo.java new file mode 100644 index 00000000..083012d1 --- /dev/null +++ b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/AbstractContrastMojo.java @@ -0,0 +1,240 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.sdk.ContrastSDK; +import com.contrastsecurity.sdk.UserAgentProduct; +import java.net.Authenticator; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.Proxy; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.settings.Settings; + +/** + * Abstract mojo for mojos that need to connect to Contrast. Handles authentication, organization + * selection (multi-tenancy), and proxy configuration. + * + *

Extensions of this class use the {@link #connectToContrast()} to obtain an instance of the + * {@link ContrastSDK} with which they can make requests to Contrast. + */ +abstract class AbstractContrastMojo extends AbstractMojo { + + @Parameter(defaultValue = "${settings}", readonly = true) + private Settings settings; + + @Parameter(defaultValue = "${maven.version}", readonly = true) + private String mavenVersion; + + /** + * User name for communicating with Contrast. Agent users lack permissions required by this + * plugin. Find your personal + * keys + */ + @Parameter(alias = "username", required = true) + private String userName; + + /** + * API Key for communicating with Contrast. Find your personal keys + */ + @Parameter(property = "apiKey", required = true) + private String apiKey; + + /** + * Service Key for communicating with Contrast. Find your personal keys + */ + @Parameter(property = "serviceKey", required = true) + private String serviceKey; + + /** Contrast API URL */ + @Parameter(alias = "apiUrl", defaultValue = "https://app.contrastsecurity.com/Contrast/api") + private String url; + + /** + * Unique ID for the Contrast Organization to which the plugin reports results. Find your Organization ID + */ + // TODO[JG] must this be required? If a user is only in one org, we can look it up using the + // endpoint /ng/profile/organizations + @Parameter(alias = "orgUuid", required = true) + private String organizationId; + + /** + * When true, will override Maven's proxy settings with Contrast Maven plugin specific proxy + * configuration + * + * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of + * standard Maven proxy configuration + * @since 2.8 + */ + @Deprecated + @Parameter(property = "useProxy", defaultValue = "false") + private boolean useProxy; + + /** + * Proxy host used to communicate to Contrast when {@code useProxy} is true + * + * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of + * standard Maven proxy configuration + * @since 2.8 + */ + @Deprecated + @Parameter(property = "proxyHost") + private String proxyHost; + + /** + * Proxy port used to communicate to Contrast when {@code useProxy} is true + * + * @deprecated in a future release, we will remove the proprietary proxy configuration in favor of + * standard Maven proxy configuration + * @since 2.8 + */ + @Deprecated + @Parameter(property = "proxyPort") + private int proxyPort; + + String getMavenVersion() { + return mavenVersion; + } + + void setMavenVersion(final String mavenVersion) { + this.mavenVersion = mavenVersion; + } + + String getUserName() { + return userName; + } + + void setUserName(final String userName) { + this.userName = userName; + } + + String getApiKey() { + return apiKey; + } + + void setApiKey(final String apiKey) { + this.apiKey = apiKey; + } + + String getServiceKey() { + return serviceKey; + } + + void setServiceKey(final String serviceKey) { + this.serviceKey = serviceKey; + } + + String getURL() { + return url; + } + + void setURL(final String url) { + this.url = url; + } + + String getOrganizationId() { + return organizationId; + } + + void setOrganizationId(final String organizationId) { + this.organizationId = organizationId; + } + + /** + * @return new ContrastSDK configured to connect with the authentication and proxy parameters + * defined by this abstract mojo + * @throws MojoFailureException when fails to connect to Contrast + */ + ContrastSDK connectToContrast() throws MojoFailureException { + final Proxy proxy = getProxy(); + final UserAgentProduct maven = getUserAgentProduct(); + try { + return new ContrastSDK.Builder(userName, serviceKey, apiKey) + .withApiUrl(url) + .withProxy(proxy) + .withUserAgentProduct(maven) + .build(); + } catch (final IllegalArgumentException e) { + throw new MojoFailureException( + "\n\nWe couldn't connect to Contrast at this address [" + url + "]. The error is: ", e); + } + } + + /** + * visible for testing + * + * @return {@link UserAgentProduct} for the contrast-maven-plugin + */ + final UserAgentProduct getUserAgentProduct() { + final String comment = "Apache Maven " + mavenVersion; + return UserAgentProduct.of("contrast-maven-plugin", Version.VERSION, comment); + } + + private Proxy getProxy() throws MojoFailureException { + Proxy proxy = Proxy.NO_PROXY; + final org.apache.maven.settings.Proxy proxySettings = settings.getActiveProxy(); + if (useProxy) { + getLog().info(String.format("Using a proxy %s:%s", proxyHost, proxyPort)); + if (proxyHost != null && proxyPort != 0) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } else { + throw new MojoFailureException( + "When useProxy is true, proxyHost and proxyPort is required."); + } + } else if (proxySettings != null) { + getLog() + .info( + String.format( + "Using a proxy %s:%s", proxySettings.getHost(), proxySettings.getPort())); + proxy = + new Proxy( + Proxy.Type.HTTP, + new InetSocketAddress(proxySettings.getHost(), proxySettings.getPort())); + + if (proxySettings.getUsername() != null || proxySettings.getPassword() != null) { + Authenticator.setDefault( + new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + if (getRequestorType() == RequestorType.PROXY + && getRequestingHost().equalsIgnoreCase(proxySettings.getHost()) + && proxySettings.getPort() == getRequestingPort()) { + return new PasswordAuthentication( + proxySettings.getUsername(), + proxySettings.getPassword() == null + ? null + : proxySettings.getPassword().toCharArray()); + } else { + return null; + } + } + }); + } + } + + return proxy; + } +} diff --git a/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojo.java b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojo.java new file mode 100644 index 00000000..99c820fa --- /dev/null +++ b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojo.java @@ -0,0 +1,408 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.exceptions.UnauthorizedException; +import com.contrastsecurity.models.AgentType; +import com.contrastsecurity.models.Applications; +import com.contrastsecurity.sdk.ContrastSDK; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.FileUtils; + +/** + * Includes the Contrast Java agent in integration testing to provide Contrast Assess runtime + * security analysis. + */ +@Mojo(name = "install", defaultPhase = LifecyclePhase.PROCESS_SOURCES, requiresOnline = true) +public final class ContrastInstallAgentMojo extends AbstractAssessMojo { + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + /** + * When {@code true}, will not alter the Maven {@code argLine} property. + * + * @since 2.0 + */ + @Parameter(property = "skipArgLine") + boolean skipArgLine; + + /** + * When "true", will configure Contrast to treat this as a standalone application (e.g. one that + * uses an embedded web server vs war packaging). + * + * @since 2.2 + */ + @Parameter(property = "standalone") + boolean standalone; + + /** + * Override the reported server environment {@see + * https://docs.contrastsecurity.com/en/server-configuration.html}. + * + * @since 2.9 + */ + @Parameter(property = "environment") + private String environment; + + /** + * Override the reported server path. Default is the present working directory of the JVM process + * Contrast is attached to. + * + *

In a multi-module build, the default value may lead Contrast to report a unique server per + * module. Multi-module Maven builds can appear as different servers in the Contrast UI. If you + * would like to discourage this behavior and would rather see all modules appear under the same + * server in Contrast, use this property to set a common server path across modules. + * + * @since 2.1 + */ + @Parameter(property = "serverPath") + String serverPath; + + /** + * Path to an existing Contrast Java agent JAR. Specifying this configures the plugin to omit the + * "retrieve Contrast JAR" step. + */ + @Parameter(property = "jarPath") + private String jarPath; + + /** + * Define a set of key=value pairs (which conforms to RFC 2253) for specifying user-defined + * metadata associated with the application. The set must be formatted as a comma-delimited list. + * of {@code key=value} pairs. + * + *

Example - "business-unit=accounting, office=Baltimore" + * + * @since 2.9 + */ + @Parameter(property = "applicationSessionMetadata") + private String applicationSessionMetadata; + + /** + * Tags to apply to the Contrast application. Must be formatted as a comma-delimited list. + * + * @since 2.9 + */ + @Parameter(property = "applicationTags") + private String applicationTags; + + /** + * The {@code appVersion} metadata associated with Contrast analysis findings. Allows users to + * compare vulnerabilities between applications versions, CI builds, etc. Contrast generates the + * appVersion in the following order: + * + *

    + *
  1. The {@code appVersion} as configured in the plugin properties. + *
  2. If your build is running in TravisCI, Contrast will use {@code + * appName-$TRAVIS_BUILD_NUMBER}. + *
  3. If your build is running in CircleCI, Contrast will use {@code + * appName-$CIRCLE_BUILD_NUM}. + *
  4. If none of the above apply, Contrast will use a timestamp {@code appName-yyyyMMddHHmmss} + * format. + *
+ */ + @Parameter(property = "appVersion") + String appVersion; + + /** visible for testing */ + String contrastAgentLocation; + + String applicationName; + + static Map environmentToSessionMetadata = new TreeMap<>(); + + static { + // Jenkins git plugin environment variables + environmentToSessionMetadata.put("GIT_BRANCH", "branchName"); + environmentToSessionMetadata.put("GIT_COMMITTER_NAME", "committer"); + environmentToSessionMetadata.put("GIT_COMMIT", "commitHash"); + environmentToSessionMetadata.put("GIT_URL", "repository"); + environmentToSessionMetadata.put("GIT_URL_1", "repository"); + + // CI build number environment variables + environmentToSessionMetadata.put("BUILD_NUMBER", "buildNumber"); + environmentToSessionMetadata.put("TRAVIS_BUILD_NUMBER", "buildNumber"); + environmentToSessionMetadata.put("CIRCLE_BUILD_NUM", "buildNumber"); + } + + public void execute() throws MojoFailureException { + verifyAppIdOrNameNotNull(); + getLog().info("Attempting to connect to Contrast and install the Java agent."); + + ContrastSDK contrast = connectToContrast(); + + Path agent = installJavaAgent(contrast); + contrastAgentLocation = agent.toAbsolutePath().toString(); + + getLog().info("Agent downloaded."); + + if (getAppId() != null) { + applicationName = getAppName(contrast, getAppId()); + + if (getAppName() != null) { + getLog().info("Using 'appId' property; 'appName' property is ignored."); + } + + } else { + applicationName = getAppName(); + } + project + .getProperties() + .setProperty( + "argLine", + buildArgLine(project.getProperties().getProperty("argLine"), applicationName)); + + for (Plugin plugin : (List) project.getBuildPlugins()) { + if ("org.springframework.boot".equals(plugin.getGroupId()) + && "spring-boot-maven-plugin".equals(plugin.getArtifactId())) { + getLog().debug("Found the spring-boot-maven-plugin, with configuration:"); + String configuration = plugin.getConfiguration().toString(); + getLog().debug(configuration); + if (configuration.contains("${argLine}")) { + getLog().info("Skipping set of -Drun.jvmArguments as it references ${argLine}"); + } else { + String jvmArguments = + buildArgLine( + project.getProperties().getProperty("run.jvmArguments"), applicationName); + getLog().info(String.format("Setting -Drun.jvmArguments=%s", jvmArguments)); + project.getProperties().setProperty("run.jvmArguments", jvmArguments); + } + + break; + } + } + } + + private String getAppName(ContrastSDK contrastSDK, String applicationId) + throws MojoFailureException { + Applications applications; + try { + final String organizationID = getOrganizationId(); + applications = contrastSDK.getApplication(organizationID, applicationId); + } catch (Exception e) { + String logMessage; + if (e.getMessage().contains("403")) { + logMessage = + "\n\n Unable to find the application on Contrast with the id [" + applicationId + "]\n"; + } else { + logMessage = + "\n\n Unable to retrieve the application list from Contrast. Please check Contrast connection configuration\n"; + } + throw new MojoFailureException(logMessage, e); + } + if (applications.getApplication() == null) { + throw new MojoFailureException( + "\n\nApplication with id '" + + applicationId + + "' not found. Make sure this application appears in Contrast under the 'Applications' tab.\n"); + } + return applications.getApplication().getName(); + } + + String computeAppVersion(Date currentDate) { + if (computedAppVersion != null) { + return computedAppVersion; + } + + if (appVersion != null) { + getLog().info("Using user-specified app version [" + appVersion + "]"); + computedAppVersion = appVersion; + return computedAppVersion; + } + + String travisBuildNumber = System.getenv("TRAVIS_BUILD_NUMBER"); + String circleBuildNum = System.getenv("CIRCLE_BUILD_NUM"); + + final String appVersionQualifier; + if (travisBuildNumber != null) { + getLog() + .info( + "Build is running in TravisCI. We'll use TRAVIS_BUILD_NUMBER [" + + travisBuildNumber + + "]"); + appVersionQualifier = travisBuildNumber; + } else if (circleBuildNum != null) { + getLog() + .info( + "Build is running in CircleCI. We'll use CIRCLE_BUILD_NUM [" + circleBuildNum + "]"); + appVersionQualifier = circleBuildNum; + } else { + getLog().info("No CI build number detected, we'll use current timestamp."); + appVersionQualifier = new SimpleDateFormat("yyyyMMddHHmmss").format(currentDate); + } + if (getAppId() != null) { + computedAppVersion = applicationName + "-" + appVersionQualifier; + } else { + computedAppVersion = getAppName() + "-" + appVersionQualifier; + } + + return computedAppVersion; + } + + String computeSessionMetadata() { + List metadata = new ArrayList<>(); + + for (Map.Entry entry : environmentToSessionMetadata.entrySet()) { + String environmentValue = System.getenv(entry.getKey()); + + if (environmentValue != null) { + metadata.add(String.format("%s=%s", entry.getValue(), environmentValue)); + } + } + + return String.join(",", metadata); + } + + String buildArgLine(String currentArgLine) { + return buildArgLine(currentArgLine, getAppName()); + } + + String buildArgLine(String currentArgLine, String applicationName) { + + if (currentArgLine == null) { + getLog().info("Current argLine is null"); + currentArgLine = ""; + } else { + getLog().info("Current argLine is [" + currentArgLine + "]"); + } + + if (skipArgLine) { + getLog().info("skipArgLine is set to false."); + getLog() + .info( + "You will need to configure the Maven argLine property manually for the Contrast agent to work."); + return currentArgLine; + } + + getLog().info("Configuring argLine property."); + + computedAppVersion = computeAppVersion(new Date()); + + StringBuilder argLineBuilder = new StringBuilder(); + argLineBuilder.append(currentArgLine); + argLineBuilder.append(" -javaagent:").append(contrastAgentLocation); + final String serverName = getServerName(); + if (serverName != null) { + argLineBuilder.append(" -Dcontrast.server=").append(serverName); + } + if (environment != null) { + argLineBuilder.append(" -Dcontrast.env=").append(environment); + } else { + argLineBuilder.append(" -Dcontrast.env=qa"); + } + argLineBuilder.append(" -Dcontrast.override.appversion=").append(computedAppVersion); + argLineBuilder.append(" -Dcontrast.reporting.period=").append("200"); + + String sessionMetadata = computeSessionMetadata(); + if (!sessionMetadata.isEmpty()) { + argLineBuilder + .append(" -Dcontrast.application.session_metadata='") + .append(sessionMetadata) + .append("'"); + } + + if (standalone) { + argLineBuilder.append(" -Dcontrast.standalone.appname=").append(applicationName); + } else { + argLineBuilder.append(" -Dcontrast.override.appname=").append(applicationName); + } + + if (serverPath != null) { + argLineBuilder.append(" -Dcontrast.path=").append(serverPath); + } + + if (applicationSessionMetadata != null) { + argLineBuilder + .append(" -Dcontrast.application.session_metadata='") + .append(applicationSessionMetadata) + .append("'"); + } + + if (applicationTags != null) { + argLineBuilder.append(" -Dcontrast.application.tags=").append(applicationTags); + } + + String newArgLine = argLineBuilder.toString(); + + getLog().info("Updated argLine is " + newArgLine); + return newArgLine.trim(); + } + + Path installJavaAgent(ContrastSDK connection) throws MojoFailureException { + if (jarPath != null) { + getLog().info("Using configured jar path " + jarPath); + final Path agent = Paths.get(jarPath); + if (!Files.exists(agent)) { + throw new MojoFailureException("Unable to load the local Java agent from " + jarPath); + } + getLog().info("Loaded the latest java agent from " + jarPath); + return agent; + } + + getLog().info("No jar path was configured. Downloading the latest contrast.jar..."); + final byte[] bytes; + final String organizationID = getOrganizationId(); + try { + bytes = connection.getAgent(AgentType.JAVA, organizationID); + } catch (IOException e) { + throw new MojoFailureException( + "\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:", + e); + } catch (UnauthorizedException e) { + throw new MojoFailureException( + "\n\nWe contacted Contrast successfully but couldn't authorize with the credentials you provided. The error is:", + e); + } + // Save the jar to the 'target' directory + final Path target = Paths.get(project.getBuild().getDirectory()); + try { + FileUtils.forceMkdir(target.toFile()); + } catch (final IOException e) { + throw new MojoFailureException("Unable to create directory " + target, e); + } + final Path agent = target.resolve(AGENT_NAME); + try { + Files.write(agent, bytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } catch (final IOException e) { + throw new MojoFailureException("Unable to save the latest java agent.", e); + } + getLog().info("Saved the latest java agent to " + agent.toAbsolutePath()); + return agent; + } + + private static final String AGENT_NAME = "contrast.jar"; +} diff --git a/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java new file mode 100644 index 00000000..675b2981 --- /dev/null +++ b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastScanMojo.java @@ -0,0 +1,398 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.exceptions.HttpResponseException; +import com.contrastsecurity.exceptions.UnauthorizedException; +import com.contrastsecurity.sdk.ContrastSDK; +import com.contrastsecurity.sdk.scan.CodeArtifact; +import com.contrastsecurity.sdk.scan.Project; +import com.contrastsecurity.sdk.scan.Projects; +import com.contrastsecurity.sdk.scan.Scan; +import com.contrastsecurity.sdk.scan.ScanSummary; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +/** + * Analyzes the Maven project's artifact with Contrast Scan to provide security insights + * + * @since 2.13.0 + */ +@Mojo( + name = "scan", + defaultPhase = LifecyclePhase.INTEGRATION_TEST, + requiresOnline = true, + threadSafe = true) +public final class ContrastScanMojo extends AbstractContrastMojo { + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject mavenProject; + + /** + * Contrast Scan project unique ID to which the plugin runs new Scans. This will be replaced + * imminently with a project name + */ + @Parameter(property = "project", defaultValue = "${project.name}") + private String projectName; + + /** + * File path of the Java artifact to upload for scanning. By default, uses the path to this + * module's Maven artifact produced in the {@code package} phase. + */ + @Parameter private File artifactPath; + + /** A label to distinguish this scan from others in your project */ + @Parameter(defaultValue = "${project.version}") + private String label; + + /** + * When {@code true}, will wait for and retrieve scan results before completing the goal. + * Otherwise, will start a scan then complete the goal without waiting for Contrast Scan to + * complete. + */ + @Parameter(defaultValue = "" + true) + private boolean waitForResults; + + /** When {@code true}, will output a summary of the scan results to the console (build log). */ + @Parameter(defaultValue = "" + true) + private boolean consoleOutput; + + /** + * File path to where the scan results (in SARIF) + * will be written at the conclusion of the scan. Note: no results are written when {@link + * #waitForResults} is {@code false}. + */ + @Parameter( + defaultValue = + "${project.build.directory}/contrast-scan-reports/contrast-scan-results.sarif.json") + private File outputPath; + + /** + * Maximum time (in milliseconds) to wait for a Scan to complete. Scans that exceed this threshold + * fail this goal. + */ + @Parameter(defaultValue = "" + 5 * 60 * 1000) + private long timeoutMs; + + private ContrastSDK contrast; + + /** visible for testing */ + String getProjectName() { + return projectName; + } + + /** visible for testing */ + void setProjectName(final String projectName) { + this.projectName = projectName; + } + + /** visible for testing */ + boolean isConsoleOutput() { + return consoleOutput; + } + + /** visible for testing */ + void setConsoleOutput(final boolean consoleOutput) { + this.consoleOutput = consoleOutput; + } + + @Override + public void execute() throws MojoFailureException, MojoFailureException { + // initialize plugin + initialize(); + final Projects projects = contrast.scan(getOrganizationId()).projects(); + + // check that file exists + final Path file = artifactPath == null ? findProjectArtifactOrFail() : artifactPath.toPath(); + if (!Files.exists(file)) { + throw new MojoFailureException( + file + + " does not exist. Make sure to bind the scan goal to a phase that will execute after the artifact to scan has been built"); + } + + // get or create project + final Project project = findOrCreateProject(projects); + + // upload code artifact + getLog().info("Uploading " + file.getFileName() + " to Contrast Scan"); + final CodeArtifact codeArtifact; + try { + codeArtifact = project.codeArtifacts().upload(file); + } catch (final IOException | HttpResponseException e) { + throw new MojoFailureException("Failed to upload code artifact to Contrast Scan", e); + } + + // start scan + getLog().info("Starting scan with label " + label); + final Scan scan; + try { + scan = + project.scans().define().withLabel(label).withExistingCodeArtifact(codeArtifact).create(); + } catch (final IOException | HttpResponseException e) { + throw new MojoFailureException("Failed to start scan for code artifact " + codeArtifact, e); + } + + // show link in build log + final URL clickableScanURL = createClickableScanURL(project.id(), scan.id()); + getLog().info("Scan results will be available at " + clickableScanURL.toExternalForm()); + + // optionally wait for results, output summary to console, output sarif to file system + if (waitForResults) { + getLog().info("Waiting for scan results"); + waitForResults(scan); + } + } + + /** + * Inspects the {@link #mavenProject} to find the project's artifact, or fails if no such artifact + * can be found. We may not find an artifact when the user has configured this goal to run before + * the artifact is created, or if the project does not produce an artifact (e.g. a module of type + * {@code pom}). + * + *

By default, some Maven plugins will skip their work instead of failing when inputs are not + * found. For example, the {@code maven-surefire-plugin} default behavior will skip tests if no + * test classes are found (and this may be overridden with configuration). + * + *

So, why does this plugin fail instead of simply skipping its work and logging a warning? + * Because we want the user to avoid one particularly problematic configuration. Users can easily + * mis-configure this plugin in a multi-module build by including it in the parent POM's build + * plugins. In this case, all child modules will inherit this plugin in their builds, and the + * build will scan not just the web application modules, but also the internal dependencies that + * are components of their applications. Contrast Scan is intended to be used on their artifact + * that represents the web application, and users should not scan the components of their web + * application individually. We can detect this mis-configuration by failing when there is no + * artifact, because the build will fail on the parent POM project (since it is of type {@code + * pom}). + * + * @throws MojoFailureException when artifact does not exist + */ + private Path findProjectArtifactOrFail() throws MojoFailureException { + final Artifact artifact = mavenProject.getArtifact(); + final File file = artifact == null ? null : artifact.getFile(); + if (file == null) { + throw new MojoFailureException( + "Project's artifact file has not been set - see https://contrastsecurity.dev/contrast-maven-plugin/troubleshooting/artifact-not-set.html"); + } + return file.toPath(); + } + + /** + * Finds a Scan project with the project name from the plugin configuration, or creates such a + * "Java" project if one does not exist. + * + *

Note: the Scan API does not expose an endpoint for doing this atomically, so it is possible + * that another process creates the project after having determined it to not-exist but before + * attempting to create it. + * + * @param projects project resource collection + * @return existing or new {@link Project} + * @throws MojoFailureException when fails to make request to the Scan API + */ + private Project findOrCreateProject(Projects projects) throws MojoFailureException { + final Optional optional; + try { + optional = projects.findByName(projectName); + } catch (final IOException e) { + throw new MojoFailureException("Failed to retrieve project " + projectName, e); + } catch (final UnauthorizedException e) { + throw new MojoFailureException( + "Authentication failure while retrieving project " + + projectName + + " - verify Contrast connection configuration", + e); + } + if (optional.isPresent()) { + getLog().debug("Found project with name " + projectName); + final Project project = optional.get(); + if (project.archived()) { + // TODO the behavior of tools like this plugin has yet to be defined with respect to + // archived projects; however, the UI exposes no way to archive projects at the moment. + // For now, simply log a warning to help debug this in the future should we encounter this + // case + getLog().warn("Project " + projectName + " is archived"); + } + return project; + } + + getLog().debug("No project exists with name " + projectName + " - creating one"); + try { + return projects.define().withName(projectName).withLanguage("JAVA").create(); + } catch (final IOException | HttpResponseException e) { + throw new MojoFailureException("Failed to create project " + projectName, e); + } + } + + /** + * visible for testing + * + * @return Contrast browser application URL for users to click-through and see their scan results + * @throws MojoFailureException when the URL is malformed + */ + URL createClickableScanURL(final String projectId, final String scanId) + throws MojoFailureException { + final String path = + String.join( + "/", + "", + "Contrast", + "static", + "ng", + "index.html#", + getOrganizationId(), + "scans", + projectId, + "scans", + scanId); + try { + final URL url = new URL(getURL()); + return new URL(url.getProtocol(), url.getHost(), url.getPort(), path); + } catch (final MalformedURLException e) { + throw new MojoFailureException( + "Error building clickable Scan URL. Please contact support@contrastsecurity.com for help", + e); + } + } + + /** + * Waits for the scan to complete, writes results in SARIF to the file system, and optionally + * displays a summary of the results in the console. Translates all errors that could occur while + * retrieving results to one of {@code MojoFailureException} or {@code MojoFailureException}. + * + * @param scan the scan to wait for and retrieve the results of + * @throws MojoFailureException when fails to retrieve scan results for unexpected reasons + * @throws MojoFailureException when the wait for scan results operation times out + */ + private void waitForResults(final Scan scan) throws MojoFailureException, MojoFailureException { + final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + try { + final Path outputFile = outputPath.toPath(); + final Path reportsDirectory = outputFile.getParent(); + try { + Files.createDirectories(reportsDirectory); + } catch (final IOException e) { + throw new MojoFailureException("Failed to create Contrast Scan reports directory", e); + } + + final CompletionStage await = scan.await(scheduler); + final CompletionStage save = + await.thenCompose( + completed -> + CompletableFuture.runAsync( + () -> { + try { + completed.saveSarif(outputFile); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }, + scheduler)); + final CompletionStage output = + await.thenAccept( + completed -> { + final ScanSummary summary; + try { + summary = completed.summary(); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + writeSummaryToConsole(summary, line -> getLog().info(line)); + }); + CompletableFuture.allOf(save.toCompletableFuture(), output.toCompletableFuture()) + .get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (final ExecutionException e) { + // try to unwrap the extraneous ExecutionException + final Throwable cause = e.getCause(); + // ExecutionException should always have a cause, but its constructor does not enforce this, + // so check if the cause is null + final Throwable inner = cause == null ? e : cause; + throw new MojoFailureException("Failed to retrieve Contrast Scan results", inner); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new MojoFailureException("Interrupted while retrieving Contrast Scan results", e); + } catch (final TimeoutException e) { + final Duration duration = Duration.ofMillis(timeoutMs); + final String durationString = + duration.toMinutes() > 0 + ? duration.toMinutes() + " minutes" + : (duration.toMillis() / 1000) + " seconds"; + throw new MojoFailureException( + "Failed to retrieve Contrast Scan results in " + durationString, e); + } finally { + scheduler.shutdown(); + } + } + + /** + * visible for testing + * + * @param summary the scan summary to write to console + * @param consoleLogger describes a console logger where each accepted string is printed to a new + * line + */ + void writeSummaryToConsole(final ScanSummary summary, final Consumer consoleLogger) { + consoleLogger.accept("Scan completed"); + if (consoleOutput) { + consoleLogger.accept("New Results\t" + summary.totalNewResults()); + consoleLogger.accept("Fixed Results\t" + summary.totalFixedResults()); + consoleLogger.accept("Total Results\t" + summary.totalResults()); + } + } + + /** + * Must be called after Maven has completed field injection. + * + *

I don't believe Maven has a post-injection callback that we bind to this method, so the + * {@link #execute()} method calls this before continuing. + * + *

This is useful for tests to initialize the {@link ContrastSDK} without running the whole + * {@link #execute()} method + * + * @throws IllegalStateException when has already been initialized + * @throws MojoFailureException when cannot connect to Contrast + */ + private synchronized void initialize() throws MojoFailureException { + if (contrast != null) { + throw new IllegalStateException("Already initialized"); + } + contrast = connectToContrast(); + } +} diff --git a/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojo.java b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojo.java new file mode 100644 index 00000000..a9837b5f --- /dev/null +++ b/maven-plugin/src/main/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojo.java @@ -0,0 +1,254 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.exceptions.UnauthorizedException; +import com.contrastsecurity.http.RuleSeverity; +import com.contrastsecurity.http.ServerFilterForm; +import com.contrastsecurity.http.TraceFilterForm; +import com.contrastsecurity.models.Application; +import com.contrastsecurity.models.Applications; +import com.contrastsecurity.models.Server; +import com.contrastsecurity.models.Servers; +import com.contrastsecurity.models.Trace; +import com.contrastsecurity.models.Traces; +import com.contrastsecurity.sdk.ContrastSDK; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Verifies that none of the vulnerabilities found by Contrast Assess during integration testing + * violate the project's security policy (fails the build when violations are detected). + */ +@Mojo(name = "verify", requiresOnline = true, defaultPhase = LifecyclePhase.VERIFY) +public final class ContrastVerifyMojo extends AbstractAssessMojo { + + /** + * Verifies that no vulnerabilities were found at this or a higher severity level. Severity levels + * include Note, Low, Medium, High, and Critical. + */ + @Parameter(property = "minSeverity", defaultValue = "Medium") + String minSeverity; + + public void execute() throws MojoFailureException { + verifyAppIdOrNameNotNull(); + ContrastSDK contrast = connectToContrast(); + + getLog().info("Successfully authenticated to Contrast."); + + getLog().info("Checking for new vulnerabilities for appVersion [" + computedAppVersion + "]"); + + final String applicationId; + if (getAppId() != null) { + applicationId = getAppId(); + if (getAppName() != null) { + getLog().info("Using 'appId' property; 'appName' property is ignored."); + } + + } else { + applicationId = getApplicationId(contrast, getAppName()); + } + + List serverIds = null; + + if (getServerName() != null) { + serverIds = getServerId(contrast, applicationId); + } + + TraceFilterForm form = getTraceFilterForm(serverIds); + + getLog().info("Sending vulnerability request to Contrast."); + + Traces traces; + + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + try { + final String organizationID = getOrganizationId(); + traces = contrast.getTraces(organizationID, applicationId, form); + } catch (IOException e) { + throw new MojoFailureException("Unable to retrieve the traces.", e); + } catch (UnauthorizedException e) { + throw new MojoFailureException("Unable to connect to Contrast.", e); + } + + if (traces != null && traces.getCount() > 0) { + getLog().info(traces.getCount() + " new vulnerability(s) were found."); + + for (Trace trace : traces.getTraces()) { + getLog().info(generateTraceReport(trace)); + } + + throw new MojoFailureException( + "Your application is vulnerable. Please see the above report for new vulnerabilities."); + } else { + getLog().info("No new vulnerabilities were found."); + } + + getLog().info("Finished verifying your application."); + } + + TraceFilterForm getTraceFilterForm(List serverIds) { + TraceFilterForm form = new TraceFilterForm(); + form.setSeverities(getSeverityList(minSeverity)); + form.setAppVersionTags(Collections.singletonList(computedAppVersion)); + if (serverIds != null) { + form.setServerIds(serverIds); + } + return form; + } + + /** + * Retrieves the server id by server name + * + * @param sdk Contrast SDK object + * @param applicationId application id to filter on + * @return List id of the servers + * @throws MojoFailureException + */ + private List getServerId(ContrastSDK sdk, String applicationId) + throws MojoFailureException { + ServerFilterForm serverFilterForm = new ServerFilterForm(); + serverFilterForm.setApplicationIds(Arrays.asList(applicationId)); + + Servers servers; + List serverIds; + + final String organizationID = getOrganizationId(); + try { + serverFilterForm.setQ(URLEncoder.encode(getServerName(), "UTF-8")); + servers = sdk.getServersWithFilter(organizationID, serverFilterForm); + } catch (IOException e) { + throw new MojoFailureException("Unable to retrieve the servers.", e); + } catch (UnauthorizedException e) { + throw new MojoFailureException("Unable to connect to Contrast.", e); + } + + if (!servers.getServers().isEmpty()) { + serverIds = new ArrayList(); + for (Server server : servers.getServers()) { + serverIds.add(server.getServerId()); + } + } else { + throw new MojoFailureException( + "\n\nServer with name '" + + getServerName() + + "' not found. Make sure this server name appears in Contrast under the 'Servers' tab.\n"); + } + + return serverIds; + } + + /** + * Retrieves the application id by application name; else null + * + * @param sdk Contrast SDK object + * @param applicationName application name to filter on + * @return String of the application + * @throws MojoFailureException + */ + private String getApplicationId(ContrastSDK sdk, String applicationName) + throws MojoFailureException { + + Applications applications; + + final String organizationID = getOrganizationId(); + try { + applications = sdk.getApplications(organizationID); + } catch (Exception e) { + throw new MojoFailureException( + "\n\nUnable to retrieve the application list from Contrast. Please check Contrast connection configuration\n", + e); + } + + for (Application application : applications.getApplications()) { + if (applicationName.equals(application.getName())) { + return application.getId(); + } + } + + throw new MojoFailureException( + "\n\nApplication with name '" + + applicationName + + "' not found. Make sure this server name appears in Contrast under the 'Applications' tab.\n"); + } + + /** + * Creates a basic report for a Trace object + * + * @param trace Trace object + * @return String report + */ + private String generateTraceReport(Trace trace) { + StringBuilder sb = new StringBuilder(); + sb.append("Trace: "); + sb.append( + trace + .getTitle() + .replaceAll("\\{\\{\\#unlicensed\\}\\}", "(") + .replaceAll("\\{\\{\\/unlicensed\\}\\}", ")")); + sb.append("\nTrace Uuid: "); + sb.append(trace.getUuid()); + sb.append("\nTrace Severity: "); + sb.append(trace.getSeverity()); + sb.append("\nTrace Likelihood: "); + sb.append(trace.getLikelihood()); + sb.append("\n"); + + return sb.toString(); + } + + /** + * Returns the sublist of severities greater than or equal to the configured severity level + * + * @param severity include severity to filter with severity list with + * @return list of severity strings + */ + private static EnumSet getSeverityList(String severity) { + + List severityList = SEVERITIES.subList(SEVERITIES.indexOf(severity), SEVERITIES.size()); + + List ruleSeverities = new ArrayList(); + + for (String severityToAdd : severityList) { + ruleSeverities.add(RuleSeverity.valueOf(severityToAdd.toUpperCase())); + } + + return EnumSet.copyOf(ruleSeverities); + } + + // Severity levels + private static final List SEVERITIES = + Arrays.asList("Note", "Low", "Medium", "High", "Critical"); +} diff --git a/maven-plugin/src/site/markdown/examples/multi-module-projects.md b/maven-plugin/src/site/markdown/examples/multi-module-projects.md new file mode 100644 index 00000000..aba2e1a5 --- /dev/null +++ b/maven-plugin/src/site/markdown/examples/multi-module-projects.md @@ -0,0 +1,69 @@ +## Multi-Module Projects + +Best practices for a multi-module project would have users configure the contrast-maven-plugin with +Contrast connection configuration in the parent POM's `` section. Doing so allows +this configuration to be reused in each child module that uses the contrast-maven-plugin. + +```xml + + + 4.0.0 + com.contrastsecurity + multi-module-example-parent + 0.0.1 + pom + + + + + + com.contrastsecurity + contrast-maven-plugin + + + ${env.CONTRAST__API__URL} + ${env.CONTRAST__API__USER_NAME} + ${env.CONTRAST__API__API_KEY} + ${env.CONTRAST__API__SERVICE_KEY} + ${env.CONTRAST__API__ORGANIZATION_ID} + + + + + + +``` + +In child modules that use contrast-maven-plugin's goals, users may simply include the goal in +their `` section with any module specific configuration: + +```xml + + + 4.0.0 + com.contrastsecurity + multi-module-example-child + 0.0.1 + war + + + + + com.contrastsecurity + contrast-maven-plugin + + + + scan + + + + + + + +``` \ No newline at end of file diff --git a/maven-plugin/src/site/markdown/index.md b/maven-plugin/src/site/markdown/index.md new file mode 100644 index 00000000..60ff2b91 --- /dev/null +++ b/maven-plugin/src/site/markdown/index.md @@ -0,0 +1,20 @@ +## Contrast Maven Plugin + +The Contrast Maven Plugin helps users include one or more Contrast Security analysis features in +their Java web application Maven projects. + +### Goals Overview + +* [contrast:install](install-mojo.html) includes the Contrast Java agent in integration testing to + provide Contrast Assess runtime security analysis. +* [contrast:verify](verify-mojo.html) verifies that none of the vulnerabilities found by Contrast + Assess during integration testing violate the project's security policy (fails the build when + violations are detected). +* [contrast:scan](scan-mojo.html) analyzes the Maven project's artifact with Contrast Scan to find + vulnerabilities using static analysis. + + +### Usage + +General instructions for how to use the Contrast Maven Plugin may be found on +the [usage page](usage.html). \ No newline at end of file diff --git a/maven-plugin/src/site/markdown/troubleshooting/artifact-not-set.md b/maven-plugin/src/site/markdown/troubleshooting/artifact-not-set.md new file mode 100644 index 00000000..d049c90a --- /dev/null +++ b/maven-plugin/src/site/markdown/troubleshooting/artifact-not-set.md @@ -0,0 +1,30 @@ +## Troubleshooting: Artifact Not Set + +This error occurs when there is no project artifact available for the `scan` goal to analyze. This +typically indicates that the `scan` goal has been: + +1. included in a module that does not produce an artifact (e.g. a module of type `pom`). +2. configured to run before the project's artifact has been built. + + +### Only Include in Modules that Produce Artifacts + +The `scan` goal should only be included in modules that produce a build artifact (e.g. a module that +produces a `jar` or `war` file). + +When configuring a [multi-module](https://maven.apache.org/guides/mini/guide-multiple-modules.html) +build, users may erroneously include the `scan` goal in the build of a parent pom, and parent poms +do not produce build artifacts. In a multi-module project, verify that the `scan` goal is only +included in projects that produce a `war` or `jar` artifact. Reference +the [multi-module example](../examples/multi-module-projects.html). + + +### Configure Scan to Run After the Build Produces an Artifact + +Maven typically generates an artifact during the `package` phase. By default, the `scan` goal runs +after the `package` phase during the `verify` phase. + +You may have overridden the plugin's default phase so that the `scan` goal runs during an earlier +phase before the artifact has been built (e.g. the `test` phase). In this case, the `scan` goal will +not be able to find an artifact to scan. Make sure to attach the scan goal to a later phase (such as +the default `verify` phase). diff --git a/maven-plugin/src/site/markdown/usage.md b/maven-plugin/src/site/markdown/usage.md new file mode 100644 index 00000000..91f3ed8b --- /dev/null +++ b/maven-plugin/src/site/markdown/usage.md @@ -0,0 +1,120 @@ +## Usage + +The Contrast Maven Plugin helps users include one or more Contrast Security analysis features in +their Maven projects. All such analysis must connect to Contrast, and therefore each plugin goal +requires Contrast connection parameters. These connection parameters may be externalized to +environment variables: + +```xml + + + + com.contrastsecurity + contrast-maven-plugin + + ${env.CONTRAST__API__USER_NAME} + ${env.CONTRAST__API__API_KEY} + ${env.CONTRAST__API__SERVICE_KEY} + ${env.CONTRAST__API__ORGANIZATION_ID} + + + + +``` + +### Contrast Assess + +[Contrast Assess](https://docs.contrastsecurity.com/en/assess.html) is an application security +testing tool that combines Static (SAST), Dynamic (DAST), and Interactive Application Security +Testing (IAST) approaches, to provide highly accurate and continuous information on security +vulnerabilities in your applications. + +To add Contrast Assess analysis in your Java web application testing, you must attach Contrast Java +agent to your application. The `install` goal retrieves the Contrast Java agent and stores it +in `${project.build.directory}/contrast.jar`. + +The `install` goal automatically detects and configures +the [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html) +, [Maven Failsafe Plugin](https://maven.apache.org/surefire/maven-failsafe-plugin/integration-test-mojo.html) +, +and [Spring Boot Maven Plugin](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/) +to include the Contrast Java agent in their `test`, `integration-test`, and `run` goals +respectively. + +Users may include the `verify` goal to fail their Maven build if Contrast Assess detects +vulnerabilities during the project's integration testing phase that violate the project's security +policies. + +The following example includes the Contrast Java agent in the project's integration testing and +fails the build if any "Medium" vulnerabilities are detected during testing + +```xml + + assess + + + + com.contrastsecurity + contrast-maven-plugin + + + + install + + + + + verify + + + + + + + +``` + +To run the integration tests with security analysis and verification, execute the `verify` lifecycle +phase with this profile activated + +```shell +mvn verify -Passess +``` + + +### Contrast Scan + +Contrast Scan is a static application security testing (SAST) tool that makes it easy for you to +find and remediate vulnerabilities in your Java web applications. + +Use the `scan` goal to find vulnerabilities in your Java web application Maven project using +Contrast Scan. The goal uploads your application package to Contrast Scan for analysis. + +The following example includes the `scan` goal in a `scan` profile + +```xml + + scan + + + + com.contrastsecurity + contrast-maven-plugin + + + + scan + + + + + + + +``` + +To run the scan, execute Maven with the scan profile activated + +```shell +mvn verify -Pscan +``` \ No newline at end of file diff --git a/maven-plugin/src/site/resources/images/contrast-logo.png b/maven-plugin/src/site/resources/images/contrast-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8b2c7564fab8e3901b33666285dc8a8770b678d3 GIT binary patch literal 12730 zcmdUWby!qg*Z0sR-QCU5Idn@(rvk$;14zS=0@5HUA)$059V&v764D_d-Q6Gv0)C*k zcRa7pb3Na8UGEFvD>*gD=a*#Wl<5bw=fjE+wTBqRb7;s*ebpCbQhWg#2kui0iJ z{<4U`nT_;|2AJR4{vDDCX3ojY5Qg(zV>bYRoALI8;8h^)al<>d)iZ({X{duBP7d7Q z+o^JUIXK@U0VKRYH>3j;4yN;Ruy=F=c}X(-kO1A#w`?8;x*sBNJ4ps34J|r(Cs!z) zFgGtZFM|{&9UYy7s}&5SqoDMM`puOjgAE++4C3MO^z`KR6ySDpwdUay6BFa%<>%q& z2i`~k-Mk&)U@xGf8{Oy}__+Caey4Zi?VpN{Zhx%C&ARY-ft_!Zc>kS}hQ@zZb8z^r zrW;(*JP3%Yi>_{9(Y;2I}~qqWlx> zht$6Zv2}EVgB>AI6$PoAtl+k_wE_uRidn&g1R+2X5g}fnAippa2o~ZK1X@91mLh^M zC+q{BW{Zjpd`>$#eJhwf^-uC~iP(W z{bKy9cYmeF|625f!~}(fpu#|YOJ07Upn#|)P)vxQ4=4PIzRs{%wHk-!_?nQ z{JiN}IYDmB{MpJud?H{$QHY=*P)rme3gqV%6as?5mI6Ra2uu(r3K4<{^TB>O^f$2o zVMygSwf0{{>!&Y&Q~w+EU(-~Pm)CN2g4xZ* zJJ~zA-rPu_QUZTh`-}ZstzVAI-ZYcj4(s;M0{+oq<-pee6%Y&+y15jygh7R@tOR&PZ@SO#dc;reKW+Tof&Vl^iQAj+ZPMMO z8u&-Ix>>US!TZDUpN9UnP5R-~PwveoObYWR8Gdc(q%e7B4dDO)i-n4UtezLb?gI>8 zBE|9jtu&Q3d@a0q0s4jNr6q(4(xUD1fClb{iiT`9yo80Lk3JPkfeod(Dx&WU6gu=J z3_j@_YKs{fexM`{d5VmujZBO#25V^gzO8-_l(@P1GLsSErGIl;#qxVY{mN1^{r5il z{dFW;s%q9*147rR#Bl;1i}^tq$EG&g`qe~eL}()|2$I;|#ay>E5gNf-0D%EUtror( zl@`@qG_<5DRkRd+nnXqHZHA8Tz z1u@zw3(m>bSgFAJyC_F8>fv+HM#CJ%BHd`6Ls-T4jneWfkKIBI%Ck?PM)cZ2)wGmg8|F0jA}f z7ZNKE@S|c)@9)pd2*_N97@Zrr%lRC*3(vbsEw2I`Pd;tRwcKPa!M-8X;r zP`@0=zG;SD?9WBifDSym10(Yr{>m>1SAQ777M@0X#i${0>5t@Rh27?~nJ(Q}QRDj7 zCNS*b^JWHCYpMlt@mGG@Cj8DMkFhl#g>NzKXx_iB%sYANPmHJ8UipSM7mIGxYrY_O z@M;8S%|p1#Gmb_%yV}>W%jMv4V9FVT*%}vm91w?>VHkuV57=Wxypx*M%fWHlRom2o z&hTY(yj+}O43*w_AUs9ipy^>ab)=GhT4F7>%jeUt@C)rl_H9m%y_g=|wsAFtfGp-UGo~3bIjDp*A*QD5;lu0 z(q0kN9aA5QrtIa#*1coi@gOo_1)2`IFC?I3S#rBXiA*gSSxC88!QUeBaY&|2)UF3_ zcH>d6m%vx@j%?Zjbv@oVUKwXe`ArF_sL&xI&C&*Nzr>`YZYdZVgEBvbA``b+ks~58 z;(t8r`|xmKaV1d$rz5p3GHJab$1jZQf|>ON$=%Ru7bB9U*aUD;f=)^l>cK?PT6ToE zOLmI3;hRtF!)g)~y#)zg#!~I|>I6j?US773EE7YRlx6D8o&;VFFePeWzGZ3a(aYWT z6~mn6V;8HlV6gGBURhn^R>IfisA}n*_Bq)YR)`x7GOiUNE~kYt#j)hcX_a|~Wkv|6 zXTDXw3MR-<$_?;LI7;=xXD23c zQTuRCC%Bfh?}`lflbvy8V&@}cs(rquUzMjFZ&t+oma~dToMRD@Tq%&0!i%0&W@y!` zl{tM%X5q{$zcMaFE=4(oc)u>bEJE|1ypfY$fQ$om`4iooq-aypPIn$VA0AQ7RZ(2E zHoJ6!L?UUx17^E7Q=o$3pH+pegZOym|!~+`32@p!T7B$hT?FNJ`P4yww7i?C}~{00v05 zOj%~NgiL6m_GP=dx;6+Zbf5ct+2BJb%Y3unno=zfMq*8|lN6llA8zxC29A+aj_2hv zpn3QsZK6r7W&35JP<)S8Mhn`4Pcm>nL219bRy=WxCpo)f%$SNeh!wW5#dCEq3`n!) zcN$6{ZE%j64UJHk;gOk({QU5}3W5Oe?ea)4_E(&NuRCX9*eN6itE;Q#5Mem8SsR() zjQykzl(ClPtG8KDi^sO{f?68IhjNS24+fJt472UT8i@&{Nb(;Jy}i9BI`hcTk)s=4 zzLW>sHZ#zqKMTX2E)p<##qmKlup3}q@HuNc(c?Ypll-l{rsNgJ;$tL&G;-@n@24+&o$mb@M z`X|pGBAXY{YAW|BOVR{>2T-CLH=1M0ekqh3*bC3ur3iVH_u$o=F6D3i9{Rd&jOd*X zk4b1Q_V+K1jeBRNh{-RWw?*0o+9rgJ8<IzBG2;zP`;tm+2wFAYrwe=TSpsq$iWJl`KyXPS|l*Xy$ngr3ihT*hxFOE51@x{pg(0 zYG4zTV@a32tnO){cKZf3=XZD~d04eHa`4l3_{+EMB-Oni&x1F}-gIi(Q$?-nWp(md z(le4>W{~A$5v>==4QEBAX}-m>VoFQ?VkCPR9T6LUmlYS%1!DfZ?6UA3EhMhJ#W%6l z8wGhR@$@9*N>nB$?HOBIy?~H7zT}6x!&7v6=?OD)VXqHW$xOw$y)pPJ;0zj(H{NLXT8n0bUkc2q@DSvIwv zi#&0Qpd&_4?kqiMBuf-^YWp1M;ikN8v`E9Ei=JVZ2VHnUNz$_h#WTWHeHP3IE+{9O zWwUtv+`clmX0|kFBb{G-a$4Ja@ynBsY@HQ&V56_ePYjVQzR46Fn z?<~{bUSTf=GjXe)Nu;R?byX42n#ss0gn~I!?vTIDxApCf#o+UJp@&f|>3?^kBE~~4 z|3EQt8(Swa4;R1RjVHLhXMz6-$8c}&|uXUv>(1&@rwIgQp;or>3up1TAlW+w30_MdC8>OxU!L)Qo6#vECtzzNA(B&dd0f%w^m8eAb7{DSr>%1~{sThlxC+v3U!WbM(P8vUphUcppKi?2Fc=4=UPO}wI2qu9 z!J(?HAU|A>K)~mh=;C##CAJM2yr6`VeMC1~a{3is(2G3(?)B-~b1pqo!|c~&=W(&u z2-|EQRu`Xyr(?PLzq+H;z2B^kOtXI>CO*@EO}Rnga2AM9i6kgrw;cJk8eE6B`w(Cu zO&mxXl7p+!m-G20oo{Ub1{UW`qS*u&v${^9&x4FlKD6&!-Tdgi)bB@!(B|Ox5tkwE z5@ZBx`$W;bLnK@QpiaH8l4c7$lU50l2+`Laivaf#wvOFd{vNWB8(=dYEAW`9eqS+R z-Gf5WAn!8=tTI=i-R2}IJT#OTwbp>0M}tGaf=f420-|DkWH9zgU7@Kl^eC*N^q%9M z7F&!6dMhWLCl2o^W*=tZTO?tbp)N8!SHm*9sT4a-PD3G!@oyHz7g-ziMqu<8oM5bH z#GD#TI7%71+2P`3a2os@-`=7Juk{C2*`g%k8JG;#C3AgD1DlL#gfYPs=6hSjTN5ma z7#oZI3y4o1#DU4_kIm9+3=Su2y3SkiYohUl!do6?5JtVWjV`m@wk%NWfXaUN(9}!U z!xK2fL@zHLYQk0ac0_Y6APO{U5p7z5KB0)x<6pJ{DSFQI`rGge+~WjxhV`zXbtZjm z=?d=X)(kIijzEK`ZKE_NS-e{0a7sl=b?9r+*|awf3wd*_=;69;VyG`hK`HM%!;>J= zU!kxtV^P(2g}J)}`y_^2_4q?vvYYhVMX>_;h`HUQ@%xATZK}avqKbq(-1G;MD`S;h z-U4^FLg`B)-bZs%W3>#P-|el;ELjE2eK}8cAoBT$GJOsSSa<7Ky?n1PeYsG~!7I3{ zI26$5>Qc`l_OUh<4|#m_>w{?VR#!f?L1m044|c_}6#+{{Ve9lA;qzy#muJsR=fz#V z7OrvC+0RumzeOll>P%kIRbOpR>}RF9eBRT;&&ETG6rD5_t*l$nu?CoPPnl$wt0u*g z-Fy>XExPq>%MbmwAsQL}|v27U2{L7YFIzjD~ne#u9C$3;j04B5$u2ZBE4`WN z4ktpr>uL{(&8c9=kEwfKBRi%-2*(QQ-tM~Q^91waaL!t_r@B@y3CQ!kGbki< zVV}>X4;DW$*Grzr-_K;Lfsp4!Zd=UBWtWsFBIj14IlOwXoVtd+7)j=k3CAbnX-j!d zo6xC7h&V9ys{M5P!HG^hG}-Dk<;Md}0%bC)GyDDy_$3lJ#19WoAZE5Mq*dpV^Wh|$^-`b z2HWG~Al6_c@pS2rE&Fy=M&e>X7aMZmjx&Wt7@PB#fs8sgw$gTD=6#&-M-xlbHp-2K z#@cmh@@Nh6v=_zFjn;U9qC!h<$4no6yMUC#@Z(hrpeZuDYAv3zCoklZCg$57dHqTO zX7|#*j0GBweD+|Y=zxm(>tIUz@uM8ZO~t$7Wcx94Z+Va`J?|i(Eq{IC94pA%FZZ&~q>-?CZ$zzLVv_mQk1> zQ)=78^C7nsocj$HQVM-+``$HF3nTZe+DT--Frci(t+)A?P9Fs_HVe6+d_Q;=lWD@H z(0A{7u+V|`T2Wbo(r_9c$c&z1&`8EgRK6t12FJo8;|~3b4`WeixT!g9fb^a{L**k+ ziVai~(f%(V_<$p6iXp^)Duw(+7r0BYZk&WZqvS>!>46#hXR_;L zq0lv(p>DT4h?+Ut*?Zv3j@k3s)Qa9UimCWwOtjI9mI%5Aq*1L33*Hj7d+&CXt2DNu z9-p;X#Aq+@^WycyFmPm4E-^u?m>5aAZS$9I3Sg;u#n>mC8UdM-Z)ABx9&)C6+*_`@{|^*@h`HI*2x%=2H>`e zG-imJWK3O)P=9r0k_35;bWFcba_rL`=TO;D))Xe{)~`}Lvz@s5=D9+F)^%Y=kQU< z<63DV(Dk;)3xE5Hi}+Ap(4Cme{_+I}ZW6Cv!-=ltNa95Z6CR?KC$Sy7ph*?Llz>6GJXfmyfGe>+Fwa0TdRkRpp=XuqB^W~F2WLN04p6J{#=UxQ*Cn6|qmc2Sy zk*yak){D9mB^)xkH~W?8v_Fu_tfj5A(1 z>sh?4kLvSR^`K&orLFh(F#rI>a7Q_3_W(-EJ&$QRm%KJVqs%K3?X1taiJN}Upw{OA}mY4cI-EN5oYeuvt6$!h06&0yD+$0#L~3U#b}mEw7CZ7TDhVIs5Oz| zvqI6|BJH9|HkiOfr8v!(akv3sG6V!R!jMq8wkns+&G6+YA4Thap952^CgVx>vRV#oYS+ z-mLCK$2mOeryG&r72?{*ayGMil$fi5B)x&T}*jorf zBG9PptC34v%+<|z+b>w?GpFCqkBc1k2*^TO_u-u}^rDk9q=&nf_@qyQ0lu{_$If~c zEp~ai7?cPSb z2?Cn*@6m7*$D78y=+IIzc5V{_k!>dxz|A$u8a#X2X2rYldHDTGl`qSKZgvG^Y+7Sw zu@w~AW^B`~C&D&mQFI1#ygnJ|ze3tD+fqnb(Bh+d&3jLy zET4C;`~si+eZ-{wJAt0s_}bRT7#$bEF$9Dd8y94KdropqyBS9v^!D~WwS&vhT9@Tc zhr7+>;^CumtqWQUca*)*{hQl(#{24U4$X5k-p75I&|2&$(Fx9mNTAz}QM-%sno(D) zU0hEc9pP$m(}$m?rHZeXpYN8Mp!wL!eh+M*dDyOnNR}t$aPr!oP9vU&gyjDGYpd=E zPkR-7C)>@PQ5VJ!ltc)q=AtDQa_T6o9>$u8ZViC4&aO!VAlb}QrbQz5s3SI<9d;_w z{6}YwVS^Kdez{0J=}*jFtKo+FE6gaNvx0Embb%Jrp6OzfW`ZKe2eL#9ia=8DdK0$n ziTpYOC4-O_XLmIoDS(-=*x$WQ$}epp{_%_=q37lSLqd+bc-(3R8=L+83p|=P7EC1*;Zq(=7iH=?o6%r7Gho=sWoQk z7RA1FFYTL4Hq+rhMoho|oiWyxHcSm^!-C^|W!7Oc!l2^w#TSNpf{qy}J}G^v?!bf2 z1wG#x`E~5NFB~xw*f);~a;HTk#HqB3g4ap6DfV&s#>gk=ziO3auqlvdR*sM94`xVs z*A%su)WgR)Q{!pX-ABl2jkUC@Sj=RjtotqqkMd1TFjhaRgc25Y1jjuyFaeb!e-Z7E zk4qn0@^k1E!t52Re1UZB-GmT=T82aYrU}UhZNV{~mS4b+HI7mw)=}5lT>#d%VdFgC zST93YW9g!ZiNxDarf+0Kc19B6BfDxIS+g4sGoe1Qv)0VyrQaj(NJucYwu_#}`fA|y zeP5lUuUWz@jXa}!eYlQ7TMk1VM9?*!grPXV{ycG2+kZ@4wwRK*2DyK zY@QxIgk~jOl89JouO{oOFeDPdxeYJ|G=V<@@$zUrnW?wMd+Akr)!J_J(C*U zYBmHkYiJ~Tcr6$^Iz(PkgBYkoDxzYEni=4S4 zyS~SaAzwbVwPAQZ`pC{|ozes;GFE=Mlp`c0)W(@-lEu?YyV6YZcpjViLq9f}aERr% zq3efqms#NZjd63u()p4Y-H8zSaESYTln*Y7xak$utV?arc^&aMBzLKb%dsT`g=t9i zx-(yRdfxQvSO0+92U-m0Sox~^UbFdEP~h1vcGHZWO)Z~x&j_Gs3gF2 z@$}3ASxroY9==#dmpl~P)5Y`F@kSckVIYn0mWwyUtcNr9t(Ju=pyie;gLpOC;cd#p zvE1d;5WJ~}!N!D(c^&r(%at0w`12z?CfySL7{<6WTMa>ba<;#X|25fR8V57#5duLY z5}a}4J3wW(_*!xzWmszTo0N_}7V1I$_bu^2syY>v^U-#Pq^X^%rKL31o2nLgRFM{h z`OJ#~(Knv3jCkJC+Q0ze?3K04;SS?aHc!n~4628dQsF*HpI|>FvGqXuXW?$YV0Rh{ zwIIx@HzdR)KJS4eeW<;ukD@S-XseFY*9cd43HiVBQ})w7ohO2YWU_N`MWQ! z*#fv!3WwbZF7HY8j`?LXk75SHrE~w`L^SJ$K@*U zf(pO4uCb!&q6c^iXO)IE_q<}lkvWW>j%36U=fb5S$58YhLYJzC&4_;EXTp@@vJoM~ zZ^sL6Lv6JUsOvG`KC=zB#NZGY`m{+e_?2_l(D)ADD`SVvkMp;Tk zdy@~Ju-ikiA}A+W6`jellmys0)pyB*wi2A{3t?v!;^l&GiU41y(2|@ z%fKDct4xm|`$Ls4u8!!NOjZJ+cy%IEBbS$#yETHREMA=_5t&+}gx@BW)z+|J33hx? zAEx`A>cQIrls$Gvhp95Pm+ZQQI}h(h?W;vH#bkx5K3fDhyn(2BvJ`d|`Xkk6WCx_j z0k`|{Oe*Ov36jGqdxzkm&>ac&34ufMXqMeTFemy~_s^e=-MXU5vjunY60-zo02va(uPq*Un&+ioPU-RQE~ zn#*_Tq2rwVpH#ZbFhBzK(uE zEvBku!ugXGM5N1%FHft1CpVna!4g;`0TV;w>D ziqGex{g`_r<(5P5v1`s@FSDeF9+FEjjgG(MR5L!)Li_mZxe87p{f?KfXF0!?iv z;y^Yq*z{KWfqhLmkBllAG7J7>Rxw(*b4StP{q)wLWUqF0As29u5Wi21Rb=@i0iyWq zFYMC?D&1HNGLPP(?XU0@e_xJyX={TL(-w5IYd|I+o3SulUw<6NKeX?`g|x4bQZIF% zWv34L8vQ$DvFZd#($^a-+d4fG;ew|WLqXhDtXhlaR%*7sRwrH1hFO>*1b&8B<6z_} zN}Bklb|hP#V7$v%G~u3lg%Ka_`FEBEeIAu&#h`XAxTDHupG)dVrVNTgqn}SgmvO-q zwmrBqHMU@g$E>WA)LGHW7F96^p^WJkh{-v)c_P;+h{vE~I z>|m%%b%BIC1VpXTsvQdER+OVe7as_D^B73ELNZUsO{R z*+avZ;i1S7)zg+}@-Fb_uXk`z2HPwbRng1W%izP-+QT165lqR$28*#Ca^CD@zLrzd zvJiv3KokuHA`FMok~kUjxbaoWao~;jdD^TR6oyZX$Eg>;dR)tlr`x2ym%NiUcGLY&QP_YRo z>R5|5zZ=n-(?Aeg>lWXx{DthD`0|C^fg>`4Mi*;QIbhDNZymu)Y|bK`(Il+b;b0Xdajow3!* zoA>TyvOo3K + + + + + org.apache.maven.skins + maven-fluido-skin + 1.9 + + + + + Contrast-Security-OSS/contrast-maven-plugin + right + green + + + + + images/contrast-logo.png + / + + +

+ + + + + + + + + + + + diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/AbstractContrastMojoTest.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/AbstractContrastMojoTest.java new file mode 100644 index 00000000..cf8a66f6 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/AbstractContrastMojoTest.java @@ -0,0 +1,49 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import com.contrastsecurity.sdk.UserAgentProduct; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AbstractContrastMojo}. */ +final class AbstractContrastMojoTest { + + @Test + void creates_user_agent_product_with_expected_values() { + // GIVEN some AbstractContrastMojo with the mavenVersion property injected + final AbstractContrastMojo mojo = + new AbstractContrastMojo() { + @Override + public void execute() {} + }; + mojo.setMavenVersion("3.8.1"); + + // WHEN build User-Agent product + final UserAgentProduct ua = mojo.getUserAgentProduct(); + + // THEN has expected values + assertThat(ua.name()).isEqualTo("contrast-maven-plugin"); + assertThat(ua.version()).matches("\\d+\\.\\d+(\\.\\d+)?(-SNAPSHOT)?"); + assertThat(ua.comment()).isEqualTo("Apache Maven 3.8.1"); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojoTest.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojoTest.java new file mode 100644 index 00000000..f554ca28 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastInstallAgentMojoTest.java @@ -0,0 +1,201 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static org.junit.Assert.assertEquals; + +import java.text.SimpleDateFormat; +import java.util.Date; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; + +public class ContrastInstallAgentMojoTest { + + private ContrastInstallAgentMojo installMojo; + private Date now; + + @Rule public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @Before + public void setUp() { + installMojo = new ContrastInstallAgentMojo(); + installMojo.setAppName("caddyshack"); + installMojo.setServerName("Bushwood"); + installMojo.contrastAgentLocation = "/usr/local/bin/contrast.jar"; + + now = new Date(); + environmentVariables.clear( + "TRAVIS_BUILD_NUMBER", + "CIRCLE_BUILD_NUM", + "GIT_BRANCH", + "GIT_COMMITTER_NAME", + "GIT_COMMIT", + "GIT_URL", + "BUILD_NUMBER"); + } + + @Test + public void testGenerateAppVersion() { + installMojo.appVersion = "mycustomversion"; + AbstractAssessMojo.computedAppVersion = null; + assertEquals("mycustomversion", installMojo.computeAppVersion(now)); + } + + @Test + public void testGenerateAppVersionNoAppVersion() { + installMojo.appVersion = null; + AbstractAssessMojo.computedAppVersion = null; + String expectedVersion = new SimpleDateFormat("yyyyMMddHHmmss").format(now); + assertEquals("caddyshack-" + expectedVersion, installMojo.computeAppVersion(now)); + assertEquals("caddyshack-" + expectedVersion, installMojo.computeAppVersion(now)); + } + + @Test + public void testGenerateAppVersionTravis() { + installMojo.appVersion = null; + AbstractAssessMojo.computedAppVersion = null; + environmentVariables.set("TRAVIS_BUILD_NUMBER", "19"); + assertEquals("caddyshack-19", installMojo.computeAppVersion(now)); + assertEquals("caddyshack-19", installMojo.computeAppVersion(now)); + } + + @Test + public void testGenerateAppVersionCircle() { + installMojo.appVersion = null; + AbstractAssessMojo.computedAppVersion = null; + environmentVariables.set("TRAVIS_BUILD_NUMBER", "circle"); + assertEquals("caddyshack-circle", installMojo.computeAppVersion(now)); + assertEquals("caddyshack-circle", installMojo.computeAppVersion(now)); + } + + @Test + public void testGenerateAppVersionAppId() { + String appName = "WebGoat"; + String appId = "12345"; + String travisBuildNumber = "travis"; + + installMojo.appVersion = null; + AbstractAssessMojo.computedAppVersion = null; + environmentVariables.set("TRAVIS_BUILD_NUMBER", travisBuildNumber); + installMojo.setAppId(appId); + installMojo.applicationName = appName; + + assertEquals(appName + "-" + travisBuildNumber, installMojo.computeAppVersion(now)); + } + + @Test + public void testGenerateSessionMetadata() { + environmentVariables.set("GIT_BRANCH", "develop"); + assertEquals("branchName=develop", installMojo.computeSessionMetadata()); + + environmentVariables.set("GIT_COMMITTER_NAME", "boh"); + assertEquals("branchName=develop,committer=boh", installMojo.computeSessionMetadata()); + + environmentVariables.set("GIT_COMMIT", "deadbeef"); + assertEquals( + "branchName=develop,commitHash=deadbeef,committer=boh", + installMojo.computeSessionMetadata()); + + environmentVariables.set( + "GIT_URL", "https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git"); + assertEquals( + "branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git", + installMojo.computeSessionMetadata()); + + environmentVariables.set("BUILD_NUMBER", "123"); + assertEquals( + "buildNumber=123,branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git", + installMojo.computeSessionMetadata()); + + environmentVariables.clear("BUILD_NUMBER"); + environmentVariables.set("CIRCLE_BUILD_NUM", "12345"); + assertEquals( + "buildNumber=12345,branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git", + installMojo.computeSessionMetadata()); + + environmentVariables.clear("CIRCLE_BUILD_NUM"); + environmentVariables.set("TRAVIS_BUILD_NUMBER", "54321"); + assertEquals( + "branchName=develop,commitHash=deadbeef,committer=boh,repository=https://github.com/Contrast-Security-OSS/contrast-maven-plugin.git,buildNumber=54321", + installMojo.computeSessionMetadata()); + } + + @Test + public void testBuildArgLine() { + AbstractAssessMojo.computedAppVersion = "caddyshack-2"; + String currentArgLine = ""; + String expectedArgLine = + "-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"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + + installMojo.setServerName(null); // no server name + currentArgLine = ""; + expectedArgLine = + "-javaagent:/usr/local/bin/contrast.jar -Dcontrast.env=qa -Dcontrast.override.appversion=caddyshack-2 -Dcontrast.reporting.period=200 -Dcontrast.override.appname=caddyshack"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + + installMojo.setServerName("Bushwood"); + installMojo.serverPath = "/home/tomcat/app/"; + currentArgLine = ""; + expectedArgLine = + "-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/"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + + installMojo.standalone = true; + expectedArgLine = + "-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/"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + + environmentVariables.set("BUILD_NUMBER", "123"); + environmentVariables.set("GIT_COMMITTER_NAME", "boh"); + expectedArgLine = + "-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/"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + } + + @Test + public void testBuildArgNull() { + AbstractAssessMojo.computedAppVersion = "caddyshack-2"; + String currentArgLine = null; + String expectedArgLine = + "-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"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + } + + @Test + public void testBuildArgLineAppend() { + AbstractAssessMojo.computedAppVersion = "caddyshack-2"; + String currentArgLine = "-Xmx1024m"; + String expectedArgLine = + "-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"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + } + + @Test + public void testBuildArgLineSkip() { + installMojo.skipArgLine = true; + String currentArgLine = "-Xmx1024m"; + String expectedArgLine = "-Xmx1024m"; + assertEquals(expectedArgLine, installMojo.buildArgLine(currentArgLine)); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastScanMojoTest.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastScanMojoTest.java new file mode 100644 index 00000000..6442780f --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastScanMojoTest.java @@ -0,0 +1,143 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.contrastsecurity.sdk.scan.ScanSummary; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import org.apache.maven.plugin.MojoFailureException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +/** Unit tests for {@link ContrastScanMojo} */ +final class ContrastScanMojoTest { + + private ContrastScanMojo mojo; + + @BeforeEach + void before() { + mojo = new ContrastScanMojo(); + mojo.setOrganizationId("organization-id"); + mojo.setProjectName("project-id"); + } + + /** + * Contrast MOJOs tolerate a variety of HTTP paths in the URL configuration. Regardless of the + * path that the user has configured, the {@link ContrastScanMojo#createClickableScanURL} method + * should generate the same URL + */ + @ValueSource( + strings = { + "https://app.contrastsecurity.com/", + "https://app.contrastsecurity.com/Contrast", + "https://app.contrastsecurity.com/Contrast/api", + "https://app.contrastsecurity.com/Contrast/api/" + }) + @ParameterizedTest + void it_generates_clickable_url(final String url) throws MojoFailureException { + // GIVEN a scan mojo with known URL, organization ID, and project ID + mojo.setURL(url); + mojo.setOrganizationId("organization-id"); + + // WHEN generate URL for the user to click-through to display the scan in their browser + final String clickableScanURL = + mojo.createClickableScanURL("project-id", "scan-id").toExternalForm(); + + // THEN outputs expected URL + assertThat(clickableScanURL) + .isEqualTo( + "https://app.contrastsecurity.com/Contrast/static/ng/index.html#/organization-id/scans/project-id/scans/scan-id"); + } + + @Test + void it_prints_summary_to_console() { + // GIVEN the plugin is configured to output scan results to the console + mojo.setConsoleOutput(true); + + // WHEN print summary to console + final int totalResults = 10; + final int totalNewResults = 8; + final int totalFixedResults = 1; + final ScanSummary summary = + FakeScanSummary.builder() + .id("summary-id") + .organizationId("organization-id") + .projectId("project-id") + .scanId("scan-id") + .createdDate(Instant.now()) + .totalResults(totalResults) + .totalNewResults(totalNewResults) + .totalFixedResults(totalFixedResults) + .duration(Duration.ofMillis(0)) + .build(); + @SuppressWarnings("unchecked") + final Consumer console = mock(Consumer.class); + mojo.writeSummaryToConsole(summary, console); + + // THEN prints expected lines + final List expected = + Arrays.asList( + "Scan completed", + "New Results\t" + totalNewResults, + "Fixed Results\t" + totalFixedResults, + "Total Results\t" + totalResults); + final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(console, times(4)).accept(captor.capture()); + assertThat(captor.getAllValues()).hasSameElementsAs(expected); + } + + @Test + void it_may_be_configured_to_omit_summary_from_console() { + // GIVEN the plugin is configured to omit scan results from the console + mojo.setConsoleOutput(false); + + // WHEN print summary to console + final ScanSummary summary = + FakeScanSummary.builder() + .id("summary-id") + .organizationId("organization-id") + .projectId("project-id") + .scanId("scan-id") + .createdDate(Instant.now()) + .totalResults(10) + .totalNewResults(8) + .totalFixedResults(1) + .duration(Duration.ofMillis(0)) + .build(); + @SuppressWarnings("unchecked") + final Consumer console = mock(Consumer.class); + mojo.writeSummaryToConsole(summary, console); + + // THEN only prints "completed" line + verify(console).accept("Scan completed"); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojoTest.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojoTest.java new file mode 100644 index 00000000..271c276e --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/ContrastVerifyMojoTest.java @@ -0,0 +1,93 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.contrastsecurity.http.RuleSeverity; +import com.contrastsecurity.http.TraceFilterForm; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public class ContrastVerifyMojoTest { + + private ContrastVerifyMojo verifyContrastMavenPluginMojo; + + @Before + public void setUp() { + verifyContrastMavenPluginMojo = new ContrastVerifyMojo(); + verifyContrastMavenPluginMojo.minSeverity = "Medium"; + } + + @Test + public void testGetTraceFilterFormServerIdsNull() { + TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null); + assertNull(traceFilterForm.getServerIds()); + } + + @Test + public void testGetTraceFilterForm() { + List serverIds = new ArrayList<>(); + long server1 = 123L; + long server2 = 456L; + long server3 = 789L; + + serverIds.add(server1); + serverIds.add(server2); + serverIds.add(server3); + + TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(serverIds); + assertNotNull(traceFilterForm.getServerIds()); + assertEquals(3, traceFilterForm.getServerIds().size()); + assertEquals((Long) server1, traceFilterForm.getServerIds().get(0)); + assertEquals((Long) server2, traceFilterForm.getServerIds().get(1)); + assertEquals((Long) server3, traceFilterForm.getServerIds().get(2)); + } + + @Test + public void testGetTraceFilterFormSeverities() { + verifyContrastMavenPluginMojo.minSeverity = "Note"; + TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null); + + assertEquals(5, traceFilterForm.getSeverities().size()); + assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.NOTE)); + assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.LOW)); + assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.MEDIUM)); + assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.HIGH)); + assertTrue(traceFilterForm.getSeverities().contains(RuleSeverity.CRITICAL)); + } + + @Test + public void testGetTraceFilterFormAppVersionTags() { + String appVersion = "WebGoat-1"; + + AbstractAssessMojo.computedAppVersion = appVersion; + TraceFilterForm traceFilterForm = verifyContrastMavenPluginMojo.getTraceFilterForm(null); + + assertEquals(1, traceFilterForm.getAppVersionTags().size()); + assertEquals(appVersion, traceFilterForm.getAppVersionTags().get(0)); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/FakeScanSummary.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/FakeScanSummary.java new file mode 100644 index 00000000..1fdffe18 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/FakeScanSummary.java @@ -0,0 +1,91 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.sdk.scan.ScanSummary; +import com.google.auto.value.AutoValue; +import java.time.Duration; +import java.time.Instant; + +/** Fake implementation of {@link ScanSummary} for testing. */ +@AutoValue +abstract class FakeScanSummary implements ScanSummary { + + /** new {@link Builder} */ + static Builder builder() { + return new AutoValue_FakeScanSummary.Builder(); + } + + /** Builder for {@link ScanSummary}. */ + @AutoValue.Builder + abstract static class Builder { + + /** + * @see ScanSummary#id() + */ + abstract Builder id(String value); + + /** + * @see ScanSummary#scanId() + */ + abstract Builder scanId(String value); + + /** + * @see ScanSummary#projectId() + */ + abstract Builder projectId(String value); + + /** + * @see ScanSummary#organizationId() + */ + abstract Builder organizationId(String value); + + /** + * @see ScanSummary#duration() + */ + abstract Builder duration(Duration value); + + /** + * @see ScanSummary#totalResults() + */ + abstract Builder totalResults(int value); + + /** + * @see ScanSummary#totalNewResults() () + */ + abstract Builder totalNewResults(int value); + + /** + * @see ScanSummary#totalFixedResults() () + */ + abstract Builder totalFixedResults(int value); + + /** + * @see ScanSummary#createdDate() + */ + abstract Builder createdDate(Instant value); + + /** + * @return new {@link ScanSummary} + */ + abstract FakeScanSummary build(); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/Resources.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/Resources.java new file mode 100644 index 00000000..f8303a61 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/Resources.java @@ -0,0 +1,67 @@ +package com.contrastsecurity.maven.plugin; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.io.File; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** static utilities for retrieving test resources */ +public final class Resources { + + /** + * Retrieves the given test resources as a {@link File}. Fails if the resource does not exist + * + * @param name resource name + * @return {@link File} which refers to the resource + * @throws NullPointerException when resource does not exist + */ + public static Path file(final String name) { + final URL resource = Resources.class.getResource(name); + if (resource == null) { + throw new NullPointerException(name + " resource not found"); + } + try { + return Paths.get(resource.toURI()); + } catch (final URISyntaxException e) { + throw new AssertionError("This should never happen", e); + } + } + + /** + * Retrieves the given test resource as an {@link InputStream}. Fails if the resource does not + * exist + * + * @param name resource name + * @return {@link InputStream} for reading the resource + * @throws NullPointerException when resource does not exist + */ + public static InputStream stream(final String name) { + final InputStream stream = Resources.class.getResourceAsStream(name); + if (stream == null) { + throw new NullPointerException(name + " resource not found"); + } + return stream; + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastInstallAgentMojoIT.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastInstallAgentMojoIT.java new file mode 100644 index 00000000..82d75be9 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastInstallAgentMojoIT.java @@ -0,0 +1,64 @@ +package com.contrastsecurity.maven.plugin.it; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.maven.plugin.it.stub.ContrastAPI; +import com.contrastsecurity.maven.plugin.it.stub.ContrastAPIStub; +import java.io.IOException; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.jupiter.api.Test; + +/** + * Functional test for the "install" goal. + * + *

Verifies that the agent downloads a Contrast Java agent from the Contrast API. This test lacks + * the following verifications for this goal: + * + *

    + *
  • Does not verify that the plugin configures the maven-surefire-plugin's argLine property to + * include the agent + *
  • Does not verify that the plugin configures the spring-boot-maven-plugin's + * "run.jvmArguments" property to include the agent + *
  • Does not verify that the agent returned by the Contrast API is preconfigured to report to + * the user's Contrast organization + *
+ * + * We accept the risk that the aforementioned features are not verified in this test, because we + * plan to make breaking changes to this goal imminently. This functional test serves mainly as a + * guiding example for future functional tests. + */ +@ContrastAPIStub +final class ContrastInstallAgentMojoIT { + + @Test + public void test(final ContrastAPI contrast) throws IOException, VerificationException { + // GIVEN a spring-boot project that uses the plugin + final Verifier verifier = Verifiers.springBoot(contrast.connection()); + + // WHEN execute the "verify" goal + verifier.executeGoal("verify"); + + // THEN the plugin retrieves a Contrast Java agent from the Contrast API without errors + verifier.verifyErrorFreeLog(); + verifier.assertFilePresent("target/contrast.jar"); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastScanMojoIT.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastScanMojoIT.java new file mode 100644 index 00000000..59931528 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/ContrastScanMojoIT.java @@ -0,0 +1,72 @@ +package com.contrastsecurity.maven.plugin.it; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.maven.plugin.it.stub.ContrastAPI; +import com.contrastsecurity.maven.plugin.it.stub.ContrastAPIStub; +import java.io.IOException; +import java.util.Arrays; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.jupiter.api.Test; + +/** Functional test for the "scan" goal */ +@ContrastAPIStub +final class ContrastScanMojoIT { + + @Test + void scan_submits_artifact_for_scanning(final ContrastAPI contrast) + throws VerificationException, IOException { + // GIVEN a spring-boot project that uses the plugin + final Verifier verifier = Verifiers.springBoot(contrast.connection()); + + // WHEN execute the "verify" goal + verifier.setCliOptions(Arrays.asList("--activate-profiles", "scan")); + verifier.executeGoal("verify"); + + // THEN plugin submits the spring-boot application artifact for scanning + verifier.verifyErrorFreeLog(); + verifier.verifyTextInLog( + "Uploading spring-test-application-0.0.1-SNAPSHOT.jar to Contrast Scan"); + verifier.verifyTextInLog("Starting scan with label 0.0.1-SNAPSHOT"); + verifier.verifyTextInLog("Scan results will be available at http"); + verifier.verifyTextInLog("Waiting for scan results"); + verifier.verifyTextInLog("Scan completed"); + verifier.assertFilePresent("./target/contrast-scan-reports/contrast-scan-results.sarif.json"); + } + + @Test + void fails_when_no_artifact_detected(final ContrastAPI contrast) + throws VerificationException, IOException { + // GIVEN a POM project that uses the plugin + final Verifier verifier = Verifiers.parentPOM(contrast.connection()); + + // WHEN execute the "verify" goal + try { + verifier.executeGoal("verify"); + } catch (VerificationException ignored) { + } + + // THEN plugin fails because there is no artifact to scan + verifier.verifyTextInLog( + "Project's artifact file has not been set - see https://contrastsecurity.dev/contrast-maven-plugin/troubleshooting/artifact-not-set.html"); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/Verifiers.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/Verifiers.java new file mode 100644 index 00000000..a5c4288a --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/Verifiers.java @@ -0,0 +1,74 @@ +package com.contrastsecurity.maven.plugin.it; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.maven.plugin.it.stub.ConnectionParameters; +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.Properties; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.apache.maven.it.util.ResourceExtractor; + +/** + * Reusable static factory methods for creating new Maven Verifier instances from well-known sample + * projects + */ +final class Verifiers { + + /** + * @return new {@link Verifier} for the /it/spring-boot sample Maven project + */ + static Verifier springBoot(final ConnectionParameters connection) + throws IOException, VerificationException { + final String path = "/it/spring-boot"; + return verifier(connection, path); + } + + /** + * @return new {@link Verifier} for the /it/parent-pom sample Maven project + */ + static Verifier parentPOM(final ConnectionParameters connection) + throws IOException, VerificationException { + final String path = "/it/parent-pom"; + return verifier(connection, path); + } + + private static Verifier verifier(final ConnectionParameters connection, final String path) + throws IOException, VerificationException { + final File projectDir = ResourceExtractor.simpleExtractResources(Verifiers.class, path); + final Verifier verifier = new Verifier(projectDir.getAbsolutePath()); + final String testRepository = + Objects.requireNonNull( + System.getProperty("contrast.test-repository"), + "required system property contrast.test-repository must be set to a directory containing a maven repository with the contrast-maven-plugin installed"); + verifier.setLocalRepo(testRepository); + // AND the user provides common agent configuration connection parameters for connecting to + // Contrast + final Properties connectionProperties = connection.toProperties(); + verifier.setSystemProperties(connectionProperties); + return verifier; + } + + /** static members only */ + private Verifiers() {} +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ConnectionParameters.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ConnectionParameters.java new file mode 100644 index 00000000..52988559 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ConnectionParameters.java @@ -0,0 +1,95 @@ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.google.auto.value.AutoValue; +import java.util.Properties; + +/** + * Value type that holds all of the connection configuration that a consumer needs to access + * authenticated Contrast API endpoints. + */ +@AutoValue +public abstract class ConnectionParameters { + + /** + * @return new {@link Builder} + */ + public static Builder builder() { + return new AutoValue_ConnectionParameters.Builder(); + } + + /** + * @return Contrast API URL e.g. https://app.contrastsecurity.com/Contrast/api + */ + public abstract String url(); + + /** + * @return Contrast API username + */ + public abstract String username(); + + /** + * @return Contrast API Key + */ + public abstract String apiKey(); + + /** + * @return Contrast API service key + */ + public abstract String serviceKey(); + + /** + * @return Contrast organization ID + */ + public abstract String organizationID(); + + /** + * @return new {@link Properties} object populated with the connection configuration with standard + * Contrast Java system property names + */ + public final Properties toProperties() { + final Properties properties = new Properties(); + properties.setProperty("contrast.api.url", url()); + properties.setProperty("contrast.api.user_name", username()); + properties.setProperty("contrast.api.api_key", apiKey()); + properties.setProperty("contrast.api.service_key", serviceKey()); + properties.setProperty("contrast.api.organization_id", organizationID()); + return properties; + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder url(String value); + + public abstract Builder username(String value); + + public abstract Builder apiKey(String value); + + public abstract Builder serviceKey(String value); + + public abstract Builder organizationID(String value); + + public abstract ConnectionParameters build(); + } +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPI.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPI.java new file mode 100644 index 00000000..39f7e76a --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPI.java @@ -0,0 +1,36 @@ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** Describes a test instance of Contrast API to which tests may requests */ +public interface ContrastAPI { + + /** starts the Contrast API instance */ + void start(); + + /** + * @return connection configuration necessary for making requests to this Contrast API instance + */ + ConnectionParameters connection(); + + /** stops the Contrast API instance */ + void stop(); +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStub.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStub.java new file mode 100644 index 00000000..0fcb32db --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStub.java @@ -0,0 +1,45 @@ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Provides a JUnit test with a {@link ContrastAPI} stub for testing. Starts the {@code ContrastAPI} + * instance before starting the test, and handles gracefully terminating the Contrast API instance + * at the conclusion of the test. + * + *
+ *   @ContrastAPIStub
+ *   @Test
+ *   public void test(final ContrastAPI contrast) { ... }
+ * 
+ */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(ContrastAPIStubExtension.class) +public @interface ContrastAPIStub {} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStubExtension.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStubExtension.java new file mode 100644 index 00000000..e860dddd --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ContrastAPIStubExtension.java @@ -0,0 +1,130 @@ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Optional; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * JUnit 5 test extension that provides test authors with a stubbed instance of the Contrast API. + */ +public final class ContrastAPIStubExtension + implements ParameterResolver, BeforeAllCallback, AfterAllCallback { + + /** + * Starts the {@link ContrastAPI} + * + * @param context JUnit context + */ + @Override + public void beforeAll(final ExtensionContext context) { + final ContrastAPI contrast = getContrastAPI(context); + contrast.start(); + context.publishReportEntry("Contrast stub started " + contrast.connection().url()); + } + + /** + * Stops the {@link ContrastAPI} + * + * @param context JUnit context + */ + @Override + public void afterAll(final ExtensionContext context) { + final ContrastAPI contrast = getContrastAPI(context); + contrast.stop(); + } + + /** + * @return true if the parameter is of type {@link ContrastAPI} + */ + @Override + public boolean supportsParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType() == ContrastAPI.class; + } + + /** + * @return the {@link ContrastAPI} in the current test context + */ + @Override + public Object resolveParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + return getContrastAPI(extensionContext); + } + + /** + * @return new or existing {@link ContrastAPI} in the current test context + */ + private static ContrastAPI getContrastAPI(final ExtensionContext context) { + return context + .getStore(NAMESPACE) + .getOrComputeIfAbsent( + "server", ignored -> createFromConfiguration(context), ContrastAPI.class); + } + + /** + * @param context the current JUnit {@link ExtensionContext} + * @return {@link ExternalContrastAPI} if Contrast connection properties are provided, otherwise + * fails. We leave open the possibility that we will once again provide a stubbed API e.g. one + * that uses Pactflow hosted stubs. + */ + private static ContrastAPI createFromConfiguration(final ExtensionContext context) { + // gather configuration parameters from the current context + final Optional url = context.getConfigurationParameter("contrast.api.url"); + final Optional username = context.getConfigurationParameter("contrast.api.user_name"); + final Optional apiKey = context.getConfigurationParameter("contrast.api.api_key"); + final Optional serviceKey = + context.getConfigurationParameter("contrast.api.service_key"); + final Optional organization = + context.getConfigurationParameter("contrast.api.organization_id"); + + // if all connection parameters are present, then use end-to-end testing mode + if (url.isPresent() + && username.isPresent() + && apiKey.isPresent() + && serviceKey.isPresent() + && organization.isPresent()) { + context.publishReportEntry( + "end-to-end testing enabled: using provided Contrast API connection instead of the stub"); + final ConnectionParameters connection = + ConnectionParameters.builder() + .url(url.get()) + .username(username.get()) + .apiKey(apiKey.get()) + .serviceKey(serviceKey.get()) + .organizationID(organization.get()) + .build(); + return new ExternalContrastAPI(connection); + } + throw new IllegalArgumentException( + "Context lacks required configuration for connecting to an external Contrast instance"); + } + + private static final Namespace NAMESPACE = Namespace.create(ContrastAPIStubExtension.class); +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ExternalContrastAPI.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ExternalContrastAPI.java new file mode 100644 index 00000000..81d7d184 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/ExternalContrastAPI.java @@ -0,0 +1,52 @@ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Objects; + +/** + * {@link ContrastAPI} implementation that represents an external system. Methods that affect the + * system such as {@code start()} and {@code stop()} are no-ops. + */ +final class ExternalContrastAPI implements ContrastAPI { + + private final ConnectionParameters connection; + + /** + * @param connection the connection parameters constant to provide to users + */ + public ExternalContrastAPI(final ConnectionParameters connection) { + this.connection = Objects.requireNonNull(connection); + } + + /** nop */ + @Override + public void start() {} + + @Override + public ConnectionParameters connection() { + return connection; + } + + /** nop */ + @Override + public void stop() {} +} diff --git a/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/package-info.java b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/package-info.java new file mode 100644 index 00000000..cada8dc4 --- /dev/null +++ b/maven-plugin/src/test/java/com/contrastsecurity/maven/plugin/it/stub/package-info.java @@ -0,0 +1,42 @@ +/** + * JUnit extension for stubbing the Contrast API for integration testing. Before a test, the + * extension starts a new web server that simulates the subset of the Contrast API that the plugin + * needs. At the conclusion of the test, the extension terminates the web server. + * + *

Some tests may be compatible with an external Contrast API system (that has already been + * configured to be in the right state) instead of a stub. In this case, test authors can configure + * this extension (using standard JUnit configuration) to provide connection parameters to the + * external system instead of starting a stub system. + * + *

Set the following configuration parameters to configure the extension to use an external + * Contrast API system instead of starting a stub: + * + *

    + *
  • {@code contrast.api.url} + *
  • {@code contrast.api.user_name} + *
  • {@code contrast.api.api_key} + *
  • {@code contrast.api.service_key} + *
  • {@code contrast.api.organization} + *
+ */ +package com.contrastsecurity.maven.plugin.it.stub; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ diff --git a/maven-plugin/src/test/resources/it/parent-pom/pom.xml b/maven-plugin/src/test/resources/it/parent-pom/pom.xml new file mode 100644 index 00000000..ac15bc7b --- /dev/null +++ b/maven-plugin/src/test/resources/it/parent-pom/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + com.contrastsecurity.test + test-parent-pom + 0.0.1-SNAPSHOT + pom + + Parent POM module that mistakenly includes the scan goal in its build + + + UTF-8 + UTF-8 + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + scan + + + 10000 + + + + + ${contrast.api.user_name} + ${contrast.api.organization_id} + ${contrast.api.api_key} + ${contrast.api.service_key} + ${contrast.api.url} + spring-test-application + + + + + diff --git a/maven-plugin/src/test/resources/it/spring-boot/pom.xml b/maven-plugin/src/test/resources/it/spring-boot/pom.xml new file mode 100644 index 00000000..9c4715bc --- /dev/null +++ b/maven-plugin/src/test/resources/it/spring-boot/pom.xml @@ -0,0 +1,104 @@ + + + + + 4.0.0 + + com.contrastsecurity.test + spring-test-application + 0.0.1-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 2.5.1 + + + + + UTF-8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + install-contrast + + install + + + + + ${contrast.api.user_name} + ${contrast.api.organization_id} + ${contrast.api.api_key} + ${contrast.api.service_key} + ${contrast.api.url} + spring-test-application + + + + + + + scan + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + scan + + + 10000 + + + + + + + + + diff --git a/maven-plugin/src/test/resources/it/spring-boot/src/main/java/com/contrastsecurity/test/Application.java b/maven-plugin/src/test/resources/it/spring-boot/src/main/java/com/contrastsecurity/test/Application.java new file mode 100644 index 00000000..638322ef --- /dev/null +++ b/maven-plugin/src/test/resources/it/spring-boot/src/main/java/com/contrastsecurity/test/Application.java @@ -0,0 +1,31 @@ +package com.contrastsecurity.test; + +/*- + * #%L + * Contrast Maven Plugin + * %% + * Copyright (C) 2021 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public final class Application { + public static void main(final String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/maven-plugin/unset-contrast.sh b/maven-plugin/unset-contrast.sh new file mode 100755 index 00000000..aa4ec4c7 --- /dev/null +++ b/maven-plugin/unset-contrast.sh @@ -0,0 +1 @@ +unset CONTRAST__API__URL CONTRAST__API__USER_NAME CONTRAST__API__API_KEY CONTRAST__API__SERVICE_KEY CONTRAST__API__ORGANIZATION_ID diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 00000000..9896e71a --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,80 @@ +# Contrast Java SDK + +[![javadoc](https://javadoc.io/badge2/com.contrastsecurity/contrast-sdk-java/javadoc.svg)](https://javadoc.io/doc/com.contrastsecurity/contrast-sdk-java) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.contrastsecurity/contrast-sdk-java/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.contrastsecurity/contrast-sdk-java) + + +This SDK gives you a quick start for programmatically accessing the [Contrast REST API](https://api.contrastsecurity.com/) using Java. + + +## Requirements + +* JDK 1.8 +* Contrast Account + + +## How to use this SDK + +1. Add the + [contrast-sdk-java](https://search.maven.org/artifact/com.contrastsecurity/contrast-sdk-java) + dependency from Maven Central to your project. +1. At a minimum, you will need to supply four basic connection parameters ([find them here](https://docs.contrastsecurity.com/en/personal-keys.html)): + * Username + * API Key + * Service Key + * Contrast REST API URL (e.g. https://app.contrastsecurity.com/Contrast/api) + + +## Example + +```java +ContrastSDK contrastSDK = new ContrastSDK.Builder("contrast_admin", "demo", "demo") + .withApiUrl("http://localhost:19080/Contrast/api") + .build(); + +String orgUuid = contrastSDK.getProfileDefaultOrganizations().getOrganization().getOrgUuid(); + +Applications apps = contrastSDK.getApplications(orgUuid); +for (Application app : apps.getApplications()) { + System.out.println(app.getName() + " (" + app.getCodeShorthand() + " LOC)"); +} +``` + +Sample output: +``` +Aneritx (48K LOC) +Default Web Site (0k LOC) +EnterpriseTPS (48K LOC) +Feynmann (48K LOC) +jhipster-sample (0k LOC) +JSPWiki (48K LOC) +Liferay (48K LOC) +OpenMRS (65K LOC) +OracleFS (48K LOC) +Security Test (< 1K LOC) +Ticketbook (2K LOC) +WebGoat (48K LOC) +WebGoat7 (106K LOC) +``` + + +## Building + +Requires JDK 11 to build + +Use `./mvnw verify` to build and test changes to the project + + +### Formatting + +To avoid distracting white space changes in pull requests and wasteful bickering +about format preferences, Contrast uses the google-java-format opinionated Java +code formatter to automatically format all code to a common specification. + +Developers are expected to configure their editors to automatically apply this +format (plugins exist for both IDEA and Eclipse). Alternatively, developers can +apply the formatting before committing changes using the Maven plugin: + +```shell +./mvnw spotless:apply +```