├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── WIKI.md ├── contrastPluginConfig.xml ├── img ├── Jenkins_Contrast_Connection.png ├── Jenkins_Global_Vulnerability_Security_Control.png ├── Jenkins_build_appId.png ├── Jenkins_build_appName.png ├── jenkins-cat.png ├── severity_trends.png └── vuln_trends.png ├── pom.xml ├── run.sh ├── src ├── main │ ├── java │ │ └── com │ │ │ └── aspectsecurity │ │ │ └── contrast │ │ │ └── contrastjenkins │ │ │ ├── App.java │ │ │ ├── ApplicationDefinition.java │ │ │ ├── Constants.java │ │ │ ├── ContrastAgentStep.java │ │ │ ├── ContrastPluginConfig.java │ │ │ ├── GlobalThresholdCondition.java │ │ │ ├── MatchBy.java │ │ │ ├── TeamServerProfile.java │ │ │ ├── ThresholdCondition.java │ │ │ ├── VulnerabilityFrequencyAction.java │ │ │ ├── VulnerabilityTrendHelper.java │ │ │ ├── VulnerabilityTrendHelperException.java │ │ │ ├── VulnerabilityTrendProjectAction.java │ │ │ ├── VulnerabilityTrendRecorder.java │ │ │ ├── VulnerabilityTrendResult.java │ │ │ ├── VulnerabilityTrendStep.java │ │ │ ├── VulnerabilityType.java │ │ │ └── plots │ │ │ ├── SeverityFrequencyPlot.java │ │ │ └── VulnerabilityFrequencyPlot.java │ ├── resources │ │ ├── com │ │ │ └── aspectsecurity │ │ │ │ └── contrast │ │ │ │ └── contrastjenkins │ │ │ │ ├── ContrastAgentStep │ │ │ │ ├── config.jelly │ │ │ │ └── help.html │ │ │ │ ├── ContrastPluginConfig │ │ │ │ ├── config.jelly │ │ │ │ └── global.jelly │ │ │ │ ├── VulnerabilityFrequencyAction │ │ │ │ └── index.jelly │ │ │ │ ├── VulnerabilityTrendProjectAction │ │ │ │ ├── floatingBox.jelly │ │ │ │ └── index.jelly │ │ │ │ ├── VulnerabilityTrendRecorder │ │ │ │ ├── config.jelly │ │ │ │ └── index.jelly │ │ │ │ └── VulnerabilityTrendStep │ │ │ │ ├── config.jelly │ │ │ │ └── help.html │ │ └── index.jelly │ └── webapp │ │ ├── help-allowGlobalThresholdConditionsOverride.html │ │ ├── help-apiKey.html │ │ ├── help-applicationId.html │ │ ├── help-applicationName.html │ │ ├── help-applicationNotInstrumented.html │ │ ├── help-applyVulnerableBuildResultOnContrastError.html │ │ ├── help-orgUuid.html │ │ ├── help-queryBy-appVersionTag.html │ │ ├── help-queryBy-appVersionTagBuildName.html │ │ ├── help-queryBy-parameter.html │ │ ├── help-queryBy-startDate.html │ │ ├── help-serviceKey.html │ │ ├── help-teamServerUrl.html │ │ ├── help-thresholdCount.html │ │ ├── help-thresholdSeverity.html │ │ ├── help-thresholdVulnType.html │ │ ├── help-username.html │ │ ├── help-vulnerabilityStatus.html │ │ ├── help-vulnerableBuildResult.html │ │ └── img │ │ └── trend_graph.png └── test │ └── java │ └── com │ └── aspectsecurity │ └── contrast │ └── contrastjenkins │ ├── ContrastPluginConfigStub.java │ ├── ContrastPluginConfigTest.java │ ├── ThresholdConditionStub.java │ ├── ThresholdConditionTest.java │ ├── VulnerabilityTrendHelperTest.java │ ├── VulnerabilityTrendRecorderTest.java │ └── VulnerabilityTrendStepTest.java └── vulnerabilityTrendRecorderConfig.xml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Maven test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | packages: write 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Set up JDK 8 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '8' 21 | distribution: 'adopt' 22 | server-id: 'ossrh' 23 | - name: Maven test 24 | run: mvn -B -Dfindbugs.skip=true clean test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | *.ipr 6 | 7 | contrast-maven-plugin.iml 8 | 9 | # Maven 10 | *.log 11 | target/ 12 | output/ 13 | logs/ 14 | bin/ 15 | work/ 16 | .DS_Store 17 | 18 | #VSCode 19 | .classpath 20 | .factorypath 21 | .project 22 | .settings 23 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Jenkins Cat](img/jenkins-cat.png "Jenkins Cat" ) 3 | 4 | # Contrast Jenkins Plugin 5 | 6 | Repository for the Contrast Jenkins plugin. This plugin adds the ability to configure a connection to a Jenkins Build. 7 | 8 | ## Requirements 9 | * Jenkins version >= 2.60.3 10 | > Note: for Jenkins versions between 1.625.3 and 2.60.3, use plugin version [2.12.1](https://github.com/jenkinsci/contrast-continuous-application-security-plugin/releases/tag/contrast-continuous-application-security-2.12.1) 11 | 12 | ## Documentation 13 | [Contrast Docs](https://docs.contrastsecurity.com/en/jenkins.html) 14 | 15 | ## Charts 16 | 17 | There are 2 charts that are generated after each build `Vulnerability Trends Across Builds` and `Severity Trends Across Builds`. 18 | 19 | Here are two examples of the charts: 20 | 21 | ![Severity Trends Across Builds](img/severity_trends.png) 22 | 23 | ![Vulnerability Trends Across Builds](img/vuln_trends.png) 24 | 25 | > **Note:** The Vulnerability Report is not supported by the pipeline step and jobs that have applications with overridden Vulnerability Security Controls. Your Contrast admin can override the Vulnerability Security Controls for certain applications using the Job Outcome Policies in Contrast. 26 | 27 | ## Exported Configurations 28 | 29 | [TeamServer Profile Config](contrastPluginConfig.xml) 30 | 31 | [Contrast Vulnerability Security Controls Config](vulnerabilityTrendRecorderConfig.xml) 32 | 33 | ## Building the plugin 34 | 35 | `mvn clean install` 36 | 37 | ## Running Locally 38 | 39 | `./run.sh` 40 | 41 | -------------------------------------------------------------------------------- /WIKI.md: -------------------------------------------------------------------------------- 1 | # Contrast Jenkins Plugin 2 | 3 | ## About 4 | 5 | This plugin verifies vulnerability conditions by checking a build's 6 | vulnerabilities found against configured filters. The plugin also graphs 7 | history of vulnerability detection found during each projects build. 8 | 9 | This plugin supports a post build action and a step in the pipeline 10 | build process. 11 | 12 | ## Use the Plugin 13 | 14 | You can view the plugin code in Jenkins' [Github 15 | repository](https://github.com/jenkinsci/contrast-continuous-application-security-plugin). 16 | 17 | ## Requirements 18 | 19 | Jenkins Version >= 2.60.3 20 | 21 | ## Documentation 22 | [Contrast Docs](https://docs.contrastsecurity.com/en/jenkins.html) 23 | -------------------------------------------------------------------------------- /contrastPluginConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebGoat 6 | contrast_admin 7 | demo 8 | demo 9 | 110a6669-82fb-4db3-8ad2-fef35b01371c 10 | http://localhost:19080/Contrast/api 11 | WebGoat 12 | false 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/Jenkins_Contrast_Connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/Jenkins_Contrast_Connection.png -------------------------------------------------------------------------------- /img/Jenkins_Global_Vulnerability_Security_Control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/Jenkins_Global_Vulnerability_Security_Control.png -------------------------------------------------------------------------------- /img/Jenkins_build_appId.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/Jenkins_build_appId.png -------------------------------------------------------------------------------- /img/Jenkins_build_appName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/Jenkins_build_appName.png -------------------------------------------------------------------------------- /img/jenkins-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/jenkins-cat.png -------------------------------------------------------------------------------- /img/severity_trends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/severity_trends.png -------------------------------------------------------------------------------- /img/vuln_trends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/img/vuln_trends.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 3.50 9 | 10 | 11 | 12 | contrast-continuous-application-security 13 | 3.11-SNAPSHOT 14 | hpi 15 | 16 | 17 | 18 | jleo 19 | Justin Leo 20 | justin.leo@contrastsecurity.com 21 | 22 | 23 | https://github.com/jenkinsci/contrast-continuous-application-security-plugin/blob/master/WIKI.md 24 | 25 | 26 | 27 | 2.60.3 28 | 29 | 8 30 | 31 | 32 | 2.56 33 | 37 | 1.6.2 38 | true 39 | 40 | 41 | Contrast Continuous Application Security 42 | Jenkins Plugin to install the Contrast agent 43 | 44 | 45 | 46 | 47 | 48 | MIT License 49 | http://opensource.org/licenses/MIT 50 | 51 | 52 | 53 | 54 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 55 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 56 | http://github.com/jenkinsci/${project.artifactId}-plugin 57 | contrast-continuous-application-security-2.0 58 | 59 | 60 | 61 | 62 | repo.jenkins-ci.org 63 | https://repo.jenkins-ci.org/public/ 64 | 65 | 66 | 67 | 68 | repo.jenkins-ci.org 69 | https://repo.jenkins-ci.org/public/ 70 | 71 | 72 | 73 | 74 | 75 | 76 | runsh 77 | 78 | 79 | 80 | org.jenkins-ci.plugins.workflow 81 | workflow-aggregator 82 | 2.5 83 | 84 | 85 | org.jenkins-ci.plugins 86 | scm-api 87 | 1.3 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | com.contrastsecurity 98 | contrast-sdk-java 99 | 3.4.1 100 | 101 | 102 | org.jenkins-ci.plugins 103 | matrix-project 104 | 1.14 105 | 106 | 107 | net.sf.opencsv 108 | opencsv 109 | 1.7 110 | 111 | 112 | org.projectlombok 113 | lombok 114 | 1.16.14 115 | provided 116 | 117 | 118 | org.jenkins-ci.plugins.workflow 119 | workflow-step-api 120 | 2.18 121 | 122 | 123 | com.google.code.gson 124 | gson 125 | 2.8.9 126 | compile 127 | 128 | 129 | commons-io 130 | commons-io 131 | 2.7 132 | 133 | 134 | commons-lang 135 | commons-lang 136 | 2.6 137 | 138 | 139 | org.apache.commons 140 | commons-lang3 141 | 3.7 142 | 143 | 144 | 145 | org.mockito 146 | mockito-all 147 | 1.10.19 148 | test 149 | 150 | 151 | 152 | org.powermock 153 | powermock-core 154 | ${powermock.version} 155 | test 156 | 157 | 158 | org.powermock 159 | powermock-module-junit4 160 | ${powermock.version} 161 | test 162 | 163 | 164 | org.powermock 165 | powermock-api-mockito 166 | ${powermock.version} 167 | test 168 | 169 | 170 | org.powermock 171 | powermock-reflect 172 | ${powermock.version} 173 | test 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | clear 2 | mvn hpi:run -Djetty.port=8090 -Pjenkins,runsh -Djava.net.preferIPv4Stack=true -Djenkins.CLI.disabled=true 3 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/App.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class App { 9 | 10 | public String name; 11 | public String title; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/ApplicationDefinition.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import hudson.util.ListBoxModel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | 8 | @Getter 9 | @Setter 10 | public class ApplicationDefinition { 11 | 12 | private MatchBy matchBy; 13 | private String applicationId; 14 | private String applicationOriginName; 15 | private String agentType; //Application Language in UI 16 | private boolean failOnAppNotFound; 17 | private String applicationShortName; //Application Code in UI 18 | 19 | @DataBoundConstructor 20 | public ApplicationDefinition(String applicationId, String applicationOriginName, String applicationShortName, String agentType, boolean failOnAppNotFound) { 21 | this.applicationId = applicationId; 22 | this.applicationOriginName = applicationOriginName; 23 | this.applicationShortName = applicationShortName; 24 | this.agentType = agentType; 25 | this.failOnAppNotFound = failOnAppNotFound; 26 | if(applicationOriginName != null) { 27 | this.matchBy = MatchBy.APPLICATION_ORIGIN_NAME; 28 | } else if(applicationShortName != null) { 29 | this.matchBy = MatchBy.APPLICATION_SHORT_NAME; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/Constants.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | public final class Constants { 4 | 5 | public static final String VULNERABILITY_STATUS_AUTO_REMEDIATED = "Auto-Remediated"; 6 | public static final String VULNERABILITY_STATUS_CONFIRMED = "Confirmed"; 7 | public static final String VULNERABILITY_STATUS_SUSPICIOUS = "Suspicious"; 8 | public static final String VULNERABILITY_STATUS_REMEDIATED = "Remediated"; 9 | public static final String VULNERABILITY_STATUS_REPORTED = "Reported"; 10 | public static final String VULNERABILITY_STATUS_FIXED = "Fixed"; 11 | public static final String VULNERABILITY_STATUS_BEING_TRACKED = "Being+Tracked"; 12 | public static final String VULNERABILITY_STATUS_UNTRACKED = "Untracked"; 13 | public static final String VULNERABILITY_STATUS_NOT_A_PROBLEM = "NotAProblem"; 14 | 15 | public static final int QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT = 1; 16 | public static final int QUERY_BY_APP_VERSION_TAG_HIERARCHICAL_FORMAT = 2; 17 | public static final int QUERY_BY_START_DATE = 3; 18 | public static final int QUERY_BY_PARAMETER = 4; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/ContrastAgentStep.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.contrastsecurity.sdk.ContrastSDK; 4 | import com.google.inject.Inject; 5 | import hudson.AbortException; 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.model.Run; 9 | import hudson.model.TaskListener; 10 | import hudson.util.IOUtils; 11 | import hudson.util.ListBoxModel; 12 | import jenkins.model.Jenkins; 13 | import lombok.Getter; 14 | import org.apache.commons.io.FileUtils; 15 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; 16 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; 17 | import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; 18 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter; 19 | import org.kohsuke.stapler.DataBoundConstructor; 20 | import org.kohsuke.stapler.DataBoundSetter; 21 | 22 | import java.io.*; 23 | import java.util.Arrays; 24 | 25 | @Getter 26 | public class ContrastAgentStep extends AbstractStepImpl { 27 | 28 | private String profile; 29 | 30 | @DataBoundSetter 31 | public void setProfile(String profile) { 32 | this.profile = profile; 33 | } 34 | 35 | private String outputDirectory; 36 | 37 | @DataBoundSetter 38 | public void setOutputDirectory(String outputDirectory) { 39 | this.outputDirectory = outputDirectory; 40 | } 41 | 42 | private String agentType; 43 | 44 | @DataBoundSetter 45 | public void setAgentType(String agentType) { 46 | this.agentType = agentType; 47 | } 48 | 49 | @DataBoundConstructor 50 | public ContrastAgentStep(String profile, String outputDirectory, String agentType) { 51 | this.profile = profile; 52 | this.outputDirectory = outputDirectory; 53 | this.agentType = agentType; 54 | } 55 | 56 | @Override 57 | public ContrastAgentStepDescriptorImpl getDescriptor() { 58 | Jenkins instance = Jenkins.getInstance(); 59 | 60 | if (instance != null) { 61 | return (ContrastAgentStepDescriptorImpl) instance.getDescriptor(getClass()); 62 | } else { 63 | return null; 64 | } 65 | } 66 | 67 | @Extension 68 | public static class ContrastAgentStepDescriptorImpl extends AbstractStepDescriptorImpl { 69 | 70 | public ContrastAgentStepDescriptorImpl() { 71 | super(Execution.class); 72 | } 73 | 74 | @Override 75 | public String getFunctionName() { 76 | return "contrastAgent"; 77 | } 78 | 79 | @Override 80 | public String getDisplayName() { 81 | return "Download latest Contrast agent"; 82 | } 83 | 84 | 85 | @SuppressWarnings("unused") 86 | public ListBoxModel doFillProfileItems() { 87 | return VulnerabilityTrendHelper.getProfileNames(); 88 | } 89 | 90 | @SuppressWarnings("unused") 91 | public ListBoxModel doFillAgentTypeItems() { 92 | return VulnerabilityTrendHelper.getAgentTypeListBoxModel(); 93 | } 94 | 95 | } 96 | 97 | public static class Execution extends AbstractSynchronousStepExecution { 98 | private static final long serialVersionUID = 1L; 99 | 100 | @StepContextParameter 101 | private transient Run build; 102 | 103 | @StepContextParameter 104 | private transient TaskListener taskListener; 105 | 106 | @StepContextParameter 107 | private transient FilePath filePath; 108 | 109 | @Inject 110 | private transient ContrastAgentStep step; 111 | 112 | @Override 113 | public Void run() throws AbortException { 114 | String agentFileName = VulnerabilityTrendHelper.getDefaultAgentFileNameFromString(step.getAgentType()); 115 | 116 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(step.getProfile()); 117 | File agentFile = new File(step.getOutputDirectory() + "/" + agentFileName); 118 | 119 | if (teamServerProfile == null) { 120 | VulnerabilityTrendHelper.logMessage(taskListener, "Unable to find TeamServer profile."); 121 | throw new AbortException("Unable to find TeamServer profile."); 122 | } 123 | 124 | VulnerabilityTrendHelper.logMessage(taskListener, "Building connected to TeamServer with profile " + step.getProfile()); 125 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(teamServerProfile.getUsername(), teamServerProfile.getServiceKey(), 126 | teamServerProfile.getApiKey(), teamServerProfile.getTeamServerUrl()); 127 | 128 | VulnerabilityTrendHelper.logMessage(taskListener, "Downloading the agent to " + agentFile.getAbsolutePath()); 129 | 130 | byte[] agent; 131 | 132 | try { 133 | agent = contrastSDK.getAgent(VulnerabilityTrendHelper.getAgentTypeFromString(step.getAgentType()), 134 | teamServerProfile.getOrgUuid()); 135 | } catch (Exception e) { 136 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 137 | throw new AbortException("Unable to download agent from TeamServer."); 138 | } 139 | 140 | VulnerabilityTrendHelper.logMessage(taskListener, "Saving agent to file."); 141 | /* Regular Java io will not work on remote Jenkins slaves. 142 | * The agent file will not persist on the slave with java.io.File, probably due to how the Jenkins agent technology works. 143 | * It is better to use the Hudson libraries. */ 144 | try { 145 | filePath.child(step.getOutputDirectory()).mkdirs(); 146 | OutputStream outputStream = null; 147 | InputStream inputStream = null; 148 | try { 149 | outputStream = filePath.child(step.getOutputDirectory() + "/" + agentFileName).write(); 150 | inputStream = new ByteArrayInputStream(agent); 151 | IOUtils.copy(inputStream,outputStream); 152 | } finally { 153 | IOUtils.closeQuietly(outputStream); 154 | } 155 | } catch (Exception e) { 156 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 157 | throw new AbortException("Unable to save file to " + step.getOutputDirectory() + " The exception message is " + e.getMessage() + " The stack trace is " + Arrays.toString(e.getStackTrace())); 158 | } 159 | 160 | return null; 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/ContrastPluginConfig.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.contrastsecurity.exceptions.UnauthorizedException; 4 | import com.contrastsecurity.models.Organizations; 5 | import com.contrastsecurity.sdk.ContrastSDK; 6 | import hudson.Extension; 7 | import hudson.model.AbstractProject; 8 | import hudson.model.JobProperty; 9 | import hudson.model.JobPropertyDescriptor; 10 | import hudson.model.Result; 11 | import hudson.util.CopyOnWriteList; 12 | import hudson.util.FormValidation; 13 | import hudson.util.ListBoxModel; 14 | import hudson.util.Secret; 15 | import jenkins.model.Jenkins; 16 | import net.sf.json.JSONArray; 17 | import net.sf.json.JSONObject; 18 | import org.apache.commons.lang.StringUtils; 19 | import org.kohsuke.stapler.DataBoundConstructor; 20 | import org.kohsuke.stapler.QueryParameter; 21 | import org.kohsuke.stapler.StaplerRequest; 22 | 23 | import javax.servlet.ServletException; 24 | import java.io.IOException; 25 | import java.util.ArrayList; 26 | import java.util.Collection; 27 | 28 | /** 29 | * Contrast Plugin Configuration 30 | *

31 | * Adds the necessary configuration options to a job's properties. Used in VulnerabilityTrendRecorder 32 | */ 33 | public class ContrastPluginConfig extends JobProperty> { 34 | 35 | @DataBoundConstructor 36 | public ContrastPluginConfig() { 37 | 38 | } 39 | 40 | @Override 41 | public ContrastPluginConfigDescriptor getDescriptor() { 42 | Jenkins instance = Jenkins.getInstance(); 43 | 44 | if (instance != null) { 45 | return (ContrastPluginConfigDescriptor) instance.getDescriptor(getClass()); 46 | } else { 47 | return null; 48 | } 49 | } 50 | 51 | @Extension 52 | public static class ContrastPluginConfigDescriptor extends JobPropertyDescriptor { 53 | 54 | private CopyOnWriteList teamServerProfiles = new CopyOnWriteList<>(); 55 | 56 | private CopyOnWriteList globalThresholdConditions = new CopyOnWriteList<>(); 57 | 58 | public ContrastPluginConfigDescriptor() { 59 | super(ContrastPluginConfig.class); 60 | load(); 61 | } 62 | 63 | @Override 64 | public boolean configure(StaplerRequest req, JSONObject json) throws FormException { 65 | final JSONArray array = json.optJSONArray("profile"); 66 | 67 | if (array != null) { 68 | teamServerProfiles.replaceBy(req.bindJSONToList(TeamServerProfile.class, array)); 69 | } else { 70 | if (json.keySet().isEmpty()) { 71 | teamServerProfiles = new CopyOnWriteList<>(); 72 | } else { 73 | teamServerProfiles.replaceBy(req.bindJSON(TeamServerProfile.class, json.getJSONObject("profile"))); 74 | } 75 | } 76 | 77 | // refresh all org rules and applications 78 | for (TeamServerProfile teamServerProfile : teamServerProfiles) { 79 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(teamServerProfile.getUsername(), teamServerProfile.getServiceKey(), 80 | teamServerProfile.getApiKey(), teamServerProfile.getTeamServerUrl()); 81 | 82 | teamServerProfile.setVulnerabilityTypes(VulnerabilityTrendHelper.saveRules(contrastSDK, teamServerProfile.getOrgUuid())); 83 | 84 | teamServerProfile.setApps(VulnerabilityTrendHelper.saveApplicationIds(contrastSDK, teamServerProfile.getOrgUuid())); 85 | } 86 | 87 | 88 | final JSONArray globalThresholdConditionJsonArray = json.optJSONArray("globalThresholdCondition"); 89 | 90 | if (globalThresholdConditionJsonArray != null) { 91 | globalThresholdConditions.replaceBy(req.bindJSONToList(GlobalThresholdCondition.class, globalThresholdConditionJsonArray)); 92 | } else { 93 | if (json.keySet().isEmpty()) { 94 | globalThresholdConditions = new CopyOnWriteList<>(); 95 | } else { 96 | globalThresholdConditions.replaceBy(req.bindJSON(GlobalThresholdCondition.class, json.getJSONObject("globalThresholdCondition"))); 97 | } 98 | } 99 | 100 | save(); 101 | 102 | return true; 103 | } 104 | 105 | @SuppressWarnings("unused") 106 | public ListBoxModel doFillTeamServerProfileNameItems() { 107 | return VulnerabilityTrendHelper.getProfileNames(); 108 | } 109 | 110 | /** 111 | * Fills the Threshold Category select drop down with vulnerability types for the configured profile. 112 | * 113 | * @return ListBoxModel filled with vulnerability types. 114 | */ 115 | public ListBoxModel doFillThresholdVulnTypeItems(@QueryParameter("teamServerProfileName") final String teamServerProfileName) throws IOException { 116 | return VulnerabilityTrendHelper.getVulnerabilityTypes(teamServerProfileName); 117 | } 118 | 119 | /** 120 | * Validates the configured TeamServer profile by attempting to get the default profile for the username. 121 | * 122 | * @param username String username of the TeamServer user 123 | * @param apiKey String apiKey of the TeamServer user 124 | * @param serviceKey String serviceKey of the TeamServer user 125 | * @param teamServerUrl String TeamServer Url 126 | * @return FormValidation 127 | * @throws IOException 128 | * @throws ServletException 129 | */ 130 | public FormValidation doTestTeamServerConnection(@QueryParameter("username") final String username, 131 | @QueryParameter("apiKey") final Secret apiKey, 132 | @QueryParameter("serviceKey") final Secret serviceKey, 133 | @QueryParameter("teamServerUrl") final String teamServerUrl) throws IOException, ServletException { 134 | 135 | if (StringUtils.isEmpty(username)) { 136 | return FormValidation.error("Connection error: Username cannot be empty."); 137 | } 138 | 139 | if (StringUtils.isEmpty(apiKey.getPlainText())) { 140 | return FormValidation.error("Connection error: Api Key cannot be empty."); 141 | } 142 | 143 | if (StringUtils.isEmpty(serviceKey.getPlainText())) { 144 | return FormValidation.error("Connection error: Service Key cannot be empty"); 145 | } 146 | 147 | if (StringUtils.isEmpty(teamServerUrl)) { 148 | return FormValidation.error("Connection error: Contrast URL cannot be empty."); 149 | } 150 | 151 | if (!teamServerUrl.endsWith("/Contrast/api")) { 152 | return FormValidation.error("Connection error: Contrast Url does not end with /Contrast/api."); 153 | } 154 | 155 | try { 156 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(username, serviceKey.getPlainText(), apiKey.getPlainText(), teamServerUrl); 157 | 158 | Organizations organizations = contrastSDK.getProfileDefaultOrganizations(); 159 | 160 | if (organizations == null || organizations.getOrganization() == null) { 161 | return FormValidation.error("Connection error: No organization found, Check your credentials and URL."); 162 | } 163 | Collection validationCollection = new ArrayList<>(); 164 | 165 | validationCollection.add(FormValidation.ok("Successfully connected to Contrast.")); 166 | 167 | if(VulnerabilityTrendHelper.isEnabledJobOutcomePolicyExist(contrastSDK,organizations.getOrganization().getOrgUuid())) { 168 | validationCollection.add(FormValidation.warning("Your Contrast administrator has set a policy for vulnerability thresholds. " + 169 | "The Contrast policy overrides Jenkins security controls for applications included in both.")); 170 | } 171 | return FormValidation.aggregate(validationCollection); 172 | } catch (IOException | UnauthorizedException e) { 173 | return FormValidation.error(String.format("Unable to connect to Contrast. %s", e.getMessage())); 174 | } 175 | } 176 | 177 | public TeamServerProfile[] getTeamServerProfiles() { 178 | final TeamServerProfile[] profileArray = new TeamServerProfile[teamServerProfiles.size()]; 179 | 180 | return teamServerProfiles.toArray(profileArray); 181 | } 182 | 183 | public GlobalThresholdCondition[] getGlobalThresholdConditions() { 184 | final GlobalThresholdCondition[] globalThresholdConditionArray = new GlobalThresholdCondition[globalThresholdConditions.size()]; 185 | 186 | return globalThresholdConditions.toArray(globalThresholdConditionArray); 187 | } 188 | 189 | /** 190 | * Fills the Threshold Severity select drop down with severities for the configured application. 191 | * 192 | * @return ListBoxModel filled with severities. 193 | */ 194 | public ListBoxModel doFillThresholdSeverityItems() { 195 | return VulnerabilityTrendHelper.getSeverityListBoxModel(); 196 | } 197 | 198 | /** 199 | * Validation of the 'thresholdCount' form Field. 200 | * 201 | * @param value This parameter receives the value that the user has typed. 202 | * @return Indicates the outcome of the validation. This is sent to the browser. 203 | */ 204 | public FormValidation doCheckThresholdCount(@QueryParameter String value) { 205 | 206 | if (!value.isEmpty()) { 207 | try { 208 | int temp = Integer.parseInt(value); 209 | 210 | if (temp < 0) { 211 | return FormValidation.error("Please enter a positive integer."); 212 | } 213 | 214 | } catch (NumberFormatException e) { 215 | return FormValidation.error("Please enter a valid integer."); 216 | } 217 | } else { 218 | return FormValidation.error("Please enter a positive integer."); 219 | } 220 | 221 | return FormValidation.ok(); 222 | } 223 | 224 | /** 225 | * Validation of the 'name' form Field. 226 | * 227 | * @param value This parameter receives the value that the user has typed. 228 | * @return Indicates the outcome of the validation. This is sent to the browser. 229 | */ 230 | public FormValidation doCheckProfileName(@QueryParameter String value) { 231 | if (value.length() == 0) 232 | return FormValidation.error("Please set a profile name."); 233 | 234 | return FormValidation.ok(); 235 | } 236 | 237 | /** 238 | * Validation of the 'username' form Field. 239 | * 240 | * @param value This parameter receives the value that the user has typed. 241 | * @return Indicates the outcome of the validation. This is sent to the browser. 242 | */ 243 | public FormValidation doCheckUsername(@QueryParameter String value) { 244 | if (value.length() == 0) 245 | return FormValidation.error("Please set a username."); 246 | return FormValidation.ok(); 247 | } 248 | 249 | /** 250 | * Validation of the 'apiKey' form Field. 251 | * 252 | * @param value This parameter receives the value that the user has typed. 253 | * @return Indicates the outcome of the validation. This is sent to the browser. 254 | */ 255 | public FormValidation doCheckApiKey(@QueryParameter String value) { 256 | if (value.length() == 0) 257 | return FormValidation.error("Please set an API Key."); 258 | return FormValidation.ok(); 259 | } 260 | 261 | /** 262 | * Validation of the 'serviceKey' form Field. 263 | * 264 | * @param value This parameter receives the value that the user has typed. 265 | * @return Indicates the outcome of the validation. This is sent to the browser. 266 | */ 267 | public FormValidation doCheckServiceKey(@QueryParameter String value) { 268 | if (value.length() == 0) 269 | return FormValidation.error("Please set a Service Key."); 270 | return FormValidation.ok(); 271 | } 272 | 273 | /** 274 | * Validation of the 'thresholdSeverity' form Field. 275 | * 276 | * @param value This parameter receives the value that the user has typed. 277 | * @return Indicates the outcome of the validation. This is sent to the browser. 278 | */ 279 | public FormValidation doCheckThresholdSeverity(@QueryParameter String value) { 280 | return FormValidation.ok(); 281 | } 282 | 283 | /** 284 | * Validation of the 'teamServerUrl' form Field. 285 | * 286 | * @param value This parameter receives the value that the user has typed. 287 | * @return Indicates the outcome of the validation. This is sent to the browser. 288 | */ 289 | public FormValidation doCheckTeamServerUrl(@QueryParameter String value) { 290 | if (value.length() == 0) 291 | return FormValidation.error("Please set a TeamServer Url."); 292 | return FormValidation.ok(); 293 | } 294 | 295 | /** 296 | * Validation of the 'orgUuid' form Field. 297 | * 298 | * @param value This parameter receives the value that the user has typed. 299 | * @return Indicates the outcome of the validation. This is sent to the browser. 300 | */ 301 | public FormValidation doCheckOrgUuid(@QueryParameter String value) { 302 | if (value.length() == 0) 303 | return FormValidation.error("Please set an Organization Uuid."); 304 | return FormValidation.ok(); 305 | } 306 | 307 | @Override 308 | public String getDisplayName() { 309 | return "Contrast Plugin Configuration"; 310 | } 311 | 312 | 313 | public ListBoxModel doFillVulnerableBuildResultItems() { 314 | ListBoxModel items = new ListBoxModel(); 315 | 316 | items.add(Result.FAILURE.toString()); 317 | items.add(Result.SUCCESS.toString()); 318 | items.add(Result.UNSTABLE.toString()); 319 | items.add(Result.NOT_BUILT.toString()); 320 | items.add(Result.ABORTED.toString()); 321 | 322 | return items; 323 | } 324 | } 325 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/GlobalThresholdCondition.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | public class GlobalThresholdCondition { 13 | 14 | private String teamServerProfileName; 15 | 16 | private Integer thresholdCount; 17 | 18 | private String thresholdSeverity; 19 | 20 | private String thresholdVulnType; 21 | 22 | private boolean autoRemediated; 23 | private boolean confirmed; 24 | private boolean suspicious; 25 | private boolean notAProblem; 26 | private boolean remediated; 27 | private boolean reported; 28 | private boolean fixed; 29 | private boolean beingTracked; 30 | private boolean untracked; 31 | 32 | @DataBoundConstructor 33 | public GlobalThresholdCondition(String teamServerProfileName, Integer thresholdCount, String thresholdSeverity, String thresholdVulnType, boolean autoRemediated, 34 | boolean confirmed, boolean suspicious, boolean notAProblem, boolean remediated, 35 | boolean reported, boolean fixed, boolean beingTracked, boolean untracked) { 36 | 37 | this.teamServerProfileName = teamServerProfileName; 38 | this.thresholdCount = thresholdCount; 39 | this.thresholdSeverity = thresholdSeverity; 40 | this.thresholdVulnType = thresholdVulnType; 41 | this.autoRemediated = autoRemediated; 42 | this.confirmed = confirmed; 43 | this.suspicious = suspicious; 44 | this.notAProblem = notAProblem; 45 | this.remediated = remediated; 46 | this.reported = reported; 47 | this.fixed = fixed; 48 | this.beingTracked = beingTracked; 49 | this.untracked = untracked; 50 | } 51 | 52 | public List getVulnerabilityStatuses() { 53 | List status = new ArrayList(); 54 | if (autoRemediated) { 55 | status.add(Constants.VULNERABILITY_STATUS_AUTO_REMEDIATED); 56 | } 57 | if (confirmed) { 58 | status.add(Constants.VULNERABILITY_STATUS_CONFIRMED); 59 | } 60 | if (suspicious) { 61 | status.add(Constants.VULNERABILITY_STATUS_SUSPICIOUS); 62 | } 63 | if (notAProblem) { 64 | status.add(Constants.VULNERABILITY_STATUS_NOT_A_PROBLEM); 65 | } 66 | if (remediated) { 67 | status.add(Constants.VULNERABILITY_STATUS_REMEDIATED); 68 | } 69 | if (reported) { 70 | status.add(Constants.VULNERABILITY_STATUS_REPORTED); 71 | } 72 | if (fixed) { 73 | status.add(Constants.VULNERABILITY_STATUS_FIXED); 74 | } 75 | if (beingTracked) { 76 | status.add(Constants.VULNERABILITY_STATUS_BEING_TRACKED); 77 | } 78 | if (untracked) { 79 | status.add(Constants.VULNERABILITY_STATUS_UNTRACKED); 80 | } 81 | return status; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/MatchBy.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | public enum MatchBy { 4 | APPLICATION_ORIGIN_NAME, //The name that the application was instrumented with. 5 | APPLICATION_ID, 6 | APPLICATION_SHORT_NAME 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/TeamServerProfile.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import hudson.util.Secret; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Setter 11 | public class TeamServerProfile { 12 | 13 | @Getter 14 | private String name; 15 | 16 | @Getter 17 | private String username; 18 | 19 | private Secret apiKey; 20 | 21 | private Secret serviceKey; 22 | 23 | @Getter 24 | private String orgUuid; 25 | 26 | @Getter 27 | private String teamServerUrl; 28 | 29 | @Getter 30 | private String applicationName; 31 | 32 | @Getter 33 | private List vulnerabilityTypes; 34 | 35 | private boolean applyVulnerableBuildResultOnContrastError; 36 | 37 | @Getter 38 | private String vulnerableBuildResult; 39 | 40 | @Getter 41 | private boolean allowGlobalThresholdConditionsOverride; 42 | 43 | @Getter 44 | private List apps; 45 | 46 | //Compatibility fix for plugin versions <= 3.6 47 | /** 48 | * @Deprecated 49 | * Use {@link TeamServerProfile#applyVulnerableBuildResultOnContrastError} 50 | */ 51 | @Getter 52 | @Deprecated 53 | private boolean failOnWrongApplicationId; 54 | 55 | /////// Compatibility fix for plugin versions <=2.6 56 | /** 57 | * @Deprecated 58 | * Use {@link TeamServerProfile#applyVulnerableBuildResultOnContrastError} 59 | */ 60 | @Getter 61 | @Deprecated 62 | private boolean failOnWrongApplicationName; 63 | 64 | @DataBoundConstructor 65 | public TeamServerProfile(String name, String username, String apiKey, String serviceKey, String orgUuid, 66 | String teamServerUrl, boolean applyVulnerableBuildResultOnContrastError, 67 | String vulnerableBuildResult, boolean allowGlobalThresholdConditionsOverride) { 68 | this.name = name; 69 | this.username = username; 70 | this.apiKey = Secret.fromString(apiKey); 71 | this.serviceKey = Secret.fromString(serviceKey); 72 | this.orgUuid = orgUuid; 73 | this.teamServerUrl = teamServerUrl; 74 | this.applyVulnerableBuildResultOnContrastError = applyVulnerableBuildResultOnContrastError; 75 | this.vulnerableBuildResult = vulnerableBuildResult; 76 | this.allowGlobalThresholdConditionsOverride = allowGlobalThresholdConditionsOverride; 77 | } 78 | 79 | public Secret getSecretApiKey() { 80 | return apiKey; 81 | } 82 | 83 | public Secret getSecretServiceKey() { 84 | return serviceKey; 85 | } 86 | 87 | String getApiKey() { 88 | return apiKey.getPlainText(); 89 | } 90 | 91 | String getServiceKey() { 92 | return serviceKey.getPlainText(); 93 | } 94 | 95 | public boolean isApplyVulnerableBuildResultOnContrastError() { 96 | //backwards compatability fix for profiles with old configurations 97 | if(!this.applyVulnerableBuildResultOnContrastError && (this.isFailOnWrongApplicationId() || this.isFailOnWrongApplicationName())) { 98 | return true; 99 | } 100 | return this.applyVulnerableBuildResultOnContrastError; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/ThresholdCondition.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | 4 | import com.contrastsecurity.exceptions.UnauthorizedException; 5 | import com.contrastsecurity.sdk.ContrastSDK; 6 | import hudson.Extension; 7 | import hudson.RelativePath; 8 | import hudson.model.AbstractDescribableImpl; 9 | import hudson.model.Descriptor; 10 | import hudson.util.ComboBoxModel; 11 | import hudson.util.FormValidation; 12 | import hudson.util.ListBoxModel; 13 | import jenkins.model.Jenkins; 14 | import lombok.Getter; 15 | import lombok.Setter; 16 | import org.kohsuke.stapler.DataBoundConstructor; 17 | import org.kohsuke.stapler.DataBoundSetter; 18 | import org.kohsuke.stapler.QueryParameter; 19 | 20 | import java.io.IOException; 21 | import java.util.ArrayList; 22 | import java.util.Calendar; 23 | import java.util.List; 24 | import java.util.StringJoiner; 25 | 26 | /** 27 | * ThresholdCondition class contains the variables and logic to populate the conditions when verifying for vulnerabilities. 28 | */ 29 | public class ThresholdCondition extends AbstractDescribableImpl { 30 | 31 | @Setter 32 | @Getter 33 | private Integer thresholdCount; 34 | 35 | @Setter 36 | @Getter 37 | private String thresholdSeverity; 38 | 39 | @Setter 40 | @Getter 41 | private String thresholdVulnType; 42 | 43 | @Setter 44 | @Getter 45 | private ApplicationDefinition applicationDefinition; 46 | 47 | @Getter 48 | private String applicationId; 49 | 50 | @DataBoundSetter 51 | public void setApplicationId(String applicationId) { 52 | this.applicationId = applicationId; 53 | } 54 | 55 | //// Compatibility fix for plugin versions <=2.6 56 | @Setter 57 | @Getter 58 | private String applicationName; 59 | 60 | /** 61 | * Name that was used to instrument the agent 62 | */ 63 | @Setter 64 | @Getter 65 | private String applicationOriginName; 66 | 67 | /** 68 | * Type of agent used to instrument the application 69 | */ 70 | @Setter 71 | @Getter 72 | private String agentType; 73 | 74 | /** 75 | * Only gets set when application is not initiated 76 | */ 77 | @Setter 78 | @Getter 79 | private boolean failOnAppNotFound; 80 | 81 | /** 82 | * 0 = instrumented 83 | * 1 = not instrumented 84 | */ 85 | @Setter 86 | @Getter 87 | private int applicationState; 88 | 89 | @Setter 90 | @Getter 91 | private MatchBy matchBy; 92 | 93 | @Setter 94 | @Getter 95 | private boolean autoRemediated; 96 | @Setter 97 | @Getter 98 | private boolean confirmed; 99 | @Setter 100 | @Getter 101 | private boolean suspicious; 102 | @Setter 103 | @Getter 104 | private boolean notAProblem; 105 | @Setter 106 | @Getter 107 | private boolean remediated; 108 | @Setter 109 | @Getter 110 | private boolean reported; 111 | 112 | @Setter 113 | @Getter 114 | private boolean fixed; 115 | @Setter 116 | @Getter 117 | private boolean beingTracked; 118 | @Setter 119 | @Getter 120 | private boolean untracked; 121 | 122 | @DataBoundConstructor 123 | public ThresholdCondition(Integer thresholdCount, String thresholdSeverity, String thresholdVulnType, int applicationState, ApplicationDefinition applicationDefinition, 124 | String applicationId, boolean autoRemediated, boolean confirmed, boolean suspicious, 125 | boolean notAProblem, boolean remediated, boolean reported, boolean fixed, 126 | boolean beingTracked, boolean untracked) { 127 | 128 | this.thresholdCount = thresholdCount; 129 | this.thresholdSeverity = thresholdSeverity; 130 | this.thresholdVulnType = thresholdVulnType; 131 | this.applicationDefinition = applicationDefinition; 132 | this.applicationId = applicationId; 133 | this.applicationState = applicationState; 134 | if(applicationState == 0) { 135 | this.matchBy = MatchBy.APPLICATION_ID; 136 | }else if(applicationDefinition != null) { 137 | this.matchBy = applicationDefinition.getMatchBy(); 138 | this.applicationOriginName = applicationDefinition.getApplicationOriginName(); 139 | this.agentType = applicationDefinition.getAgentType(); 140 | this.failOnAppNotFound = applicationDefinition.isFailOnAppNotFound(); 141 | } 142 | 143 | this.autoRemediated = autoRemediated; 144 | this.confirmed = confirmed; 145 | this.suspicious = suspicious; 146 | this.notAProblem = notAProblem; 147 | this.remediated = remediated; 148 | this.reported = reported; 149 | this.fixed = fixed; 150 | this.beingTracked = beingTracked; 151 | this.untracked = untracked; 152 | } 153 | 154 | @Override 155 | public String toString() { 156 | StringBuilder sb = new StringBuilder(); 157 | 158 | sb.append("count is ").append(thresholdCount); 159 | 160 | if (thresholdSeverity != null) { 161 | sb.append(", severity is ").append(thresholdSeverity); 162 | } 163 | 164 | if (thresholdVulnType != null) { 165 | sb.append(", rule type is ").append(thresholdVulnType); 166 | } 167 | 168 | if (applicationId != null) { 169 | sb.append(", application ID is ").append(applicationId); 170 | } 171 | 172 | sb.append("."); 173 | 174 | return sb.toString(); 175 | } 176 | 177 | /** 178 | * Returns the description of a condition that has been overridden with a job outcome policy. 179 | * @return 180 | */ 181 | public String getStringForOverriden() { 182 | StringJoiner sj = new StringJoiner(", "); 183 | String preString = "["; 184 | String postString = "]"; 185 | 186 | if(applicationOriginName != null) { 187 | sj.add("name='"+applicationOriginName+"'"); 188 | } 189 | 190 | if(agentType != null) { 191 | sj.add("language='"+agentType+"'"); 192 | } 193 | 194 | if(applicationId != null) { 195 | sj.add("applicationId='"+getPreparedApplicationId()+"'"); 196 | } 197 | 198 | if(applicationId != null && applicationOriginName == null) { 199 | sj.add(applicationId); 200 | } 201 | 202 | return preString + sj.toString() + postString; 203 | } 204 | 205 | /** 206 | * Background: Some configurations can make the Application ID to be stored as 'AppName (AppId)' 207 | * This is a getter that only returns the actual application id. 208 | * Returns a proper application Id. 209 | * @return Returns the app id without the name attached 210 | */ 211 | public String getPreparedApplicationId() { 212 | if (VulnerabilityTrendHelper.getAppIdFromAppTitle(applicationId) != null) { 213 | return VulnerabilityTrendHelper.getAppIdFromAppTitle(applicationId); 214 | } 215 | return applicationId; 216 | } 217 | 218 | /** 219 | * Descriptor for {@link ThresholdCondition}. 220 | */ 221 | @Extension 222 | public static class DescriptorImpl extends Descriptor { 223 | 224 | Calendar lastAppsRefresh; 225 | int appsRefreshIntervalMinutes = 1; 226 | 227 | /** 228 | * Validation of the 'thresholdCount' form Field. 229 | * 230 | * @param value This parameter receives the value that the user has typed. 231 | * @return Indicates the outcome of the validation. This is sent to the browser. 232 | */ 233 | public FormValidation doCheckThresholdCount(@QueryParameter String value) { 234 | 235 | if (!value.isEmpty()) { 236 | try { 237 | int temp = Integer.parseInt(value); 238 | 239 | if (temp < 0) { 240 | return FormValidation.error("Please enter a positive integer."); 241 | } 242 | 243 | } catch (NumberFormatException e) { 244 | return FormValidation.error("Please enter a valid integer."); 245 | } 246 | } else { 247 | return FormValidation.error("Please enter a positive integer."); 248 | } 249 | 250 | return FormValidation.ok(); 251 | } 252 | 253 | /** 254 | * Validation of the 'thresholdSeverity' form Field. 255 | * 256 | * @param value This parameter receives the value that the user has typed. 257 | * @return Indicates the outcome of the validation. This is sent to the browser. 258 | */ 259 | public FormValidation doCheckThresholdSeverity(@QueryParameter String value) { 260 | return FormValidation.ok(); 261 | } 262 | 263 | /** 264 | * Validation of the 'thresholdCategory' form Field. 265 | * 266 | * @param value This parameter receives the value that the user has typed. 267 | * @return Indicates the outcome of the validation. This is sent to the browser. 268 | */ 269 | public FormValidation doCheckThresholdVulnType(@QueryParameter String value) { 270 | return FormValidation.ok(); 271 | } 272 | 273 | /** 274 | * Validation of the 'applicationId' form Field. 275 | * 276 | * @param value This parameter receives the value that the user has typed. 277 | * @return Indicates the outcome of the validation. This is sent to the browser. 278 | */ 279 | public FormValidation doCheckApplicationId(@QueryParameter("teamServerProfileName") @RelativePath("..") final String teamServerProfileName, @QueryParameter String value) { 280 | if (VulnerabilityTrendHelper.appExistsInProfile(teamServerProfileName, value)) { 281 | TeamServerProfile profile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 282 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(profile.getUsername(), profile.getServiceKey(), profile.getApiKey(), profile.getTeamServerUrl()); 283 | String appId = value; 284 | if(value.contains("(") && value.contains(")")) { 285 | appId = VulnerabilityTrendHelper.getAppIdFromAppTitle(value); 286 | } 287 | try { 288 | if (VulnerabilityTrendHelper.isApplicableEnabledJobOutcomePolicyExist(contrastSDK, profile.getOrgUuid(), appId)) { 289 | return FormValidation.warning("Your Contrast administrator has set a policy for vulnerability thresholds for this application. The Contrast policy overrides Jenkins vulnerability security controls and 'query vulnerabilities by' selection."); 290 | } 291 | } catch (IOException | UnauthorizedException e) { 292 | return FormValidation.warning("Unable to make connection with Contrast: " + e.getMessage()); 293 | } 294 | } else if(!value.isEmpty()) { 295 | return FormValidation.warning("Application not found."); 296 | } 297 | return FormValidation.ok(); 298 | 299 | } 300 | 301 | /** 302 | * Fills the Threshold Category combo box with application ids. 303 | * 304 | * @return ComboBoxModel filled with application ids. 305 | */ 306 | public ComboBoxModel doFillApplicationIdItems(@QueryParameter("teamServerProfileName") @RelativePath("..") final String teamServerProfileName) { 307 | 308 | // Refresh apps every ${appsRefreshIntervalMinutes} minutes before filling in the combobox 309 | if (lastAppsRefresh == null || (Calendar.getInstance().getTimeInMillis() - lastAppsRefresh.getTimeInMillis()) / 60000 >= appsRefreshIntervalMinutes) { 310 | refreshApps(teamServerProfileName); 311 | lastAppsRefresh = Calendar.getInstance(); 312 | } 313 | 314 | return VulnerabilityTrendHelper.getApplicationIdsComboBoxModel(teamServerProfileName); 315 | } 316 | 317 | public void refreshApps(String teamServerProfileName) { 318 | Jenkins jenkins = Jenkins.getInstance(); 319 | if (jenkins != null) { 320 | ContrastPluginConfig.ContrastPluginConfigDescriptor contrastPluginConfigDescriptor = jenkins.getDescriptorByType(ContrastPluginConfig.ContrastPluginConfigDescriptor.class); 321 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(teamServerProfileName, contrastPluginConfigDescriptor); 322 | 323 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(teamServerProfile.getUsername(), teamServerProfile.getServiceKey(), 324 | teamServerProfile.getApiKey(), teamServerProfile.getTeamServerUrl()); 325 | 326 | teamServerProfile.setApps(VulnerabilityTrendHelper.saveApplicationIds(contrastSDK, teamServerProfile.getOrgUuid())); 327 | contrastPluginConfigDescriptor.save(); 328 | } 329 | } 330 | 331 | public ListBoxModel doFillAgentTypeItems() { 332 | return VulnerabilityTrendHelper.getAgentTypeListBoxModel(); 333 | } 334 | 335 | /** 336 | * Fills the Threshold Category select drop down with vulnerability types for the configured profile. 337 | * 338 | * @return ListBoxModel filled with vulnerability types. 339 | */ 340 | public ListBoxModel doFillThresholdVulnTypeItems(@QueryParameter("teamServerProfileName") @RelativePath("..") final String teamServerProfileName) throws IOException { 341 | return VulnerabilityTrendHelper.getVulnerabilityTypes(teamServerProfileName); 342 | } 343 | 344 | /** 345 | * Fills the Threshold Severity select drop down with severities for the configured application. 346 | * 347 | * @return ListBoxModel filled with severities. 348 | */ 349 | public ListBoxModel doFillThresholdSeverityItems() { 350 | return VulnerabilityTrendHelper.getSeverityListBoxModel(); 351 | } 352 | 353 | /** 354 | * Display name in the Build Action dropdown. 355 | * 356 | * @return String 357 | */ 358 | public String getDisplayName() { 359 | return "Threshold Condition"; 360 | } 361 | } 362 | 363 | public List getVulnerabilityStatuses() { 364 | List status = new ArrayList(); 365 | if (autoRemediated) { 366 | status.add(Constants.VULNERABILITY_STATUS_AUTO_REMEDIATED); 367 | } 368 | if (confirmed) { 369 | status.add(Constants.VULNERABILITY_STATUS_CONFIRMED); 370 | } 371 | if (suspicious) { 372 | status.add(Constants.VULNERABILITY_STATUS_SUSPICIOUS); 373 | } 374 | if (notAProblem) { 375 | status.add(Constants.VULNERABILITY_STATUS_NOT_A_PROBLEM); 376 | } 377 | if (remediated) { 378 | status.add(Constants.VULNERABILITY_STATUS_REMEDIATED); 379 | } 380 | if (reported) { 381 | status.add(Constants.VULNERABILITY_STATUS_REPORTED); 382 | } 383 | if (fixed) { 384 | status.add(Constants.VULNERABILITY_STATUS_FIXED); 385 | } 386 | if (beingTracked) { 387 | status.add(Constants.VULNERABILITY_STATUS_BEING_TRACKED); 388 | } 389 | if (untracked) { 390 | status.add(Constants.VULNERABILITY_STATUS_UNTRACKED); 391 | } 392 | return status; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityFrequencyAction.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import hudson.model.AbstractBuild; 4 | import hudson.model.Action; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @Getter 12 | @Setter 13 | public class VulnerabilityFrequencyAction implements Action { 14 | 15 | private AbstractBuild build; 16 | private VulnerabilityTrendResult result; 17 | 18 | public VulnerabilityFrequencyAction(VulnerabilityTrendResult result, AbstractBuild build) { 19 | this.result = result; 20 | this.build = build; 21 | } 22 | 23 | public String getIconFileName() { 24 | return "/plugin/contrast-continuous-application-security/img/trend_graph.png"; 25 | } 26 | 27 | public String getDisplayName() { 28 | return "Vulnerability Report"; 29 | } 30 | 31 | public String getUrlName() { 32 | return "vulnReport"; 33 | } 34 | 35 | public int getBuildNumber() { 36 | return this.build.getNumber(); 37 | } 38 | 39 | public String getReport() { 40 | return "Vulnerability Report"; 41 | } 42 | 43 | public Map getTraceResult() { 44 | return result.getTraceResult(); 45 | } 46 | 47 | public Map getSeverityResult() { 48 | return result.getSeverityResult(); 49 | } 50 | 51 | public List getSeverities() { 52 | return VulnerabilityTrendHelper.SEVERITIES; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "Build Number # " + build.getNumber() + "
" + result.toString(); 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendHelper.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | 4 | import com.contrastsecurity.exceptions.UnauthorizedException; 5 | import com.contrastsecurity.http.RuleSeverity; 6 | import com.contrastsecurity.http.SecurityCheckFilter; 7 | import com.contrastsecurity.http.SecurityCheckForm; 8 | import com.contrastsecurity.http.TraceFilterForm; 9 | import com.contrastsecurity.models.AgentType; 10 | import com.contrastsecurity.models.Application; 11 | import com.contrastsecurity.models.Applications; 12 | import com.contrastsecurity.models.JobOutcomePolicy; 13 | import com.contrastsecurity.models.Rules; 14 | import com.contrastsecurity.models.SecurityCheck; 15 | import com.contrastsecurity.models.Trace; 16 | import com.contrastsecurity.models.Traces; 17 | import com.contrastsecurity.sdk.ContrastSDK; 18 | import hudson.ProxyConfiguration; 19 | import hudson.model.Result; 20 | import hudson.model.Run; 21 | import hudson.model.TaskListener; 22 | import hudson.util.ComboBoxModel; 23 | import hudson.util.ListBoxModel; 24 | import jenkins.model.Jenkins; 25 | import org.apache.commons.lang3.StringUtils; 26 | 27 | import javax.annotation.Nullable; 28 | import java.io.IOException; 29 | import java.net.MalformedURLException; 30 | import java.net.Proxy; 31 | import java.net.URL; 32 | import java.util.ArrayList; 33 | import java.util.Arrays; 34 | import java.util.Collections; 35 | import java.util.EnumSet; 36 | import java.util.List; 37 | 38 | 39 | public class VulnerabilityTrendHelper { 40 | 41 | public static ContrastSDK createSDK(String username, String serviceKey, String apiKey, String teamServerUrl) { 42 | ContrastSDK contrastSDK; 43 | Jenkins jenkinsInstance = Jenkins.getInstance(); 44 | ProxyConfiguration proxyConfig = null; 45 | if (jenkinsInstance != null) { 46 | proxyConfig = jenkinsInstance.proxy; 47 | } 48 | 49 | URL url = null; 50 | Proxy proxyToUse = Proxy.NO_PROXY; 51 | try { 52 | url = new URL(teamServerUrl); 53 | } catch (MalformedURLException e) { 54 | e.printStackTrace(); 55 | } 56 | 57 | if (proxyConfig != null && url != null) { 58 | Proxy proxy = proxyConfig.createProxy(url.getHost()); 59 | if (proxy != null && proxy.type() == Proxy.Type.HTTP) { 60 | proxyToUse = proxy; 61 | } 62 | } 63 | contrastSDK = new ContrastSDK.Builder(username, serviceKey, apiKey).withApiUrl(teamServerUrl).withProxy(proxyToUse).build(); 64 | 65 | return contrastSDK; 66 | } 67 | 68 | public static TeamServerProfile getProfile(String profileName) { 69 | if (profileName == null) 70 | return null; 71 | 72 | ContrastPluginConfig.ContrastPluginConfigDescriptor contrastPluginConfigDescriptor = new ContrastPluginConfig.ContrastPluginConfigDescriptor(); 73 | final TeamServerProfile[] profiles = contrastPluginConfigDescriptor.getTeamServerProfiles(); 74 | 75 | // Return the first profile; it is assumed that if it is empty, 76 | // it's the first element in a drop down that hasn't fully loaded yet 77 | if (StringUtils.isEmpty(profileName)) { 78 | return profiles[0]; 79 | } 80 | 81 | for (TeamServerProfile profile : profiles) { 82 | if (profileName.trim().equalsIgnoreCase(profile.getName().trim())) { 83 | return profile; 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | public static TeamServerProfile getProfile(String profileName, ContrastPluginConfig.ContrastPluginConfigDescriptor contrastPluginConfigDescriptor) { 90 | if (profileName == null) 91 | return null; 92 | 93 | final TeamServerProfile[] profiles = contrastPluginConfigDescriptor.getTeamServerProfiles(); 94 | 95 | // Return the first profile; it is assumed that if it is empty, 96 | // it's the first element in a drop down that hasn't fully loaded yet 97 | if (StringUtils.isEmpty(profileName)) { 98 | return profiles[0]; 99 | } 100 | 101 | for (TeamServerProfile profile : profiles) { 102 | if (profileName.equals(profile.getName())) { 103 | return profile; 104 | } 105 | } 106 | return null; 107 | } 108 | 109 | public static List getGlobalThresholdConditions(String profileName) { 110 | final GlobalThresholdCondition[] globalThresholdConditions = new ContrastPluginConfig.ContrastPluginConfigDescriptor().getGlobalThresholdConditions(); 111 | List globalThresholdConditionList = new ArrayList<>(); 112 | 113 | if (globalThresholdConditions[0] == null) { 114 | return null; 115 | } 116 | 117 | for (GlobalThresholdCondition globalThresholdCondition : globalThresholdConditions) { 118 | if (profileName.equals(globalThresholdCondition.getTeamServerProfileName())) { 119 | globalThresholdConditionList.add(globalThresholdCondition); 120 | } 121 | } 122 | return globalThresholdConditionList; 123 | } 124 | 125 | /** 126 | * Helper method for combining global threshold conditions and threshold conditions configured in jobs. 127 | * 128 | * @param thresholdConditions 129 | * @param globalThresholdConditions 130 | */ 131 | public static List getThresholdConditions(final List thresholdConditions, 132 | final List globalThresholdConditions) { 133 | List newThresholdConditions = new ArrayList<>(); 134 | 135 | if (thresholdConditions == null || globalThresholdConditions == null) { 136 | return newThresholdConditions; 137 | } 138 | for (ThresholdCondition thresholdCondition : thresholdConditions) { 139 | for (GlobalThresholdCondition globalThresholdCondition : globalThresholdConditions) { 140 | ThresholdCondition newThresholdCondition = new ThresholdCondition( 141 | globalThresholdCondition.getThresholdCount(), 142 | globalThresholdCondition.getThresholdSeverity(), globalThresholdCondition.getThresholdVulnType(), thresholdCondition.getApplicationState(), thresholdCondition.getApplicationDefinition(), 143 | thresholdCondition.getApplicationId(),globalThresholdCondition.isAutoRemediated(), 144 | globalThresholdCondition.isConfirmed(), globalThresholdCondition.isSuspicious(), 145 | globalThresholdCondition.isNotAProblem(), globalThresholdCondition.isRemediated(), 146 | globalThresholdCondition.isReported(), globalThresholdCondition.isFixed(), globalThresholdCondition.isBeingTracked(), 147 | globalThresholdCondition.isUntracked()); 148 | if (thresholdCondition.getApplicationName() != null) { 149 | newThresholdCondition.setApplicationName(thresholdCondition.getApplicationName()); 150 | } 151 | newThresholdConditions.add(newThresholdCondition); 152 | } 153 | } 154 | return newThresholdConditions; 155 | } 156 | 157 | /** 158 | * Helper method for logging messages. 159 | * 160 | * @param listener Listener 161 | * @param msg String to log 162 | */ 163 | public static void logMessage(TaskListener listener, String msg) { 164 | listener.getLogger().println("[Contrast] - " + msg); 165 | } 166 | 167 | /** 168 | * Returns the sublist of severities greater than or equal to the configured severity level 169 | * 170 | * @param severity include severity to filter with severity list with 171 | * @return list of severity strings 172 | */ 173 | public static EnumSet getSeverityList(String severity) { 174 | 175 | List severityList = SEVERITIES.subList(SEVERITIES.indexOf(severity), SEVERITIES.size()); 176 | 177 | List ruleSeverities = new ArrayList<>(); 178 | 179 | for (String severityToAdd : severityList) { 180 | ruleSeverities.add(RuleSeverity.valueOf(severityToAdd.toUpperCase())); 181 | } 182 | 183 | return EnumSet.copyOf(ruleSeverities); 184 | } 185 | 186 | /** 187 | * Retrieves the enabled rules for an organization 188 | * 189 | * @param sdk Contrast SDK object 190 | * @param organizationUuid uuid of the organization 191 | */ 192 | public static List saveRules(ContrastSDK sdk, String organizationUuid) { 193 | Rules rules; 194 | List vulnerabilityTypes = new ArrayList<>(); 195 | 196 | try { 197 | rules = sdk.getRules(organizationUuid); 198 | } catch (IOException | UnauthorizedException e) { 199 | return vulnerabilityTypes; 200 | } 201 | 202 | for (Rules.Rule rule : rules.getRules()) { 203 | vulnerabilityTypes.add(new VulnerabilityType(rule.getName(), rule.getTitle())); 204 | } 205 | 206 | return vulnerabilityTypes; 207 | } 208 | 209 | 210 | /** 211 | * The available severities for the configuration dropdowns 212 | * 213 | * @return ListBoxModel of severities 214 | */ 215 | public static ListBoxModel getSeverityListBoxModel() { 216 | ListBoxModel items = new ListBoxModel(); 217 | items.add(VulnerabilityTrendHelper.EMPTY_SELECT, null); 218 | 219 | for (String severity : VulnerabilityTrendHelper.SEVERITIES) { 220 | items.add(severity, severity); 221 | } 222 | 223 | return items; 224 | } 225 | 226 | /** 227 | * The configured profile names for the dropdowns 228 | * 229 | * @return ListBoxModel of TeamServer profile names 230 | */ 231 | public static ListBoxModel getProfileNames() { 232 | final ListBoxModel model = new ListBoxModel(); 233 | 234 | for (TeamServerProfile profile : new ContrastPluginConfig.ContrastPluginConfigDescriptor().getTeamServerProfiles()) { 235 | model.add(profile.getName(), profile.getName()); 236 | } 237 | 238 | return model; 239 | } 240 | 241 | /** 242 | * The vulnerability types for a profile 243 | * 244 | * @param teamServerProfileName Name of the profile 245 | * @return ListBoxModel of vulnerability types 246 | */ 247 | public static ListBoxModel getVulnerabilityTypes(String teamServerProfileName) { 248 | ListBoxModel items = new ListBoxModel(); 249 | 250 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 251 | 252 | items.add(VulnerabilityTrendHelper.EMPTY_SELECT, null); 253 | 254 | if (teamServerProfile != null) { 255 | for (VulnerabilityType vulnerabilityType : teamServerProfile.getVulnerabilityTypes()) { 256 | items.add(vulnerabilityType.getTitle(), vulnerabilityType.getName()); 257 | } 258 | } 259 | 260 | return items; 261 | } 262 | 263 | /** 264 | * The available agent types for the configuration dropdown 265 | * 266 | * @return ListBoxModel of agent types 267 | */ 268 | public static ListBoxModel getAgentTypeListBoxModel() { 269 | ListBoxModel items = new ListBoxModel(); 270 | 271 | items.add("Java", AgentType.JAVA.toString()); 272 | items.add(".NET", AgentType.DOTNET.toString()); 273 | items.add("Node", AgentType.NODE.toString()); 274 | items.add("Ruby", AgentType.RUBY.toString()); 275 | items.add("Python", AgentType.PYTHON.toString()); 276 | items.add(".NET_Core", AgentType.DOTNET_CORE.toString()); 277 | items.add("Proxy", AgentType.PROXY.toString()); 278 | 279 | return items; 280 | } 281 | 282 | public static AgentType getAgentTypeFromString(String type) { 283 | switch (type.toUpperCase()) { 284 | case ".NET": 285 | return AgentType.DOTNET; 286 | case "NODE": 287 | return AgentType.NODE; 288 | case "RUBY": 289 | return AgentType.RUBY; 290 | case "PYTHON": 291 | return AgentType.PYTHON; 292 | case ".NET_CORE": 293 | return AgentType.DOTNET_CORE; 294 | case "PROXY": 295 | return AgentType.PROXY; 296 | case "JAVA": 297 | default: 298 | return AgentType.JAVA; 299 | } 300 | } 301 | 302 | public static String getDefaultAgentFileNameFromString(String type) { 303 | switch (type.toUpperCase()) { 304 | case ".NET": 305 | return "dotnet-contrast.zip"; 306 | case "NODE": 307 | return "node-contrast.tgz"; 308 | case "RUBY": 309 | return "ruby-contrast.gem"; 310 | case "PYTHON": 311 | return "python-contrast.tar.gz"; 312 | case ".NET_CORE": 313 | return "dotnet_core-contrast.zip"; 314 | case "JAVA": 315 | default: 316 | return "contrast.jar"; 317 | } 318 | } 319 | 320 | public static String buildAppVersionTag(Run build, String applicationId) { 321 | return applicationId + "-" + build.getNumber(); 322 | } 323 | 324 | public static String buildAppVersionTagHierarchical(Run build, String applicationId) { 325 | return applicationId + "-" + build.getParent().getFullName() + "-" + build.getNumber(); 326 | } 327 | 328 | /** 329 | * Number of traces by severity 330 | * 331 | * @param traces 332 | * @return String with the number of traces for each severity. 333 | */ 334 | public static String getVulnerabilityInfoString(Traces traces) { 335 | StringBuilder info = new StringBuilder(); 336 | int note = 0; 337 | int low = 0; 338 | int medium = 0; 339 | int high = 0; 340 | int critical = 0; 341 | 342 | if (traces == null || traces.getTraces() == null || traces.getTraces().isEmpty()) { 343 | return info.toString(); 344 | } else { 345 | info.append("Found vulnerabilities: "); 346 | for (Trace trace : traces.getTraces()) { 347 | switch (trace.getSeverity()) { 348 | case "Note": 349 | note++; 350 | break; 351 | case "Low": 352 | low++; 353 | break; 354 | case "Medium": 355 | medium++; 356 | break; 357 | case "High": 358 | high++; 359 | break; 360 | case "Critical": 361 | critical++; 362 | break; 363 | default: 364 | break; 365 | } 366 | 367 | } 368 | if (note > 0) { 369 | info.append("Note - " + String.valueOf(note) + " "); 370 | } 371 | if (low > 0) { 372 | info.append("Low - " + String.valueOf(low) + " "); 373 | } 374 | if (medium > 0) { 375 | info.append("Medium - " + String.valueOf(medium) + " "); 376 | } 377 | if (high > 0) { 378 | info.append("High - " + String.valueOf(high) + " "); 379 | } 380 | if (critical > 0) { 381 | info.append("Critical - " + String.valueOf(critical) + " "); 382 | } 383 | info.append("."); 384 | return info.toString(); 385 | } 386 | 387 | } 388 | /** 389 | * Collection of all traces 390 | * 391 | * @param sdk ContrastSDK instance 392 | * @param organizationId Organization ID of the application 393 | * @param applicationId Application ID (optional) 394 | * @param filter TraceFormFilter to limit results 395 | * @return Traces object 396 | */ 397 | public static Traces getAllTraces(ContrastSDK sdk, String organizationId, String applicationId, TraceFilterForm filter) throws IOException, UnauthorizedException { 398 | Traces traces = new Traces(); 399 | 400 | // Contrast API returns traces paginated at 20 per page by default. Increase page size for faster transfer. 401 | int page = 0; 402 | int pageSize = 50; 403 | filter.setLimit(pageSize); 404 | 405 | do { 406 | filter.setOffset(page * pageSize); 407 | 408 | Traces intermediateTraces; 409 | 410 | if (applicationId == null) { 411 | intermediateTraces = sdk.getTracesInOrg(organizationId, filter); 412 | 413 | } else { 414 | intermediateTraces = sdk.getTraces(organizationId, applicationId, filter); 415 | } 416 | 417 | if (page == 0) { 418 | traces = intermediateTraces; 419 | } else { 420 | traces.getTraces().addAll(intermediateTraces.getTraces()); 421 | } 422 | 423 | page++; 424 | } while (traces.getTraces().size() < traces.getCount()); 425 | 426 | return traces; 427 | } 428 | 429 | static boolean applicationIdExists(ContrastSDK sdk, String organizationUuid, String applicationId) { 430 | 431 | if (applicationId == null || applicationId.isEmpty()) { 432 | return false; 433 | } 434 | 435 | Applications applications; 436 | 437 | try { 438 | applications = sdk.getApplications(organizationUuid); 439 | } catch (IOException | UnauthorizedException e) { 440 | return false; 441 | } 442 | 443 | for (Application application : applications.getApplications()) { 444 | if (applicationId.equals(application.getId())) { 445 | return true; 446 | } 447 | } 448 | 449 | return false; 450 | } 451 | 452 | /** 453 | * The apps for a profile 454 | * 455 | * @param teamServerProfileName Name of the profile 456 | * @return ListBoxModel of apps 457 | */ 458 | public static ListBoxModel getApplicationIds(String teamServerProfileName) { 459 | ListBoxModel items = new ListBoxModel(); 460 | 461 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 462 | 463 | if (teamServerProfile != null) { 464 | for (App app : teamServerProfile.getApps()) { 465 | items.add(app.getTitle(), app.getName()); 466 | } 467 | } 468 | 469 | return items; 470 | } 471 | 472 | /** 473 | * The apps for a profile 474 | * 475 | * @param teamServerProfileName Name of the profile 476 | * @return ComboBoxModel of apps 477 | */ 478 | public static ComboBoxModel getApplicationIdsComboBoxModel(String teamServerProfileName) { 479 | ComboBoxModel items = new ComboBoxModel(); 480 | 481 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 482 | 483 | if (teamServerProfile != null) { 484 | for (App app : teamServerProfile.getApps()) { 485 | items.add(app.getTitle()); 486 | } 487 | } 488 | 489 | return items; 490 | } 491 | 492 | public static boolean appExistsInProfile(String teamServerProfileName, String appTitle) { 493 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 494 | 495 | if (teamServerProfile != null) { 496 | for (App app : teamServerProfile.getApps()) { 497 | if (appTitle.equals(app.getTitle()) || appTitle.equals(app.getName())) 498 | return true; 499 | } 500 | } 501 | return false; 502 | } 503 | 504 | /** 505 | * get app id from app title in the post build action combo box 506 | * @param appTitle for example: WebGoat (a123745f-5857-45e4-a278-ddb5012e1996) 507 | * @return appId 508 | */ 509 | @Nullable 510 | public static String getAppIdFromAppTitle(String appTitle) { 511 | int beginIndex = org.apache.commons.lang.StringUtils.lastIndexOf(appTitle, "("); 512 | int endIndex = org.apache.commons.lang.StringUtils.lastIndexOf(appTitle, ")"); 513 | 514 | if (beginIndex >= 0 && endIndex >= 0 && endIndex > beginIndex) { 515 | return appTitle.substring(beginIndex + 1, endIndex); 516 | } 517 | return null; 518 | } 519 | 520 | 521 | /** 522 | * get the application name from the app title. 523 | * @param appTitle 524 | * @return 525 | */ 526 | public static String getAppNameFromAppTitle(String appTitle) { 527 | return appTitle.substring(0, appTitle.lastIndexOf(" (")); 528 | } 529 | 530 | /** 531 | * Retrieves the applications 532 | * 533 | * @param sdk Contrast SDK object 534 | * @param organizationUuid uuid of the organization 535 | */ 536 | public static List saveApplicationIds(ContrastSDK sdk, String organizationUuid) { 537 | Applications applications; 538 | List apps = new ArrayList<>(); 539 | 540 | try { 541 | applications = sdk.getApplications(organizationUuid); 542 | } catch (IOException | UnauthorizedException e) { 543 | return apps; 544 | } 545 | 546 | for (Application application : applications.getApplications()) { 547 | apps.add(new App(application.getId(), application.getName() + " (" + application.getId() + ")")); 548 | } 549 | 550 | return apps; 551 | } 552 | 553 | public static SecurityCheck makeSecurityCheck(ContrastSDK sdk, String organizationUuid, String applicationId, Long jobStartTime, int queryBy, TraceFilterForm filterForm) throws IOException, UnauthorizedException { 554 | SecurityCheckForm form = new SecurityCheckForm(applicationId); 555 | form.setJobStartTime(jobStartTime); 556 | 557 | SecurityCheckFilter securityCheckFilter = new SecurityCheckFilter(); 558 | if(Constants.QUERY_BY_START_DATE == queryBy) { 559 | securityCheckFilter.setQueryBy(SecurityCheckFilter.QueryBy.START_DATE); 560 | securityCheckFilter.setStartDate(filterForm.getStartDate().toInstant().toEpochMilli()); 561 | } else { 562 | securityCheckFilter.setQueryBy(SecurityCheckFilter.QueryBy.APP_VERSION_TAG); 563 | securityCheckFilter.setAppVersionTags(filterForm.getAppVersionTags()); 564 | } 565 | 566 | form.setSecurityCheckFilter(securityCheckFilter); 567 | 568 | return sdk.makeSecurityCheck(organizationUuid, form); 569 | } 570 | 571 | public static Result getJenkinsResultFromJobOutcome(JobOutcomePolicy.Outcome outcome) throws VulnerabilityTrendHelperException { 572 | switch(outcome) { 573 | case FAIL: 574 | return Result.FAILURE; 575 | case SUCCESS: 576 | return Result.SUCCESS; 577 | case UNSTABLE: 578 | return Result.UNSTABLE; 579 | default: 580 | throw new VulnerabilityTrendHelperException("Unrecognized Job Outcome: " + outcome.toString()); 581 | } 582 | } 583 | 584 | /** 585 | * Checks to see if the organization has a job outcome policy that is enabled 586 | * @param sdk Contrast SDK 587 | * @param organizationUuid uuid of the organization 588 | * @return true = an enabled job outcome policy exists, false = no enabled job outcome policy exist 589 | */ 590 | public static boolean isEnabledJobOutcomePolicyExist(ContrastSDK sdk, String organizationUuid) throws IOException, UnauthorizedException { 591 | return sdk.getEnabledJobOutcomePolicies(organizationUuid).size() > 0; 592 | } 593 | 594 | public static boolean isApplicableEnabledJobOutcomePolicyExist(ContrastSDK sdk, String organizationUuid, String applicationId) throws IOException, UnauthorizedException { 595 | return applicationIdExists(sdk, organizationUuid, applicationId) && sdk.getEnabledJoboutcomePoliciesByApplication(organizationUuid, applicationId).size() > 0; 596 | } 597 | 598 | 599 | 600 | public static final String EMPTY_SELECT = "All"; 601 | public static final List SEVERITIES = Collections.unmodifiableList(Arrays.asList("Note", "Low", "Medium", "High", "Critical")); 602 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendHelperException.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | public class VulnerabilityTrendHelperException extends Exception { 4 | public VulnerabilityTrendHelperException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendProjectAction.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.aspectsecurity.contrast.contrastjenkins.plots.SeverityFrequencyPlot; 4 | import com.aspectsecurity.contrast.contrastjenkins.plots.VulnerabilityFrequencyPlot; 5 | import hudson.model.AbstractBuild; 6 | import hudson.model.AbstractProject; 7 | import hudson.model.Action; 8 | import hudson.util.Graph; 9 | 10 | import java.util.List; 11 | 12 | public class VulnerabilityTrendProjectAction implements Action { 13 | 14 | private AbstractProject project; 15 | 16 | public VulnerabilityTrendProjectAction(final AbstractProject project) { 17 | this.project = project; 18 | } 19 | 20 | @Override 21 | public String getIconFileName() { 22 | return null; 23 | } 24 | 25 | @Override 26 | public String getDisplayName() { 27 | return null; 28 | } 29 | 30 | @Override 31 | public String getUrlName() { 32 | return "vulnTrendCharts"; 33 | } 34 | 35 | public AbstractProject getProject() { 36 | return this.project; 37 | } 38 | 39 | // used in floatingBox.jelly 40 | public Graph getVulnerabilityGraph() { 41 | return new VulnerabilityFrequencyPlot(project); 42 | } 43 | 44 | // used in floatingBox.jelly 45 | public Graph getSeverityGraph() { 46 | return new SeverityFrequencyPlot(project); 47 | } 48 | 49 | public String getProjectName() { 50 | return this.project.getName(); 51 | } 52 | 53 | // used to hide charts 54 | public boolean getHasBuilds() { 55 | return !project.getBuilds().isEmpty(); 56 | } 57 | 58 | public String getBuildResult() { 59 | List> builds = project.getBuilds(); 60 | StringBuilder buildResult = new StringBuilder(); 61 | 62 | final Class buildClass = VulnerabilityFrequencyAction.class; 63 | 64 | for (AbstractBuild currentBuild : builds) { 65 | buildResult.append(currentBuild.getAction(buildClass).toString()); 66 | } 67 | 68 | return buildResult.toString(); 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendRecorder.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.contrastsecurity.exceptions.UnauthorizedException; 4 | import com.contrastsecurity.http.TraceFilterForm; 5 | import com.contrastsecurity.models.Application; 6 | import com.contrastsecurity.models.Organizations; 7 | import com.contrastsecurity.models.SecurityCheck; 8 | import com.contrastsecurity.models.Trace; 9 | import com.contrastsecurity.models.Traces; 10 | import com.contrastsecurity.sdk.ContrastSDK; 11 | import hudson.AbortException; 12 | import hudson.EnvVars; 13 | import hudson.Extension; 14 | import hudson.Launcher; 15 | import hudson.model.AbstractBuild; 16 | import hudson.model.AbstractProject; 17 | import hudson.model.Action; 18 | import hudson.model.BuildListener; 19 | import hudson.model.Result; 20 | import hudson.tasks.BuildStepDescriptor; 21 | import hudson.tasks.BuildStepMonitor; 22 | import hudson.tasks.Publisher; 23 | import hudson.tasks.Recorder; 24 | import hudson.util.ListBoxModel; 25 | import jenkins.model.Jenkins; 26 | import lombok.Getter; 27 | import lombok.Setter; 28 | import net.sf.json.JSONArray; 29 | import net.sf.json.JSONObject; 30 | import org.kohsuke.stapler.DataBoundConstructor; 31 | import org.kohsuke.stapler.StaplerRequest; 32 | import org.kohsuke.stapler.bind.JavaScriptMethod; 33 | 34 | import java.io.IOException; 35 | import java.util.ArrayList; 36 | import java.util.Collections; 37 | import java.util.Date; 38 | import java.util.HashMap; 39 | import java.util.HashSet; 40 | import java.util.List; 41 | import java.util.Map; 42 | import java.util.Set; 43 | 44 | 45 | /** 46 | * Vulnerability Trend Builder 47 | *

48 | * Checks the number of vulnerabilities in the application against the configured threshold. 49 | */ 50 | @Getter 51 | @Setter 52 | public class VulnerabilityTrendRecorder extends Recorder { 53 | 54 | private List conditions; 55 | private String teamServerProfileName; 56 | private boolean overrideGlobalThresholdConditions; 57 | private int queryBy; 58 | 59 | 60 | @DataBoundConstructor 61 | public VulnerabilityTrendRecorder(List conditions, String teamServerProfileName, boolean overrideGlobalThresholdConditions, int queryBy) { 62 | this.conditions = conditions; 63 | this.teamServerProfileName = teamServerProfileName; 64 | this.overrideGlobalThresholdConditions = overrideGlobalThresholdConditions; 65 | this.queryBy = queryBy; 66 | } 67 | 68 | protected Object readResolve() { 69 | 70 | Jenkins jenkins = Jenkins.getInstance(); 71 | if (jenkins != null) { 72 | ContrastPluginConfig.ContrastPluginConfigDescriptor contrastPluginConfigDescriptor = jenkins.getDescriptorByType(ContrastPluginConfig.ContrastPluginConfigDescriptor.class); 73 | 74 | final TeamServerProfile[] profiles = contrastPluginConfigDescriptor.getTeamServerProfiles(); 75 | for (TeamServerProfile profile : profiles) { 76 | if (profile.getApps() == null) { 77 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(profile.getUsername(), profile.getServiceKey(), 78 | profile.getApiKey(), profile.getTeamServerUrl()); 79 | List apps = VulnerabilityTrendHelper.saveApplicationIds(contrastSDK, profile.getOrgUuid()); 80 | profile.setApps(apps); 81 | profile.setFailOnWrongApplicationId(profile.isFailOnWrongApplicationName()); 82 | 83 | contrastPluginConfigDescriptor.save(); 84 | } 85 | } 86 | 87 | for (ThresholdCondition thresholdCondition : conditions) { 88 | if (thresholdCondition.getApplicationId() == null && thresholdCondition.getApplicationName() != null) { 89 | TeamServerProfile profile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 90 | for (App app : profile.getApps()) { 91 | String subStr = app.getTitle().substring(0, app.getTitle().lastIndexOf(" (")); 92 | if (subStr.equals(thresholdCondition.getApplicationName())) { 93 | thresholdCondition.setApplicationId(app.getName()); 94 | break; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | return this; 101 | } 102 | 103 | private TraceFilterForm buildFilterFormForCondition(final ThresholdCondition condition, final AbstractBuild build, final String appId, final BuildListener listener) throws IOException, InterruptedException { 104 | TraceFilterForm filterForm = new TraceFilterForm(); 105 | 106 | if (queryBy == Constants.QUERY_BY_APP_VERSION_TAG_HIERARCHICAL_FORMAT) { 107 | String appVersionTag = VulnerabilityTrendHelper.buildAppVersionTagHierarchical(build, appId); 108 | 109 | List appVersionTagsList = new ArrayList<>(); 110 | appVersionTagsList.add(appVersionTag); 111 | 112 | if (condition.getApplicationName() != null) { 113 | String appVersionTagAppName = VulnerabilityTrendHelper.buildAppVersionTagHierarchical(build, condition.getApplicationName()); 114 | appVersionTagsList.add(appVersionTagAppName); 115 | } 116 | 117 | filterForm.setAppVersionTags(appVersionTagsList); 118 | } else if (queryBy == Constants.QUERY_BY_START_DATE) { 119 | filterForm.setStartDate(new Date(build.getStartTimeInMillis())); 120 | } else if (queryBy == Constants.QUERY_BY_PARAMETER) { 121 | final EnvVars env = build.getEnvironment(listener); 122 | String appVersionTag; 123 | 124 | if (env.get("APPVERSIONTAG") != null) { 125 | appVersionTag = env.get("APPVERSIONTAG"); 126 | } else { 127 | appVersionTag = ""; 128 | } 129 | 130 | if (appVersionTag.isEmpty()) { 131 | VulnerabilityTrendHelper.logMessage(listener, "Warning: queryBy Parameter is configured, but APPVERSIONTAG is not set. All vulnerabilities will be returned for this application"); 132 | } 133 | 134 | List appVersionTagsList = new ArrayList<>(); 135 | appVersionTagsList.add(appVersionTag); 136 | 137 | filterForm.setAppVersionTags(appVersionTagsList); 138 | } else { 139 | String appVersionTag = VulnerabilityTrendHelper.buildAppVersionTag(build, appId); 140 | 141 | List appVersionTagsList = new ArrayList<>(); 142 | appVersionTagsList.add(appVersionTag); 143 | 144 | if (condition.getApplicationName() != null) { 145 | String appVersionTagAppName = VulnerabilityTrendHelper.buildAppVersionTag(build, condition.getApplicationName()); 146 | appVersionTagsList.add(appVersionTagAppName); 147 | } 148 | 149 | filterForm.setAppVersionTags(appVersionTagsList); 150 | } 151 | 152 | if (condition.getThresholdSeverity() != null) { 153 | filterForm.setSeverities(VulnerabilityTrendHelper.getSeverityList(condition.getThresholdSeverity())); 154 | } 155 | 156 | if (condition.getThresholdVulnType() != null) { 157 | filterForm.setVulnTypes(Collections.singletonList(condition.getThresholdVulnType())); 158 | } 159 | 160 | if (!condition.getVulnerabilityStatuses().isEmpty()) { 161 | filterForm.setStatus(condition.getVulnerabilityStatuses()); 162 | } 163 | return filterForm; 164 | } 165 | 166 | /** 167 | * Set the build result based on contrast configuration 168 | * @param build build object 169 | * @param profile teamserver profile 170 | * @param message Message to log when status is failure 171 | * @return true = the result was set, false = the result was not set 172 | */ 173 | private boolean updateBuildResultOnError(AbstractBuild build, TeamServerProfile profile, String message, BuildListener listener) throws AbortException { 174 | if(profile.isApplyVulnerableBuildResultOnContrastError()) { 175 | Result profileVulnerableBuildResult = Result.fromString(profile.getVulnerableBuildResult()); 176 | if(Result.FAILURE.equals(profileVulnerableBuildResult)) { 177 | throw new AbortException(message); 178 | } else { 179 | build.setResult(profileVulnerableBuildResult); 180 | return true; 181 | } 182 | } else { 183 | VulnerabilityTrendHelper.logMessage(listener, "Warning: Build result was not updated because the connection "+profile.getName()+" is configured to not update the build status when an error occurs."); 184 | } 185 | return false; 186 | } 187 | 188 | private boolean globalThresholdRequired(List conditions, ContrastSDK contrastSDK, TeamServerProfile profile) 189 | throws IOException, UnauthorizedException { 190 | for(ThresholdCondition condition : conditions) { 191 | if((!profile.isAllowGlobalThresholdConditionsOverride() || !overrideGlobalThresholdConditions) 192 | && !VulnerabilityTrendHelper.isApplicableEnabledJobOutcomePolicyExist(contrastSDK, profile.getOrgUuid(), condition.getPreparedApplicationId())) { 193 | return true; 194 | } 195 | } 196 | return false; 197 | } 198 | 199 | @Override 200 | public boolean perform(AbstractBuild build, Launcher launcher, final BuildListener listener) throws IOException, InterruptedException { 201 | boolean errorEncountered = false; 202 | if (!build.isBuilding()) { 203 | return false; 204 | } 205 | 206 | VulnerabilityTrendHelper.logMessage(listener, "Checking the number of vulnerabilities for this application."); 207 | ContrastSDK contrastSDK; 208 | Traces traces; 209 | Set resultTraces = new HashSet<>(); 210 | 211 | TeamServerProfile profile = VulnerabilityTrendHelper.getProfile(teamServerProfileName); 212 | if(profile == null) { 213 | throw new AbortException("Unable to find TeamServer profile."); 214 | } 215 | 216 | final String CONTRAST_ERROR_PREFIX = profile.isApplyVulnerableBuildResultOnContrastError() ? "Error: " : "Warning: "; 217 | 218 | contrastSDK = VulnerabilityTrendHelper.createSDK(profile.getUsername(), profile.getServiceKey(), profile.getApiKey(), profile.getTeamServerUrl()); 219 | 220 | 221 | 222 | try { 223 | final Organizations organizations = contrastSDK.getProfileDefaultOrganizations(); 224 | if (organizations == null || organizations.getOrganization() == null) { 225 | String errorMessage = CONTRAST_ERROR_PREFIX + "No organization found, Check your credentials and URL."; 226 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 227 | updateBuildResultOnError(build, profile, errorMessage, listener); 228 | return true; 229 | } 230 | 231 | } catch (UnauthorizedException | IOException e) { 232 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to connect to Contrast."; 233 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 234 | VulnerabilityTrendHelper.logMessage(listener, e.getMessage()); 235 | updateBuildResultOnError(build, profile, errorMessage, listener); 236 | return true; 237 | } 238 | 239 | boolean ignoreContrastFindings = Boolean.parseBoolean(build.getBuildVariableResolver().resolve("ignoreContrastFindings")); 240 | List globalThresholdConditions = VulnerabilityTrendHelper.getGlobalThresholdConditions(profile.getName()); 241 | List thresholdConditions = conditions; 242 | 243 | // initialize app id in conditions 244 | for (ThresholdCondition condition : thresholdConditions) { 245 | MatchBy matchBy = condition.getMatchBy() == null ? MatchBy.APPLICATION_ID : condition.getMatchBy(); 246 | switch(matchBy){ 247 | case APPLICATION_ORIGIN_NAME: 248 | try { 249 | Application app = contrastSDK.getApplicationByNameAndLanguage(profile.getOrgUuid(), condition.getApplicationOriginName(), VulnerabilityTrendHelper.getAgentTypeFromString(condition.getAgentType())); 250 | 251 | if (app == null) { 252 | String errorMessage = String.format(CONTRAST_ERROR_PREFIX + "Application with [name = %s, agentType = %s] not found.", condition.getApplicationOriginName(), condition.getAgentType()); 253 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 254 | 255 | if (condition.isFailOnAppNotFound()) { 256 | throw new AbortException(errorMessage); 257 | } 258 | 259 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 260 | return true; 261 | } else { 262 | errorEncountered = true; 263 | continue; 264 | } 265 | } else { 266 | condition.setApplicationId(app.getId()); 267 | VulnerabilityTrendHelper.logMessage(listener, "Fetched Application : [name = '" + condition.getApplicationOriginName() + "', displayName = '" + app.getName() + "', agentType='" + app.getLanguage() + "'] with ID: [" + condition.getPreparedApplicationId() + "]"); 268 | } 269 | } catch (UnauthorizedException e) { 270 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve application information from Contrast."; 271 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 272 | VulnerabilityTrendHelper.logMessage(listener, e.getMessage()); 273 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 274 | return true; 275 | } else { 276 | errorEncountered = true; 277 | continue; 278 | } 279 | } 280 | break; 281 | case APPLICATION_ID: 282 | default: 283 | break; 284 | } 285 | } 286 | 287 | try { 288 | if (globalThresholdRequired(thresholdConditions, contrastSDK, profile)) { 289 | thresholdConditions = 290 | VulnerabilityTrendHelper.getThresholdConditions(conditions, globalThresholdConditions); 291 | if (thresholdConditions.isEmpty()) { 292 | String errorMessage = CONTRAST_ERROR_PREFIX + "Vulnerability Security Controls for connection '" + profile.getName() + "' are not defined."; 293 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 294 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 295 | return true; 296 | } else { 297 | errorEncountered = true; 298 | } 299 | } 300 | } 301 | } catch (UnauthorizedException e) { 302 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve job outcome policy information from Contrast"; 303 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 304 | VulnerabilityTrendHelper.logMessage(listener, e.getMessage()); 305 | updateBuildResultOnError(build, profile, errorMessage, listener); 306 | return true; 307 | } 308 | 309 | // iterate over conditions; fail on first 310 | for (ThresholdCondition condition : thresholdConditions) { 311 | String appId = condition.getPreparedApplicationId(); 312 | 313 | boolean applicationIdExists = VulnerabilityTrendHelper.applicationIdExists(contrastSDK, profile.getOrgUuid(), appId); 314 | if (!applicationIdExists) { 315 | String errorMessage = CONTRAST_ERROR_PREFIX + "Application with ID '" + appId + "' not found."; 316 | VulnerabilityTrendHelper.logMessage(listener, CONTRAST_ERROR_PREFIX + "Application with ID '" + appId + "' not found."); 317 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 318 | return true; 319 | } else { 320 | errorEncountered = true; 321 | continue; 322 | } 323 | } else { 324 | try { 325 | 326 | TraceFilterForm filterForm = buildFilterFormForCondition(condition, build, appId, listener); 327 | SecurityCheck securityCheck = VulnerabilityTrendHelper.makeSecurityCheck(contrastSDK, profile.getOrgUuid(), appId, build.getTimeInMillis(), queryBy, filterForm); 328 | 329 | if(securityCheck.getResult() != null) { 330 | String applicationDisplayForConsoleOutput = condition.getStringForOverriden(); 331 | VulnerabilityTrendHelper.logMessage(listener, "Checking application " + applicationDisplayForConsoleOutput); 332 | VulnerabilityTrendHelper.logMessage(listener,"Your Contrast admin has overridden policies you may have set in Vulnerability Security Controls or the 'query by' parameter"); 333 | 334 | if(securityCheck.getResult()) { //failed a policy 335 | VulnerabilityTrendHelper.logMessage(listener, "This application did not violate any Contrast policies"); 336 | return true; 337 | } else { 338 | try { 339 | Result jobResult = VulnerabilityTrendHelper.getJenkinsResultFromJobOutcome(securityCheck.getJobOutcomePolicy().getOutcome()); 340 | String message = "This application "+applicationDisplayForConsoleOutput+" violated the Contrast policy '"+securityCheck.getJobOutcomePolicy().getName()+"'"; 341 | VulnerabilityTrendHelper.logMessage(listener,message); 342 | if(Result.FAILURE.equals(jobResult)) { 343 | throw new AbortException(message); 344 | } else { 345 | build.setResult(jobResult); 346 | return true; 347 | } 348 | } catch (VulnerabilityTrendHelperException e) { 349 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve outcome from job outcome policy"; 350 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 351 | VulnerabilityTrendHelper.logMessage(listener, e.getMessage()); 352 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 353 | return true; 354 | } else { 355 | errorEncountered = true; 356 | continue; 357 | } 358 | } 359 | } 360 | } else { 361 | VulnerabilityTrendHelper.logMessage(listener, "filterForm: " + filterForm); 362 | VulnerabilityTrendHelper.logMessage(listener, "Checking the threshold condition where " + condition.toString()); 363 | if (queryBy == Constants.QUERY_BY_START_DATE || queryBy == Constants.QUERY_BY_PARAMETER) { 364 | traces = VulnerabilityTrendHelper.getAllTraces(contrastSDK, profile.getOrgUuid(), appId, filterForm); 365 | } else { 366 | traces = VulnerabilityTrendHelper.getAllTraces(contrastSDK, profile.getOrgUuid(), null, filterForm); 367 | } 368 | resultTraces.addAll(traces.getTraces()); 369 | int thresholdCount = condition.getThresholdCount(); // Integer.parseInt(condition.getThresholdCount()); 370 | 371 | if (traces.getCount() > thresholdCount && !ignoreContrastFindings) { 372 | // save results before failing build 373 | buildResult(resultTraces, build); 374 | 375 | Result buildResult = Result.fromString(profile.getVulnerableBuildResult()); 376 | VulnerabilityTrendHelper.logMessage(listener, "Failed on the threshold condition where " + condition.toString()); 377 | VulnerabilityTrendHelper.logMessage(listener, VulnerabilityTrendHelper.getVulnerabilityInfoString(traces)); 378 | if (buildResult.toString().equals(Result.FAILURE.toString())) { 379 | throw new AbortException("Failed on the threshold condition where " + condition.toString()); 380 | } else { 381 | build.setResult(buildResult); 382 | return true; 383 | } 384 | } 385 | } 386 | } catch (UnauthorizedException e) { 387 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve vulnerability information from TeamServer."; 388 | VulnerabilityTrendHelper.logMessage(listener, errorMessage); 389 | VulnerabilityTrendHelper.logMessage(listener, e.getMessage()); 390 | if(updateBuildResultOnError(build, profile, errorMessage, listener)) { 391 | return true; 392 | } else { 393 | errorEncountered = true; 394 | continue; 395 | } 396 | 397 | } 398 | } 399 | } 400 | 401 | buildResult(resultTraces, build); 402 | if(!errorEncountered) { 403 | VulnerabilityTrendHelper.logMessage(listener, "This build passes all vulnerability threshold conditions!"); 404 | } 405 | 406 | return true; 407 | 408 | } 409 | 410 | @Override 411 | public DescriptorImpl getDescriptor() { 412 | return (DescriptorImpl) super.getDescriptor(); 413 | } 414 | 415 | @Override 416 | public BuildStepMonitor getRequiredMonitorService() { 417 | return BuildStepMonitor.NONE; 418 | } 419 | 420 | @Override 421 | public Action getProjectAction(AbstractProject project) { 422 | return new VulnerabilityTrendProjectAction(project); 423 | } 424 | 425 | 426 | /** 427 | * Descriptor for {@link VulnerabilityTrendRecorder}. 428 | */ 429 | @Extension 430 | public static class DescriptorImpl extends BuildStepDescriptor { 431 | 432 | private List conditions; 433 | private String teamServerProfileName; 434 | private Boolean overrideGlobalThresholdConditions; 435 | private Integer queryBy; 436 | 437 | public DescriptorImpl() { 438 | super(VulnerabilityTrendRecorder.class); 439 | load(); 440 | } 441 | 442 | @JavaScriptMethod 443 | public boolean isAllowGlobalThresholdConditionsOverride(String teamServerProfileName) { 444 | return VulnerabilityTrendHelper.getProfile(teamServerProfileName).isAllowGlobalThresholdConditionsOverride(); 445 | } 446 | 447 | @SuppressWarnings("unused") 448 | public ListBoxModel doFillTeamServerProfileNameItems() { 449 | return VulnerabilityTrendHelper.getProfileNames(); 450 | } 451 | 452 | /** 453 | * Allows this builder to be available for all classes. 454 | * 455 | * @param aClass Passed in class. 456 | * @return true 457 | */ 458 | public boolean isApplicable(Class aClass) { 459 | return true; 460 | } 461 | 462 | /** 463 | * Display name in the Build Action dropdown. 464 | * 465 | * @return String 466 | */ 467 | public String getDisplayName() { 468 | return "Contrast Assess"; 469 | } 470 | 471 | /** 472 | * Save's the publisher's config.jelly data. 473 | * 474 | * @param req StaplerRequest 475 | * @param json Json of the form for this Publisher 476 | * @return if the save was successful 477 | */ 478 | @Override 479 | public Publisher newInstance(StaplerRequest req, JSONObject json) { 480 | final JSONArray array = json.optJSONArray("conditions"); 481 | 482 | if (array != null) { 483 | conditions = req.bindJSONToList(ThresholdCondition.class, array); 484 | } else { 485 | conditions = new ArrayList<>(); 486 | 487 | if (!json.keySet().isEmpty()) { 488 | conditions.add(req.bindJSON(ThresholdCondition.class, json.getJSONObject("conditions"))); 489 | } 490 | } 491 | teamServerProfileName = (String) json.get("teamServerProfileName"); 492 | overrideGlobalThresholdConditions = (Boolean) json.get("overrideGlobalThresholdConditions"); 493 | queryBy = Integer.parseInt((String) json.get("queryBy")); 494 | 495 | save(); 496 | 497 | return new VulnerabilityTrendRecorder(conditions, teamServerProfileName, overrideGlobalThresholdConditions, queryBy); 498 | } 499 | 500 | public List getConditions() { 501 | return conditions; 502 | } 503 | 504 | public void setConditions(List conditions) { 505 | this.conditions = conditions; 506 | } 507 | } 508 | 509 | 510 | /** 511 | * Builds a String representation of the Traces found when checking for vulnerabilities. 512 | * 513 | * @param traces - traces founding during build 514 | * @param build - current build 515 | */ 516 | private void buildResult(Set traces, AbstractBuild build) { 517 | Map traceResult = new HashMap<>(); 518 | Map severityResult = new HashMap<>(); 519 | 520 | for (Trace trace : traces) { 521 | 522 | if (severityResult.containsKey(trace.getSeverity())) { 523 | Integer previousCount = severityResult.get(trace.getSeverity()); 524 | severityResult.put(trace.getSeverity(), previousCount + 1); 525 | } else { 526 | severityResult.put(trace.getSeverity(), 1); 527 | } 528 | 529 | if (traceResult.containsKey(trace.getRule())) { 530 | Integer previousCount = traceResult.get(trace.getRule()); 531 | traceResult.put(trace.getRule(), previousCount + 1); 532 | } else { 533 | traceResult.put(trace.getRule(), 1); 534 | } 535 | } 536 | 537 | // Add remaining severities for chart 538 | for (String severity : VulnerabilityTrendHelper.SEVERITIES) { 539 | if (!severityResult.containsKey(severity)) { 540 | severityResult.put(severity, 0); 541 | } 542 | } 543 | 544 | VulnerabilityTrendResult result = new VulnerabilityTrendResult(traceResult, severityResult); 545 | 546 | build.addAction(new VulnerabilityFrequencyAction(result, build)); 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendResult.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.Map; 7 | 8 | @Getter 9 | @Setter 10 | public class VulnerabilityTrendResult { 11 | 12 | private Map traceResult; 13 | private Map severityResult; 14 | 15 | public VulnerabilityTrendResult(Map traceResult, Map severityResult) { 16 | this.traceResult = traceResult; 17 | this.severityResult = severityResult; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendStep.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.contrastsecurity.exceptions.UnauthorizedException; 4 | import com.contrastsecurity.http.TraceFilterForm; 5 | import com.contrastsecurity.models.Application; 6 | import com.contrastsecurity.models.Organizations; 7 | import com.contrastsecurity.models.SecurityCheck; 8 | import com.contrastsecurity.models.Traces; 9 | import com.contrastsecurity.sdk.ContrastSDK; 10 | import com.google.inject.Inject; 11 | import hudson.AbortException; 12 | import hudson.EnvVars; 13 | import hudson.Extension; 14 | import hudson.model.Result; 15 | import hudson.model.Run; 16 | import hudson.model.TaskListener; 17 | import hudson.util.ListBoxModel; 18 | import jenkins.model.Jenkins; 19 | import lombok.Getter; 20 | import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; 21 | import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; 22 | import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; 23 | import org.jenkinsci.plugins.workflow.steps.Step; 24 | import org.jenkinsci.plugins.workflow.steps.StepContextParameter; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | import org.kohsuke.stapler.DataBoundSetter; 27 | import org.kohsuke.stapler.QueryParameter; 28 | 29 | import java.io.IOException; 30 | import java.util.ArrayList; 31 | import java.util.Collections; 32 | import java.util.Date; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | @Getter 37 | public class VulnerabilityTrendStep extends AbstractStepImpl { 38 | 39 | private String profile; 40 | 41 | @DataBoundSetter 42 | public void setProfile(String profile) { 43 | this.profile = profile; 44 | } 45 | 46 | private int count; 47 | 48 | @DataBoundSetter 49 | public void setCount(int count) { 50 | this.count = count; 51 | } 52 | 53 | private String rule; 54 | 55 | @DataBoundSetter 56 | public void setRule(String rule) { 57 | this.rule = rule; 58 | } 59 | 60 | private String severity; 61 | 62 | @DataBoundSetter 63 | public void setSeverity(String severity) { 64 | this.severity = severity; 65 | } 66 | 67 | private String applicationId; 68 | 69 | @DataBoundSetter 70 | public void setApplicationId(String applicationId) { 71 | this.applicationId = applicationId; 72 | } 73 | 74 | private String applicationName; 75 | 76 | @DataBoundSetter 77 | public void setApplicationName(String applicationName) { 78 | this.applicationName = applicationName; 79 | } 80 | 81 | private String appVersionTag; 82 | 83 | @DataBoundSetter 84 | public void setAppVersionTag(String appVersionTag) { 85 | this.appVersionTag = appVersionTag; 86 | } 87 | 88 | private int queryBy; 89 | 90 | @DataBoundSetter 91 | public void setQueryBy(int queryBy) { 92 | this.queryBy = queryBy; 93 | } 94 | 95 | /** 96 | * Type of agent used to instrument the application 97 | */ 98 | private String agentType; 99 | 100 | @DataBoundSetter 101 | public void setAgentType(String agentType) { this.agentType = agentType; } 102 | 103 | @DataBoundConstructor 104 | public VulnerabilityTrendStep(String profile, int count, String rule, String severity, String applicationId, int queryBy) { 105 | this.profile = profile; 106 | this.count = count; 107 | this.rule = rule; 108 | this.severity = severity; 109 | this.applicationId = applicationId; 110 | this.queryBy = queryBy; 111 | } 112 | 113 | // Used to build the new instance 114 | public VulnerabilityTrendStep() { 115 | 116 | } 117 | 118 | @Override 119 | public VulnerabilityTrendStepDescriptorImpl getDescriptor() { 120 | Jenkins instance = Jenkins.getInstance(); 121 | 122 | if (instance != null) { 123 | return (VulnerabilityTrendStepDescriptorImpl) instance.getDescriptor(getClass()); 124 | } else { 125 | return null; 126 | } 127 | } 128 | 129 | @Extension 130 | public static class VulnerabilityTrendStepDescriptorImpl extends AbstractStepDescriptorImpl { 131 | 132 | public VulnerabilityTrendStepDescriptorImpl() { 133 | super(Execution.class); 134 | } 135 | 136 | @Override 137 | public String getFunctionName() { 138 | return "contrastVerification"; 139 | } 140 | 141 | @Override 142 | public String getDisplayName() { 143 | return "Verify vulnerabilities in a build"; 144 | } 145 | 146 | @Override 147 | public Step newInstance(Map arguments) { 148 | VulnerabilityTrendStep step = new VulnerabilityTrendStep(); 149 | 150 | if (arguments.containsKey("profile")) { 151 | Object profile = arguments.get("profile"); 152 | 153 | if (profile != null) { 154 | step.setProfile((String) profile); 155 | } else { 156 | throw new IllegalArgumentException("Profile must be set."); 157 | } 158 | } 159 | 160 | if (arguments.containsKey("count")) { 161 | Object count = arguments.get("count"); 162 | step.setCount((int) count); 163 | } 164 | 165 | if (arguments.containsKey("rule")) { 166 | Object rule = arguments.get("rule"); 167 | step.setRule((String) rule); 168 | } 169 | 170 | if (arguments.containsKey("severity")) { 171 | Object severity = arguments.get("severity"); 172 | step.setSeverity((String) severity); 173 | } 174 | 175 | if (arguments.containsKey("applicationId")) { 176 | String applicationId = (String) arguments.get("applicationId"); 177 | step.setApplicationId(applicationId); 178 | } 179 | 180 | if (step.getApplicationId() == null) { 181 | Object applicationName = arguments.get("applicationName"); 182 | 183 | if (applicationName != null) { 184 | step.setApplicationName((String) applicationName); 185 | } else { 186 | throw new IllegalArgumentException("If Application ID is not set, Application Name must be set."); 187 | } 188 | } 189 | 190 | if (step.getApplicationId() == null 191 | //// Compatibility fix for plugin versions <=2.6 192 | && arguments.containsKey("agentType")) { 193 | Object agentType = arguments.get("agentType"); 194 | 195 | if (agentType != null) { 196 | step.setAgentType((String) agentType); 197 | } else { 198 | throw new IllegalArgumentException("If Application ID is not set, Agent Type must be set."); 199 | } 200 | } 201 | 202 | 203 | if (arguments.containsKey("queryBy")) { 204 | Object queryBy = arguments.get("queryBy"); 205 | 206 | step.setQueryBy((int) queryBy); 207 | 208 | if (step.getQueryBy() == Constants.QUERY_BY_PARAMETER) { 209 | step.setAppVersionTag((String) arguments.get("appVersionTag")); 210 | } 211 | } else if (arguments.containsKey("appVersionTagFormat")) { 212 | Object queryBy = arguments.get("appVersionTagFormat"); 213 | step.setQueryBy((int) queryBy); 214 | } 215 | 216 | return step; 217 | } 218 | 219 | @SuppressWarnings("unused") 220 | public ListBoxModel doFillProfileItems() { 221 | return VulnerabilityTrendHelper.getProfileNames(); 222 | } 223 | 224 | /** 225 | * Fills the Threshold Category select drop down with application ids. 226 | * 227 | * @return ListBoxModel filled with application ids. 228 | */ 229 | public ListBoxModel doFillApplicationIdItems(@QueryParameter("profile") final String teamServerProfileName) throws IOException { 230 | return VulnerabilityTrendHelper.getApplicationIds(teamServerProfileName); 231 | } 232 | 233 | @SuppressWarnings("unused") 234 | public ListBoxModel doFillRuleItems(@QueryParameter("profile") final String teamServerProfileName) { 235 | return VulnerabilityTrendHelper.getVulnerabilityTypes(teamServerProfileName); 236 | } 237 | 238 | @SuppressWarnings("unused") 239 | public ListBoxModel doFillSeverityItems() { 240 | return VulnerabilityTrendHelper.getSeverityListBoxModel(); 241 | } 242 | } 243 | 244 | public static class Execution extends AbstractSynchronousStepExecution { 245 | private static final long serialVersionUID = 1L; 246 | 247 | @StepContextParameter 248 | transient Run build; 249 | 250 | @StepContextParameter 251 | transient TaskListener taskListener; 252 | 253 | @Inject 254 | transient VulnerabilityTrendStep step; 255 | 256 | private TraceFilterForm makeFilterFormWithQueryBy() throws IOException, InterruptedException { 257 | TraceFilterForm filterForm = new TraceFilterForm(); 258 | 259 | if (step.getQueryBy() == Constants.QUERY_BY_APP_VERSION_TAG_HIERARCHICAL_FORMAT) { 260 | 261 | String appVersionTag = VulnerabilityTrendHelper.buildAppVersionTagHierarchical(build, step.getApplicationId()); 262 | 263 | List appVersionTagsList = new ArrayList<>(); 264 | appVersionTagsList.add(appVersionTag); 265 | 266 | if (step.getApplicationName() != null) { 267 | String appVersionTagAppName = VulnerabilityTrendHelper.buildAppVersionTagHierarchical(build, step.getApplicationName()); 268 | appVersionTagsList.add(appVersionTagAppName); 269 | } 270 | 271 | filterForm.setAppVersionTags(appVersionTagsList); 272 | } else if (step.getQueryBy() == Constants.QUERY_BY_START_DATE) { 273 | filterForm.setStartDate(new Date(build.getStartTimeInMillis())); 274 | } else if (step.getQueryBy() == Constants.QUERY_BY_PARAMETER) { 275 | final EnvVars env = build.getEnvironment(taskListener); 276 | String appVersionTag; 277 | 278 | if (step.getAppVersionTag() != null) { 279 | appVersionTag = step.getAppVersionTag(); 280 | } else if (env.get("APPVERSIONTAG") != null) { 281 | appVersionTag = env.get("APPVERSIONTAG"); 282 | } else { 283 | appVersionTag = ""; 284 | } 285 | 286 | if (appVersionTag.isEmpty()) { 287 | VulnerabilityTrendHelper.logMessage(taskListener, "Warning: queryBy Parameter is configured, but appVersionTag is not set. All vulnerabilities will be returned for this application"); 288 | } 289 | 290 | List appVersionTagsList = new ArrayList<>(); 291 | appVersionTagsList.add(appVersionTag); 292 | 293 | filterForm.setAppVersionTags(appVersionTagsList); 294 | } else { 295 | String appVersionTag = VulnerabilityTrendHelper.buildAppVersionTag(build, step.getApplicationId()); 296 | 297 | List appVersionTagsList = new ArrayList<>(); 298 | appVersionTagsList.add(appVersionTag); 299 | 300 | if (step.getApplicationName() != null) { 301 | String appVersionTagAppName = VulnerabilityTrendHelper.buildAppVersionTag(build, step.getApplicationName()); 302 | appVersionTagsList.add(appVersionTagAppName); 303 | } 304 | 305 | filterForm.setAppVersionTags(appVersionTagsList); 306 | } 307 | 308 | return filterForm; 309 | } 310 | 311 | /** 312 | * Set the build result based on contrast configuration 313 | * @param profile teamserver profile 314 | * @parma message Message to log when result is failure 315 | * @return true = the result was set, false = the result was not set 316 | */ 317 | private boolean updateBuildResult(TeamServerProfile profile, String message) throws AbortException { 318 | if(profile.isApplyVulnerableBuildResultOnContrastError()) { 319 | Result profileVulnerableBuildResult = Result.fromString(profile.getVulnerableBuildResult()); 320 | VulnerabilityTrendHelper.logMessage(taskListener, "Setting build result to : "+profileVulnerableBuildResult.toString()); 321 | if(Result.FAILURE.equals(profileVulnerableBuildResult)) { 322 | throw new AbortException(message); 323 | } else { 324 | build.setResult(profileVulnerableBuildResult); 325 | } 326 | return true; 327 | } 328 | return false; 329 | } 330 | 331 | @Override 332 | public Void run() throws AbortException, InterruptedException { 333 | TeamServerProfile teamServerProfile = VulnerabilityTrendHelper.getProfile(step.getProfile()); 334 | 335 | if (teamServerProfile == null) { 336 | VulnerabilityTrendHelper.logMessage(taskListener, "Unable to find TeamServer profile."); 337 | throw new AbortException("Unable to find TeamServer profile."); 338 | } 339 | 340 | final String CONTRAST_ERROR_PREFIX = teamServerProfile.isApplyVulnerableBuildResultOnContrastError() ? "Error: " : "Warning: "; 341 | ContrastSDK contrastSDK = VulnerabilityTrendHelper.createSDK(teamServerProfile.getUsername(), teamServerProfile.getServiceKey(), 342 | teamServerProfile.getApiKey(), teamServerProfile.getTeamServerUrl()); 343 | 344 | try { 345 | final Organizations organizations = contrastSDK.getProfileDefaultOrganizations(); 346 | if (organizations == null || organizations.getOrganization() == null) { 347 | String errorMessage = CONTRAST_ERROR_PREFIX + "No organization found, Check your credentials and URL."; 348 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 349 | updateBuildResult(teamServerProfile, errorMessage); 350 | return null; 351 | } 352 | 353 | } catch (UnauthorizedException | IOException e) { 354 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to connect to Contrast."; 355 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 356 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 357 | updateBuildResult(teamServerProfile, errorMessage); 358 | return null; 359 | } 360 | 361 | //Convert app name and agent type to app id 362 | if(step.getApplicationId() == null && step.getApplicationName() != null && step.getAgentType() != null) { 363 | try { 364 | Application app = contrastSDK.getApplicationByNameAndLanguage(teamServerProfile.getOrgUuid(), 365 | step.getApplicationName(), 366 | VulnerabilityTrendHelper.getAgentTypeFromString(step.getAgentType())); 367 | 368 | if(app == null) { 369 | String errorMessage = String.format(CONTRAST_ERROR_PREFIX + "Application with [name = %s, agentType = %s] not found.", step.getApplicationName(), step.getAgentType()); 370 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 371 | updateBuildResult(teamServerProfile, errorMessage); 372 | return null; 373 | } else { 374 | step.setApplicationId(app.getId()); 375 | VulnerabilityTrendHelper.logMessage(taskListener, "Fetched Application : [name = '"+step.getApplicationName()+"', displayName = '"+app.getName()+"', agentType='"+app.getLanguage()+"'] with ID: ["+step.getApplicationId()+"]"); 376 | } 377 | } catch (UnauthorizedException | IOException e) { 378 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve information from TeamServer."; 379 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 380 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 381 | updateBuildResult(teamServerProfile, errorMessage); 382 | return null; 383 | } 384 | } 385 | 386 | // Convert app display name to app id 387 | //// Compatibility fix for plugin versions <=2.6 388 | if (step.getApplicationId() == null && step.getApplicationName() != null && step.getAgentType() == null) { 389 | for (App app : teamServerProfile.getApps()) { 390 | String subStr = app.getTitle().substring(0, app.getTitle().lastIndexOf(" (")); 391 | if (subStr.equals(step.getApplicationName())) { 392 | step.setApplicationId(app.getName()); 393 | break; 394 | } 395 | } 396 | } 397 | //validation 398 | boolean applicationIdExists = VulnerabilityTrendHelper.applicationIdExists(contrastSDK, teamServerProfile.getOrgUuid(), step.getApplicationId()); 399 | if (!applicationIdExists) { 400 | String errorMessage = CONTRAST_ERROR_PREFIX + "Application with ID '" + step.getApplicationId() + "' not found."; 401 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 402 | updateBuildResult(teamServerProfile, errorMessage); 403 | return null; 404 | } else { 405 | VulnerabilityTrendHelper.logMessage(taskListener, "Checking the number of vulnerabilities for " + step.getApplicationId()); 406 | 407 | String stepString = step.buildStepString(); 408 | 409 | try { 410 | //makeFilterForm 411 | TraceFilterForm filterForm = makeFilterFormWithQueryBy(); 412 | 413 | SecurityCheck securityCheck = VulnerabilityTrendHelper.makeSecurityCheck(contrastSDK, teamServerProfile.getOrgUuid(), step.getApplicationId(), build.getStartTimeInMillis(), step.queryBy, filterForm); 414 | StringBuilder applicationDisplayForConsoleOutputBuilder = new StringBuilder("["); 415 | 416 | if(step.getApplicationName() != null && !step.getApplicationName().isEmpty()) { 417 | applicationDisplayForConsoleOutputBuilder.append("name = " + step.getApplicationName()); 418 | } 419 | if(step.getAgentType() != null && !step.getAgentType().isEmpty()) { 420 | applicationDisplayForConsoleOutputBuilder.append(", agentType = "+step.getAgentType()); 421 | } 422 | if(step.getApplicationId() != null && !step.getApplicationId().isEmpty()) { 423 | applicationDisplayForConsoleOutputBuilder.append(", appId = "+step.getApplicationId()); 424 | } 425 | applicationDisplayForConsoleOutputBuilder.append("]"); 426 | 427 | String appicationDisplayForConsoleOutput = applicationDisplayForConsoleOutputBuilder.toString(); 428 | 429 | if(securityCheck.getResult() != null) { // jop is defined for app 430 | VulnerabilityTrendHelper.logMessage(taskListener,"Your Contrast admin has overridden policies you may have set in Vulnerability Security Controls or the 'query by' parameter"); 431 | if(securityCheck.getResult()) { //failed a policy 432 | VulnerabilityTrendHelper.logMessage(taskListener, "This application did not violate any Contrast policies"); 433 | } else { 434 | try { 435 | Result jobResult = VulnerabilityTrendHelper.getJenkinsResultFromJobOutcome(securityCheck.getJobOutcomePolicy().getOutcome()); 436 | String message = "This application "+appicationDisplayForConsoleOutput+" has failed the Contrast policy '"+securityCheck.getJobOutcomePolicy().getName()+"'"; 437 | VulnerabilityTrendHelper.logMessage(taskListener,message); 438 | VulnerabilityTrendHelper.logMessage(taskListener, "Setting build result to : " + jobResult); 439 | if(Result.FAILURE.equals(jobResult)) { 440 | throw new AbortException(message); 441 | } else { 442 | build.setResult(jobResult); 443 | return null; 444 | } 445 | } catch (VulnerabilityTrendHelperException e) { 446 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve outcome from job outcome policy"; 447 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 448 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 449 | updateBuildResult(teamServerProfile, errorMessage); 450 | return null; 451 | } 452 | } 453 | } else { //regular verify 454 | 455 | VulnerabilityTrendHelper.logMessage(taskListener, "Checking the step condition where " + stepString); 456 | 457 | Traces traces; 458 | 459 | if (step.getSeverity() != null) { 460 | filterForm.setSeverities(VulnerabilityTrendHelper.getSeverityList(step.getSeverity())); 461 | } 462 | 463 | if (step.getRule() != null) { 464 | filterForm.setVulnTypes(Collections.singletonList(step.getRule())); 465 | } 466 | VulnerabilityTrendHelper.logMessage(taskListener, "filterForm: " + filterForm); 467 | if (step.getQueryBy() == Constants.QUERY_BY_START_DATE || step.getQueryBy() == Constants.QUERY_BY_PARAMETER) { 468 | traces = VulnerabilityTrendHelper.getAllTraces(contrastSDK, teamServerProfile.getOrgUuid(), step.getApplicationId(), filterForm); 469 | } else { 470 | traces = VulnerabilityTrendHelper.getAllTraces(contrastSDK, teamServerProfile.getOrgUuid(), null, filterForm); 471 | } 472 | 473 | if (traces.getCount() > step.getCount()) { 474 | Result buildResult = Result.fromString(teamServerProfile.getVulnerableBuildResult()); 475 | VulnerabilityTrendHelper.logMessage(taskListener, "Failed on the condition where " + stepString); 476 | VulnerabilityTrendHelper.logMessage(taskListener, VulnerabilityTrendHelper.getVulnerabilityInfoString(traces)); 477 | VulnerabilityTrendHelper.logMessage(taskListener, "Setting build result to : " + buildResult); 478 | if (buildResult.toString().equals(Result.FAILURE.toString())) { 479 | throw new AbortException("Failed on the condition where " + stepString); 480 | } else { 481 | build.setResult(buildResult); 482 | return null; 483 | } 484 | 485 | } 486 | VulnerabilityTrendHelper.logMessage(taskListener, "This step has passed successfully"); 487 | } 488 | } catch (AbortException e) { //Fail Fast when Failure is selected for a vulnerable build 489 | throw e; 490 | }catch (UnauthorizedException | IOException e) { 491 | String errorMessage = CONTRAST_ERROR_PREFIX + "Unable to retrieve vulnerability information from TeamServer."; 492 | VulnerabilityTrendHelper.logMessage(taskListener, errorMessage); 493 | VulnerabilityTrendHelper.logMessage(taskListener, e.getMessage()); 494 | updateBuildResult(teamServerProfile, errorMessage); 495 | return null; 496 | } 497 | } 498 | return null; 499 | } 500 | 501 | String getBuildName() { 502 | return build.getParent().getFullName(); 503 | } 504 | } 505 | 506 | private String buildStepString() { 507 | StringBuilder sb = new StringBuilder(); 508 | 509 | sb.append("count is ").append(count); 510 | 511 | if (severity != null) { 512 | sb.append(", severity is ").append(severity); 513 | } 514 | 515 | if (rule != null) { 516 | sb.append(", rule type is ").append(rule); 517 | } 518 | 519 | if (applicationId != null) { 520 | sb.append(", applicationId is ").append(applicationId); 521 | } 522 | 523 | if (queryBy != 0) { 524 | sb.append(", queryBy is ").append(queryBy); 525 | } 526 | 527 | sb.append("."); 528 | 529 | return sb.toString(); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityType.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class VulnerabilityType { 9 | 10 | public String name; 11 | public String title; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/plots/SeverityFrequencyPlot.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins.plots; 2 | 3 | import com.aspectsecurity.contrast.contrastjenkins.VulnerabilityFrequencyAction; 4 | import com.aspectsecurity.contrast.contrastjenkins.VulnerabilityTrendHelper; 5 | import hudson.model.AbstractProject; 6 | import hudson.model.Run; 7 | import hudson.util.Graph; 8 | import hudson.util.RunList; 9 | import hudson.util.ShiftedCategoryAxis; 10 | import org.jfree.chart.ChartFactory; 11 | import org.jfree.chart.JFreeChart; 12 | import org.jfree.chart.axis.CategoryAxis; 13 | import org.jfree.chart.axis.NumberAxis; 14 | import org.jfree.chart.plot.CategoryPlot; 15 | import org.jfree.chart.plot.PlotOrientation; 16 | import org.jfree.chart.renderer.category.BarRenderer; 17 | import org.jfree.data.category.DefaultCategoryDataset; 18 | 19 | import java.awt.*; 20 | import java.util.ArrayList; 21 | import java.util.Calendar; 22 | import java.util.Collections; 23 | import java.util.Map; 24 | 25 | 26 | public class SeverityFrequencyPlot extends Graph { 27 | 28 | private AbstractProject project; 29 | 30 | public SeverityFrequencyPlot(AbstractProject project) { 31 | super(Calendar.getInstance(), 500, 200); 32 | this.project = project; 33 | } 34 | 35 | @Override 36 | protected JFreeChart createGraph() { 37 | DefaultCategoryDataset dataset = createSeverityFrequencyDataset(); 38 | 39 | JFreeChart chart = ChartFactory.createStackedBarChart( 40 | null, // title 41 | "Build Number", // x axis 42 | "Severity Count", // y axis 43 | dataset, // data 44 | PlotOrientation.VERTICAL, // orientation 45 | true, // legend 46 | true, // tooltips 47 | false); //urls 48 | 49 | chart.setBackgroundPaint(Color.white); 50 | 51 | CategoryPlot plot = (CategoryPlot) chart.getPlot(); 52 | plot.setBackgroundPaint(Color.WHITE); 53 | plot.setOutlinePaint(null); 54 | plot.setRangeGridlinesVisible(true); 55 | plot.setRangeGridlinePaint(Color.black); 56 | 57 | BarRenderer renderer = (BarRenderer) plot.getRenderer(); 58 | setColors(renderer); 59 | 60 | // Set colors here 61 | plot.setRenderer(renderer); 62 | 63 | CategoryAxis domainAxis = new ShiftedCategoryAxis("Build Number"); 64 | plot.setDomainAxis(domainAxis); 65 | 66 | NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); 67 | rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 68 | rangeAxis.setAutoRange(true); 69 | 70 | return chart; 71 | } 72 | 73 | private DefaultCategoryDataset createSeverityFrequencyDataset() { 74 | java.util.List actions = new ArrayList<>(); 75 | RunList builds = project.getBuilds().limit(10); 76 | 77 | // Get all build actions 78 | for (Run run : builds) { 79 | VulnerabilityFrequencyAction action = run.getAction(VulnerabilityFrequencyAction.class); 80 | 81 | if (action == null) { 82 | continue; 83 | } 84 | actions.add(action); 85 | } 86 | 87 | // put them in chronological order 88 | Collections.reverse(actions); 89 | 90 | // build data setup 91 | DefaultCategoryDataset ds = new DefaultCategoryDataset(); 92 | for (VulnerabilityFrequencyAction action : actions) { 93 | Map result = action.getResult().getSeverityResult(); 94 | 95 | String buildNumber = Integer.toString(action.getBuildNumber()); 96 | 97 | for (String severity : VulnerabilityTrendHelper.SEVERITIES) { 98 | ds.addValue(result.get(severity), severity, buildNumber); 99 | } 100 | } 101 | 102 | return ds; 103 | } 104 | 105 | 106 | private java.util.List setColors(BarRenderer renderer) { 107 | java.util.List colors = new ArrayList<>(); 108 | 109 | renderer.setSeriesPaint(0, new Color(232, 232, 232)); 110 | renderer.setSeriesPaint(1, new Color(186, 186, 186)); 111 | renderer.setSeriesPaint(2, new Color(247, 182, 0)); 112 | renderer.setSeriesPaint(3, new Color(247, 138, 49)); 113 | renderer.setSeriesPaint(4, new Color(230, 48, 37)); 114 | 115 | return colors; 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/java/com/aspectsecurity/contrast/contrastjenkins/plots/VulnerabilityFrequencyPlot.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins.plots; 2 | 3 | import com.aspectsecurity.contrast.contrastjenkins.VulnerabilityFrequencyAction; 4 | import hudson.model.AbstractProject; 5 | import hudson.model.Run; 6 | import hudson.util.Graph; 7 | import hudson.util.RunList; 8 | import hudson.util.ShiftedCategoryAxis; 9 | import org.jfree.chart.ChartFactory; 10 | import org.jfree.chart.JFreeChart; 11 | import org.jfree.chart.axis.CategoryAxis; 12 | import org.jfree.chart.axis.NumberAxis; 13 | import org.jfree.chart.plot.CategoryPlot; 14 | import org.jfree.chart.plot.PlotOrientation; 15 | import org.jfree.chart.renderer.category.BarRenderer; 16 | import org.jfree.data.category.DefaultCategoryDataset; 17 | 18 | import java.awt.*; 19 | import java.util.ArrayList; 20 | import java.util.Calendar; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | 26 | public class VulnerabilityFrequencyPlot extends Graph { 27 | 28 | private AbstractProject project; 29 | 30 | public VulnerabilityFrequencyPlot(AbstractProject project) { 31 | super(Calendar.getInstance(), 500, 200); 32 | this.project = project; 33 | } 34 | 35 | @Override 36 | protected JFreeChart createGraph() { 37 | DefaultCategoryDataset dataset = createVulnerabilityFrequencyDataset(); 38 | 39 | JFreeChart chart = ChartFactory.createStackedBarChart( 40 | null, // title 41 | "Build Number", // x axis title 42 | "Vulnerability Count", // y axis title 43 | dataset, // data 44 | PlotOrientation.VERTICAL, // orientation 45 | true, // legend 46 | true, // tooltips 47 | false); // urls 48 | 49 | chart.setBackgroundPaint(Color.white); 50 | 51 | CategoryPlot plot = (CategoryPlot) chart.getPlot(); 52 | plot.setBackgroundPaint(Color.WHITE); 53 | plot.setOutlinePaint(null); 54 | plot.setRangeGridlinesVisible(true); 55 | plot.setRangeGridlinePaint(Color.black); 56 | 57 | BarRenderer renderer = (BarRenderer) plot.getRenderer(); 58 | setColors(renderer); 59 | 60 | // Set colors here 61 | plot.setRenderer(renderer); 62 | 63 | CategoryAxis domainAxis = new ShiftedCategoryAxis("Build Number"); 64 | plot.setDomainAxis(domainAxis); 65 | 66 | NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); 67 | rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 68 | rangeAxis.setAutoRange(true); 69 | 70 | return chart; 71 | } 72 | 73 | private DefaultCategoryDataset createVulnerabilityFrequencyDataset() { 74 | List actions = new ArrayList<>(); 75 | RunList builds = project.getBuilds().limit(10); 76 | 77 | // Get all build actions 78 | for (Run run : builds) { 79 | VulnerabilityFrequencyAction action = run.getAction(VulnerabilityFrequencyAction.class); 80 | 81 | if (action == null) { 82 | continue; 83 | } 84 | actions.add(action); 85 | } 86 | 87 | // put them in chronological order 88 | Collections.reverse(actions); 89 | 90 | // build data setup 91 | DefaultCategoryDataset ds = new DefaultCategoryDataset(); 92 | for (VulnerabilityFrequencyAction action : actions) { 93 | Map result = action.getResult().getTraceResult(); 94 | 95 | for (Map.Entry entry : result.entrySet()) { 96 | ds.addValue(entry.getValue(), entry.getKey(), Integer.toString(action.getBuildNumber())); 97 | } 98 | } 99 | return ds; 100 | } 101 | 102 | private List setColors(BarRenderer renderer) { 103 | List colors = new ArrayList<>(); 104 | 105 | renderer.setSeriesPaint(0, new Color(60, 195, 178)); 106 | renderer.setSeriesPaint(1, new Color(174, 205, 67)); 107 | renderer.setSeriesPaint(2, new Color(247, 182, 0)); 108 | renderer.setSeriesPaint(3, new Color(94, 68, 130)); 109 | renderer.setSeriesPaint(4, new Color(247, 138, 49)); 110 | 111 | renderer.setSeriesPaint(5, new Color(60, 195, 178, 128)); 112 | renderer.setSeriesPaint(6, new Color(174, 205, 67, 128)); 113 | renderer.setSeriesPaint(7, new Color(247, 182, 0, 128)); 114 | renderer.setSeriesPaint(8, new Color(94, 68, 130, 128)); 115 | renderer.setSeriesPaint(9, new Color(247, 138, 49, 128)); 116 | 117 | renderer.setSeriesPaint(10, new Color(49, 67, 78)); 118 | renderer.setSeriesPaint(11, new Color(37, 140, 191)); 119 | renderer.setSeriesPaint(12, new Color(230, 48, 37)); 120 | 121 | renderer.setSeriesPaint(13, new Color(49, 67, 78, 128)); 122 | renderer.setSeriesPaint(14, new Color(37, 140, 191, 128)); 123 | renderer.setSeriesPaint(15, new Color(230, 48, 37, 128)); 124 | 125 | return colors; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/ContrastAgentStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/ContrastAgentStep/help.html: -------------------------------------------------------------------------------- 1 |

2 | Pipeline step for adding a Contrast agent to your build.
3 | 4 | Usage Example:
5 | 6 | contrastAgent profile: 'Localhost', outputDirectory: "${project.build.directory} + '/tmp'" 7 | 8 |
-------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/ContrastPluginConfig/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/ContrastPluginConfig/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 |

Configure the security controls for your Contrast Connection by defining these vulnerability filters.

62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 |
116 |
117 |
118 |
119 |
120 |
121 | 122 |
123 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityFrequencyAction/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Vulnerability Report

6 |
7 | Vulnerability Rule Count 8 |
    9 | 10 |
  • ${trace.key} : ${trace.value}
  • 11 |
    12 |
13 |
14 | Vulnerability Severity Count 15 |
    16 | 17 |
  • ${severity} : ${it.severityResult.get(severity)}
  • 18 |
    19 |
20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendProjectAction/floatingBox.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 |

Vulnerability Trends Across Builds

9 | 10 |
11 | 12 |
13 |

Severity Trends Across Builds

14 | 15 |
16 | 17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendProjectAction/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendRecorder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
84 | 85 |
86 |
87 |
88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 117 | 208 | 209 |
210 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendRecorder/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendStep/help.html: -------------------------------------------------------------------------------- 1 |
2 | Documentation: Contrast Docs 3 |
-------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | This plugin is for continuous application security with Contrast integration. 5 |
6 | -------------------------------------------------------------------------------- /src/main/webapp/help-allowGlobalThresholdConditionsOverride.html: -------------------------------------------------------------------------------- 1 |
2 | This option allows you to choose if the global Contrast Vulnerability Threshold Conditions can be overridden in a job configuration. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-apiKey.html: -------------------------------------------------------------------------------- 1 |
2 | You may find your Contrast API Key in the Your Account->Profile section of the Contrast UI. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help-applicationId.html: -------------------------------------------------------------------------------- 1 |
2 | ID of the application on TeamServer. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-applicationName.html: -------------------------------------------------------------------------------- 1 |
2 | The application name that you will provide to the Contrast agent during application startup to begin instrumentation. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-applicationNotInstrumented.html: -------------------------------------------------------------------------------- 1 |
2 | Contrast will not know of your application's existence if it has not yet been run with the Contrast agent. 3 | In this case, enter your application name and language. If the application display name is different from the application name, use the application name. 4 | The Contrast-Jenkins plugin will query Contrast for vulnerability information using the exact name provided. 5 |
-------------------------------------------------------------------------------- /src/main/webapp/help-applyVulnerableBuildResultOnContrastError.html: -------------------------------------------------------------------------------- 1 |
2 | This option allows you to fail builds if the specified application ID is not found on TeamServer. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-orgUuid.html: -------------------------------------------------------------------------------- 1 |
2 | You may find your Organization ID in the Your Account->Profile section of the Contrast UI. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-queryBy-appVersionTag.html: -------------------------------------------------------------------------------- 1 |
2 | Filter vulnerabilities using the appVersionTag parameter in the following format "${applicationId}-${BUILD_NUMBER}".
3 | It may be necessary to override the value reported by the agent at runtime. For example, with the Java agent: -Dcontrast.override.appversion=${applicationId}-${BUILD_NUMBER}.
4 | applicationId is the ID of the application selected. 5 |
-------------------------------------------------------------------------------- /src/main/webapp/help-queryBy-appVersionTagBuildName.html: -------------------------------------------------------------------------------- 1 |
2 | Filter vulnerabilities using the appVersionTag parameter in the following format "${applicationId}-${JOB_NAME}-${BUILD_NUMBER}".
3 | It may be necessary to override the value reported by the agent at runtime. For example, with the Java agent: -Dcontrast.override.appversion=${applicationId}-${JOB_NAME}-${BUILD_NUMBER}.
4 | applicationId is the ID of the application selected. 5 |
-------------------------------------------------------------------------------- /src/main/webapp/help-queryBy-parameter.html: -------------------------------------------------------------------------------- 1 |
2 | Filter vulnerabilities in this build based on the value of the APPVERSIONTAG environment variable. This requires the application version reported by the Contrast to agent match the value of APPVERSIONTAG.
3 | It may be necessary to override the value reported by the agent at runtime. For example, with the Java agent: -Dcontrast.override.appversion=v1.2.3 . 4 |
-------------------------------------------------------------------------------- /src/main/webapp/help-queryBy-startDate.html: -------------------------------------------------------------------------------- 1 |
2 | Filter vulnerabilities based on the startDate parameter (build timestamp), when the build was scheduled. Choosing this option excludes vulnerabilities found before the build was scheduled. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-serviceKey.html: -------------------------------------------------------------------------------- 1 |
2 | You may find your Contrast Service Key (Not the Agent Service Key) in the Your Account->Profile section of the Contrast UI. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help-teamServerUrl.html: -------------------------------------------------------------------------------- 1 |
2 |

This is your TeamServer URL. If you are a SaaS customer then this is http://app.contrastsecurity.com/Contrast/api.

3 | 4 |

If you are EOP then use your hostname.

5 |
6 | -------------------------------------------------------------------------------- /src/main/webapp/help-thresholdCount.html: -------------------------------------------------------------------------------- 1 |
2 | Use 0 to fail on any number of vulnerabilities. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help-thresholdSeverity.html: -------------------------------------------------------------------------------- 1 |
2 | This is the threshold severity to filter on. 3 |
4 | 5 |
6 | This variable is inclusive so selection 'Low' will filter on severities 'Low' and above. 7 |
8 | -------------------------------------------------------------------------------- /src/main/webapp/help-thresholdVulnType.html: -------------------------------------------------------------------------------- 1 |
2 | This is the vulnerability type (rule) to filter on. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-username.html: -------------------------------------------------------------------------------- 1 |
2 | This is the username (usually your email) from your Contrast account login credentials. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help-vulnerabilityStatus.html: -------------------------------------------------------------------------------- 1 |
2 | Only vulnerabilities with the selected statuses will be verified against the Threshold Conditions. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/help-vulnerableBuildResult.html: -------------------------------------------------------------------------------- 1 |
2 | This option allows you to select the result of the build that violates the Vulnerability Threshold Conditions. 3 |
4 | -------------------------------------------------------------------------------- /src/main/webapp/img/trend_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/contrast-continuous-application-security-plugin/725c21f4a7452656bb4ef2ad05b301b54da66817/src/main/webapp/img/trend_graph.png -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/ContrastPluginConfigStub.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | 4 | public class ContrastPluginConfigStub extends ContrastPluginConfig { 5 | 6 | public ContrastPluginConfigStub() { 7 | super(); 8 | } 9 | 10 | public static class ContrastPluginConfigDescriptorStub extends ContrastPluginConfig.ContrastPluginConfigDescriptor { 11 | 12 | @Override 13 | public synchronized void load() { 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/ContrastPluginConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import hudson.util.FormValidation; 4 | import junit.framework.TestCase; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | public class ContrastPluginConfigTest extends TestCase { 9 | 10 | private ContrastPluginConfig.ContrastPluginConfigDescriptor descriptor; 11 | 12 | @Before 13 | @Override 14 | public void setUp() { 15 | descriptor = new ContrastPluginConfigStub.ContrastPluginConfigDescriptorStub(); 16 | } 17 | 18 | @Test 19 | public void testDoCheckUsernameValid() { 20 | FormValidation result = descriptor.doCheckUsername("contrast_admin"); 21 | assertEquals(result.kind, FormValidation.Kind.OK); 22 | } 23 | 24 | @Test 25 | public void testDoCheckUsernameInvalid() { 26 | FormValidation result = descriptor.doCheckUsername(""); 27 | assertEquals(result.kind, FormValidation.Kind.ERROR); 28 | } 29 | 30 | @Test 31 | public void testDoCheckApiKeyValid() { 32 | FormValidation result = descriptor.doCheckApiKey("ABCDEFG"); 33 | assertEquals(result.kind, FormValidation.Kind.OK); 34 | } 35 | 36 | @Test 37 | public void testDoCheckApiKeyInvalid() { 38 | FormValidation result = descriptor.doCheckApiKey(""); 39 | assertEquals(result.kind, FormValidation.Kind.ERROR); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/ThresholdConditionStub.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | 4 | public class ThresholdConditionStub extends ThresholdCondition { 5 | 6 | public ThresholdConditionStub() { 7 | super(0, "test", "test", 0,null,"test", 8 | false, false,false, false, false, false, 9 | false, false, false); 10 | } 11 | 12 | public static class ThresholdConditionDescriptorStub extends ThresholdCondition.DescriptorImpl { 13 | 14 | @Override 15 | public synchronized void load() { 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/ThresholdConditionTest.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import hudson.util.FormValidation; 4 | import hudson.util.ListBoxModel; 5 | import junit.framework.TestCase; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | public class ThresholdConditionTest extends TestCase { 10 | 11 | private ThresholdConditionStub.ThresholdConditionDescriptorStub descriptor; 12 | 13 | @Before 14 | @Override 15 | public void setUp() { 16 | descriptor = new ThresholdConditionStub.ThresholdConditionDescriptorStub(); 17 | } 18 | 19 | @Test 20 | public void testDoFillThresholdSeverityItems() { 21 | ListBoxModel result = descriptor.doFillThresholdSeverityItems(); 22 | assertTrue(result.size() > 0); 23 | } 24 | 25 | @Test 26 | public void testDoCheckThresholdCountValid() { 27 | FormValidation result = descriptor.doCheckThresholdCount("10"); 28 | assertEquals(result.kind, FormValidation.Kind.OK); 29 | } 30 | 31 | @Test 32 | public void testDoCheckThresholdCountInvalid() { 33 | FormValidation result = descriptor.doCheckThresholdCount("blah"); 34 | assertEquals(result.kind, FormValidation.Kind.ERROR); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.aspectsecurity.contrast.contrastjenkins.ContrastPluginConfig.ContrastPluginConfigDescriptor; 4 | import com.contrastsecurity.http.TraceFilterForm; 5 | import com.contrastsecurity.models.Application; 6 | import com.contrastsecurity.models.Applications; 7 | import com.contrastsecurity.models.JobOutcomePolicy; 8 | import com.contrastsecurity.models.Trace; 9 | import com.contrastsecurity.models.Traces; 10 | import com.contrastsecurity.sdk.ContrastSDK; 11 | import com.google.common.collect.Lists; 12 | import hudson.model.AbstractProject; 13 | import hudson.model.ItemGroup; 14 | import hudson.model.Job; 15 | import hudson.model.Run; 16 | import junit.framework.TestCase; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.mockito.Mockito; 20 | import org.powermock.api.mockito.PowerMockito; 21 | import org.powermock.core.classloader.annotations.PrepareForTest; 22 | import org.powermock.modules.junit4.PowerMockRunner; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | import static org.mockito.BDDMockito.times; 28 | import static org.mockito.BDDMockito.verify; 29 | import static org.mockito.Matchers.*; 30 | import static org.mockito.Mockito.when; 31 | import static org.powermock.api.mockito.PowerMockito.mock; 32 | 33 | @RunWith(PowerMockRunner.class) 34 | @PrepareForTest({VulnerabilityTrendHelper.class, ContrastPluginConfigDescriptor.class}) 35 | public class VulnerabilityTrendHelperTest extends TestCase { 36 | 37 | @Test 38 | public void testGetVulnerabilityInfoString() { 39 | Trace traceMock = mock(Trace.class); 40 | when(traceMock.getSeverity()).thenReturn("Medium"); 41 | 42 | Trace traceMock2 = mock(Trace.class); 43 | when(traceMock2.getSeverity()).thenReturn("High"); 44 | 45 | List traces = new ArrayList<>(); 46 | traces.add(traceMock); 47 | traces.add(traceMock2); 48 | 49 | Traces tracesMock = mock(Traces.class); 50 | when(tracesMock.getCount()).thenReturn(2); 51 | when(tracesMock.getTraces()).thenReturn(traces); 52 | 53 | String info = VulnerabilityTrendHelper.getVulnerabilityInfoString(tracesMock); 54 | assertEquals("Found vulnerabilities: Medium - 1 High - 1 .", info); 55 | } 56 | 57 | @Test 58 | public void testGetVulnerabilityInfoStringEmptyTraces() { 59 | 60 | List traces = new ArrayList<>(); 61 | 62 | Traces tracesMock = mock(Traces.class); 63 | when(tracesMock.getCount()).thenReturn(0); 64 | when(tracesMock.getTraces()).thenReturn(traces); 65 | 66 | String info = VulnerabilityTrendHelper.getVulnerabilityInfoString(tracesMock); 67 | assertEquals("", info); 68 | } 69 | 70 | @Test 71 | public void testGetVulnerabilityInfoStringNullTraces() { 72 | 73 | String info = VulnerabilityTrendHelper.getVulnerabilityInfoString(null); 74 | assertEquals("", info); 75 | } 76 | 77 | @Test 78 | public void testBuildAppVersionTagHierarchical() { 79 | String parentFullName = "project"; 80 | int buildNumber = 1; 81 | String applicationId = "NodeTestBench"; 82 | String appVersionTagActual = applicationId + "-" + parentFullName + "-" + buildNumber; 83 | 84 | Run build = mock(Run.class); 85 | 86 | Job parent = mock(Job.class); 87 | ItemGroup itemGroup = mock(ItemGroup.class); 88 | when(itemGroup.getFullName()).thenReturn(""); 89 | when(parent.getParent()).thenReturn(itemGroup); 90 | 91 | when(build.getNumber()).thenReturn(buildNumber); 92 | when(build.getParent()).thenReturn(parent); 93 | when(parent.getFullName()).thenReturn(parentFullName); 94 | 95 | String appVersionTag = VulnerabilityTrendHelper.buildAppVersionTagHierarchical(build, applicationId); 96 | assertEquals(appVersionTag, appVersionTagActual); 97 | } 98 | 99 | @Test 100 | public void testGetAllTracesForApplication() throws Exception { 101 | Application application = mock(Application.class); 102 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 103 | TraceFilterForm mockTraceFilterForm = mock(TraceFilterForm.class); 104 | Traces page1 = mock(Traces.class); 105 | Traces page2 = mock(Traces.class); 106 | 107 | List traceList1 = new ArrayList<>(); 108 | List traceList2 = new ArrayList<>(); 109 | 110 | for (int i = 0; i < 50; i++) { 111 | traceList1.add(mock(Trace.class)); 112 | } 113 | 114 | traceList2.add(mock(Trace.class)); 115 | 116 | PowerMockito.stub(PowerMockito.method(VulnerabilityTrendHelper.class, "createSDK")).toReturn(contrastSDKMock); 117 | 118 | when(application.getId()).thenReturn("test"); 119 | when(page1.getCount()).thenReturn(51); 120 | when(page2.getCount()).thenReturn(51); 121 | when(page1.getTraces()).thenReturn(traceList1); 122 | when(page2.getTraces()).thenReturn(traceList2); 123 | when(contrastSDKMock.getTraces(anyString(), anyString(), any(TraceFilterForm.class))) 124 | .thenReturn(page1) 125 | .thenReturn(page2); 126 | 127 | Traces tracesReturned = VulnerabilityTrendHelper.getAllTraces(contrastSDKMock, "1", application.getId(), mockTraceFilterForm); 128 | 129 | assertEquals(51, tracesReturned.getTraces().size()); 130 | verify(contrastSDKMock, times(2)).getTraces(anyString(), anyString(), any(TraceFilterForm.class)); 131 | } 132 | 133 | @Test 134 | public void testGetAllTracesNullApplication() throws Exception { 135 | Application application = mock(Application.class); 136 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 137 | TraceFilterForm mockTraceFilterForm = mock(TraceFilterForm.class); 138 | Traces page1 = mock(Traces.class); 139 | Traces page2 = mock(Traces.class); 140 | 141 | List traceList1 = new ArrayList<>(); 142 | List traceList2 = new ArrayList<>(); 143 | 144 | for (int i = 0; i < 50; i++) { 145 | traceList1.add(mock(Trace.class)); 146 | } 147 | 148 | traceList2.add(mock(Trace.class)); 149 | 150 | PowerMockito.stub(PowerMockito.method(VulnerabilityTrendHelper.class, "createSDK")).toReturn(contrastSDKMock); 151 | 152 | when(application.getId()).thenReturn("test"); 153 | when(page1.getCount()).thenReturn(51); 154 | when(page2.getCount()).thenReturn(51); 155 | when(page1.getTraces()).thenReturn(traceList1); 156 | when(page2.getTraces()).thenReturn(traceList2); 157 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))) 158 | .thenReturn(page1) 159 | .thenReturn(page2); 160 | 161 | Traces tracesReturned = VulnerabilityTrendHelper.getAllTraces(contrastSDKMock, "1", null, mockTraceFilterForm); 162 | 163 | assertEquals(51, tracesReturned.getTraces().size()); 164 | verify(contrastSDKMock, times(2)).getTracesInOrg(anyString(), any(TraceFilterForm.class)); 165 | } 166 | 167 | @Test 168 | public void testGetAllTracesWhenEmptyResponse() throws Exception { 169 | Application application = mock(Application.class); 170 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 171 | TraceFilterForm mockTraceFilterForm = mock(TraceFilterForm.class); 172 | Traces page1 = mock(Traces.class); 173 | List traceList = new ArrayList<>(); 174 | 175 | PowerMockito.stub(PowerMockito.method(VulnerabilityTrendHelper.class, "createSDK")).toReturn(contrastSDKMock); 176 | 177 | when(application.getId()).thenReturn("test"); 178 | when(page1.getCount()).thenReturn(0); 179 | when(page1.getTraces()).thenReturn(traceList); 180 | when(contrastSDKMock.getTraces(anyString(), anyString(), any(TraceFilterForm.class))) 181 | .thenReturn(page1); 182 | 183 | Traces tracesReturned = VulnerabilityTrendHelper.getAllTraces(contrastSDKMock, "1", application.getId(), mockTraceFilterForm); 184 | 185 | assertEquals(0, tracesReturned.getTraces().size()); 186 | verify(contrastSDKMock, times(1)).getTraces(anyString(), anyString(), any(TraceFilterForm.class)); 187 | } 188 | 189 | @Test 190 | public void testGetDefaultAgentFileNameFromStringJava() throws Exception { 191 | assertEquals("contrast.jar", VulnerabilityTrendHelper.getDefaultAgentFileNameFromString("Java")); 192 | } 193 | @Test 194 | public void testGetDefaultAgtestGetDefaultAgentFileNameFromStringNode() throws Exception { 195 | assertEquals("node-contrast.tgz", VulnerabilityTrendHelper.getDefaultAgentFileNameFromString("Node")); 196 | } 197 | @Test 198 | public void testGetDefaultAgtestGetDefaultAgentFileNameFromStringDotNet() throws Exception { 199 | assertEquals("dotnet-contrast.zip", VulnerabilityTrendHelper.getDefaultAgentFileNameFromString(".NET")); 200 | } 201 | @Test 202 | public void testGetDefaultAgtestGetDefaultAgentFileNameFromStringEmpty() throws Exception { 203 | assertEquals("contrast.jar", VulnerabilityTrendHelper.getDefaultAgentFileNameFromString("")); 204 | } 205 | 206 | @Test 207 | public void testIsEnabledJobOutcomePolicyExistFalse() throws Exception{ 208 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 209 | String orgUuId = "fakeOrgUuid"; 210 | 211 | when(contrastSDKMock.getEnabledJobOutcomePolicies(orgUuId)).thenReturn(Lists.newArrayList()); 212 | 213 | assertEquals(false, VulnerabilityTrendHelper.isEnabledJobOutcomePolicyExist(contrastSDKMock, orgUuId)); 214 | } 215 | 216 | @Test 217 | public void testIsEnabledJobOutcomePolicyExistTrue() throws Exception{ 218 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 219 | String orgUuId = "fakeOrgUuid"; 220 | ArrayList jobOutcomePolicies = new ArrayList<>(); 221 | jobOutcomePolicies.add(new JobOutcomePolicy()); 222 | 223 | when(contrastSDKMock.getEnabledJobOutcomePolicies(orgUuId)).thenReturn(jobOutcomePolicies); 224 | 225 | assertEquals(true, VulnerabilityTrendHelper.isEnabledJobOutcomePolicyExist(contrastSDKMock, orgUuId)); 226 | } 227 | 228 | @Test 229 | public void testIsApplicableEnabledJobOutcomePolicyExistFalse() throws Exception{ 230 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 231 | String orgUuId = "fakeOrgUuid"; 232 | String appId = "fakeAppId"; 233 | 234 | List applicationList = new ArrayList<>(); 235 | Application application = mock(Application.class); 236 | when(application.getId()).thenReturn(appId); 237 | applicationList.add(application); 238 | 239 | Applications applications = mock(Applications.class); 240 | when(applications.getApplications()).thenReturn(applicationList); 241 | when(contrastSDKMock.getApplications(anyString())).thenReturn(applications); 242 | when(contrastSDKMock.getEnabledJoboutcomePoliciesByApplication(orgUuId, appId)).thenReturn(Lists.newArrayList()); 243 | 244 | assertFalse(VulnerabilityTrendHelper 245 | .isApplicableEnabledJobOutcomePolicyExist(contrastSDKMock, orgUuId, appId)); 246 | } 247 | 248 | @Test 249 | public void testIsApplicableEnabledJobOutcomePolicyExistTrue() throws Exception{ 250 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 251 | String orgUuId = "fakeOrgUuid"; 252 | String appId = "fakeAppId"; 253 | ArrayList jobOutcomePolicies = new ArrayList<>(); 254 | jobOutcomePolicies.add(new JobOutcomePolicy()); 255 | 256 | List applicationList = new ArrayList<>(); 257 | Application application = mock(Application.class); 258 | when(application.getId()).thenReturn(appId); 259 | applicationList.add(application); 260 | 261 | Applications applications = mock(Applications.class); 262 | when(applications.getApplications()).thenReturn(applicationList); 263 | when(contrastSDKMock.getApplications(anyString())).thenReturn(applications); 264 | when(contrastSDKMock.getEnabledJoboutcomePoliciesByApplication(orgUuId, appId)).thenReturn(jobOutcomePolicies); 265 | 266 | assertTrue(VulnerabilityTrendHelper 267 | .isApplicableEnabledJobOutcomePolicyExist(contrastSDKMock, orgUuId, appId)); 268 | } 269 | 270 | @Test 271 | public void testGetProfileSpaceExists() throws Exception { 272 | ContrastPluginConfigDescriptor descriptorMock = mock(ContrastPluginConfigDescriptor.class); 273 | TeamServerProfile profile = mock(TeamServerProfile.class); 274 | when(profile.getName()).thenReturn("MyProfile"); 275 | TeamServerProfile[] profiles = {profile}; 276 | when(descriptorMock.getTeamServerProfiles()).thenReturn(profiles); 277 | 278 | PowerMockito.whenNew(ContrastPluginConfigDescriptor.class).withNoArguments().thenReturn(descriptorMock); 279 | TeamServerProfile actualProfile = VulnerabilityTrendHelper.getProfile("myprofile"); 280 | 281 | assertNotNull(actualProfile); 282 | assertEquals("MyProfile", actualProfile.getName()); 283 | } 284 | 285 | @Test 286 | public void testGetProfileMisMatchedCase() throws Exception { 287 | ContrastPluginConfigDescriptor descriptorMock = mock(ContrastPluginConfigDescriptor.class); 288 | TeamServerProfile profile = mock(TeamServerProfile.class); 289 | when(profile.getName()).thenReturn("MyProfile"); 290 | TeamServerProfile[] profiles = {profile}; 291 | when(descriptorMock.getTeamServerProfiles()).thenReturn(profiles); 292 | 293 | PowerMockito.whenNew(ContrastPluginConfigDescriptor.class).withNoArguments().thenReturn(descriptorMock); 294 | TeamServerProfile actualProfile = VulnerabilityTrendHelper.getProfile(" MyProfile "); 295 | 296 | assertNotNull(actualProfile); 297 | assertEquals("MyProfile", actualProfile.getName()); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/test/java/com/aspectsecurity/contrast/contrastjenkins/VulnerabilityTrendStepTest.java: -------------------------------------------------------------------------------- 1 | package com.aspectsecurity.contrast.contrastjenkins; 2 | 3 | import com.contrastsecurity.exceptions.UnauthorizedException; 4 | import com.contrastsecurity.http.TraceFilterForm; 5 | import com.contrastsecurity.models.JobOutcomePolicy; 6 | import com.contrastsecurity.models.Organization; 7 | import com.contrastsecurity.models.Organizations; 8 | import com.contrastsecurity.models.SecurityCheck; 9 | import com.contrastsecurity.models.Traces; 10 | import com.contrastsecurity.sdk.ContrastSDK; 11 | import hudson.AbortException; 12 | import hudson.model.Result; 13 | import hudson.model.Run; 14 | import hudson.model.TaskListener; 15 | import jenkins.model.Jenkins; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.mockito.Mock; 20 | import org.powermock.api.mockito.PowerMockito; 21 | import org.powermock.core.classloader.annotations.PrepareForTest; 22 | import org.powermock.modules.junit4.PowerMockRunner; 23 | 24 | import java.io.IOException; 25 | import java.io.PrintStream; 26 | 27 | import static org.junit.Assert.assertNull; 28 | import static org.mockito.BDDMockito.given; 29 | import static org.mockito.Matchers.any; 30 | import static org.mockito.Matchers.anyInt; 31 | import static org.mockito.Matchers.anyLong; 32 | import static org.mockito.Matchers.anyString; 33 | import static org.mockito.Mockito.doNothing; 34 | import static org.mockito.Mockito.verify; 35 | import static org.mockito.Mockito.when; 36 | import static org.powermock.api.mockito.PowerMockito.doReturn; 37 | import static org.powermock.api.mockito.PowerMockito.mock; 38 | import static org.powermock.api.mockito.PowerMockito.spy; 39 | 40 | 41 | @RunWith(PowerMockRunner.class) 42 | @PrepareForTest({Jenkins.class, VulnerabilityTrendStep.class, VulnerabilityTrendHelper.class}) 43 | public class VulnerabilityTrendStepTest { 44 | 45 | @Mock 46 | TaskListener taskListenerMock; 47 | @Mock 48 | PrintStream printStreamMock; 49 | @Mock 50 | Jenkins jenkins; 51 | @Mock 52 | VulnerabilityTrendHelper vulnerabilityTrendHelper; 53 | @Mock 54 | VulnerabilityTrendStep.VulnerabilityTrendStepDescriptorImpl vulnerabilityTrendStepDescriptor; 55 | 56 | @Before 57 | public void setUp() { 58 | PowerMockito.mockStatic(Jenkins.class); 59 | PowerMockito.mockStatic(VulnerabilityTrendStep.class); 60 | PowerMockito.mockStatic(VulnerabilityTrendHelper.class); 61 | 62 | when(jenkins.getDescriptorByType(VulnerabilityTrendStep.VulnerabilityTrendStepDescriptorImpl.class)).thenReturn(vulnerabilityTrendStepDescriptor); 63 | } 64 | 65 | @Test 66 | public void testSuccessfulBuild() throws Exception { 67 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 68 | stepExecution.step = new VulnerabilityTrendStep("local", 10, null, null, "WebGoat", Constants.QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT); 69 | 70 | when(Jenkins.getInstance()).thenReturn(jenkins); 71 | 72 | stepExecution.taskListener = taskListenerMock; 73 | 74 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 75 | doNothing().when(printStreamMock).println(); 76 | 77 | Traces tracesMock = mock(Traces.class); 78 | when(tracesMock.getCount()).thenReturn(0); 79 | 80 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 81 | 82 | doReturn("test").when(stepExecution).getBuildName(); 83 | 84 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 85 | 86 | TeamServerProfile profile = mock(TeamServerProfile.class); 87 | 88 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 89 | 90 | Organizations mockOrgs = mock(Organizations.class); 91 | Organization mockOrg = mock(Organization.class); 92 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 93 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 94 | 95 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 96 | 97 | assertNull(stepExecution.run()); 98 | } 99 | 100 | @Test 101 | public void testSuccessfulBuildWithParameter() throws Exception { 102 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 103 | stepExecution.step = new VulnerabilityTrendStep("local", 10, null, null, "WebGoat", Constants.QUERY_BY_PARAMETER); 104 | 105 | when(Jenkins.getInstance()).thenReturn(jenkins); 106 | 107 | stepExecution.taskListener = taskListenerMock; 108 | 109 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 110 | doNothing().when(printStreamMock).println(); 111 | 112 | Traces tracesMock = mock(Traces.class); 113 | when(tracesMock.getCount()).thenReturn(0); 114 | 115 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 116 | 117 | doReturn("test").when(stepExecution).getBuildName(); 118 | 119 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 120 | 121 | TeamServerProfile profile = mock(TeamServerProfile.class); 122 | 123 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 124 | 125 | Organizations mockOrgs = mock(Organizations.class); 126 | Organization mockOrg = mock(Organization.class); 127 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 128 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 129 | 130 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 131 | 132 | assertNull(stepExecution.run()); 133 | } 134 | 135 | @Test 136 | public void testUnsuccessfulBuild() throws Exception { 137 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 138 | stepExecution.step = new VulnerabilityTrendStep("local", 10, "xss", "High", "WebGoat", Constants.QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT); 139 | stepExecution.build = mock(Run.class); 140 | 141 | when(Jenkins.getInstance()).thenReturn(jenkins); 142 | 143 | stepExecution.taskListener = taskListenerMock; 144 | 145 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 146 | doNothing().when(printStreamMock).println(); 147 | 148 | Traces tracesMock = mock(Traces.class); 149 | when(tracesMock.getCount()).thenReturn(11); 150 | 151 | 152 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 153 | SecurityCheck undifinedPolicySecurityCheck = mock(SecurityCheck.class); 154 | given(undifinedPolicySecurityCheck.getResult()).willReturn(null); 155 | 156 | Organizations mockOrgs = mock(Organizations.class); 157 | Organization mockOrg = mock(Organization.class); 158 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 159 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 160 | 161 | doReturn("test").when(stepExecution).getBuildName(); 162 | 163 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 164 | given(VulnerabilityTrendHelper.applicationIdExists(any(ContrastSDK.class), anyString(), anyString())).willReturn(true); 165 | given(VulnerabilityTrendHelper.makeSecurityCheck(any(ContrastSDK.class), anyString(), anyString(), anyLong(), anyInt(), any(TraceFilterForm.class))).willReturn(undifinedPolicySecurityCheck); 166 | 167 | TeamServerProfile profile = mock(TeamServerProfile.class); 168 | given(profile.getVulnerableBuildResult()).willReturn(Result.UNSTABLE.toString()); 169 | given(profile.isApplyVulnerableBuildResultOnContrastError()).willReturn(true); 170 | 171 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 172 | given(VulnerabilityTrendHelper.getAllTraces(any(ContrastSDK.class), anyString(), anyString(), any(TraceFilterForm.class))).willReturn(tracesMock); 173 | 174 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 175 | 176 | assertNull(stepExecution.run()); 177 | verify(stepExecution.build).setResult(Result.fromString(profile.getVulnerableBuildResult())); 178 | } 179 | 180 | @Test 181 | public void testUnsuccessfulBuildJop() throws Exception { 182 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 183 | stepExecution.step = new VulnerabilityTrendStep("local", 10, "xss", "High", "WebGoat", Constants.QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT); 184 | stepExecution.build = mock(Run.class); 185 | 186 | when(Jenkins.getInstance()).thenReturn(jenkins); 187 | 188 | stepExecution.taskListener = taskListenerMock; 189 | 190 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 191 | doNothing().when(printStreamMock).println(); 192 | 193 | Traces tracesMock = mock(Traces.class); 194 | when(tracesMock.getCount()).thenReturn(11); 195 | 196 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 197 | 198 | Organizations mockOrgs = mock(Organizations.class); 199 | Organization mockOrg = mock(Organization.class); 200 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 201 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 202 | 203 | JobOutcomePolicy jobOutcomePolicy = mock(JobOutcomePolicy.class); 204 | when(jobOutcomePolicy.getOutcome()).thenReturn(JobOutcomePolicy.Outcome.UNSTABLE); 205 | 206 | SecurityCheck undifinedPolicySecurityCheck = mock(SecurityCheck.class); 207 | when(undifinedPolicySecurityCheck.getResult()).thenReturn(false); 208 | when(undifinedPolicySecurityCheck.getJobOutcomePolicy()).thenReturn(jobOutcomePolicy); 209 | 210 | 211 | 212 | doReturn("test").when(stepExecution).getBuildName(); 213 | 214 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 215 | given(VulnerabilityTrendHelper.applicationIdExists(any(ContrastSDK.class), anyString(), anyString())).willReturn(true); 216 | given(VulnerabilityTrendHelper.makeSecurityCheck(any(ContrastSDK.class), anyString(), anyString(), anyLong(), anyInt(), any(TraceFilterForm.class))).willReturn(undifinedPolicySecurityCheck); 217 | given(VulnerabilityTrendHelper.getJenkinsResultFromJobOutcome(any(JobOutcomePolicy.Outcome.class))).willReturn(Result.UNSTABLE); 218 | 219 | TeamServerProfile profile = mock(TeamServerProfile.class); 220 | given(profile.getVulnerableBuildResult()).willReturn(Result.UNSTABLE.toString()); 221 | given(profile.isApplyVulnerableBuildResultOnContrastError()).willReturn(false); 222 | 223 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 224 | given(VulnerabilityTrendHelper.getAllTraces(any(ContrastSDK.class), anyString(), anyString(), any(TraceFilterForm.class))).willReturn(tracesMock); 225 | 226 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 227 | 228 | assertNull(stepExecution.run()); 229 | verify(stepExecution.build).setResult(Result.fromString(profile.getVulnerableBuildResult())); 230 | 231 | } 232 | 233 | 234 | @Test(expected = AbortException.class) 235 | public void testVulnerableBuildFailureSelected() throws Exception { 236 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 237 | stepExecution.step = new VulnerabilityTrendStep("local", 10, "xss", "High", "WebGoat", Constants.QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT); 238 | stepExecution.build = mock(Run.class); 239 | 240 | when(Jenkins.getInstance()).thenReturn(jenkins); 241 | 242 | stepExecution.taskListener = taskListenerMock; 243 | 244 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 245 | doNothing().when(printStreamMock).println(); 246 | 247 | Traces tracesMock = mock(Traces.class); 248 | when(tracesMock.getCount()).thenReturn(11); 249 | 250 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 251 | 252 | Organizations mockOrgs = mock(Organizations.class); 253 | Organization mockOrg = mock(Organization.class); 254 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 255 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 256 | 257 | SecurityCheck undifinedPolicySecurityCheck = mock(SecurityCheck.class); 258 | given(undifinedPolicySecurityCheck.getResult()).willReturn(null); 259 | 260 | 261 | doReturn("test").when(stepExecution).getBuildName(); 262 | 263 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 264 | given(VulnerabilityTrendHelper.applicationIdExists(any(ContrastSDK.class), anyString(), anyString())).willReturn(true); 265 | given(VulnerabilityTrendHelper.makeSecurityCheck(any(ContrastSDK.class), anyString(), anyString(), anyLong(), anyInt(), any(TraceFilterForm.class))).willReturn(undifinedPolicySecurityCheck); 266 | 267 | TeamServerProfile profile = mock(TeamServerProfile.class); 268 | given(profile.getVulnerableBuildResult()).willReturn(Result.FAILURE.toString()); 269 | given(profile.isApplyVulnerableBuildResultOnContrastError()).willReturn(false); 270 | 271 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 272 | given(VulnerabilityTrendHelper.getAllTraces(any(ContrastSDK.class), anyString(), anyString(), any(TraceFilterForm.class))).willReturn(tracesMock); 273 | 274 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 275 | 276 | stepExecution.run(); 277 | } 278 | 279 | @Test(expected = AbortException.class) 280 | public void testVulnerableBuildFailureSelectedJOP() throws Exception { 281 | VulnerabilityTrendStep.Execution stepExecution = spy(new VulnerabilityTrendStep.Execution()); 282 | stepExecution.step = new VulnerabilityTrendStep("local", 10, "xss", "High", "WebGoat", Constants.QUERY_BY_APP_VERSION_TAG_DEFAULT_FORMAT); 283 | stepExecution.build = mock(Run.class); 284 | 285 | when(Jenkins.getInstance()).thenReturn(jenkins); 286 | 287 | stepExecution.taskListener = taskListenerMock; 288 | 289 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 290 | doNothing().when(printStreamMock).println(); 291 | 292 | Traces tracesMock = mock(Traces.class); 293 | when(tracesMock.getCount()).thenReturn(11); 294 | 295 | ContrastSDK contrastSDKMock = mock(ContrastSDK.class); 296 | 297 | Organizations mockOrgs = mock(Organizations.class); 298 | Organization mockOrg = mock(Organization.class); 299 | when(mockOrgs.getOrganization()).thenReturn(mockOrg); 300 | given(contrastSDKMock.getProfileDefaultOrganizations()).willReturn(mockOrgs); 301 | 302 | JobOutcomePolicy jobOutcomePolicy = mock(JobOutcomePolicy.class); 303 | when(jobOutcomePolicy.getOutcome()).thenReturn(JobOutcomePolicy.Outcome.FAIL); 304 | 305 | SecurityCheck undifinedPolicySecurityCheck = mock(SecurityCheck.class); 306 | when(undifinedPolicySecurityCheck.getResult()).thenReturn(false); 307 | when(undifinedPolicySecurityCheck.getJobOutcomePolicy()).thenReturn(jobOutcomePolicy); 308 | 309 | 310 | 311 | doReturn("test").when(stepExecution).getBuildName(); 312 | 313 | given(VulnerabilityTrendHelper.createSDK(anyString(), anyString(), anyString(), anyString())).willReturn(contrastSDKMock); 314 | given(VulnerabilityTrendHelper.applicationIdExists(any(ContrastSDK.class), anyString(), anyString())).willReturn(true); 315 | given(VulnerabilityTrendHelper.makeSecurityCheck(any(ContrastSDK.class), anyString(), anyString(), anyLong(), anyInt(), any(TraceFilterForm.class))).willReturn(undifinedPolicySecurityCheck); 316 | given(VulnerabilityTrendHelper.getJenkinsResultFromJobOutcome(any(JobOutcomePolicy.Outcome.class))).willReturn(Result.FAILURE); 317 | 318 | TeamServerProfile profile = mock(TeamServerProfile.class); 319 | given(profile.getVulnerableBuildResult()).willReturn(Result.FAILURE.toString()); 320 | given(profile.isApplyVulnerableBuildResultOnContrastError()).willReturn(false); 321 | 322 | given(VulnerabilityTrendHelper.getProfile(anyString())).willReturn(profile); 323 | given(VulnerabilityTrendHelper.getAllTraces(any(ContrastSDK.class), anyString(), anyString(), any(TraceFilterForm.class))).willReturn(tracesMock); 324 | 325 | when(contrastSDKMock.getTracesInOrg(anyString(), any(TraceFilterForm.class))).thenReturn(tracesMock); 326 | 327 | stepExecution.run(); 328 | } 329 | 330 | 331 | } -------------------------------------------------------------------------------- /vulnerabilityTrendRecorderConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 6 | 7 | 8 | 0 9 | Critical 10 | 11 | 12 | 2 13 | High 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------