├── Jenkinsfile ├── src └── main │ ├── webapp │ └── appray.png │ ├── resources │ ├── index.jelly │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── appray │ │ ├── AppRayBuilder │ │ ├── help-appRayUrl.html │ │ ├── help-waitTimeout.html │ │ ├── help-outputFilePath.html │ │ ├── help-riskScoreThreshold.html │ │ ├── config.properties │ │ ├── help-credentialsId.html │ │ └── config.jelly │ │ ├── Messages.properties │ │ └── AppRayResultAction │ │ └── summary.jelly │ └── java │ └── io │ └── jenkins │ └── plugins │ └── appray │ ├── User.java │ ├── AppRayConnectorException.java │ ├── ScanJob.java │ ├── AppRayResultAction.java │ ├── AppRayConnector.java │ └── AppRayBuilder.java ├── .gitignore ├── README.md ├── Makefile ├── LICENSE ├── pom.xml └── GettingStarted.md /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin() 2 | -------------------------------------------------------------------------------- /src/main/webapp/appray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/appray-plugin/master/src/main/webapp/appray.png -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | App-Ray Security Test 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/help-appRayUrl.html: -------------------------------------------------------------------------------- 1 |
2 | App-Ray instance URL. Default: https://demo.app-ray.co 3 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /work 3 | 4 | # Editors, IDEs 5 | .*.sw? 6 | .sw? 7 | *~ 8 | .project 9 | .pydevproject 10 | .idea 11 | 12 | # patch 13 | *.rej 14 | *.orig 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/help-waitTimeout.html: -------------------------------------------------------------------------------- 1 |
2 | Timeout of waiting for ending of a scan. (For big application sometimes it take more than 30 minute!) 3 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App-Ray Jenkins plugin 2 | 3 | This plugin provides an integration with App-Ray mobile security testing which enables users to automatically scan their Android and iOS applications for potential security vulnerabilities. 4 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/help-outputFilePath.html: -------------------------------------------------------------------------------- 1 |
2 | Name of the binary file (apk, ipa) to be scanned relative to the workspace. 3 | You can use Jenkins variables to specify the filename. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/help-riskScoreThreshold.html: -------------------------------------------------------------------------------- 1 |
2 | The minimum risk score to check the apk validity. It is the output of then scan. You can check the scores on the 3 | app-ray instance. 4 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/config.properties: -------------------------------------------------------------------------------- 1 | Url=App-Ray instance URL 2 | OutputFilePath=Application file path 3 | WaitTimeout=Wait timeout (in minutes) 4 | RiskScoreThreshold=Risk score threshold 5 | TestConnection=Test connection 6 | Testing=Loading 7 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/help-credentialsId.html: -------------------------------------------------------------------------------- 1 |
2 | Use email address as a username and please provide a password. 3 | The user must have full-access access right in App-Ray in order to be able to submit applications for scanning. 4 |
5 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/User.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | public class User { 4 | enum Role { 5 | FULL, 6 | READONLY, 7 | OBSERVER, 8 | UNKNOWN 9 | } 10 | 11 | public String name; 12 | public String email; 13 | public Role role; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/AppRayConnectorException.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | public class AppRayConnectorException extends Exception { 4 | public AppRayConnectorException(String message) { 5 | super(message); 6 | } 7 | 8 | public AppRayConnectorException(String message, Throwable e) { 9 | super(message, e); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/Messages.properties: -------------------------------------------------------------------------------- 1 | AppRayBuilder.DescriptorImpl.errors.missingName=Please set a email 2 | AppRayBuilder.DescriptorImpl.errors.wrongFormat=Please set a valid email 3 | AppRayBuilder.DescriptorImpl.warnings.tooShort=Isn't the name too short? 4 | AppRayBuilder.DescriptorImpl.warnings.reallyFrench=Are you actually French? 5 | AppRayBuilder.DescriptorImpl.DisplayName=App-Ray security check -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/ScanJob.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | public class ScanJob { 4 | enum Status { 5 | queued, 6 | processing, 7 | finished, 8 | failed 9 | } 10 | 11 | public Status status; 12 | public int progress_total; 13 | public int progress_finished; 14 | public int risk_score; 15 | public String package_name; 16 | public String label; 17 | public String version; 18 | public String platform; 19 | public String app_hash; 20 | public String failure_reason; 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # build 3 | 4 | .PHONY: build 5 | build: 6 | docker run -it --rm -v "${PWD}":/usr/src/mymaven -v "${HOME}/.m2":/root/.m2 -v "${PWD}/target:/usr/src/mymaven/target" -w /usr/src/mymaven maven:3.6.3-jdk-8 mvn verify -Denforcer.skip=true -DskipTests=true -Dfindbugs.skip=true -Dspotbugs.skip=true 7 | 8 | .PHONY: package 9 | package: 10 | docker run -it --rm -v "${PWD}":/usr/src/mymaven -v "${HOME}/.m2":/root/.m2 -v "${PWD}/target:/usr/src/mymaven/target" -w /usr/src/mymaven maven:3.6.3-jdk-8 mvn package 11 | 12 | .PHONY: clean 13 | clean: 14 | docker run -it --rm -v "${PWD}":/usr/src/mymaven -v "${HOME}/.m2":/root/.m2 -v "${PWD}/target:/usr/src/mymaven/target" -w /usr/src/mymaven maven:3.6.3-jdk-8 mvn clean 15 | 16 | .PHONY: run 17 | run: 18 | docker run -it --rm -p 8090:8090 -v "${PWD}":/usr/src/mymaven -v "${HOME}/.m2":/root/.m2 -v "${PWD}/target:/usr/src/mymaven/target" -w /usr/src/mymaven maven:3.6.3-jdk-8 mvn hpi:run -Djetty.port=8090 -Dhost=0.0.0.0 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayBuilder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 App-Ray GmbH. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/AppRayResultAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | import hudson.model.Action; 4 | 5 | 6 | public class AppRayResultAction implements Action { 7 | private String resultUrl; 8 | private ScanJob job; 9 | 10 | public AppRayResultAction(String resultUrl, ScanJob job) { 11 | this.resultUrl = resultUrl; 12 | this.job = job; 13 | } 14 | 15 | @Override 16 | public String getIconFileName() { 17 | if (this.resultUrl != null) { 18 | return "/plugin/appray/appray.png"; 19 | } else { 20 | return null; 21 | } 22 | } 23 | 24 | @Override 25 | public String getDisplayName() { 26 | if (this.resultUrl != null) { 27 | return "Result @ App-Ray"; 28 | } else { 29 | return null; 30 | } 31 | } 32 | 33 | @Override 34 | public String getUrlName() { 35 | return this.resultUrl; 36 | } 37 | 38 | public ScanJob getJob() { 39 | return this.job; 40 | } 41 | 42 | public String getUrl() { 43 | return this.resultUrl; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/appray/AppRayResultAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | App-Ray scan results for ${job.label} ${job.version} "${job.package}" SHA1:${job.app_hash}:
9 | Status: ${job.status}
10 | 11 | Result on App-Ray: ${it.url}
12 |
13 | 14 | 15 | Risk score: ${job.risk_score}
16 |
17 | 18 | Failure reason: ${job.failure_reason} 19 | 20 | 21 | App-Ray scan wait timeout exceeded, try to check the result on App-Ray. 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 4.0 8 | 9 | 10 | 11 | io.jenkins.plugins 12 | appray 13 | 1.1.2-SNAPSHOT 14 | hpi 15 | 16 | 2.164.3 17 | 8 18 | 19 | App-Ray Security check plugin 20 | App-Ray service connector 21 | 22 | 23 | MIT License 24 | https://opensource.org/licenses/MIT 25 | 26 | 27 | 28 | 29 | org.jenkins-ci.plugins 30 | credentials 31 | 2.3.8 32 | 33 | 34 | com.konghq 35 | unirest-java 36 | 3.7.02 37 | 38 | 39 | 40 | 41 | 42 | marci_appray 43 | Marton ILLES 44 | marton.illes@app-ray.co 45 | 46 | 47 | 48 | https://github.com/jenkinsci/appray-plugin 49 | 50 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 51 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 52 | https://github.com/jenkinsci/${project.artifactId}-plugin 53 | HEAD 54 | 55 | 56 | 57 | 58 | repo.jenkins-ci.org 59 | https://repo.jenkins-ci.org/public/ 60 | 61 | 62 | 63 | 64 | repo.jenkins-ci.org 65 | https://repo.jenkins-ci.org/public/ 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /GettingStarted.md: -------------------------------------------------------------------------------- 1 | # App-Ray Mobile Security: Jenkins plugin 2 | Learn more: https://app-ray.co 3 | 4 | App-Ray’s Jenkins integration plugin allows users to add a security check step to their Jenkins workflow, to improve their CI/CD pipeline with automated Static and Dynamic Application Analysis. ARM Android and iOS apps are supported. 5 | 6 | ![App-Ray Mobile Security Jenkins Plugin Screenshot](https://app-ray.co/jenkins/jenkins-01.jpg) 7 | 8 | ## Table of contents 9 | * [Capabilities](#capabilities) 10 | * [Installation](#installation) 11 | * [Configuration](#configuration) 12 | * [Building](#building) 13 | 14 | ## Capabilities 15 | - Supporting analysis in the cloud or locally (on-premises) 16 | - Authentication data stored securely in Jenkins credentials store 17 | - Detailed threat findings, References to OWASP, CVE and other vulnerability databases 18 | - Remediation suggestions provided (a.k.a. How to fix) 19 | - Threat finding reports available for detailed documentation 20 | - Output in JUnit and JSON formats 21 | - Sophisticated configuration of success/failure conditions 22 | - Detailed logging in the console output 23 | 24 | ## Requirements 25 | - The latest stable version of Jenkins is suggested to be used, according to Jenkins recommendations. 26 | - The minimum tested compatible version of Jenkins is: `2.164.3` 27 | - You will need access to a Cloud or On-premises App-Ray instance. Contact us to get started: https://app-ray.co 28 | 29 | ## Installation 30 | - Locate and install the plugin via Jenkins Plugin Manager. Browse plugins or review documentation at Jenkins Plugins page: https://plugins.jenkins.io/ 31 | - Or download and install our plugin directly from App-Ray website: https://app-ray.co/jenkins/appray.hpi 32 | and upload it in Jenkins Plugin Manager (Advanced tab). 33 | 34 | ## Configuration 35 | 2. Configure your App-Ray credentials (email + password) in Jenkins Credentials page. 36 | 3. Set up a Jenkins build job, or select your existing one. 37 | 4. At section Bindings, bind the previously set App-Ray crendentials to the build job. 38 | 5. Add a new build step, select 'App-Ray security check'. 39 | 6. Provide your configuration parameters, such as Risk score threshold (0-100), location of binary app file (Jenkins environment variables can be used) and access point of the App-Ray instance you use (local or remote). 40 | 7. Run your build, your security results will appear shortly. A security analysis may take a few minutes, depending on your configuration and the complexity of the app. 41 | ![App-Ray Mobile Security Jenkins Plugin Screenshot 2](https://app-ray.co/jenkins/jenkins-02.jpg) 42 | 8. Click on any of these findings to reveal more information. 43 | 44 | 45 | # Building 46 | 47 | ``` 48 | mvn hpi:run 49 | ``` 50 | 51 | ## Findbugs 52 | ``` 53 | mvn findbugs:gui 54 | ``` 55 | 56 | ## Local Installing 57 | ``` 58 | mvn clean install 59 | cp target/appray.hpi ~/.jenkins/plugins/ 60 | ``` 61 | Then redeploy Jenkins. 62 | 63 | --- 64 | 65 | _Any questions? We are happy to help! Contact us via email: info (at) app-ray. co_ 66 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/AppRayConnector.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | import java.io.File; 4 | 5 | import hudson.ProxyConfiguration; 6 | 7 | import kong.unirest.Unirest; 8 | import kong.unirest.UnirestInstance; 9 | import kong.unirest.HttpResponse; 10 | import kong.unirest.MultipartBody; 11 | import kong.unirest.JsonNode; 12 | import kong.unirest.UnirestParsingException; 13 | 14 | 15 | public class AppRayConnector { 16 | private UnirestInstance unirest; 17 | 18 | public void setup(String url, String email, String password, ProxyConfiguration proxy) throws AppRayConnectorException { 19 | UnirestInstance auth_unirest = Unirest.spawnInstance(); 20 | 21 | MultipartBody request = auth_unirest.post(url + "/api/v1/authentication") 22 | .field("username", email) 23 | .field("password", password) 24 | .field("grant_type", "password"); 25 | 26 | if (proxy != null) { 27 | request.proxy(proxy.name, proxy.port); 28 | } 29 | 30 | HttpResponse response = request.asJson(); 31 | 32 | auth_unirest.shutDown(); 33 | 34 | if (response.getStatus() == 401) { 35 | throw new AppRayConnectorException("Authentication failure (" + email + ")"); 36 | } 37 | if (response.getStatus() == 404) { 38 | throw new AppRayConnectorException("Authentication endpoint is missing, specify a valid URL; " + request.getUrl()); 39 | } 40 | 41 | if (!response.isSuccess()) { 42 | throw new AppRayConnectorException("Authentication failure, status code: " + response.getStatus()); 43 | } 44 | 45 | this.unirest = Unirest.spawnInstance(); 46 | this.unirest.config() 47 | .setDefaultHeader("Accept", "application/json") 48 | .addDefaultHeader("Authorization", "Bearer " + response.getBody().getObject().getString("access_token")) 49 | .defaultBaseUrl(url); 50 | 51 | if (proxy != null) { 52 | this.unirest.config().proxy(proxy.name, proxy.port); 53 | } 54 | } 55 | 56 | public void close() { 57 | if (this.unirest != null) { 58 | this.unirest.shutDown(); 59 | } 60 | } 61 | 62 | public String getOrganization() throws AppRayConnectorException { 63 | HttpResponse response = this.unirest.get("/api/v1/organization").asJson(); 64 | this.checkResponse(response); 65 | 66 | return response.getBody().getObject().getString("name"); 67 | } 68 | 69 | public User getUser() throws AppRayConnectorException { 70 | User user = new User(); 71 | 72 | HttpResponse response = this.unirest.get("/api/v1/user").asJson(); 73 | this.checkResponse(response); 74 | 75 | user.name = response.getBody().getObject().getString("name"); 76 | user.email = response.getBody().getObject().getString("email"); 77 | switch (response.getBody().getObject().getString("role")) { 78 | case "full-access": 79 | user.role = User.Role.FULL; 80 | break; 81 | case "read-only": 82 | user.role = User.Role.READONLY; 83 | break; 84 | case "observer": 85 | user.role = User.Role.OBSERVER; 86 | break; 87 | default: 88 | user.role = User.Role.UNKNOWN; 89 | } 90 | 91 | return user; 92 | } 93 | 94 | public String submitApp(String application) throws AppRayConnectorException { 95 | HttpResponse response = this.unirest.post("/api/v1/jobs").field("app_file", new File(application)).asString(); 96 | String result = response.getBody(); 97 | 98 | if (response.getStatus() > 400) { 99 | JsonNode error = new JsonNode(result); 100 | 101 | throw new AppRayConnectorException("Error(" + response.getStatus() +") submitting application for scanning: " + error.getObject().getString("title") + " " + error.getObject().getString("detail")); 102 | } 103 | 104 | return result.substring(1, result.length() - 2); 105 | } 106 | 107 | public String getJUnit(String jobId) throws AppRayConnectorException { 108 | HttpResponse response = this.unirest.get("/api/v1/jobs/{job_id}/junit") 109 | .routeParam("job_id", jobId) 110 | .asString(); 111 | String result = response.getBody(); 112 | 113 | if (response.getStatus() > 400) { 114 | JsonNode error = new JsonNode(result); 115 | 116 | throw new AppRayConnectorException("Error(" + response.getStatus() +") fetching JUnit results; " + error.getObject().getString("title") + " " + error.getObject().getString("detail")); 117 | } 118 | 119 | return result; 120 | } 121 | 122 | public ScanJob getJobDetails(String jobId) throws AppRayConnectorException { 123 | ScanJob job = new ScanJob(); 124 | HttpResponse response = this.unirest.get("/api/v1/jobs/{job_id}") 125 | .routeParam("job_id", jobId) 126 | .asJson(); 127 | 128 | this.checkResponse(response); 129 | 130 | job.status = ScanJob.Status.valueOf(response.getBody().getObject().getString("status")); 131 | job.progress_total = response.getBody().getObject().optInt("progress_total", 0); 132 | job.progress_finished = response.getBody().getObject().optInt("progress_finished", 0); 133 | job.risk_score = response.getBody().getObject().optInt("risk_score", 0); 134 | job.package_name= response.getBody().getObject().getString("package"); 135 | job.label= response.getBody().getObject().getString("label"); 136 | job.version = response.getBody().getObject().getString("version"); 137 | job.platform = response.getBody().getObject().getString("platform"); 138 | job.app_hash = response.getBody().getObject().getString("app_hash"); 139 | job.failure_reason= response.getBody().getObject().optString("failure_reason"); 140 | 141 | return job; 142 | } 143 | 144 | private void checkResponse(HttpResponse response) throws AppRayConnectorException { 145 | if (!response.isSuccess()) { 146 | if (response.getBody() == null) { 147 | UnirestParsingException ex = response.getParsingError().get(); 148 | throw new AppRayConnectorException("Error(" + response.getStatus() + ") response: " + ex.getMessage() + ", body: " + ex.getOriginalBody()); 149 | } else { 150 | throw new AppRayConnectorException("Error(" + response.getStatus() + ") response: " + response.getBody().getObject().getString("title") + " " + response.getBody().getObject().getString("detail")); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/appray/AppRayBuilder.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.appray; 2 | 3 | import hudson.Launcher; 4 | import hudson.Extension; 5 | import hudson.FilePath; 6 | import hudson.util.FormValidation; 7 | import hudson.util.ListBoxModel; 8 | import hudson.util.Secret; 9 | import hudson.model.AbstractProject; 10 | import hudson.model.Run; 11 | import hudson.model.TaskListener; 12 | import hudson.model.Result; 13 | import hudson.model.Item; 14 | import hudson.tasks.Builder; 15 | import hudson.tasks.BuildStepDescriptor; 16 | import hudson.security.ACL; 17 | import org.kohsuke.stapler.DataBoundConstructor; 18 | import org.kohsuke.stapler.QueryParameter; 19 | import org.kohsuke.stapler.AncestorInPath; 20 | import org.kohsuke.stapler.verb.POST; 21 | 22 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 23 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 24 | import com.cloudbees.plugins.credentials.CredentialsProvider; 25 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; 26 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 27 | 28 | import jenkins.model.Jenkins; 29 | import jenkins.tasks.SimpleBuildStep; 30 | 31 | import javax.servlet.ServletException; 32 | import java.io.IOException; 33 | import java.util.Date; 34 | import java.util.Collections; 35 | 36 | import org.jenkinsci.Symbol; 37 | 38 | 39 | public class AppRayBuilder extends Builder implements SimpleBuildStep { 40 | 41 | private final String appRayUrl; 42 | private final String outputFilePath; 43 | private final Integer waitTimeout; 44 | private final Integer riskScoreThreshold; 45 | private final String credentialsId; 46 | 47 | @DataBoundConstructor 48 | public AppRayBuilder(String appRayUrl, String outputFilePath, Integer waitTimeout, Integer riskScoreThreshold, String credentialsId) { 49 | this.appRayUrl = appRayUrl; 50 | this.outputFilePath = outputFilePath; 51 | this.waitTimeout = waitTimeout; 52 | this.riskScoreThreshold = riskScoreThreshold; 53 | this.credentialsId = credentialsId; 54 | } 55 | 56 | public String getAppRayUrl() { 57 | return appRayUrl; 58 | } 59 | 60 | public String getOutputFilePath() { 61 | return outputFilePath; 62 | } 63 | 64 | public Integer getWaitTimeout() { 65 | return waitTimeout; 66 | } 67 | 68 | public Integer getRiskScoreThreshold() { 69 | return riskScoreThreshold; 70 | } 71 | 72 | public String getCredentialsId() { 73 | return credentialsId; 74 | } 75 | 76 | @Override 77 | public DescriptorImpl getDescriptor() { 78 | return (DescriptorImpl) super.getDescriptor(); 79 | } 80 | 81 | @Override 82 | public void perform(Run run, FilePath workspace, Launcher launcher, TaskListener listener) 83 | throws InterruptedException, IOException { 84 | 85 | AppRayConnector connector = new AppRayConnector(); 86 | ScanJob job; 87 | String application = run.getEnvironment(listener).expand(this.outputFilePath); 88 | FilePath application_path = workspace.child(application); 89 | 90 | // use domain req 91 | StandardUsernamePasswordCredentials credential = CredentialsProvider.findCredentialById( 92 | this.credentialsId, 93 | StandardUsernamePasswordCredentials.class, 94 | run, Collections. emptyList()); 95 | 96 | if (credential == null) { 97 | listener.error("Credentials are missing: " + this.credentialsId); 98 | run.setResult(Result.FAILURE); 99 | return; 100 | } 101 | if (appRayUrl == null || appRayUrl.length() == 0) { 102 | listener.fatalError("Required App-Ray URL is missing"); 103 | run.setResult(Result.FAILURE); 104 | return; 105 | } 106 | if (!application_path.exists()) { 107 | listener.error("Application does not exist: " + application_path.getRemote()); 108 | run.setResult(Result.FAILURE); 109 | return; 110 | } 111 | 112 | listener.getLogger().println("App-Ray scanning application: " + application_path.getRemote()); 113 | 114 | try { 115 | connector.setup(this.appRayUrl, credential.getUsername(), Secret.toString(credential.getPassword()), Jenkins.get().proxy); 116 | User user = connector.getUser(); 117 | String organization = connector.getOrganization(); 118 | 119 | listener.getLogger().println("App-Ray scanning on " + this.appRayUrl + " -> " + user.name + " (" + user.email + ") @ " + organization); 120 | 121 | String jobId = connector.submitApp(application_path.getRemote()); 122 | job = connector.getJobDetails(jobId); 123 | String resultUrl = this.appRayUrl + "/scan-details/" + jobId; 124 | listener.getLogger().println("App-Ray scanning " + job.platform + " application " + job.package_name + " (" + job.label + ") " + job.version + " (SHA1: " + job.app_hash + "), scan job ID: " + jobId); 125 | listener.getLogger().println("App-Ray scan result details will be available at: " + resultUrl); 126 | 127 | job = this.waitForJob(jobId, connector, listener); 128 | 129 | this.processJobResult(run, resultUrl, job, connector, jobId, workspace, listener); 130 | 131 | } catch (AppRayConnectorException e) { 132 | connector.close(); 133 | listener.error(e.getMessage()); 134 | run.setResult(Result.FAILURE); 135 | } catch (Exception e) { 136 | connector.close(); 137 | listener.error("Exception: " + e.getMessage()); 138 | e.printStackTrace(); 139 | run.setResult(Result.FAILURE); 140 | } finally { 141 | connector.close(); 142 | } 143 | } 144 | 145 | private ScanJob waitForJob(String jobId, AppRayConnector connector, TaskListener listener) 146 | throws AppRayConnectorException, InterruptedException { 147 | 148 | final long ONE_SEC_IN_MILLIS = 1000; 149 | final long ONE_MINUTE_IN_MILLIS = 60 * ONE_SEC_IN_MILLIS; 150 | long sleep = 10; 151 | ScanJob job; 152 | 153 | Date now = new Date(System.currentTimeMillis()); 154 | Date endDate = new Date(System.currentTimeMillis() + this.waitTimeout * ONE_MINUTE_IN_MILLIS); 155 | 156 | job = connector.getJobDetails(jobId); 157 | 158 | while (now.compareTo(endDate) < 0) { 159 | if (job.status == ScanJob.Status.queued) { 160 | listener.getLogger().println("Application is queued for scanning"); 161 | sleep = 20; 162 | } else if (job.status == ScanJob.Status.processing) { 163 | listener.getLogger().println("Application is being scanned: " + job.progress_finished + " / " + job.progress_total); 164 | if ((job.progress_total - job.progress_finished) > 1) { 165 | sleep = 10; 166 | } else { 167 | sleep = 5; 168 | } 169 | } else { 170 | break; 171 | } 172 | 173 | Thread.sleep(sleep * ONE_SEC_IN_MILLIS); 174 | now = new Date(System.currentTimeMillis()); 175 | 176 | job = connector.getJobDetails(jobId); 177 | } 178 | 179 | return job; 180 | } 181 | 182 | private void processJobResult(Run run, String resultUrl, ScanJob job, AppRayConnector connector, String jobId, FilePath workspace, TaskListener listener) 183 | throws AppRayConnectorException, IOException, InterruptedException { 184 | 185 | final String junit_file = "appray.junit.xml"; 186 | 187 | if (job.status == ScanJob.Status.finished) { 188 | run.addAction(new AppRayResultAction(resultUrl, job)); 189 | 190 | if (this.riskScoreThreshold < job.risk_score) { 191 | listener.error("App-Ray scan finished, application has too high risk score: " + job.risk_score); 192 | run.setResult(Result.FAILURE); 193 | } else { 194 | listener.getLogger().println("App-Ray scan finished, application is below configured threshold, risk score: " + job.risk_score); 195 | run.setResult(Result.SUCCESS); 196 | } 197 | 198 | workspace.child(junit_file).write(connector.getJUnit(jobId), null); 199 | 200 | } else if (job.status == ScanJob.Status.failed) { 201 | run.addAction(new AppRayResultAction(null, job)); 202 | listener.error("App-Ray scan failed: " + job.failure_reason); 203 | run.setResult(Result.FAILURE); 204 | } else { 205 | run.addAction(new AppRayResultAction(resultUrl, job)); 206 | listener.error("App-Ray scan wait timeout exceeded, try to increase waitTimeout"); 207 | run.setResult(Result.FAILURE); 208 | } 209 | } 210 | 211 | @Symbol("appray") 212 | @Extension 213 | public static final class DescriptorImpl extends BuildStepDescriptor { 214 | 215 | public static final String defaultAppRayUrl = "https://demo.app-ray.co"; 216 | public static final Integer defaultWaitTimeout = 10; 217 | public static final Integer defaultRiskScoreThreshold = 30; 218 | 219 | @POST 220 | public FormValidation doTestConnection( 221 | @AncestorInPath Item item, 222 | @QueryParameter("appRayUrl") String appRayUrl, @QueryParameter("credentialsId") final String credentialsId) throws IOException, ServletException { 223 | 224 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 225 | 226 | AppRayConnector connector = new AppRayConnector(); 227 | 228 | // domain req 229 | StandardUsernamePasswordCredentials credential = CredentialsMatchers.firstOrNull( 230 | CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, item, ACL.SYSTEM, Collections. emptyList()), 231 | CredentialsMatchers.withId(credentialsId)); 232 | 233 | if (credential == null) 234 | return FormValidation.error("Credentials are missing, please check the configured credentials and make sure they are not deleted or moved."); 235 | if (appRayUrl.length() == 0) 236 | return FormValidation.error("App-Ray URL is required"); 237 | 238 | try { 239 | connector.setup(appRayUrl, credential.getUsername(), Secret.toString(credential.getPassword()), Jenkins.get().proxy); 240 | 241 | User user = connector.getUser(); 242 | String organization = connector.getOrganization(); 243 | 244 | if (user.role != User.Role.FULL) { 245 | return FormValidation.error("User must have full-access role: " + credential.getUsername() + " but current role: " + user.role); 246 | } 247 | 248 | return FormValidation.ok("Successfully connected. " + user.name + " (" + user.email + ") @ " + organization); 249 | } catch (Exception e) { 250 | connector.close(); 251 | e.printStackTrace(); 252 | return FormValidation.error("Connection error: " + e.getMessage()); 253 | } finally { 254 | connector.close(); 255 | } 256 | } 257 | 258 | public ListBoxModel doFillCredentialsIdItems( 259 | @AncestorInPath Item item, 260 | @QueryParameter String credentialsId, 261 | @QueryParameter("appRayUrl") String appRayUrl) { 262 | 263 | StandardListBoxModel result = new StandardListBoxModel(); 264 | if (item == null) { 265 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 266 | return result.includeCurrentValue(credentialsId); 267 | } 268 | } else { 269 | if (!item.hasPermission(Item.EXTENDED_READ) 270 | && !item.hasPermission(CredentialsProvider.USE_ITEM)) { 271 | return result.includeCurrentValue(credentialsId); 272 | } 273 | } 274 | 275 | // use domain name? 276 | return result 277 | .includeAs(ACL.SYSTEM, item, StandardUsernamePasswordCredentials.class, Collections. emptyList()) 278 | .includeCurrentValue(credentialsId); 279 | } 280 | 281 | public FormValidation doCheckCredentialsId( 282 | @AncestorInPath Item item, 283 | @QueryParameter String value) { 284 | 285 | if (item == null) { 286 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 287 | return FormValidation.ok(); 288 | } 289 | } else { 290 | if (!item.hasPermission(Item.EXTENDED_READ) 291 | && !item.hasPermission(CredentialsProvider.USE_ITEM)) { 292 | return FormValidation.ok(); 293 | } 294 | } 295 | 296 | if (value.startsWith("${") && value.endsWith("}")) { 297 | return FormValidation.warning("Cannot validate expression-based credentials"); 298 | } 299 | 300 | // use domain name? 301 | if (CredentialsProvider.listCredentials(StandardUsernamePasswordCredentials.class, item, ACL.SYSTEM, 302 | Collections. emptyList(), 303 | CredentialsMatchers.withId(value) 304 | ).isEmpty()) { 305 | return FormValidation.error("Cannot find currently selected credentials, please make sure they have not been deleted."); 306 | } 307 | 308 | return FormValidation.ok(); 309 | } 310 | 311 | @Override 312 | public boolean isApplicable(Class aClass) { 313 | return true; 314 | } 315 | 316 | @Override 317 | public String getDisplayName() { 318 | return Messages.AppRayBuilder_DescriptorImpl_DisplayName(); 319 | } 320 | } 321 | 322 | } 323 | --------------------------------------------------------------------------------