From 30a76dfe40d7cf5d2864cbe33366ace6a1006eab Mon Sep 17 00:00:00 2001 From: Brian Phillips Date: Wed, 23 Oct 2024 09:45:43 -0400 Subject: [PATCH] JAVA-3738 add Download and Attach Agent Task --- gradle-plugin/README.md | 26 +++ gradle-plugin/build.gradle | 1 + .../ContrastConfigurationExtension.java | 85 ++++++++++ .../gradle/plugin/ContrastGradlePlugin.java | 9 + .../gradle/plugin/InstallAgentTask.java | 157 ++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 gradle-plugin/README.md create mode 100644 gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastConfigurationExtension.java create mode 100644 gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/InstallAgentTask.java diff --git a/gradle-plugin/README.md b/gradle-plugin/README.md new file mode 100644 index 0000000..114b856 --- /dev/null +++ b/gradle-plugin/README.md @@ -0,0 +1,26 @@ +# Contrast Gradle Plugin + +Gradle plugin for including the Contrast Security analysis in Java web applications + +## Building +Requires Java 21 to build + +Use `./gradlew build` to build the plugin + +## Publishing to MavenLocal +To publish this plugin to your mavenLocal apply the `maven-publish` plugin to this project's `build.gradle` file and run: + + +```shell +./gradlew publishToMavenLocal +``` + +## Coniguration +Attaching the Java agent with this plugin relies on your API credentials being set in the following env 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= +``` diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle index 30df447..60973c6 100644 --- a/gradle-plugin/build.gradle +++ b/gradle-plugin/build.gradle @@ -48,6 +48,7 @@ spotless{ } dependencies { + implementation("com.contrastsecurity:contrast-sdk-java:3.4.2") testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation 'org.junit.jupiter:junit-jupiter' } diff --git a/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastConfigurationExtension.java b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastConfigurationExtension.java new file mode 100644 index 0000000..d26fe96 --- /dev/null +++ b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastConfigurationExtension.java @@ -0,0 +1,85 @@ +package com.contrastsecurity.gradle.plugin; + +/** Extension for configuring TeamServer API Credentials for downloading agent */ +public class ContrastConfigurationExtension { + final String username; + final String apiKey; + final String serviceKey; + final String apiUrl; + final String orgUuid; + final String appName; + final String serverName; + final String jarPath; + final String appVersion; + + // default constructor with null values + // this is shit figure out what gradle wants + public ContrastConfigurationExtension() { + this.username = null; + this.apiKey = null; + this.serviceKey = null; + this.apiUrl = null; + this.orgUuid = null; + this.appName = null; + this.serverName = null; + this.jarPath = null; + this.appVersion = null; + } + + public ContrastConfigurationExtension( + final String username, + final String apiKey, + final String serviceKey, + final String apiUrl, + final String orgUuid, + final String appName, + final String serverName, + final String jarPath, + final String appVersion) { + this.username = username; + this.apiKey = apiKey; + this.serviceKey = serviceKey; + this.apiUrl = apiUrl; + this.orgUuid = orgUuid; + this.appName = appName; + this.serverName = serverName; + this.jarPath = jarPath; + this.appVersion = appVersion; + } + + public String getUsername() { + return username; + } + + public String getApiKey() { + return apiKey; + } + + public String getServiceKey() { + return serviceKey; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getOrgUuid() { + return orgUuid; + } + + public String getAppName() { + return appName; + } + + public String getServerName() { + return serverName; + } + + public String getJarPath() { + return jarPath; + } + + public String getAppVersion() { + return appVersion; + } +} diff --git a/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastGradlePlugin.java b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastGradlePlugin.java index 0594387..a456303 100644 --- a/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastGradlePlugin.java +++ b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/ContrastGradlePlugin.java @@ -8,11 +8,20 @@ * href=https://contrast.atlassian.net/browse/JAVA-8252>JAVA-8252 */ public class ContrastGradlePlugin implements Plugin { + public void apply(final Project target) { + + ContrastConfigurationExtension extension = + target.getExtensions().create(EXTENSION_NAME, ContrastConfigurationExtension.class); + target .getTasks() .register("hello", task -> task.doLast(s -> System.out.println("HelloWorld!"))); target.getTasks().register("empty", EmptyTask.class); + + target.getTasks().register("installAgent", InstallAgentTask.class); } + + public static final String EXTENSION_NAME = "contrastConfiguration"; } diff --git a/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/InstallAgentTask.java b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/InstallAgentTask.java new file mode 100644 index 0000000..3f613b4 --- /dev/null +++ b/gradle-plugin/src/main/java/com/contrastsecurity/gradle/plugin/InstallAgentTask.java @@ -0,0 +1,157 @@ +package com.contrastsecurity.gradle.plugin; + +import static com.contrastsecurity.gradle.plugin.ContrastGradlePlugin.EXTENSION_NAME; + +import com.contrastsecurity.exceptions.UnauthorizedException; +import com.contrastsecurity.models.AgentType; +import com.contrastsecurity.sdk.ContrastSDK; +import com.contrastsecurity.sdk.UserAgentProduct; +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.Collection; +import java.util.Collections; +import java.util.Date; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.TaskAction; +import org.gradle.internal.impldep.org.apache.commons.io.FileUtils; + +/** + * Downloads the current java agent from TeamServer using Credentials provided by the + * AgentCredentialsExtension + */ +public class InstallAgentTask extends DefaultTask { + + final ContrastConfigurationExtension config = + (ContrastConfigurationExtension) getProject().getExtensions().getByName(EXTENSION_NAME); + + @TaskAction + void installAgent() { + + // create sdk object for connecting to Contrast + final ContrastSDK sdk = connectToContrast(); + + // get agent, either from configured jar path or from TS + final Path agent = retrieveAgent(sdk); + + attachAgentToTasks(agent.toAbsolutePath()); + } + + /** Configures JavaExec tasks to run with the agent attached */ + private void attachAgentToTasks(final Path agentPath) { + getProject() + .getTasks() + .withType(JavaExec.class) + .configureEach( + task -> { + task.jvmArgs(createContrastArgs(agentPath)); + }); + } + + /** + * Creates the jvmArgs for running the java agent against JavaExec tasks + * + * @param agentPath preconfigured path to an agent defined by the ContrastConfigurationExtension + * @return Set of arguments + */ + private Collection createContrastArgs(final Path agentPath) { + final Collection args = Collections.emptySet(); + args.add("-javaagent:" + agentPath.toAbsolutePath()); + args.add("-Dcontrast.override.appname=" + config.appName); + args.add("-Dcontrast.server=" + config.serverName); + args.add("-Dcontrast.env=qa"); + + String appVersion = config.getAppVersion(); + if (appVersion == null) { + appVersion = computeAppVersion(); + } + + args.add("-Dcontrast.override.appversion=" + appVersion); + + return args; + } + + /** + * Shamelessly stolen from the maven plugin TODO check if we still want to do this based on travis + * or circle build number + * + * @return computed AppVersion + */ + private String computeAppVersion() { + final Date currentDate = new Date(); + String travisBuildNumber = System.getenv("TRAVIS_BUILD_NUMBER"); + String circleBuildNum = System.getenv("CIRCLE_BUILD_NUM"); + + final String appVersionQualifier; + if (travisBuildNumber != null) { + appVersionQualifier = travisBuildNumber; + } else if (circleBuildNum != null) { + appVersionQualifier = circleBuildNum; + } else { + appVersionQualifier = new SimpleDateFormat("yyyyMMddHHmmss").format(currentDate); + } + return config.getAppName() + "-" + appVersionQualifier; + } + + /** Use ContrastSDK to download agent and return the path where agent jar is stored */ + private Path retrieveAgent(final ContrastSDK connection) { + // Initially attempt to run agent from the previously configured location + final String jarPath = config.getJarPath(); + if (jarPath != null) { + final Path agent = Paths.get(jarPath); + if (!Files.exists(agent)) { + throw new RuntimeException("Unable to find java agent at " + jarPath); + } + return agent; + } + + // If no jar is provided, and no jarpath configured, attempt to retrieve the agent from TS + final byte[] bytes; + try { + bytes = connection.getAgent(AgentType.JAVA, config.getOrgUuid()); + } catch (IOException e) { + throw new RuntimeException("Failed to retrieve Contrast Java Agent: " + e); + } catch (UnauthorizedException e) { + throw new RuntimeException( + "\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(getProject().getProjectDir().getPath()); + try { + FileUtils.forceMkdir(target.toFile()); + } catch (final IOException e) { + throw new RuntimeException("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 RuntimeException("Unable to save the latest java agent.", e); + } + return agent; + } + + /** Use ContrastSDK to download agent creds for running the agent */ + private void downloadAgentCredentials(final ContrastSDK connection) {} + + /** Create ContrastSDK for connecting to TeamServer */ + private ContrastSDK connectToContrast() { + // TODO get plugin version for this as well + final UserAgentProduct gradle = UserAgentProduct.of("contrast-gradle-plugin"); + return new ContrastSDK.Builder(config.getUsername(), config.getServiceKey(), config.getApiKey()) + .withApiUrl(config.getApiUrl()) + // TODO figure out how to define this proxy + // .withProxy(proxy) //with proxy? + .withUserAgentProduct(gradle) + .build(); + } + + private static final String AGENT_NAME = "contrast.jar"; +}