├── .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 | 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 | ![Credentials](images/jenkins1.png) 18 | 19 | Store Appknox Access Token as Global Credential: 20 | 21 | ![Global Credentials](images/jenkins2.png) 22 | 23 | Select Kind as "Secret Text" and store the Appknox Access Token with desired "ID" and "Description": 24 | 25 | ![Kind Credentials](images/jenkins4.png) 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 | ![Jenkins Job](images/jenkins5.png) 34 | 35 | ### Step 2: Add Appknox Plugin 36 | 37 | Add Appknox Plugin from build step: 38 | 39 | ![Appknox Plugin](images/jenkins6.png) 40 | 41 | ### Step 3: Configure Appknox Plugin 42 | 43 | Select Access Token from the dropdown: 44 | 45 | ![Appknox Plugin Token](images/jenkins10.png) 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 | ![Appknox Plugin Configuration](images/jenkins7.png) 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 | ![Jenkins Job](images/jenkins8.png) 63 | 64 | ### Step 2: Appknox Plugin Pipeline Script 65 | 66 | Add Appknox Plugin Stage: 67 | 68 | ![Appknox Plugin Pipeline](images/jenkins9.png) 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 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 | --------------------------------------------------------------------------------