├── 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 | 
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 | 
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 extends AbstractProject> aClass) {
313 | return true;
314 | }
315 |
316 | @Override
317 | public String getDisplayName() {
318 | return Messages.AppRayBuilder_DescriptorImpl_DisplayName();
319 | }
320 | }
321 |
322 | }
323 |
--------------------------------------------------------------------------------