├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── acunetix │ ├── BuildScanner.java │ ├── Engine.java │ └── SR.java └── resources ├── Messages.properties ├── com └── acunetix │ └── BuildScanner │ ├── config.jelly │ ├── global.jelly │ ├── help-gApiKey.html │ ├── help-gApiKeyID.html │ ├── help-incScan.html │ ├── help-profile.html │ ├── help-repTemp.html │ ├── help-stopScan.html │ ├── help-stopTargetScans.html │ ├── help-svRep.html │ ├── help-target.html │ ├── help-threat.html │ └── help.html └── index.jelly /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | # IntelliJ files 3 | *.iml 4 | *.iws 5 | *.ipr 6 | .idea 7 | out 8 | pom.xsd 9 | target 10 | work -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.0.0 (March 15, 2017) 4 | 5 | 1. Initial release 6 | 2. New features 7 | 1. Trigger Acunetix scans from within Jenkins upon each build 8 | 2. Trigger Acunetix scans with built-in or custom Scan Types to 9 | only scan for specific vulnerabilities 10 | 3. Configure Jenkins to fail a build (and optionally abort the 11 | scan) as soon as a specific threat-level (high, medium or low 12 | severity) is reached 13 | 4. Automatically generate reports and save them within Jenkins 14 | 3. Improvements 15 | 1. N/A 16 | 4. Bugfixes 17 | 1. N/A 18 | 19 | ## Version 1.1.0 (October 24, 2018) 20 | 21 |       Improvements: Use Jenkins credentials for storing the API Key 22 | 23 | ## Version 1.2.0 (October 25, 2018) 24 | 25 |       Improvements: Better exception handling like situations when 26 | configured target or profile have been deleted in main application 27 | 28 |       Bug fixes: 29 | 30 | - Plugin retrieve only first 100 targets 31 | - Scans can now be executed on the online version of the scanner 32 | - Reports cannot be downloaded. Now links to the reports will be 33 | provided on the output. 34 | 35 | ## Version 1.2.1 (January 10, 2019) 36 | 37 |       Bug fixes: 38 | 39 | - Fixed 429 error when pairing with online build 40 | 41 | ## Version 1.2.2 (January 18, 2019) 42 | 43 |       Bug fixes: 44 | 45 | - Fixed 429 error for reports 46 | 47 | ## Version 1.2.3 (February 06, 2019) 48 | 49 |       Bug fixes: 50 | 51 | - Saved API URL is not loaded and shown in Jenkins system page 52 | 53 | ## Version 1.2.5 (February 14, 2020) 54 | 55 | - Compatibility with Acunetix version 13 56 | - New feature: incremental scans 57 | 58 | ## Version 1.2.6 (February 26, 2020) 59 | 60 | - Save reports in workspace 61 | - Improved logging 62 | 63 | ## Version 1.2.8 (April 21, 2020) 64 | 65 | - Fixed compatibility with online version 66 | - Fix: report was generated even when the scan could not be performed 67 | - Specific error when the target was deleted and plugin configuration was not updated 68 | - Re-added report download link in console 69 | - Report save to workspace configurable through a checkbox 70 | 71 | ## Version 1.2.9 (May 19, 2020) 72 | 73 | - Provide all report templates when choosing for a report type. Before were available only standard reports 74 | 75 | ## Version 1.2.11 (February 15, 2021) 76 | 77 | - Fix: use correct workspace on slaves 78 | - Fix: Comprehensive report not downloaded properly 79 | 80 | ## Version 1.2.12 (March 22, 2021) 81 | 82 | - Added option to stop scans on the Target before starting a new scan 83 | 84 | ## Version 1.2.13 (March 29, 2021) 85 | 86 | - Changed description 87 | 88 | ## Version 1.2.14 (June 28, 2021) 89 | 90 | - Fix: error trying to make pipeline script with Snippet Generator 91 | - Fix: incremental scan added multiple times 92 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Acunetix Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | Plugin Information | 2 | |-----------------------------------------------------------------------------------------------| 3 | | View Acunetix [on the plugin site](https://plugins.jenkins.io/acunetix) for more information. | 4 | 5 | Older versions of this plugin may not be safe to use. Please review the 6 | following warnings before using an older version: 7 | 8 | - [SSRF 9 | vulnerability](https://jenkins.io/security/advisory/2019-02-19/#SECURITY-980) 10 | - [Credentials stored in plain 11 | text](https://jenkins.io/security/advisory/2019-02-19/#SECURITY-951) 12 | 13 | 14 | 15 | # 16 | 17 | # Plugin Information 18 | 19 | This plugin allows you to trigger automated Acunetix scans as part of 20 | your web application's build process inside of Jenkins. 21 | 22 | # Description 23 | 24 | Acunetix is an automated web application security testing and 25 | vulnerability management platform. Acunetix automatically crawls and 26 | scans off-the-shelf and custom-built websites and web applications for 27 | over 3000 web vulnerabilities to help organizations shore up their web 28 | security. 29 | The [Acunetix Jenkins 30 | Plugin](http://www.acunetix.com/blog/web-security-zone/acunetix-jenkins-plugin/) 31 | enables you to: 32 | 33 | 1. Trigger Acunetix scans from within Jenkins upon each build 34 | 2. Trigger Acunetix scans with built-in or custom Scan Types to only 35 | scan for specific vulnerabilities 36 | 3. Configure Jenkins to fail a build (and optionally abort the scan) as 37 | soon as a specific threat-level (high, medium or low severity) is 38 | reached 39 | 4. Automatically generate reports 40 | 41 | 42 | After setting up the Acunetix Jenkins Plugin, you can configure any 43 | Jenkins job with a build step action to trigger an Acunetix scan. When 44 | an Acunetix scan is triggered, Jenkins will launch a scan against a 45 | Target you specify and is scanned with settings configured in Acunetix. 46 | Jenkins will pass or fail the build based on criteria you provided. 47 | 48 | | | 49 | |------------------------------------------------------------------------------------------------------------------------| 50 | | **Note** – The Acunetix Jenkins Plugin requires an Acunetix API key, which is only available in *Acunetix Enterprise*. | 51 | 52 | # Installation 53 | 54 | To install the Acunetix Jenkins Plugin: 55 | 56 | 1. In Jenkins, navigate to *Manage Jenkins \> Manage Plugins* and 57 | select the *Available* tab 58 | 2. Search the Jenkins Plugin Index for *Acunetix* 59 | 3. Select *Install without restart* or *Download and install after 60 | restart*. Jenkins will install the plugin based on your preference 61 | 62 | # Configuration 63 | 64 | To configure the Acunetix Jenkins Plugin: 65 | 66 | 1. [Make Acunetix reachable from hosts other than 67 | localhost](https://www.acunetix.com/blog/docs/use-acunetix-host-localhost/) 68 | 2. [Add the Acunetix Root CA Certificate to 69 | Jenkins](http://www.acunetix.com/blog/docs/installing-and-configuring-the-acunetix-jenkins-plugin) 70 | 3. Obtain an Acunetix API key 71 | 4. Modify the Jenkins Content Security Policy (optional) 72 | 73 | # Usage 74 | 75 | 1. In Jenkins, navigate to the job you wish to run an Acunetix scan in, 76 | and select *Configure* in the sidebar 77 | 2. In the *Build* section, select *Acunetix* from the *Add build step* 78 | drop-down menu 79 | 3. You will then be presented with the options outlined below. 80 | 1. **Scan Type** – Choose a *Scan Type* with which you want the 81 | scan to run. *Scan Types* are used to reduce the scope of the 82 | tests the scanner runs during the scan. 83 | 2. **Scan Target** – Choose a *Scan Target* you wish to scan. *Scan 84 | Targets* are obtained from Acunetix, with the exception of 85 | Targets requiring Manual Intervention. Targets contain part of 86 | the Target description for distinguishability between Targets 87 | that have the same URL. 88 | 3. **Fail build if threat level is** – Choose at which threat level 89 | to fail the Jenkins build based upon the scan's threat level 90 | (High severity, Medium severity or Low Severity). 91 | 4. **Stop the scan when build fails** – Check this checkbox if you 92 | would like to abort the scan when the fail condition in *Fail 93 | build if threat level is* is met. This is setting is enabled by 94 | default. 95 | 5. **Generate Report** – Choose to a report to generate upon 96 | completion of the scan. The report will be saved in project workspace 97 | 4. Click *Save* 98 | 99 | # FAQs 100 | 101 | 1. **Which edition of Acunetix do I need to use the Acunetix Jenkins 102 | Plugin?** 103 | 104 | The Acunetix Jenkins Plugin requires access to the Acunetix API and API 105 | key, which is only available in *Acunetix Enterprise*. 106 | 107 | 1. **The Target I have set-up in Acunetix is not showing in drop-down 108 | list inside Jenkins.** 109 | 110 | The Acunetix Jenkins Plugin will display all Targets in an Acunetix 111 | installation, with the exception of Targets requiring Manual 112 | Intervention as part of their Login Sequence. Please make sure that the 113 | Target you wish to select does not make use of Manual Intervention. 114 | 115 | 1. **How can I differentiate between multiple Targets with the same 116 | URL?** 117 | 118 | If you have multiple Targets with the same URL, it is advised that you 119 | enter a description in the Target's settings to be able to differentiate 120 | between them. The Target's description will show up in Jenkins if one is 121 | available. 122 | 123 | 1. **Why does a scan take long for to start?** 124 | 125 | When Jenkins attempts to start a scan, the scan is placed in a scan 126 | queue. If the scan queue is empty, then the scan will start immediately. 127 | However, if the maximum number of scans (including scheduled scans) in 128 | the scan queue is reached, the scan will wait in the queue until other 129 | scans finish processing. This also means that the Jenkins build will not 130 | finish processing until the scan is complete. 131 | 132 | 1. **What happens to the scan if I abort the Jenkins build?** 133 | 134 | Aborting the Jenkins build will also abort the scan. You may still view 135 | partial results inside of Acunetix. Reports will not be automatically 136 | generated if the Jenkins build is aborted (you can manually generate 137 | reports from within the Acunetix UI). 138 | 139 | 1. **What happens if I stop an Acunetix scan from outside Jenkins?** 140 | 141 | If a scan that was started by Jenkins is stopped from the Acunetix UI or 142 | via the Acunetix API, the Jenkins build will also be aborted. Reports 143 | will not be automatically generated if the scan is stopped (you can 144 | manually generate reports from within the Acunetix UI) 145 | 146 | 1. **What kind of reports can be generated from Jenkins?** 147 | 148 | All *Standard* reports can be generated from Jenkins (Affected Items, 149 | Developer, Executive Summary and Quick reports). Compliance reports (PCI 150 | DSS, OWASP Top 10, ISO 27001…) for the scans run by Jenkins may be 151 | generated from within the Acunetix UI. 152 | 153 | 1. **What happens to reports generated from Jenkins?** 154 | 155 | Reports generated from Jenkins are generated on the main application and then downloaded in project workspace. 156 | The report can be archived within job folder with post-build action "Archive the artifacts" and deleted from workspace 157 | 158 | 1. **How do I disable or remove the Acunetix Jenkins Plugin** 159 | 160 | Please refer to [this Jenkins 161 | article](https://wiki.jenkins-ci.org/display/JENKINS/Removing+and+disabling+plugins) 162 | on disabling and removing Jenkins plugins and associated plugin data 163 | 164 | # Changelog 165 | 166 | Please refer to [this](https://github.com/jenkinsci/acunetix-plugin/blob/master/CHANGELOG.md) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | credentials 8 | 2.1.19 9 | 10 | 11 | org.jenkins-ci.plugins 12 | plain-credentials 13 | 1.4 14 | 15 | 16 | 17 | org.jenkins-ci.plugins 18 | plugin 19 | 2.11 20 | 21 | 22 | com.acunetix 23 | acunetix 24 | 1.2.15-SNAPSHOT 25 | hpi 26 | 27 | 28 | 2.73.3 29 | 8 30 | 2.33 31 | 32 | 33 | Acunetix 34 | Plugin for starting Acunetix Premium scans 35 | https://github.com/jenkinsci/acunetix-plugin/blob/master/README.md 36 | 37 | 38 | 39 | MIT License 40 | http://opensource.org/licenses/MIT 41 | 42 | 43 | 44 | 45 | 46 | Acunetix 47 | Acunetix 48 | product@acunetix.com 49 | 50 | 51 | 52 | scm:git:git://github.com/jenkinsci/acunetix-plugin.git 53 | scm:git:git@github.com:jenkinsci/acunetix-plugin.git 54 | http://github.com/jenkinsci/acunetix-plugin 55 | HEAD 56 | 57 | 58 | 59 | repo.jenkins-ci.org 60 | https://repo.jenkins-ci.org/public/ 61 | 62 | 63 | 64 | 65 | repo.jenkins-ci.org 66 | https://repo.jenkins-ci.org/public/ 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/java/com/acunetix/BuildScanner.java: -------------------------------------------------------------------------------- 1 | package com.acunetix; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.common.StandardCredentials; 6 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 7 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 8 | import hudson.Extension; 9 | import hudson.FilePath; 10 | import hudson.Launcher; 11 | import hudson.model.AbstractProject; 12 | import hudson.model.Item; 13 | import hudson.model.Run; 14 | import hudson.model.TaskListener; 15 | import hudson.remoting.VirtualChannel; 16 | import hudson.security.ACL; 17 | import hudson.tasks.BuildStepDescriptor; 18 | import hudson.util.FormValidation; 19 | import hudson.util.ListBoxModel; 20 | import jenkins.model.Jenkins; 21 | import jenkins.tasks.SimpleBuildStep; 22 | import net.sf.json.JSONArray; 23 | import net.sf.json.JSONObject; 24 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 25 | import org.kohsuke.stapler.AncestorInPath; 26 | import org.kohsuke.stapler.DataBoundConstructor; 27 | import org.kohsuke.stapler.QueryParameter; 28 | import org.kohsuke.stapler.StaplerRequest; 29 | import org.kohsuke.stapler.verb.POST; 30 | 31 | import javax.annotation.Nonnull; 32 | import javax.net.ssl.SSLHandshakeException; 33 | import java.io.FileNotFoundException; 34 | import java.io.IOException; 35 | import java.io.PrintStream; 36 | import java.util.ArrayList; 37 | import java.util.Collections; 38 | 39 | import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials; 40 | 41 | 42 | public class BuildScanner extends hudson.tasks.Builder implements SimpleBuildStep { 43 | private final String profile; 44 | private final String target; 45 | private String targetName; 46 | private final String repTemp; 47 | private String reportTemplateName; 48 | private final String threat; 49 | private final Boolean stopScan; 50 | private Boolean incScan; 51 | private String incScanId; 52 | private final Boolean svRep; 53 | private final Boolean stopTargetScans; 54 | 55 | 56 | @DataBoundConstructor 57 | public BuildScanner(String profile, String target, String repTemp, String threat, Boolean stopScan, Boolean svRep, Boolean incScan, String incScanId, Boolean stopTargetScans) { 58 | this.profile = profile; 59 | this.target = target; 60 | this.repTemp = repTemp; 61 | this.threat = threat; 62 | this.stopScan = stopScan; 63 | this.svRep = svRep; 64 | this.stopTargetScans = stopTargetScans; 65 | this.incScan = incScan; 66 | try { 67 | Engine aac = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey()); 68 | if (aac.getVersion() > 12) { 69 | if (incScan) { 70 | if (aac.checkIncScanExist(target, profile)) { 71 | this.incScanId = aac.getIncScanId(target,profile); 72 | } 73 | else { 74 | this.incScanId = aac.createIncScan(profile, target); 75 | } 76 | } 77 | else{ 78 | this.incScanId = incScanId; 79 | } 80 | } 81 | else { 82 | this.incScan = false; 83 | } 84 | this.targetName = aac.getTargetName(this.target); 85 | this.reportTemplateName = aac.getReportTemplateName(this.repTemp); 86 | } catch (IOException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | 91 | 92 | public String getProfile() { 93 | return profile; 94 | } 95 | 96 | 97 | public String getTarget() { 98 | return target; 99 | } 100 | 101 | public String getRepTemp() { 102 | return repTemp; 103 | } 104 | 105 | public String getThreat() { 106 | return threat; 107 | } 108 | 109 | public Boolean getStopScan() { 110 | return stopScan; 111 | } 112 | 113 | private String getTargetName() { 114 | return targetName; 115 | } 116 | 117 | private String getReportTemplateName() { 118 | return reportTemplateName; 119 | } 120 | 121 | public Boolean getIncScan() { 122 | return incScan; 123 | } 124 | 125 | public String getIncScanId() { 126 | return incScanId; 127 | } 128 | 129 | public Boolean getStopTargetScans() { 130 | return stopTargetScans; 131 | } 132 | 133 | public Boolean getSvRep() { return svRep; } 134 | 135 | @Override 136 | public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws hudson.AbortException, InterruptedException { 137 | final String PROCESSING = "processing"; 138 | final String COMPLETED = "completed"; 139 | final String ABORTED = "aborted"; 140 | final String SCHEDULED = "scheduled"; 141 | final String QUEUED = "queued"; 142 | final String NOREPORT = "no_report"; 143 | final PrintStream listenerLogger = listener.getLogger(); 144 | 145 | Engine engine = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey()); 146 | String scanId = null; 147 | Boolean scanAbortedExternally = false; 148 | Boolean scanAbortedByUser = false; 149 | String scanStatus = ""; 150 | String scanThreat; 151 | Boolean started = false; 152 | Boolean bThreat = false; 153 | Boolean bScheduled = false; 154 | 155 | try { 156 | Engine testengine = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey()); 157 | int rc = testengine.doTestConnection(getDescriptor().getgApiUrl() + "/me"); 158 | if (rc != 200) { 159 | listenerLogger.println(SR.getString("aborting.the.build")); 160 | throw new hudson.AbortException(SR.getString("cannot.connect.to.application")); 161 | } 162 | if ((engine.getTargetName(target) == null)) { 163 | listenerLogger.println(SR.getString("aborting.the.build")); 164 | throw new hudson.AbortException(SR.getString("invalid.target")); 165 | } 166 | else if (!engine.checkScanProfileExists(profile)) { 167 | listenerLogger.println(SR.getString("aborting.the.build")); 168 | throw new hudson.AbortException(SR.getString("invalid.scan_type")); 169 | } 170 | listenerLogger.println(SR.getString("starting.scan.on.target.0", getTargetName())); 171 | if (this.stopTargetScans) 172 | { 173 | engine.stopTargetScans(target); 174 | } 175 | if (this.incScan) { 176 | if (!engine.checkScanExist(incScanId)) { 177 | throw new hudson.AbortException(SR.getString("could.not.find.scan.with.scanid.0.create.new", this.incScanId)); 178 | } 179 | engine.triggerIncScan(this.incScanId, false); 180 | scanId = this.incScanId; 181 | } 182 | else { 183 | scanId = engine.startScan(profile, target, false); 184 | if (scanId == null) { 185 | listenerLogger.println(SR.getString("aborting.the.build")); 186 | throw new hudson.AbortException(SR.getString("cannot.connect.to.application")); 187 | } 188 | } 189 | while (!scanStatus.equals(COMPLETED)) { 190 | if (scanStatus.equals(PROCESSING) && !started) { 191 | started = true; 192 | listenerLogger.println(SR.getString("scan.started")); 193 | listenerLogger.println(SR.getString("view.scan.status") + getDescriptor().getgApiUrl().replace("api/v1", "#") + "/scans/" + scanId + "/info"); 194 | } 195 | if (scanStatus.equals(SCHEDULED) && !bScheduled) { 196 | bScheduled = true; 197 | listenerLogger.println(SR.getString("the.scan.is.in.scheduled.state")); 198 | } 199 | if (scanStatus.equals(ABORTED)) { 200 | scanAbortedExternally = true; 201 | listenerLogger.println(SR.getString("aborting.the.build")); 202 | throw new hudson.AbortException(SR.getString("scan.aborted.outside")); 203 | } 204 | 205 | scanThreat = engine.getScanThreat(scanId); 206 | if (engine.checkThreat(threat, scanThreat)) { 207 | bThreat = true; 208 | //listenerLogger.println(SR.getString("scan.threat.0.1", Engine.getThreatName(scanThreat), this.getThreat())); 209 | listenerLogger.println(SR.getString("scan.threat.0", Engine.getThreatName(scanThreat))); 210 | listenerLogger.println(SR.getString("check.vulnerabilities.found") + getDescriptor().getgApiUrl().replace("api/v1", "#") + "/scans/" + scanId + "/vulnerabilities"); 211 | listenerLogger.println(SR.getString("aborting.the.build")); 212 | throw new hudson.AbortException(SR.getString("scan.threat")); 213 | } 214 | Thread.sleep(10000); 215 | scanStatus = engine.getScanStatus(scanId); 216 | } 217 | listenerLogger.println(SR.getString("scan.completed")); 218 | } catch (InterruptedException e) { 219 | scanAbortedByUser = true; 220 | listenerLogger.println(SR.getString("aborting.the.build")); 221 | throw new hudson.AbortException(SR.getString("build.aborted")); 222 | } catch (hudson.AbortException e) { 223 | throw e; 224 | } catch (SSLHandshakeException e) { 225 | e.printStackTrace(); 226 | throw new hudson.AbortException(SR.getString("certificate.to.the.java.ca.store")); 227 | } catch (FileNotFoundException e) { 228 | e.printStackTrace(); 229 | if (!engine.checkScanExist(scanId)) { 230 | listenerLogger.println(SR.getString("aborting.the.build")); 231 | scanAbortedExternally = true; 232 | throw new hudson.AbortException(SR.getString("could.not.find.scan.with.scanid.0", scanId)); 233 | } 234 | } catch (java.net.ConnectException e) { 235 | e.printStackTrace(); 236 | listenerLogger.println(SR.getString("aborting.the.build")); 237 | scanAbortedExternally = true; 238 | throw new hudson.AbortException(SR.getString("could.not.connect.to.application.connection.refused")); 239 | } catch (ConnectionException e) { 240 | listenerLogger.println(SR.getString("aborting.the.build")); 241 | throw new hudson.AbortException(SR.getString("cannot.connect.to.application")); 242 | } catch (Exception e) { 243 | e.printStackTrace(); 244 | listenerLogger.println(e.getMessage()); 245 | } finally { 246 | try { 247 | if (stopScan && scanId != null && !scanAbortedExternally && (bThreat || scanAbortedByUser) && !bScheduled) { 248 | engine.stopScan(scanId); 249 | try { 250 | String status = ""; 251 | while (!status.equals(ABORTED) && !status.equals(COMPLETED)) { 252 | Thread.sleep(10000); 253 | status = engine.getScanStatus(scanId); 254 | } 255 | listenerLogger.println(SR.getString("the.scan.was.stopped")); 256 | } catch (InterruptedException | IOException e) { 257 | e.printStackTrace(); 258 | listenerLogger.println(e.getMessage()); 259 | } 260 | } 261 | if (!repTemp.equals(NOREPORT) && scanId != null && !scanAbortedByUser && !scanAbortedExternally) { 262 | listenerLogger.println(SR.getString("generating.0.report", getReportTemplateName())); 263 | Thread.sleep(10000); 264 | String downloadLink = engine.generateReport(scanId, repTemp, "scans"); 265 | listenerLogger.println(SR.getString("scan.report.download.link.0", engine.getUrl(getDescriptor().getgApiUrl(), downloadLink))); 266 | if (svRep) { 267 | VirtualChannel channel = workspace.getChannel(); 268 | String reportName = engine.getReportFileName(engine.getUrl(getDescriptor().getgApiUrl(), downloadLink)); 269 | FilePath reportFile = new hudson.FilePath(channel, workspace.getRemote() + "/" + reportName); 270 | engine.doDownload(engine.getUrl(getDescriptor().getgApiUrl(), downloadLink), reportFile); 271 | if (reportFile.exists()) { 272 | String savedReportUrl = Jenkins.getInstance().getRootUrl() + build.getParent().getUrl() + "ws/" + reportName; 273 | listenerLogger.println(SR.getString("report.saved.in.workspace.0", reportName)); 274 | } else { 275 | listenerLogger.println(SR.getString("invalid.report.file.path.0", reportName)); 276 | } 277 | } 278 | } 279 | } catch (InterruptedException | IOException e) { 280 | e.printStackTrace(); 281 | listenerLogger.println(e.getMessage()); 282 | } 283 | } 284 | } 285 | 286 | @Override 287 | public DescriptorImpl getDescriptor() { 288 | return (DescriptorImpl) super.getDescriptor(); 289 | } 290 | 291 | @Extension 292 | public static final class DescriptorImpl extends BuildStepDescriptor { 293 | private String gApiUrl; 294 | private String gApiKeyID; 295 | 296 | public DescriptorImpl() { 297 | load(); 298 | } 299 | @POST 300 | public FormValidation doTestConnection(@QueryParameter("gApiUrl") final String ApiUrl, 301 | @QueryParameter("gApiKey") final String apiKey) { 302 | Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); 303 | try { 304 | if (ApiUrl.length() == 0) 305 | return FormValidation.error(SR.getString("please.set.the.api.url")); 306 | if (!ApiUrl.contains("/api/v1")) 307 | return FormValidation.error(SR.getString("invalid.api.url")); 308 | Engine apio = new Engine(ApiUrl, getgApiKey()); 309 | int respCode = apio.doTestConnection(ApiUrl + "/me"); 310 | if (respCode == 200) { 311 | return FormValidation.ok(SR.getString("connected.successfully")); 312 | } 313 | } catch (SSLHandshakeException e) { 314 | e.printStackTrace(); 315 | return FormValidation.error(SR.getString("certificate.to.the.java.ca.store")); 316 | } catch (IOException e) { 317 | e.printStackTrace(); 318 | return FormValidation.error(e.getMessage()); 319 | } 320 | return FormValidation.error(SR.getString("cannot.connect.to.application")); 321 | } 322 | 323 | public boolean isApplicable(Class aClass) { 324 | return true; 325 | } 326 | 327 | /** 328 | * This human readable name is used in the configuration screen. 329 | */ 330 | public String getDisplayName() { 331 | return "Acunetix"; 332 | } 333 | 334 | @Override 335 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 336 | gApiUrl = formData.getString("gApiUrl"); 337 | gApiKeyID = formData.getString("gApiKeyID"); 338 | save(); 339 | return super.configure(req, formData); 340 | } 341 | 342 | public String getgApiUrl() { 343 | return gApiUrl; 344 | } 345 | 346 | private String getgApiKeyID() {return gApiKeyID;} 347 | 348 | private String getgApiKey() { 349 | StandardCredentials credentials = null; 350 | try { 351 | credentials = CredentialsMatchers.firstOrNull( 352 | lookupCredentials(StandardCredentials.class, (Item) null, ACL.SYSTEM, new ArrayList()), 353 | CredentialsMatchers.withId(gApiKeyID)); 354 | } 355 | catch (NullPointerException e) { 356 | throw new ConnectionException(SR.getString("api.key.not.set")); 357 | } 358 | if (credentials != null) { 359 | if (credentials instanceof StringCredentials) { 360 | return ((StringCredentials) credentials).getSecret().getPlainText(); 361 | } 362 | } 363 | throw new IllegalStateException("Could not find Acunetix API Key ID: " + gApiKeyID); 364 | } 365 | 366 | 367 | 368 | public ListBoxModel doFillProfileItems() throws IOException { 369 | ListBoxModel items = new ListBoxModel(); 370 | Engine apio = new Engine(gApiUrl, getgApiKey()); 371 | JSONArray jsa = apio.getScanningProfiles(); 372 | for (int i = 0; i < jsa.size(); i++) { 373 | JSONObject item = jsa.getJSONObject(i); 374 | String profile_name = item.getString("name"); 375 | String profile_id = item.getString("profile_id"); 376 | items.add(profile_name, profile_id); 377 | } 378 | return items; 379 | } 380 | 381 | public ListBoxModel doFillTargetItems() throws IOException { 382 | ListBoxModel items = new ListBoxModel(); 383 | Engine apio = new Engine(gApiUrl, getgApiKey()); 384 | JSONArray jsa = apio.getTargets(); 385 | for (int i = 0; i < jsa.size(); i++) { 386 | JSONObject item = jsa.getJSONObject(i); 387 | String mi = item.getString("manual_intervention"); 388 | if (mi.equals("null") || mi.equals("false")) { 389 | String address = item.getString("address"); 390 | String target_id = item.getString("target_id"); 391 | String description = item.getString("description"); 392 | String target_name = address; 393 | if (description.length() > 0) { 394 | if (description.length() > 100) { 395 | description = description.substring(0, 100); 396 | } 397 | target_name += " (" + description + ")"; 398 | } 399 | items.add(target_name, target_id); 400 | } 401 | } 402 | return items; 403 | } 404 | 405 | public ListBoxModel doFillRepTempItems() throws IOException { 406 | ListBoxModel items = new ListBoxModel(); 407 | Engine apio = new Engine(gApiUrl, getgApiKey()); 408 | JSONArray jsa = apio.getReportTemplates(); 409 | items.add("Do not generate a report", "no_report"); 410 | for (int i = 0; i < jsa.size(); i++) { 411 | JSONObject item = jsa.getJSONObject(i); 412 | String group = item.getString("group"); 413 | String reportTemplate_name = item.getString("name"); 414 | String template_id = item.getString("template_id"); 415 | if (!reportTemplate_name.equals("Scan Comparison")) { 416 | items.add(reportTemplate_name, template_id); 417 | } 418 | } 419 | return items; 420 | } 421 | 422 | public ListBoxModel doFillThreatItems() throws IOException { 423 | ListBoxModel items = new ListBoxModel(); 424 | items.add("Do not fail the build", "DoNotFail"); 425 | items.add("High", "High"); 426 | items.add("Medium or High", "Medium"); 427 | items.add("Low, Medium or High", "Low"); 428 | return items; 429 | } 430 | 431 | public ListBoxModel doFillGApiKeyIDItems( 432 | @AncestorInPath Item item) { 433 | StandardListBoxModel result = new StandardListBoxModel(); 434 | if (item == null) { 435 | if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { 436 | return result.includeCurrentValue(gApiKeyID); 437 | } 438 | } else { 439 | if (!item.hasPermission(Item.EXTENDED_READ) 440 | && !item.hasPermission(CredentialsProvider.USE_ITEM)) { 441 | return result.includeCurrentValue(gApiKeyID); 442 | } 443 | } 444 | if (gApiKeyID != null) { 445 | result.includeMatchingAs(ACL.SYSTEM, Jenkins.getInstance(), StringCredentials.class, 446 | Collections. emptyList(), CredentialsMatchers.allOf(CredentialsMatchers.withId(gApiKeyID))); 447 | } 448 | return result 449 | .includeMatchingAs(ACL.SYSTEM, Jenkins.getInstance(), StringCredentials.class, 450 | Collections. emptyList(), CredentialsMatchers.allOf(CredentialsMatchers.instanceOf(StringCredentials.class))); 451 | } 452 | 453 | public Boolean testVersion() { 454 | Boolean res = false; 455 | try { 456 | Engine aac = new Engine(getgApiUrl(), getgApiKey()); 457 | res = aac.getVersion() > 12; 458 | } catch (IOException e) { 459 | e.printStackTrace(); 460 | } 461 | return res; 462 | } 463 | } 464 | } 465 | 466 | -------------------------------------------------------------------------------- /src/main/java/com/acunetix/Engine.java: -------------------------------------------------------------------------------- 1 | package com.acunetix; 2 | 3 | import hudson.FilePath; 4 | import net.sf.json.JSONArray; 5 | import net.sf.json.JSONNull; 6 | import net.sf.json.JSONObject; 7 | 8 | import javax.net.ssl.HttpsURLConnection; 9 | import java.io.BufferedReader; 10 | import java.io.DataOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.net.MalformedURLException; 14 | import java.net.URL; 15 | import java.net.URLConnection; 16 | import java.util.*; 17 | 18 | 19 | public class Engine { 20 | private String apiUrl; 21 | private String apiKey; 22 | private static final Map threatCategory = new HashMap<>(); 23 | static { 24 | threatCategory.put("High", new String[]{"3"}); 25 | threatCategory.put("Medium", new String[]{"3", "2"}); 26 | threatCategory.put("Low", new String[]{"3", "2", "1"}); 27 | } 28 | private static final Map scan_status_categories = new HashMap<>(); 29 | static { 30 | scan_status_categories.put("All", new String[]{"scheduled", "queued", "starting", "processing", "aborting", 31 | "aborted", "pausing", "paused", "completed", "failed"}); 32 | scan_status_categories.put("Active", new String[]{"scheduled", "queued", "starting", "processing"}); 33 | scan_status_categories.put("Finished", new String[]{"aborting","aborted", "pausing", "paused", "completed", 34 | "failed"}); 35 | } 36 | 37 | public Engine(String apiUrl, String apiKey) { 38 | this.apiUrl = apiUrl; 39 | this.apiKey = apiKey; 40 | } 41 | 42 | public static String getThreatName(String threat) { 43 | switch (threat) { 44 | case "3": 45 | return "High"; 46 | case "2": 47 | return "Medium"; 48 | case "1": 49 | return "Low"; 50 | } 51 | return null; 52 | } 53 | 54 | private static class Resp { 55 | int respCode; 56 | String respStr = null; 57 | JSONObject jso = null; 58 | } 59 | 60 | private HttpsURLConnection openConnection(String endpoint, String method) throws IOException { 61 | return openConnection(endpoint, method, "application/json; charset=UTF-8"); 62 | } 63 | 64 | private HttpsURLConnection openConnection(String endpoint) throws IOException { 65 | return openConnection(endpoint, "GET", "application/json; charset=UTF-8"); 66 | } 67 | 68 | private HttpsURLConnection openConnection(String endpoint, String method, String contentType) throws IOException { 69 | URL url = new URL(endpoint); 70 | HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); 71 | connection.setRequestMethod(method); 72 | connection.setRequestProperty("Content-Type", contentType); 73 | connection.setRequestProperty("User-Agent", "Mozilla/5.0"); 74 | connection.addRequestProperty("X-AUTH", apiKey); 75 | 76 | return connection; 77 | } 78 | 79 | private Resp doGet(String urlStr) throws IOException { 80 | HttpsURLConnection connection = openConnection(urlStr); 81 | try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"))) { 82 | String inputLine; 83 | StringBuilder resbuf = new StringBuilder(); 84 | while ((inputLine = in.readLine()) != null) { 85 | resbuf.append(inputLine); 86 | } 87 | Resp resp = new Resp(); 88 | resp.respCode = connection.getResponseCode(); 89 | resp.jso = JSONObject.fromObject(resbuf.toString()); 90 | return resp; 91 | } 92 | } 93 | 94 | public String getUrl(String apiUrl, String downloadLink) throws MalformedURLException { 95 | URL url = new URL(apiUrl); 96 | if (downloadLink.matches("^(http|https)://.*$")) { 97 | return downloadLink; 98 | } else { 99 | return url.getProtocol() + "://" + url.getAuthority() + downloadLink; 100 | } 101 | } 102 | 103 | public int doTestConnection(String urlStr) throws IOException { 104 | HttpsURLConnection connection = openConnection(urlStr); 105 | return connection.getResponseCode(); 106 | } 107 | 108 | private Resp doPost(String urlStr) throws IOException { 109 | HttpsURLConnection connection = openConnection(urlStr,"POST"); 110 | connection.setUseCaches(false); 111 | connection.setDoInput(true); 112 | connection.setDoOutput(true); 113 | Resp resp = new Resp(); 114 | resp.respCode = connection.getResponseCode(); 115 | return resp; 116 | } 117 | 118 | private Resp doDelete(String urlStr) throws IOException { 119 | HttpsURLConnection connection = openConnection(urlStr,"DELETE"); 120 | connection.setUseCaches(false); 121 | connection.setDoInput(true); 122 | connection.setDoOutput(true); 123 | Resp resp = new Resp(); 124 | resp.respCode = connection.getResponseCode(); 125 | return resp; 126 | } 127 | 128 | private Resp doPostLoc(String urlStr, String urlParams) throws IOException, NullPointerException { 129 | HttpsURLConnection connection = openConnection(urlStr, "POST"); 130 | connection.setUseCaches(false); 131 | connection.setDoInput(true); 132 | connection.setDoOutput(true); 133 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 134 | outputStream.writeBytes(urlParams); 135 | } 136 | String location = connection.getHeaderField("Location"); 137 | Resp resp = new Resp(); 138 | resp.respCode = connection.getResponseCode(); 139 | if (400 <= resp.respCode && resp.respCode <= 599) { 140 | throw new RuntimeException("HTTP request failed with status code " + resp.respCode); 141 | } 142 | try { 143 | resp.respStr = location.substring(location.lastIndexOf("/") + 1); 144 | } catch (NullPointerException e){ 145 | e.printStackTrace(); 146 | throw new ConnectionException(); 147 | } 148 | return resp; 149 | } 150 | 151 | private JSONArray getObjects(String objectName) throws IOException, NullPointerException { 152 | JSONArray objects = null; 153 | JSONArray cursors; 154 | 155 | Resp resp = doGet(apiUrl + "/" + objectName); 156 | if (resp.respCode != 200) { 157 | throw new IOException(SR.getString("bad.response.0", resp.respCode)); 158 | } 159 | objects = resp.jso.getJSONArray(objectName); 160 | JSONObject pagination = resp.jso.getJSONObject("pagination"); 161 | if (pagination.containsKey("next_cursor")) { 162 | Integer cursor = 0; 163 | while ((cursor >= 100) || (cursor%100>0)) { 164 | resp = doGet(apiUrl + "/" + objectName + "?c=" + cursor); 165 | objects.addAll(resp.jso.getJSONArray(objectName)); 166 | pagination = resp.jso.getJSONObject("pagination"); 167 | if (pagination.getString("next_cursor").equals("null")) { 168 | break; 169 | } 170 | cursor = pagination.getInt("next_cursor"); 171 | } 172 | } 173 | else{ 174 | if (pagination.size() > 0) { 175 | cursors = pagination.getJSONArray("cursors"); 176 | String cursor; 177 | while (cursors.size() > 1) { 178 | cursor = cursors.getString(1); 179 | resp = doGet(apiUrl + "/" + objectName + "?c=" + cursor); 180 | objects.addAll(resp.jso.getJSONArray(objectName)); 181 | pagination = resp.jso.getJSONObject("pagination"); 182 | cursors = pagination.getJSONArray("cursors"); 183 | } 184 | } 185 | } 186 | 187 | // } 188 | return objects; 189 | } 190 | 191 | public JSONArray getTargets() throws IOException { 192 | return getObjects("targets"); 193 | } 194 | 195 | 196 | public String getTargetName(String targetId) throws IOException { 197 | JSONArray targets = getTargets(); 198 | for (int i = 0; i < targets.size(); i++) { 199 | JSONObject item = targets.getJSONObject(i); 200 | String target_id = item.getString("target_id"); 201 | if (target_id.equals(targetId)) { 202 | String address = item.getString("address"); 203 | String description = item.getString("description"); 204 | String target_name = address; 205 | if (description.length() > 0) { 206 | if (description.length() > 100) { 207 | description = description.substring(0, 100); 208 | } 209 | target_name += " (" + description + ")"; 210 | } 211 | return target_name; 212 | } 213 | } 214 | return null; 215 | } 216 | 217 | public JSONArray getScanningProfiles() throws IOException { 218 | return getObjects("scanning_profiles"); 219 | } 220 | 221 | public Boolean checkScanProfileExists(String profileId) throws IOException { 222 | JSONArray profiles = getScanningProfiles(); 223 | for (int i = 0; i < profiles.size(); i++) { 224 | JSONObject item = profiles.getJSONObject(i); 225 | String profile_id = item.getString("profile_id"); 226 | if (profile_id.equals(profileId)) { 227 | return true; 228 | } 229 | } 230 | return false; 231 | } 232 | 233 | public Boolean checkIncScanExist(String target_id, String profile_id) { 234 | try { 235 | JSONArray scans = getScans(); 236 | for (int i = 0; i < scans.size(); i++) { 237 | JSONObject item = scans.getJSONObject(i); 238 | if (item.getBoolean("incremental")) { 239 | if ((item.getString("target_id").equals(target_id)) && (item.getString("profile_id").equals(profile_id))) { 240 | return true; 241 | } 242 | } 243 | } 244 | } 245 | catch (IOException e){ 246 | e.printStackTrace(); 247 | } 248 | return false; 249 | } 250 | 251 | public Boolean checkScanExist(String scanId) { 252 | try { 253 | JSONArray scans = getScans(); 254 | for (int i = 0; i < scans.size(); i++) { 255 | JSONObject item = scans.getJSONObject(i); 256 | String id = item.getString("scan_id"); 257 | if (id.equals(scanId)) { 258 | return true; 259 | } 260 | } 261 | } 262 | catch (IOException e){ 263 | e.printStackTrace(); 264 | } 265 | return false; 266 | } 267 | 268 | public String getIncScanId(String target_id, String profile_id) { 269 | try { 270 | JSONArray scans = getScans(); 271 | for (int i = 0; i < scans.size(); i++) { 272 | JSONObject item = scans.getJSONObject(i); 273 | if (item.getBoolean("incremental")) { 274 | if ((item.getString("target_id").equals(target_id)) && (item.getString("profile_id").equals(profile_id))) { 275 | return item.getString("scan_id"); 276 | } 277 | } 278 | } 279 | } 280 | catch (IOException e){ 281 | e.printStackTrace(); 282 | } 283 | return null; 284 | } 285 | 286 | 287 | public String startScan(String scanningProfileId, String targetId, Boolean waitFinish) throws IOException { 288 | JSONObject jso = new JSONObject(); 289 | jso.put("target_id", targetId); 290 | jso.put("profile_id", scanningProfileId); 291 | jso.put("user_authorized_to_scan", "yes"); 292 | JSONObject jsoChild = new JSONObject(); 293 | jsoChild.put("disable", false); 294 | jsoChild.put("start_date", JSONNull.getInstance()); 295 | jsoChild.put("time_sensitive", false); 296 | jso.put("schedule", jsoChild); 297 | String scanId = doPostLoc(apiUrl + "/scans", jso.toString()).respStr; 298 | if (waitFinish) { 299 | while (!getScanStatus(scanId).equals("completed")) { 300 | try { 301 | Thread.sleep(10000); 302 | } catch (InterruptedException e) { 303 | e.printStackTrace(); 304 | } 305 | } 306 | } 307 | return scanId; 308 | } 309 | 310 | public String createIncScan(String scanningProfileId, String targetId) throws IOException { 311 | JSONObject jso = new JSONObject(); 312 | jso.put("target_id", targetId); 313 | jso.put("profile_id", scanningProfileId); 314 | jso.put("user_authorized_to_scan", "yes"); 315 | jso.put("incremental", true); 316 | JSONObject jsoChild = new JSONObject(); 317 | jsoChild.put("disable", false); 318 | jsoChild.put("start_date", JSONNull.getInstance()); 319 | jsoChild.put("time_sensitive", false); 320 | jsoChild.put("triggerable", true); 321 | jso.put("schedule", jsoChild); 322 | String scanId = doPostLoc(apiUrl + "/scans", jso.toString()).respStr; 323 | return scanId; 324 | } 325 | 326 | public String triggerIncScan(String scanId, Boolean waitFinish) throws IOException { 327 | String resScanId = doPost(apiUrl + "/scans/" + scanId + "/trigger").respStr; 328 | if (waitFinish) { 329 | while (!getScanStatus(scanId).equals("completed")) { 330 | try { 331 | Thread.sleep(10000); 332 | } catch (InterruptedException e) { 333 | e.printStackTrace(); 334 | } 335 | } 336 | } 337 | return resScanId; 338 | } 339 | 340 | public JSONArray getScans() throws IOException { 341 | return getObjects("scans"); 342 | } 343 | 344 | public String getScanThreat(String scanId) throws IOException { 345 | JSONObject jso = doGet(apiUrl + "/scans/" + scanId).jso; 346 | return jso.getJSONObject("current_session").getString("threat"); 347 | } 348 | 349 | public String getScanStatus(String scanId) throws IOException { 350 | JSONObject jso = doGet(apiUrl + "/scans/" + scanId).jso; 351 | return jso.getJSONObject("current_session").getString("status"); 352 | } 353 | 354 | public String getScanProfile(String scanId) throws IOException { 355 | JSONObject jso = doGet(apiUrl + "/scans/" + scanId).jso; 356 | return jso.getString("profile_id"); 357 | } 358 | 359 | public String getScanTarget(String scanId) throws IOException { 360 | JSONObject jso = doGet(apiUrl + "/scans/" + scanId).jso; 361 | return jso.getString("target_id"); 362 | } 363 | 364 | public void stopScan(String scanId) { 365 | try { 366 | Resp resp = doPost(apiUrl + "/scans/" + scanId + "/abort"); 367 | if (resp.respCode != 204) { 368 | throw new IOException(SR.getString("bad.response.0", resp.respCode)); 369 | } 370 | } catch (IOException e) { 371 | e.printStackTrace(); 372 | } 373 | } 374 | 375 | public void deleteScan(String scanId) { 376 | try { 377 | doDelete(apiUrl + "/scans/" + scanId); 378 | } catch (IOException e) { 379 | e.printStackTrace(); 380 | } 381 | } 382 | 383 | public void stopTargetScans(String targetId) throws IOException { 384 | JSONArray scans = getScans(); 385 | for (int i=0; i < scans.size(); i++) { 386 | JSONObject item = scans.getJSONObject(i); 387 | if (item.getString("target_id").equals(targetId)) { 388 | String status = item.getJSONObject("current_session").getString("status"); 389 | if(Arrays.asList(scan_status_categories.get("Active")).contains(status)) { 390 | stopScan(item.getString("scan_id")); 391 | } 392 | } 393 | } 394 | } 395 | 396 | 397 | public JSONArray getReportTemplates() throws IOException { 398 | Resp resp = doGet(apiUrl + "/report_templates"); 399 | if (resp.respCode == 200) { 400 | return resp.jso.getJSONArray("templates"); 401 | } 402 | throw new IOException(SR.getString("bad.response.0", resp.respCode)); 403 | } 404 | 405 | public String getReportTemplateName(String reportTemplateId) throws IOException { 406 | JSONArray jsa = getReportTemplates(); 407 | for (int i = 0; i < jsa.size(); i++) { 408 | JSONObject item = jsa.getJSONObject(i); 409 | if (item.getString("template_id").equals(reportTemplateId)) { 410 | return item.getString("name"); 411 | } 412 | } 413 | return null; 414 | } 415 | 416 | private String getReportStatus(String reportId) throws IOException { 417 | JSONObject jso = doGet(apiUrl + "/reports/" + reportId).jso; 418 | return jso.getString("status"); 419 | } 420 | 421 | public void waitReportStatus(String reportId) throws IOException, InterruptedException { 422 | while (!getReportStatus(reportId).equals("completed")) { 423 | Thread.sleep(10000); 424 | } 425 | } 426 | 427 | public String generateReport(String sourceId, String reportTemplateId, String listType) throws IOException, InterruptedException { 428 | //returns download link of html report 429 | JSONObject jso = new JSONObject(); 430 | jso.put("template_id", reportTemplateId); 431 | JSONObject jsoChild = new JSONObject(); 432 | jsoChild.put("list_type", listType); 433 | List id_list = new ArrayList<>(); 434 | id_list.add(sourceId); 435 | jsoChild.put("id_list", id_list); 436 | jso.put("source", jsoChild); 437 | String reportId = doPostLoc(apiUrl + "/reports", jso.toString()).respStr; 438 | waitReportStatus(reportId); 439 | String[] downloadLinkList = doGet(apiUrl + "/reports/" + reportId).jso.getString("download").split(","); 440 | String downloadLink = null; 441 | for (String item : downloadLinkList) { 442 | if (item.contains(".html")) { 443 | downloadLink = item.replaceAll("\"", "").replaceAll("\\[", "".replaceAll("]", "")); 444 | break; 445 | } 446 | } 447 | // download report 448 | return downloadLink; 449 | } 450 | 451 | public Boolean checkThreat(String checkThreat, String scanThreat) { 452 | //return true if the threat detected is equal or greater than threat set 453 | //checkthreat is the level set in plugin config and scanThreat from the scan result 454 | if (checkThreat.equals("DoNotFail")) { 455 | return false; 456 | } 457 | return Arrays.asList(threatCategory.get(checkThreat)).contains(scanThreat); 458 | } 459 | 460 | public Integer getVersion() throws IOException { 461 | if (apiUrl.matches(":\\d+")) { 462 | JSONObject jso = doGet(apiUrl + "/info").jso; 463 | return jso.getInt("major_version"); 464 | } 465 | else { 466 | return 13; 467 | } 468 | } 469 | 470 | public String getReportFileName(String urlSource) throws IOException { 471 | URLConnection connection = new URL(urlSource).openConnection(); 472 | connection.addRequestProperty("User-Agent", "Mozilla"); 473 | String cd = connection.getHeaderField("Content-Disposition"); 474 | String fileName = null; 475 | if (cd != null && cd.contains("=")) { 476 | fileName = "Acunetix_" + cd.split("=")[1].trim().replaceAll("\"", ""); 477 | } 478 | return fileName; 479 | } 480 | 481 | public void doDownload(String urlSource, FilePath savePath) throws IOException, InterruptedException { 482 | URL url = new URL(urlSource); 483 | savePath.copyFrom(url); 484 | } 485 | 486 | 487 | } 488 | 489 | class ConnectionException extends RuntimeException { 490 | public ConnectionException() { 491 | super(SR.getString("cannot.connect.to.application")); 492 | } 493 | public ConnectionException(String message) { 494 | super(message); 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /src/main/java/com/acunetix/SR.java: -------------------------------------------------------------------------------- 1 | package com.acunetix; 2 | 3 | import java.text.MessageFormat; 4 | 5 | import static java.util.ResourceBundle.getBundle; 6 | 7 | /** 8 | * Expose message resources 9 | */ 10 | class SR { 11 | private static java.util.ResourceBundle acunetixBundle = getBundle("Messages"); 12 | 13 | private SR() { 14 | } 15 | 16 | static String getString(String key, Object... args) { 17 | String message = acunetixBundle.getString(key); 18 | 19 | if (args != null && args.length > 0) { 20 | return MessageFormat.format(message, args); 21 | } 22 | return message; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/Messages.properties: -------------------------------------------------------------------------------- 1 | cannot.connect.to.application=Cannot connect to Acunetix API engine 2 | connected.successfully=Connected successfully 3 | please.set.the.api.url=Please set the API URL 4 | please.set.the.api.key=Please set the API Key 5 | invalid.api.url=Invalid API URL 6 | api.key.not.set=API Key is not set 7 | starting.scan.on.target.0=Starting scan on target: {0} 8 | bad.response.0=Bad response received: {0} 9 | scan.threat.0=At least one \"{0}\" vulnerability was found 10 | scan.aborted.outside=The scan was aborted outside of this instance 11 | scan.aborted=The scan was aborted 12 | scan.completed=The scan was completed 13 | scan.scheduled=The scan was scheduled. Please ensure the configured scan can run immediately 14 | abort.scan.scheduled=The build was aborted because the scan is scheduled 15 | aborting.the.build=Aborting the build 16 | build.aborted=The build was aborted 17 | scan.threat=The scan threat level is greater or equal than the configured level 18 | the.scan.was.stopped=The scan was stopped 19 | certificate.to.the.java.ca.store=Please add the Acunetix scanner certificate to Java CA store 20 | scan.started=Scan started 21 | the.scan.is.in.scheduled.state=The scan is in scheduled state! Waiting to start ... 22 | the.scan.was.deleted=The scan was deleted 23 | could.not.connect.to.application.connection.refused=Could not connect to application. Connection refused 24 | could.not.find.scan.with.scanid.0=Could not find the scan with scanId: {0} 25 | could.not.connect=The connection could not be established. Check if the server is responsive and API key is valid 26 | invalid.target=The target is invalid 27 | invalid.scan_type=The scan type is invalid 28 | could.not.find.scan.with.scanid.0.create.new=Could not find the scan with scanId: {0}\n Please create a new one by saving the project configuration 29 | view.scan.status=View scan on Acunetix: 30 | check.vulnerabilities.found=Check vulnerabilities found by this scan: 31 | generating.0.report=Generating \"{0}\" report 32 | report.saved.in.workspace.0=Report saved in workspace: \"{0}\" 33 | scan.report.download.link.0=Scan report download link: \"{0}\" 34 | invalid.report.file.path.0=Invalid report file path: \"{0}\" 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ${%Incremental scan} 23 | 24 | 25 | 26 | 27 | ${%Stop scans on the Target before starting a new scan} 28 | 29 | 30 | 31 | 32 | 33 | 34 | ${%Stop the scan when build fails} 35 | 36 | 37 | 38 | 39 | 40 | 41 | ${%Save report in workspace} 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-gApiKey.html: -------------------------------------------------------------------------------- 1 |
2 | Acunetix API Key. The API Key may be obtained from the Acunetix User Interface under the Administrator's Profile section 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-gApiKeyID.html: -------------------------------------------------------------------------------- 1 |
2 | The URL of the Acunetix API. Example: https://localhost:3443/api/v1.
3 | Note: When change the API key make sure it is saved, using "Apply" button, before trying "Test Connection" 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-incScan.html: -------------------------------------------------------------------------------- 1 |
2 | This will create an Incremental scan. The first scan will scan all the site. Subsequent scans will only scan the changes in the site, resulting in smaller scans. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-profile.html: -------------------------------------------------------------------------------- 1 |
2 | The Scan Type used to scan the target 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-repTemp.html: -------------------------------------------------------------------------------- 1 |
2 | Generate a Report after the scan completes. 3 | The report is available for download from Acunetix backend within a time frame of one hour 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-stopScan.html: -------------------------------------------------------------------------------- 1 |
2 | If selected, the scan will be stopped if the build fails (Threat Level is reached) 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-stopTargetScans.html: -------------------------------------------------------------------------------- 1 |
2 | This is useful when older versions of the web application are discarded by the CI/CD when an updated version is created. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-svRep.html: -------------------------------------------------------------------------------- 1 |
2 | Option to download the report in the project workspace from where it can be used for other operations like attaching 3 | to an email or archiving.
4 | Note: Archiving can be done with "Archive the artifacts" post-build action 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-target.html: -------------------------------------------------------------------------------- 1 |
2 | The Scan Target on which to run a scan (excludes Targets requiring Manual Intervention) 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help-threat.html: -------------------------------------------------------------------------------- 1 |
2 | The Threat Level (High, Medium, Low) to fail the build on 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/acunetix/BuildScanner/help.html: -------------------------------------------------------------------------------- 1 |
2 | Trigger automated Acunetix scans as part of your web application's build process 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | Plugin for starting Acunetix Premium scans 7 |
8 | --------------------------------------------------------------------------------