├── .github
├── CODEOWNERS
├── release-drafter.yml
├── dependabot.yml
└── workflows
│ ├── release-drafter.yml
│ └── jenkins-security-scan.yml
├── images
├── jenkins1.png
├── jenkins10.png
├── jenkins2.png
├── jenkins3.png
├── jenkins4.png
├── jenkins5.png
├── jenkins6.png
├── jenkins7.png
├── jenkins8.png
└── jenkins9.png
├── .mvn
├── maven.config
└── extensions.xml
├── src
└── main
│ ├── resources
│ ├── index.jelly
│ └── io
│ │ └── jenkins
│ │ └── plugins
│ │ └── scanner
│ │ └── AppknoxScanner
│ │ ├── help-riskThreshold.html
│ │ ├── help-apiHost.html
│ │ ├── help-filePath.html
│ │ ├── help-credentialsId.html
│ │ ├── config.properties
│ │ └── config.jelly
│ └── java
│ └── io
│ └── jenkins
│ └── plugins
│ └── scanner
│ └── AppknoxScanner.java
├── Jenkinsfile
├── .gitignore
├── LICENSE.md
├── pom.xml
└── README.md
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @jenkinsci/appknox-jenkins-extension-plugin-developers
2 |
--------------------------------------------------------------------------------
/images/jenkins1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins1.png
--------------------------------------------------------------------------------
/images/jenkins10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins10.png
--------------------------------------------------------------------------------
/images/jenkins2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins2.png
--------------------------------------------------------------------------------
/images/jenkins3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins3.png
--------------------------------------------------------------------------------
/images/jenkins4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins4.png
--------------------------------------------------------------------------------
/images/jenkins5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins5.png
--------------------------------------------------------------------------------
/images/jenkins6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins6.png
--------------------------------------------------------------------------------
/images/jenkins7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins7.png
--------------------------------------------------------------------------------
/images/jenkins8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins8.png
--------------------------------------------------------------------------------
/images/jenkins9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jenkinsci/appknox-scanner-plugin/main/images/jenkins9.png
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | _extends: .github
2 | tag-template: appknox-jenkins-plugin-$NEXT_MINOR_VERSION
3 |
--------------------------------------------------------------------------------
/.mvn/maven.config:
--------------------------------------------------------------------------------
1 | -Pconsume-incrementals
2 | -Pmight-produce-incrementals
3 | -Pconsume-incrementals
4 | -Pmight-produce-incrementals
5 |
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 | This plugin allows you to perform Appknox security scan on your mobile application binary.
4 |
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/help-riskThreshold.html:
--------------------------------------------------------------------------------
1 |
2 | Select the risk threshold for the scan. Options are LOW, MEDIUM, HIGH, and CRITICAL.
3 |
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/help-apiHost.html:
--------------------------------------------------------------------------------
1 |
2 | Select a region if you're using a different cloud instance of Appknox than the default global region.
3 |
4 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | buildPlugin(
2 | configurations: [
3 | [platform: 'linux', jdk: 21], // Update to the desired JDK version
4 | [platform: 'windows', jdk: 21] // Update to the desired JDK version
5 | ]
6 | )
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 |
3 | # mvn hpi:run
4 | work
5 |
6 | # IntelliJ IDEA project files
7 | *.iml
8 | *.iws
9 | *.ipr
10 | .idea
11 |
12 | # Eclipse project files
13 | .settings
14 | .classpath
15 | .project
16 | .vscode
17 |
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/help-filePath.html:
--------------------------------------------------------------------------------
1 |
2 | Specify the build file name or absolute path of the application's binary (APK/IPA) file for the scan. E.g. app-debug.apk, app/build/apk/app-debug.apk
3 |
4 |
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/help-credentialsId.html:
--------------------------------------------------------------------------------
1 |
2 | Select the Appknox Access Token. Ensure the Access Token matches with the Access Token given while configuring Appknox Access Token in the credentials.
3 |
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/config.properties:
--------------------------------------------------------------------------------
1 | filePath = File Path *
2 | note = Note
3 | riskThreshold = Risk Threshold *
4 | credentialsId = Appknox Access Token *
5 | requiredFields = * indicates required fields
6 | region = Appknox Regions *
7 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: maven
6 | directory: /
7 | schedule:
8 | interval: monthly
9 | - package-ecosystem: github-actions
10 | directory: /
11 | schedule:
12 | interval: monthly
13 |
--------------------------------------------------------------------------------
/.mvn/extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | io.jenkins.tools.incrementals
4 | git-changelist-maven-extension
5 | 1.8
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | # Automates creation of Release Drafts using Release Drafter
2 | # More Info: https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc
3 |
4 | on:
5 | push:
6 | branches:
7 | - master
8 | - main
9 |
10 | jobs:
11 | update_release_draft:
12 | runs-on: ubuntu-latest
13 | steps:
14 | # Drafts your next Release notes as Pull Requests are merged into the default branch
15 | - uses: release-drafter/release-drafter@v6
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/.github/workflows/jenkins-security-scan.yml:
--------------------------------------------------------------------------------
1 | # More information about the Jenkins security scan can be found at the developer docs: https://www.jenkins.io/redirect/jenkins-security-scan/
2 |
3 | name: Jenkins Security Scan
4 | on:
5 | push:
6 | branches:
7 | - "master"
8 | - "main"
9 | pull_request:
10 | types: [ opened, synchronize, reopened ]
11 | workflow_dispatch:
12 |
13 | permissions:
14 | security-events: write
15 | contents: read
16 | actions: read
17 |
18 | jobs:
19 | security-scan:
20 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2
21 | with:
22 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate.
23 | java-version: 11 # What version of Java to set up for the build.
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright 2024
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/main/resources/io/jenkins/plugins/scanner/AppknoxScanner/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | LOW
14 | MEDIUM
15 | HIGH
16 | CRITICAL
17 |
18 |
19 |
20 |
21 | Global
22 | Saudi
23 |
24 |
25 |
26 | ${%requiredFields}
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.0
5 |
6 |
7 | org.jenkins-ci.plugins
8 | plugin
9 | 4.85
10 |
11 |
12 | io.jenkins.plugins
13 | appknox-scanner
14 | ${revision}${changelist}
15 | hpi
16 |
17 | Appknox Security Scanner
18 | Send an Android or iOS application binary file to Appknox scanner for mobile application security testing
19 | https://github.com/jenkinsci/appknox-scanner-plugin
20 |
21 |
22 |
23 | MIT License
24 | https://opensource.org/licenses/MIT
25 |
26 |
27 |
28 |
29 | scm:git:https://github.com/${gitHubRepo}.git
30 | scm:git:https://github.com/${gitHubRepo}
31 | https://github.com/${gitHubRepo}
32 | ${scmTag}
33 |
34 |
35 |
36 | 1.0.3
37 | -SNAPSHOT
38 | jenkinsci/appknox-scanner-plugin
39 | 2.440.3
40 |
41 |
42 |
43 |
44 |
45 | io.jenkins.tools.bom
46 | bom-2.440.x
47 | 3180.vc1df4d5b_8097
48 | pom
49 | import
50 |
51 |
52 |
53 |
54 |
55 |
56 | repo.jenkins-ci.org
57 | https://repo.jenkins-ci.org/public/
58 |
59 |
60 |
61 |
62 |
63 | repo.jenkins-ci.org
64 | https://repo.jenkins-ci.org/public/
65 |
66 |
67 |
68 |
69 |
70 | org.jenkins-ci.plugins
71 | credentials
72 |
73 |
74 | org.jenkins-ci.plugins
75 | plain-credentials
76 |
77 |
78 |
79 |
80 | appknox
81 | Appknox
82 | support@appknox.com
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Appknox Security Scan Plugin
2 |
3 | The Appknox Security Scan Plugin allows you to perform Appknox security scan on your mobile application binary. The APK/IPA built from your CI pipeline will be uploaded to Appknox platform which performs static scan and the build will be errored according to the chosen risk threshold.
4 |
5 | ## How to use it?
6 |
7 | ### Step 1: Get your Appknox Access Token
8 |
9 | Sign up on [Appknox](https://appknox.com).
10 |
11 | Generate a personal access token from Developer Settings
12 |
13 | ### Step 2: Store Appknox Access Token in Credentials
14 |
15 | Select credentials options from Manage Jenkins -> Credentials:
16 |
17 | 
18 |
19 | Store Appknox Access Token as Global Credential:
20 |
21 | 
22 |
23 | Select Kind as "Secret Text" and store the Appknox Access Token with desired "ID" and "Description":
24 |
25 | 
26 |
27 | ## Appknox Plugin as Jenkins Job
28 |
29 | ### Step 1: Define Job Name
30 |
31 | Add job name and select Freestyle project:
32 |
33 | 
34 |
35 | ### Step 2: Add Appknox Plugin
36 |
37 | Add Appknox Plugin from build step:
38 |
39 | 
40 |
41 | ### Step 3: Configure Appknox Plugin
42 |
43 | Select Access Token from the dropdown:
44 |
45 | 
46 |
47 | #### Note:
48 |
49 | Ensure the Access Token matches with the Access Token given while configuring Appknox Access Token in the credentials.
50 |
51 | Add other details in the Appknox Plugin Configuration:
52 |
53 | 
54 |
55 |
56 | ## Appknox Plugin as Pipeline
57 |
58 | ### Step 1: Define Pipeline Name
59 |
60 | Add Pipeline name and select Pipeline project:
61 |
62 | 
63 |
64 | ### Step 2: Appknox Plugin Pipeline Script
65 |
66 | Add Appknox Plugin Stage:
67 |
68 | 
69 |
70 | #### Note:
71 |
72 | Ensure the Appknox Access Token ID matches with the ID given while configuring Appknox Access Token in the credentials.
73 |
74 | #### In your Pipeline Stages Add this after building your Application stage:-
75 |
76 | ```
77 | stages {
78 | stage('Appknox Scan') {
79 | steps {
80 | script {
81 | // Perform Appknox scan using AppknoxScanner
82 | appKnoxScanner(
83 | credentialsId: 'your-appknox-access-token-ID', //Specify the Appknox Access Token ID. Ensure the ID matches with the ID given while configuring Appknox Access Token in the credentials.
84 | filePath: FILE_PATH,
85 | riskThreshold: params.RISK_THRESHOLD.toUpperCase(),
86 | region: params.Region // Pass the region parameter as expected
87 | )
88 |
89 | }
90 | }
91 | }
92 | }
93 |
94 | ```
95 |
96 | ## Inputs
97 |
98 | | Key | Value |
99 | |-------------------|------------------------------|
100 | | `credentialsId` | Personal appknox access token ID |
101 | | `file_path` | Specify the build file name or path for the mobile application binary to upload, E.g. app-debug.apk, app/build/apk/app-debug.apk |
102 | | `risk_threshold` | Risk threshold value for which the CI should fail. Accepted values: `CRITICAL, HIGH, MEDIUM & LOW` Default: `LOW` |
103 | | `region` | Specify the Appknox region. Accepted values: 'Global, Saudi' Default: 'Global' |
104 |
105 | ---
106 |
107 | ## Example Script:
108 | ```
109 | pipeline {
110 | agent any
111 | parameters {
112 | choice(name: 'RISK_THRESHOLD', choices: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'], description: 'Risk Threshold')
113 | choice(name: 'Region', choices: ['global', 'saudi'], description: 'Appknox Regions')
114 | }
115 | stages {
116 | stage('Checkout') {
117 | steps {
118 | git 'https://github.com/yourgithub/reponame'
119 | }
120 | }
121 | stage('Build App') {
122 | steps {
123 | // Build the app using specific build, Example is given using gradle
124 | script {
125 | sh './gradlew build'
126 | FILE_PATH = "app/build/outputs/apk/debug/app.aab"
127 | }
128 | }
129 | }
130 | stage('Appknox Scan') {
131 | steps {
132 | script {
133 | // Perform Appknox scan using AppknoxScanner
134 | appKnoxScanner(
135 | credentialsId: 'your-appknox-access-token-ID', //Specify the Appknox Access Token ID. Ensure the ID matches with the ID given while configuring Appknox Access Token in the credentials.
136 | filePath: FILE_PATH,
137 | riskThreshold: params.RISK_THRESHOLD.toUpperCase(),
138 | region: params.Region // Pass the region parameter as expected
139 | )
140 |
141 | }
142 | }
143 | }
144 | }
145 | }
146 |
147 | ```
148 |
--------------------------------------------------------------------------------
/src/main/java/io/jenkins/plugins/scanner/AppknoxScanner.java:
--------------------------------------------------------------------------------
1 | package io.jenkins.plugins.scanner;
2 |
3 | import hudson.Extension;
4 | import hudson.FilePath;
5 | import hudson.Launcher;
6 | import hudson.Proc;
7 | import hudson.Launcher.ProcStarter;
8 | import hudson.EnvVars;
9 | import hudson.model.AbstractBuild;
10 | import hudson.model.AbstractProject;
11 | import hudson.model.BuildListener;
12 | import hudson.model.Item;
13 | import hudson.model.ItemGroup;
14 | import hudson.model.Queue;
15 | import hudson.model.Result;
16 | import hudson.model.Run;
17 | import hudson.model.TaskListener;
18 | import hudson.model.queue.Tasks;
19 | import hudson.model.ItemGroup;
20 | import hudson.model.Item;
21 | import hudson.security.ACL;
22 | import hudson.security.AccessControlled;
23 | import hudson.tasks.ArtifactArchiver;
24 | import hudson.tasks.BuildStepDescriptor;
25 | import hudson.tasks.Builder;
26 | import hudson.util.ArgumentListBuilder;
27 | import hudson.util.FormValidation;
28 | import hudson.util.ListBoxModel;
29 | import jenkins.model.ArtifactManager;
30 | import jenkins.model.Jenkins;
31 | import jenkins.tasks.SimpleBuildStep;
32 | import jenkins.util.VirtualFile;
33 |
34 | import org.jenkinsci.Symbol;
35 | import org.jenkinsci.plugins.plaincredentials.StringCredentials;
36 | import org.kohsuke.stapler.AncestorInPath;
37 | import org.kohsuke.stapler.DataBoundConstructor;
38 | import org.kohsuke.stapler.DataBoundSetter;
39 | import hudson.util.FormValidation;
40 | import hudson.util.ListBoxModel;
41 |
42 | import org.kohsuke.stapler.QueryParameter;
43 | import org.kohsuke.stapler.verb.POST;
44 |
45 | import com.cloudbees.plugins.credentials.CredentialsMatchers;
46 | import com.cloudbees.plugins.credentials.CredentialsProvider;
47 | import com.cloudbees.plugins.credentials.common.StandardCredentials;
48 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
49 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
50 | import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
51 |
52 | import java.io.BufferedReader;
53 | import java.io.ByteArrayOutputStream;
54 | import java.io.File;
55 | import java.io.IOException;
56 | import java.io.InputStreamReader;
57 | import java.io.StringReader;
58 | import java.io.InputStream;
59 | import java.io.OutputStream;
60 | import java.net.URL;
61 | import java.nio.charset.StandardCharsets;
62 | import java.util.ArrayList;
63 | import java.util.Arrays;
64 | import java.util.Collections;
65 | import java.util.HashMap;
66 | import java.util.List;
67 | import java.util.Map;
68 |
69 | import org.apache.commons.io.FileUtils;
70 |
71 | public class AppknoxScanner extends Builder implements SimpleBuildStep {
72 | private final String credentialsId;
73 | private final String filePath;
74 | private final String riskThreshold;
75 | private final String region;
76 |
77 | @DataBoundConstructor
78 | public AppknoxScanner(String credentialsId, String filePath, String riskThreshold, String region) {
79 | this.credentialsId = credentialsId;
80 | this.filePath = filePath;
81 | this.riskThreshold = riskThreshold;
82 | this.region = region;
83 | }
84 |
85 | public String getCredentialsId() {
86 | return credentialsId;
87 | }
88 |
89 | public String getFilePath() {
90 | return filePath;
91 | }
92 |
93 | public String getRiskThreshold() {
94 | return riskThreshold;
95 | }
96 |
97 | public String getRegion() {
98 | return region;
99 | }
100 |
101 | @Override
102 | public void perform(Run, ?> run, FilePath workspace, Launcher launcher, TaskListener listener)
103 | throws InterruptedException, IOException {
104 | if (workspace == null) {
105 | listener.getLogger().println("Workspace is null.");
106 | return;
107 | }
108 | // Determine if running on controller or agent
109 | if (workspace.isRemote()) {
110 | // Running on agent
111 | listener.getLogger().println("Running on Agent...");
112 | } else {
113 | // Running on Controller
114 | listener.getLogger().println("Running on Controller...");
115 | }
116 |
117 | String reportName = "summary-report.csv";
118 |
119 | boolean success = executeAppknoxCommands(run, workspace, reportName, launcher, listener);
120 |
121 | if (success) {
122 | archiveArtifact(run, workspace, reportName, launcher, listener);
123 | } else {
124 | if (run != null) {
125 | run.setResult(Result.FAILURE);
126 | }
127 | }
128 | }
129 |
130 | private boolean executeAppknoxCommands(Run, ?> run, FilePath workspace, String reportName, Launcher launcher, TaskListener listener) {
131 | try {
132 | String accessToken = getAccessToken(listener);
133 | if (accessToken == null) {
134 | return false;
135 | }
136 |
137 | // Create environment variables
138 | EnvVars env = new EnvVars();
139 | env.put("APPKNOX_ACCESS_TOKEN", accessToken);
140 |
141 | String appknoxPath = downloadAndInstallAppknox(workspace, listener, launcher);
142 |
143 | listener.getLogger().println("Selected Region: " + region);
144 |
145 | // Determine if the file is an APK or IPA based on extension
146 | String appFilePath = findAppFilePath(workspace, filePath, listener);
147 | if (appFilePath == null) {
148 | listener.getLogger().println("Neither APK nor IPA file found in the expected directories.");
149 | return false;
150 | }
151 |
152 | String uploadOutput = uploadFile(appknoxPath, listener, env, appFilePath, launcher, workspace);
153 | String fileID = extractFileID(uploadOutput, listener);
154 | if (fileID == null) {
155 | return false;
156 | }
157 |
158 | // Run CICheck and capture the result
159 | boolean ciCheckSuccess = runCICheck(appknoxPath, run, fileID, listener, env, launcher, workspace);
160 | if (!ciCheckSuccess) {
161 | // Set the build result to FAILURE
162 | if (run != null) {
163 | listener.getLogger().println(
164 | "Vulnerabilities detected. Aborting the build process.");
165 | run.setResult(Result.FAILURE);
166 | }
167 | // Continue execution to generate the report and archive the artifact
168 | }
169 |
170 | String reportOutput = createReport(appknoxPath, fileID, listener, env, launcher, workspace);
171 | String reportID = extractReportID(reportOutput, listener);
172 | if (reportID == null) {
173 | return false;
174 | }
175 |
176 | downloadReportSummaryCSV(appknoxPath, reportName, reportID, run, workspace, listener, env, launcher);
177 | } catch (Exception e) {
178 | listener.getLogger().println("Error executing Appknox commands: " + e.getMessage());
179 | return false;
180 | }
181 | return true;
182 | }
183 |
184 | private String downloadAndInstallAppknox(FilePath workspace, TaskListener listener, Launcher launcher)
185 | throws IOException, InterruptedException {
186 | // Get the OS name of the node where the build is running
187 | String osName = getOSName(launcher, listener);
188 |
189 | String appknoxURL = getAppknoxDownloadURL(osName);
190 | String binaryName = getBinaryName(osName);
191 | FilePath appknoxFile = workspace.child(binaryName);
192 |
193 | if (!appknoxFile.exists()) {
194 | listener.getLogger().println("Downloading Appknox CLI from: " + appknoxURL);
195 | downloadFile(appknoxURL, appknoxFile, listener);
196 | listener.getLogger().println("Appknox CLI downloaded successfully.");
197 | } else {
198 | listener.getLogger().println("Appknox CLI already exists at: " + appknoxFile.getRemote());
199 | }
200 |
201 | // Make the file executable (for Unix-based systems)
202 | if (launcher.isUnix()) {
203 | appknoxFile.chmod(0755);
204 | }
205 |
206 | listener.getLogger().println("Appknox CLI located at: " + appknoxFile.getRemote());
207 | return appknoxFile.getRemote();
208 | }
209 |
210 | private String getBinaryName(String os) {
211 | if (os.contains("win")) {
212 | return "appknox-Windows-x86_64.exe";
213 | } else if (os.contains("mac")) {
214 | return "appknox-Darwin-x86_64";
215 | } else if (os.contains("linux")) {
216 | return "appknox-Linux-x86_64";
217 | } else {
218 | throw new UnsupportedOperationException("Unsupported operating system for Appknox CLI download.");
219 | }
220 | }
221 |
222 | private String getOSName(Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
223 | if (launcher.isUnix()) {
224 | // Determine if it's Linux or macOS
225 | ProcStarter procStarter = launcher.launch();
226 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
227 |
228 | procStarter.cmds("uname", "-s");
229 | procStarter.stdout(outputStream);
230 | procStarter.stderr(listener.getLogger());
231 | int exitCode = procStarter.join();
232 |
233 | if (exitCode == 0) {
234 | String osName = outputStream.toString("UTF-8").trim();
235 | listener.getLogger().println("Detected OS: " + osName);
236 | if (osName.equalsIgnoreCase("Darwin")) {
237 | return "mac";
238 | } else {
239 | return "linux";
240 | }
241 | } else {
242 | listener.getLogger().println("Failed to determine OS using 'uname -s', defaulting to 'linux'");
243 | return "linux";
244 | }
245 | } else {
246 | return "win";
247 | }
248 | }
249 |
250 | private void downloadFile(String url, FilePath destinationFile, TaskListener listener) throws IOException, InterruptedException {
251 | URL downloadUrl = new URL(url);
252 | try (InputStream in = downloadUrl.openStream()) {
253 | destinationFile.copyFrom(in);
254 | }
255 | }
256 |
257 | private String getAppknoxDownloadURL(String os) {
258 | String binaryName;
259 | if (os.contains("win")) {
260 | binaryName = "appknox-Windows-x86_64.exe";
261 | } else if (os.contains("mac")) {
262 | binaryName = "appknox-Darwin-x86_64";
263 | } else if (os.contains("linux")) {
264 | binaryName = "appknox-Linux-x86_64";
265 | } else {
266 | throw new UnsupportedOperationException("Unsupported operating system for Appknox CLI download.");
267 | }
268 |
269 | // Use the 'latest' tag to always get the latest release
270 | return "https://github.com/appknox/appknox-go/releases/latest/download/" + binaryName;
271 | }
272 |
273 | private String findAppFilePath(FilePath workspace, String fileName, TaskListener listener) throws IOException, InterruptedException {
274 | // Determine if the file is an APK or IPA based on the extension
275 | boolean isApk = fileName.endsWith(".apk");
276 | boolean isIpa = fileName.endsWith(".ipa");
277 |
278 | // Directories to search in order
279 | List possibleDirs = new ArrayList<>();
280 |
281 | if (isApk) {
282 | possibleDirs.addAll(Arrays.asList(
283 | "app/build/outputs/apk/",
284 | "app/build/outputs/apk/release/",
285 | "app/build/outputs/apk/debug/"
286 | ));
287 | } else if (isIpa) {
288 | possibleDirs.addAll(Arrays.asList(
289 | "Build/Products/",
290 | "Build/Products/Debug-iphoneos/",
291 | "Build/Products/Release-iphoneos/"
292 | ));
293 | }
294 |
295 | // Search in specified directories
296 | for (String dir : possibleDirs) {
297 | FilePath appFile = workspace.child(dir).child(fileName);
298 | if (appFile.exists() && !appFile.isDirectory()) {
299 | listener.getLogger().println("File found at: " + appFile.getRemote());
300 | return appFile.getRemote();
301 | }
302 | }
303 |
304 | // Fallback to recursive search starting from the build directory if not found in the above directories
305 | String buildDir = isApk ? "app/build" : "Build";
306 | FilePath buildDirPath = workspace.child(buildDir);
307 | String result = findAppFilePathRecursive(buildDirPath, fileName, listener);
308 | if (result != null) {
309 | listener.getLogger().println("File found during recursive search at: " + result);
310 | return result;
311 | }
312 |
313 | // Handle the case where an absolute path is given as part of the fileName
314 | FilePath customFile = workspace.child(fileName);
315 | if (customFile.exists() && !customFile.isDirectory()) {
316 | listener.getLogger().println("File found at specified path: " + customFile.getRemote());
317 | return customFile.getRemote();
318 | } else if (new File(fileName).isAbsolute()) {
319 | listener.getLogger().println("File not found at specified absolute path: " + fileName);
320 | return null;
321 | }
322 |
323 | // File not found
324 | listener.getLogger().println("File not found in specified directories, through recursive search, or at the specified path.");
325 | return null;
326 | }
327 |
328 | private String findAppFilePathRecursive(FilePath dir, String fileName, TaskListener listener) throws IOException, InterruptedException {
329 | List files = dir.list();
330 | if (files != null) {
331 | for (FilePath file : files) {
332 | if (file.isDirectory()) {
333 | String result = findAppFilePathRecursive(file, fileName, listener);
334 | if (result != null) {
335 | return result;
336 | }
337 | } else if (file.getName().equals(fileName)) {
338 | listener.getLogger().println("File found during recursive search at: " + file.getRemote());
339 | return file.getRemote();
340 | }
341 | }
342 | }
343 | return null;
344 | }
345 |
346 | private String uploadFile(String appknoxPath, TaskListener listener, EnvVars env, String appFilePath, Launcher launcher, FilePath workspace)
347 | throws IOException, InterruptedException {
348 | List command = new ArrayList<>();
349 | command.add(appknoxPath);
350 | command.add("upload");
351 | command.add(appFilePath);
352 | command.add("--region");
353 | command.add(region);
354 |
355 | ArgumentListBuilder args = new ArgumentListBuilder(command.toArray(new String[0]));
356 |
357 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
358 |
359 | Proc proc = launcher.launch().cmds(args).envs(env).stdout(outputStream).pwd(workspace).quiet(true).start();
360 | int exitCode = proc.join();
361 |
362 | if (exitCode != 0) {
363 | listener.getLogger().println("Upload failed with exit code: " + exitCode);
364 | return null;
365 | }
366 |
367 | String output = outputStream.toString("UTF-8").trim();
368 | String fileID = extractFileID(output, listener);
369 | if (fileID == null) {
370 | return null;
371 | }
372 | listener.getLogger().println("Upload Command Output:");
373 | listener.getLogger().println("File ID = " + fileID);
374 |
375 | return fileID;
376 | }
377 |
378 | private boolean runCICheck(String appknoxPath, Run, ?> run, String fileID, TaskListener listener, EnvVars env, Launcher launcher, FilePath workspace)
379 | throws IOException, InterruptedException {
380 | // Construct the cicheck command
381 | List command = new ArrayList<>();
382 | command.add(appknoxPath);
383 | command.add("cicheck");
384 | command.add(fileID);
385 | command.add("--risk-threshold");
386 | command.add(riskThreshold);
387 | command.add("--region");
388 | command.add(region);
389 |
390 | // Build the command arguments
391 | ArgumentListBuilder args = new ArgumentListBuilder(command.toArray(new String[0]));
392 |
393 | // Capture the output of the cicheck command
394 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
395 |
396 | // Launch the cicheck process using Jenkins' Launcher and Proc
397 | Proc proc = launcher.launch().cmds(args).envs(env).stdout(outputStream).pwd(workspace).quiet(true).start();
398 | int exitCode = proc.join();
399 |
400 | // Convert the output to a string
401 | String output = outputStream.toString("UTF-8").trim();
402 | listener.getLogger().println("Ci Check Output:");
403 |
404 | // Initialize a reader to process the output
405 | BufferedReader reader = new BufferedReader(new StringReader(output));
406 | StringBuilder outputBuilder = new StringBuilder();
407 | boolean foundStarted = false;
408 |
409 | String line;
410 | while ((line = reader.readLine()) != null) {
411 | // Start capturing output from lines containing "Found" or "No"
412 | if (!foundStarted) {
413 | if (line.contains("Found") || line.contains("No")) {
414 | outputBuilder.append(line).append("\n");
415 | if (run != null) {
416 | run.setDescription(outputBuilder.toString() + " Check Console Output for more details.");
417 | }
418 | listener.getLogger().println(); // Adds a blank line
419 | foundStarted = true;
420 | }
421 | } else {
422 | outputBuilder.append(line).append("\n");
423 | }
424 | }
425 |
426 | // If no relevant lines were found, log and return false
427 | if (!foundStarted) {
428 | listener.getLogger().println("No line with 'Found' or 'No' encountered in the output.");
429 | return false;
430 | }
431 |
432 | // Print the captured output
433 | String finalOutput = outputBuilder.toString().trim();
434 | listener.getLogger().println(finalOutput);
435 |
436 | // Handle the process exit code by returning success based on exit code
437 | return exitCode == 0;
438 | }
439 |
440 | private String createReport(String appknoxPath, String fileID, TaskListener listener, EnvVars env, Launcher launcher, FilePath workspace)
441 | throws IOException, InterruptedException {
442 | List command = new ArrayList<>();
443 | command.add(appknoxPath);
444 | command.add("reports");
445 | command.add("create");
446 | command.add(fileID);
447 | command.add("--region");
448 | command.add(region);
449 |
450 | ArgumentListBuilder args = new ArgumentListBuilder(command.toArray(new String[0]));
451 |
452 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
453 |
454 | Proc proc = launcher.launch().cmds(args).envs(env).stdout(outputStream).pwd(workspace).quiet(true).start();
455 | int exitCode = proc.join();
456 |
457 | String output = outputStream.toString("UTF-8").trim();
458 | String reportID = extractReportID(output, listener);
459 | if (reportID != null) {
460 | listener.getLogger().println("Create Report Command Output:");
461 | listener.getLogger().println("Report Id = " + reportID);
462 | listener.getLogger().println(); // Adds a blank line
463 | } else {
464 | listener.getLogger().println("Failed to create report. Output: " + output);
465 | }
466 |
467 | if (exitCode != 0) {
468 | listener.getLogger().println("Report Creation failed with exit code: " + exitCode);
469 | return null;
470 | }
471 |
472 | return reportID;
473 | }
474 |
475 | private void downloadReportSummaryCSV(String appknoxPath, String reportName, String reportID, Run, ?> run, FilePath workspace, TaskListener listener, EnvVars env, Launcher launcher)
476 | throws IOException, InterruptedException {
477 | List command = new ArrayList<>();
478 | command.add(appknoxPath);
479 | command.add("reports");
480 | command.add("download");
481 | command.add("summary-csv");
482 | command.add(reportID);
483 | command.add("--output");
484 | command.add(workspace.child(reportName).getRemote());
485 | command.add("--region");
486 | command.add(region);
487 |
488 | ArgumentListBuilder args = new ArgumentListBuilder(command.toArray(new String[0]));
489 |
490 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
491 |
492 | Proc proc = launcher.launch().cmds(args).envs(env).stdout(outputStream).pwd(workspace).quiet(true).start();
493 | int exitCode = proc.join();
494 |
495 | if (exitCode != 0) {
496 | listener.getLogger().println("Download CSV failed. Exit code: " + exitCode);
497 | } else {
498 | listener.getLogger().println("Summary report saved at: " + workspace.child(reportName).getRemote());
499 | }
500 | }
501 |
502 | private void archiveArtifact(Run, ?> run, FilePath workspace, String reportName, Launcher launcher, TaskListener listener) {
503 | try {
504 | FilePath artifactFile = workspace.child(reportName);
505 |
506 | if (!artifactFile.exists()) {
507 | listener.error("Artifact file does not exist: " + artifactFile.getRemote());
508 | return;
509 | }
510 |
511 | ArtifactManager artifactManager = run.getArtifactManager();
512 | Map artifacts = new HashMap<>();
513 | artifacts.put(reportName, artifactFile.getName());
514 | artifactManager.archive(workspace, launcher, (BuildListener) listener, artifacts);
515 |
516 | listener.getLogger().println("Artifact archived: " + artifactFile.getRemote());
517 | } catch (IOException | InterruptedException e) {
518 | listener.error("Error archiving artifact: " + e.getMessage());
519 | e.printStackTrace(listener.getLogger());
520 | }
521 | }
522 |
523 | private String getAccessToken(TaskListener listener) {
524 | Jenkins jenkins = Jenkins.get();
525 | @SuppressWarnings("deprecation")
526 | StringCredentials credentials = CredentialsMatchers.firstOrNull(
527 | CredentialsProvider.lookupCredentials(StringCredentials.class, jenkins, ACL.SYSTEM,
528 | URIRequirementBuilder.create().build()),
529 | CredentialsMatchers.withId(credentialsId));
530 |
531 | if (credentials != null) {
532 | return credentials.getSecret().getPlainText();
533 | } else {
534 | listener.getLogger().println("Failed to retrieve access token from credentials.");
535 | return null;
536 | }
537 | }
538 |
539 | private String extractFileID(String uploadOutput, TaskListener listener) {
540 | String[] lines = uploadOutput.split("\\r?\\n");
541 | for (int i = lines.length - 1; i >= 0; i--) {
542 | String line = lines[i].trim();
543 | if (line.matches("\\d+")) {
544 | // Line contains only digits, assume it's the file ID
545 | return line;
546 | }
547 | }
548 | listener.getLogger().println("Could not extract file ID from upload output.");
549 | return null;
550 | }
551 |
552 | private String extractReportID(String createReportOutput, TaskListener listener) {
553 | if (createReportOutput != null && !createReportOutput.isEmpty()) {
554 | return createReportOutput.trim();
555 | } else {
556 | listener.getLogger().println("Report output does not contain any lines.");
557 | return null;
558 | }
559 | }
560 |
561 | @Extension
562 | @Symbol("appKnoxScanner")
563 | public static final class DescriptorImpl extends BuildStepDescriptor {
564 | public DescriptorImpl() {
565 | super(AppknoxScanner.class);
566 | load();
567 | }
568 |
569 | @Override
570 | public boolean isApplicable(@SuppressWarnings("rawtypes") Class extends hudson.model.AbstractProject> aClass) {
571 | return true;
572 | }
573 |
574 | @Override
575 | public String getDisplayName() {
576 | return "Appknox Security Scanner";
577 | }
578 |
579 | @POST
580 | public ListBoxModel doFillRegionItems() {
581 | return new ListBoxModel(
582 | new ListBoxModel.Option("Global", "global"),
583 | new ListBoxModel.Option("Saudi", "saudi")
584 | );
585 | }
586 |
587 | @SuppressWarnings("deprecation")
588 | @POST
589 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup> context) {
590 | if(context == null){
591 | Jenkins.get().checkPermission(Jenkins.ADMINISTER);
592 | }else{
593 | ((AccessControlled) context).checkPermission(Item.CONFIGURE);
594 | }
595 |
596 | return new StandardListBoxModel()
597 | .includeEmptyValue()
598 | .includeMatchingAs(
599 | ACL.SYSTEM,
600 | context,
601 | StringCredentials.class,
602 | URIRequirementBuilder.fromUri("").build(),
603 | CredentialsMatchers.instanceOf(StringCredentials.class));
604 | }
605 |
606 | @POST
607 | public FormValidation doCheckCredentialsId(@QueryParameter String value) {
608 | Jenkins.get().checkPermission(Item.CONFIGURE);
609 | if (value.isEmpty()) {
610 | return FormValidation.error("Appknox Access Token must be selected");
611 | }
612 | return FormValidation.ok();
613 | }
614 |
615 | @POST
616 | public FormValidation doCheckFilePath(@QueryParameter String value) {
617 | Jenkins.get().checkPermission(Item.CONFIGURE);
618 | if (value.isEmpty()) {
619 | return FormValidation.error("File Path must not be empty");
620 | }
621 | return FormValidation.ok();
622 | }
623 |
624 | @POST
625 | public FormValidation doCheckRiskThreshold(@QueryParameter String value) {
626 | Jenkins.get().checkPermission(Item.CONFIGURE);
627 | if (value.isEmpty() || (!value.equals("LOW") && !value.equals("MEDIUM") && !value.equals("HIGH")
628 | && !value.equals("CRITICAL"))) {
629 | return FormValidation.error("Risk Threshold must be one of: LOW, MEDIUM, HIGH, CRITICAL");
630 | }
631 | return FormValidation.ok();
632 | }
633 | }
634 | }
635 |
--------------------------------------------------------------------------------