CVE-2024-28155
Description
Jenkins AppSpider Plugin 1.0.16 and earlier does not perform permission checks in several HTTP endpoints, allowing attackers with Overall/Read permission to obtain information about available scan config names, engine group names, and client names.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins AppSpider Plugin 1.0.16 and earlier lacks permission checks in HTTP endpoints, allowing attackers with Overall/Read to leak scan config, engine group, and client names.
Vulnerability
Description
The Jenkins AppSpider Plugin versions 1.0.16 and earlier contain a missing authorization vulnerability. Several HTTP endpoints in the plugin do not perform proper permission checks before returning information. The root cause is that these endpoints rely solely on the Overall/Read permission for access, which is insufficiently restrictive for exposing potentially sensitive configuration data [1][2].
Exploitation
An attacker with only Overall/Read permission (a basic permission often granted to many Jenkins users) can exploit this by sending crafted HTTP requests to the vulnerable endpoints. No additional privileges or authentication beyond a Jenkins account with this permission are required. The attack surface is the Jenkins web interface or API, making it remotely exploitable from within the Jenkins network [1][3].
Impact
Successful exploitation allows the attacker to enumerate available scan configuration names, engine group names, and client names used by the AppSpider plugin. While this does not directly expose scan results or credentials, it provides reconnaissance information that could be used in further attacks, such as targeting specific configurations or identifying internal naming conventions [1][3].
Mitigation
Jenkins released AppSpider Plugin version 1.0.17 which fixes this vulnerability by implementing proper permission checks on the affected HTTP endpoints. Users should upgrade to version 1.0.17 or later. The fix is also visible in the project's repository, where the plugin parent version was updated to include security improvements [1][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
com.rapid7:jenkinsci-appspider-pluginMaven | < 1.0.17 | 1.0.17 |
Affected products
3<=1.0.16+ 1 more
- (no CPE)range: <=1.0.16
- (no CPE)range: 0
Patches
11677f098fbe4SECURITY-3144
15 files changed · +193 −134
pom.xml+34 −21 modified@@ -1,11 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> - <version>4.40</version> + <version>4.52</version> <relativePath /> </parent> @@ -23,29 +24,30 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> - <version>3.8.1</version> + <version>3.11.0</version> <configuration> - <source>1.8</source> - <target>1.8</target> + <source>11</source> + <target>11</target> </configuration> </plugin> <plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> - <version>4.7.0.0</version> + <version>4.8.1.0</version> <dependencies> <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs --> <dependency> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs</artifactId> - <version>4.7.0</version> + <version>4.8.0</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>animal-sniffer-maven-plugin</artifactId> + <version>1.23</version> <configuration> <signature> <groupId>org.codehaus.mojo.signature</groupId> @@ -77,13 +79,14 @@ <connection>scm:git:git://github.com/jenkinsci/appspider-build-scanner-plugin.git</connection> <developerConnection>scm:git:git@github.com:jenkinsci/appspider-build-scanner-plugin.git</developerConnection> <url>https://github.com/jenkinsci/appspider-build-scanner-plugin.git</url> - <tag>jenkinsci-appspider-plugin-1.0.12</tag> + <tag>jenkinsci-appspider-plugin-1.0.16</tag> </scm> <!-- Turning off doclint --> <properties> <additionalparam>-Xdoclint:none</additionalparam> - <jenkins.version>2.348</jenkins.version> + <jenkins.version>2.375.1</jenkins.version> + <maven.compiler.release>11</maven.compiler.release> </properties> <repositories> @@ -112,13 +115,13 @@ <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>3.3.3</version> + <version>5.7.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> - <version>3.3.3</version> + <version>5.7.0</version> <scope>test</scope> </dependency> <dependency> @@ -130,7 +133,7 @@ <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> - <version>20180130</version> + <version>20231013</version> </dependency> <dependency> <groupId>org.glassfish.jersey.core</groupId> @@ -147,26 +150,36 @@ <artifactId>jakarta.annotation-api</artifactId> <version>2.1.0</version> </dependency> + <dependency> + <groupId>jakarta.inject</groupId> + <artifactId>jakarta.inject-api</artifactId> + <version>2.0.0</version> + </dependency> + <dependency> + <groupId>jakarta.validation</groupId> + <artifactId>jakarta.validation-api</artifactId> + <version>3.0.0</version> + </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpcore</artifactId> - <version>4.4.13</version> + <artifactId>httpmime</artifactId> + <version>4.5.12</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> - <version>4.5.13</version> + <version>4.5.12</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + <version>4.4.16</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> - <dependency> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpmime</artifactId> - <version>4.5.12</version> - </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> @@ -200,7 +213,7 @@ <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> - <version>1.15</version> + <version>1.16.0</version> </dependency> <dependency> <groupId>javax.ws.rs</groupId>
src/main/java/com/rapid7/appspider/ApiSerializer.java+9 −4 modified@@ -8,6 +8,7 @@ import com.rapid7.appspider.datatransferobjects.ScanResult; import freemarker.template.Template; import freemarker.template.TemplateException; +import hudson.model.Api; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -18,13 +19,17 @@ import java.net.URL; import java.util.*; -public class ApiSerializer { +public final class ApiSerializer { private final LoggerFacade logger; - public ApiSerializer(LoggerFacade logger) { - if (logger == null) + public static ApiSerializer createInstanceOrThrow(LoggerFacade logger) { + if (logger == null) { throw new IllegalArgumentException("logger cannot be null"); + } + return new ApiSerializer(logger); + } + private ApiSerializer(LoggerFacade logger) { this.logger = logger; } @@ -67,7 +72,7 @@ public boolean getIsSuccess(JSONObject jsonObject) { public ScanResult getScanResult(JSONObject jsonObject) { try { - return new ScanResult(jsonObject); + return ScanResult.createInstanceFromJsonOrThrow(jsonObject); } catch (IllegalArgumentException e) { logger.severe(e.toString()); return new ScanResult(false, "");
src/main/java/com/rapid7/appspider/ContentHelper.java+9 −7 modified@@ -25,18 +25,20 @@ import java.util.Objects; import java.util.Optional; -import static com.rapid7.appspider.Utility.isSuccessStatusCode; - /** * parsing and serializing helper methods for handling JSONObject manipulation */ public class ContentHelper { private final LoggerFacade logger; - public ContentHelper(LoggerFacade logger) { + public static ContentHelper createInstanceOrThrow(LoggerFacade logger) { if (Objects.isNull(logger)) throw new IllegalArgumentException("logger cannot be null"); + return new ContentHelper(logger); + } + + private ContentHelper(LoggerFacade logger) { this.logger = logger; } @@ -111,11 +113,11 @@ public NameValuePair pairFrom(String key, String value) { * @return on success an Optional containing a JSONObject; otherwise, Optional.empty() */ public Optional<JSONObject> responseToJSONObject(HttpResponse response, String path) { - if (isSuccessStatusCode(response)) { + if (FunctionalUtility.isSuccessStatusCode(response)) { return asJson(response.getEntity()); } - logResponseFailure("request failed", response); + logResponseFailure("request failed " + path, response); return Optional.empty(); } @@ -140,8 +142,8 @@ public Optional<JSONObject> asJson(HttpEntity entity) { * @param key key in the json object to serve as key in the map * @param value value in the json object to serve as value in the map * @param optionalJsonObject Optional{JSONObject} to extract key/value pairs from if present - * @return on successs a Map{String, String} of key/value pairs from JSONObject; - * otherwiwse Optional.empty() + * @return on successes a Map{String, String} of key/value pairs from JSONObject; + * otherwise Optional.empty() */ public Optional<Map<String, String>> asMapOfStringToString(String key, String value, Optional<JSONObject> optionalJsonObject) { return optionalJsonObject.flatMap(json ->
src/main/java/com/rapid7/appspider/DastScan.java+10 −6 renamed@@ -13,7 +13,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -public class Scan { +public class DastScan { private static final String SUCCESSFUL_SCAN = "Completed|Stopped"; private static final String UNSUCCESSFUL_SCAN = "ReportError"; private static final String FAILED_SCAN = "Failed"; @@ -25,13 +25,17 @@ public class Scan { private final LoggerFacade log; private Optional<String> id; - public Scan(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { + public static DastScan createInstanceOrThrow(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { if (Objects.isNull(client)) throw new IllegalArgumentException("client cannot be null"); if (Objects.isNull(settings)) throw new IllegalArgumentException("settings cannot be null"); if (Objects.isNull(log)) throw new IllegalArgumentException("log cannot be null"); + return new DastScan(client, settings, log); + } + + private DastScan(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { this.client = client; this.settings = settings; this.log = log; @@ -47,7 +51,7 @@ public Optional<String> getId() { public boolean process(AuthenticationModel authModel) throws InterruptedException { Optional<String> maybeAuthToken = client.login(authModel); - if (!maybeAuthToken.isPresent()) { + if (maybeAuthToken.isEmpty()) { log.println(UNAUTHORIZED_ERROR); return false; } @@ -71,7 +75,7 @@ public boolean process(AuthenticationModel authModel) throws InterruptedExceptio waitForScanCompletion(runResult.getScanId(), authModel); maybeAuthToken = client.login(authModel); - if (!maybeAuthToken.isPresent()) { + if (maybeAuthToken.isEmpty()) { log.println(UNAUTHORIZED_ERROR); return false; } @@ -100,7 +104,7 @@ private boolean createScanBeforeRunIfNeeded(String authToken) { log.println("Value of Scan Config Engine Group name: " + settings.getScanConfigEngineGroupName()); Optional<String> engineGroupId = client.getEngineGroupIdFromName(authToken, settings.getScanConfigEngineGroupName()); - if (!engineGroupId.isPresent()) { + if (engineGroupId.isEmpty()) { log.println(String.format("no engine group matching %s was found.", settings.getScanConfigEngineGroupName())); return false; } @@ -144,7 +148,7 @@ private void waitForScanCompletion(String scanId, AuthenticationModel authModel) } private Optional<String> getStatus(String scanId, AuthenticationModel authModel) { Optional<String> authToken = client.login(authModel); - if (!authToken.isPresent()) { + if (authToken.isEmpty()) { log.println(UNAUTHORIZED_ERROR); return Optional.empty(); }
src/main/java/com/rapid7/appspider/datatransferobjects/ScanResult.java+6 −6 modified@@ -11,18 +11,18 @@ public ScanResult(boolean isSuccess, String scanId) { this.isSuccess = isSuccess; this.scanId = scanId; } - public ScanResult(JSONObject jsonObject) { + + public static ScanResult createInstanceFromJsonOrThrow(JSONObject jsonObject) { if (jsonObject == null) throw new IllegalArgumentException("jsonObject cannot be null"); try { - isSuccess = jsonObject.getBoolean("IsSuccess"); - scanId = jsonObject.getJSONObject("Scan").getString("Id"); - + boolean isSuccess = jsonObject.getBoolean("IsSuccess"); + String scanId = jsonObject.getJSONObject("Scan").getString("Id"); + return new ScanResult(isSuccess, scanId); } catch(JSONException e) { - throw new IllegalArgumentException("unexpected error occured parsing scan result", e); + throw new IllegalArgumentException("unexpected error occurred parsing scan result", e); } - } public String getScanId() {
src/main/java/com/rapid7/appspider/EnterpriseRestClient.java+2 −2 modified@@ -125,7 +125,7 @@ public boolean testAuthentication(AuthenticationModel authModel) { public Optional<String[]> getEngineGroupNamesForClient(String authToken) { return getEngineGroupsForClient(authToken) .map(map -> new ArrayList<>(map.keySet())) - .map(Utility::toStringArray); + .map(FunctionalUtility::toStringArray); } /** @@ -260,7 +260,7 @@ private Optional<JSONArray> getConfigs(String authToken) { public Optional<String[]> getConfigNames(String authToken) { return getConfigs(authToken) .flatMap(apiSerializer::getConfigNames) - .map(Utility::toStringArray); + .map(FunctionalUtility::toStringArray); } /**
src/main/java/com/rapid7/appspider/FreemarkerConfiguration.java+4 −4 modified@@ -21,14 +21,14 @@ class FreemarkerConfiguration { private final Configuration configuration; /** - * get the singelton instance, initializing it if necessary + * get the singleton instance, initializing it if necessary */ public static FreemarkerConfiguration getInstance() { - return Container.INSTANCE; + return InstanceContainer.CONFIGURATION_INSTANCE; } - private static class Container { - private static final FreemarkerConfiguration INSTANCE = new FreemarkerConfiguration(); + private static class InstanceContainer { + private static final FreemarkerConfiguration CONFIGURATION_INSTANCE = new FreemarkerConfiguration(); } /**
src/main/java/com/rapid7/appspider/FunctionalUtility.java+3 −3 renamed@@ -10,11 +10,11 @@ import java.util.Objects; /** - * various utility merthods that have no better home + * various utility methods that have no better home */ -public class Utility { +public class FunctionalUtility { - private Utility() { + private FunctionalUtility() { } /**
src/main/java/com/rapid7/appspider/HttpClientFactory.java+9 −5 modified@@ -24,21 +24,25 @@ public class HttpClientFactory { private final SSLConnectionSocketFactory socketFactory; final SSLContext sslContext; - public HttpClientFactory(boolean allowSelfSignedCertificates) throws SslContextCreationException { + public static HttpClientFactory createInstanceOrThrow(boolean allowSelfSignedCertificates) + throws SslContextCreationException { try { // ignore self-signed certs since we have no control over the server setup and as such can't // enforce proper certificate usage if (allowSelfSignedCertificates) { - sslContext = new SSLContextBuilder() + return new HttpClientFactory(new SSLContextBuilder() .loadTrustMaterial(null, (x509CertChain, authType) -> true) - .build(); + .build()); } else { - sslContext = SSLContexts.createDefault(); + return new HttpClientFactory(SSLContexts.createDefault()); } - } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { throw new SslContextCreationException("Unable to configure SSL Context", e); } + } + + private HttpClientFactory(SSLContext context) { + sslContext = context; socketFactory = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);
src/main/java/com/rapid7/appspider/HttpClientService.java+5 −4 modified@@ -21,22 +21,23 @@ import java.util.Objects; import java.util.Optional; -import static com.rapid7.appspider.Utility.isSuccessStatusCode; - public class HttpClientService implements ClientService { private final HttpClient httpClient; private final LoggerFacade logger; private final ContentHelper contentHelper; - public HttpClientService(HttpClient httpClient, ContentHelper contentHelper, LoggerFacade logger) { + public static HttpClientService createInstanceOrThrow(HttpClient httpClient, ContentHelper contentHelper, LoggerFacade logger) { if (Objects.isNull(httpClient)) throw new IllegalArgumentException("httpClient cannot be null"); if (Objects.isNull(contentHelper)) throw new IllegalArgumentException("jsonHelper cannot be null"); if (Objects.isNull(logger)) throw new IllegalArgumentException("logger cannot be null"); + return new HttpClientService(httpClient, contentHelper, logger); + } + private HttpClientService(HttpClient httpClient, ContentHelper contentHelper, LoggerFacade logger) { this.httpClient = httpClient; this.contentHelper = contentHelper; this.logger = logger; @@ -67,7 +68,7 @@ public Optional<JSONObject> executeJsonRequest(HttpRequestBase request) { public Optional<HttpEntity> executeEntityRequest(HttpRequestBase request) { try { HttpResponse response = httpClient.execute(request); - return isSuccessStatusCode(response) + return FunctionalUtility.isSuccessStatusCode(response) ? Optional.of(response.getEntity()) : Optional.empty();
src/main/java/com/rapid7/appspider/models/AuthenticationModel.java+17 −8 modified@@ -14,23 +14,28 @@ public class AuthenticationModel { private final String username; private final String password; - private final Optional<String> clientId; + private final String clientId; /** * instantiates a new instance of the {@code AuthenticationModel} class with no client Id */ public AuthenticationModel(String username, String password) { - this(username, password, Optional.empty()); + this(username, password, null); + } + + public static AuthenticationModel createInstanceOrThrow(String username, String password, String clientId) { + if (username == null || username.isEmpty() || password == null) { + throw new IllegalArgumentException(); + } + + return new AuthenticationModel(username, password, clientId); } /** * instantiates a new instance of the {@code AuthenticationModel} class ensuring that * username and password are both non-null and non-empty */ - public AuthenticationModel(String username, String password, Optional<String> clientId) { - if (username == null || username.isEmpty() || password == null || password.isEmpty()) { - throw new IllegalArgumentException(); - } + public AuthenticationModel(String username, String password, String clientId) { this.username = username; this.password = password; this.clientId = clientId; @@ -62,7 +67,7 @@ public String getUsername() { * @return true if client id is set; otherwise, false */ public boolean hasClientId() { - return clientId.isPresent(); + return clientId != null; } /** @@ -71,7 +76,11 @@ public boolean hasClientId() { * @throws NoSuchElementException if this instance does not have a clientId */ public String getClientId() throws NoSuchElementException { - return clientId.orElseThrow(NoSuchElementException::new); + if (clientId == null) { + throw new NoSuchElementException(); + } + + return clientId; }
src/main/java/com/rapid7/appspider/Report.java+12 −9 modified@@ -7,6 +7,7 @@ import hudson.FilePath; import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -27,15 +28,17 @@ public class Report { private final ScanSettings settings; private final LoggerFacade log; - public Report(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { - + public static Report createInstanceOrThrow(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { if (Objects.isNull(client)) throw new IllegalArgumentException("client cannot be null"); if (Objects.isNull(settings)) throw new IllegalArgumentException("settings cannot be null"); if (Objects.isNull(log)) throw new IllegalArgumentException("log cannot be null"); + return new Report(client, settings, log); + } + private Report(EnterpriseClient client, ScanSettings settings, LoggerFacade log) { this.client = client; this.settings = settings; this.log = log; @@ -51,24 +54,24 @@ public boolean saveReport(AuthenticationModel authModel, String scanId, FilePath log.println("Generating xml report and downloading report zip file to:" + directory); Optional<String> maybeAuthToken = client.login(authModel); - if (!maybeAuthToken.isPresent()) { + if (maybeAuthToken.isEmpty()) { log.println("Unauthorized: unable to retrieve vulnerabilities summary and report.zip"); return false; } String authToken = maybeAuthToken.get(); String dateTimeStamp = "_" + getNowAsFormattedString(); - Path vulnerabiltiesFilename = Paths.get(reportFolder, settings.getReportName() + dateTimeStamp + ".xml"); + Path vulnerabilitiesFilename = Paths.get(reportFolder, settings.getReportName() + dateTimeStamp + ".xml"); Path reportZipFilename = Paths.get(reportFolder, settings.getReportName() + dateTimeStamp + ".zip"); - return saveVulnerabilities(authToken, scanId, vulnerabiltiesFilename) && + return saveVulnerabilities(authToken, scanId, vulnerabilitiesFilename) && saveReportZip(authToken, scanId, reportZipFilename); } private boolean saveVulnerabilities(String authToken, String scanId, Path file) { Optional<String> xml = client.getVulnerabilitiesSummaryXml(authToken, scanId); - if (!xml.isPresent()) { + if (xml.isEmpty()) { log.println("Unable to retrieve vulnerabilities summary."); return false; } @@ -82,9 +85,9 @@ private boolean saveReportZip(String authToken, String scanId, Path file) { private boolean saveXmlFile(Path file, String content) { try { if (!Files.exists(file) ) - file = Files.createFile(file); + Files.createFile(file); - try (BufferedWriter writer = Files.newBufferedWriter(file)) { + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { writer.write(content); writer.flush(); } @@ -100,7 +103,7 @@ private boolean saveInputStreamToFile(Path file, InputStream inputStream) { try { if (!Files.exists(file) ) - file = Files.createFile(file); + Files.createFile(file); try (InputStream bufferedInput = new BufferedInputStream(inputStream); OutputStream outputStream = Files.newOutputStream(file)) {
src/main/java/com/rapid7/jenkinspider/PostBuildScan.java+55 −33 modified@@ -4,12 +4,11 @@ import com.rapid7.appspider.datatransferobjects.ClientIdNamePair; import com.rapid7.appspider.models.AuthenticationModel; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; -import hudson.model.AbstractBuild; -import hudson.model.AbstractProject; -import hudson.model.BuildListener; +import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; @@ -20,6 +19,7 @@ import jenkins.model.Jenkins; import org.apache.commons.validator.routines.UrlValidator; import org.apache.http.impl.client.CloseableHttpClient; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -44,7 +44,7 @@ */ public class PostBuildScan extends Notifier { - private String clientName; // Not set to final since it may change + private final String clientName; // Not set to final since it may change private final String configName; // Not set to final since it may change // if user decided to create a new scan config @@ -135,15 +135,14 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen log.println("Value of Allow Self-Signed certificate : " + allowSelfSignedCertificate); try { - ContentHelper contentHelper = new ContentHelper(log); + ContentHelper contentHelper = ContentHelper.createInstanceOrThrow(log); EnterpriseRestClient client = new EnterpriseRestClient( - new HttpClientService(new HttpClientFactory(allowSelfSignedCertificate).getClient(), contentHelper, - log), - appSpiderEntUrl, new ApiSerializer(log), contentHelper, log); + HttpClientService.createInstanceOrThrow(HttpClientFactory.createInstanceOrThrow(allowSelfSignedCertificate).getClient(), contentHelper, log), + appSpiderEntUrl, ApiSerializer.createInstanceOrThrow(log), contentHelper, log); ScanSettings settings = new ScanSettings(configName, reportName, true, generateReport, scanConfigName, scanConfigUrl, scanConfigEngineGroupName); - Scan scan = new Scan(client, settings, log); + DastScan scan = DastScan.createInstanceOrThrow(client, settings, log); if (!scan.process(authModel)) return false; @@ -158,7 +157,7 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen return false; } - return new Report(client, settings, log).saveReport(authModel, scanId, filePath); + return Report.createInstanceOrThrow(client, settings, log).saveReport(authModel, scanId, filePath); } catch (IllegalArgumentException | SslContextCreationException e) { log.println(e.toString()); @@ -215,8 +214,8 @@ public DescriptorImp() { * prevent the form from being saved. It just means that a message will * be displayed to the user. */ - public FormValidation doCheckappSpiderEntUrl(@QueryParameter String value) { - if (value.length() == 0) + public FormValidation doCheckAppSpiderEntUrl(@QueryParameter String value) { + if (value.isEmpty()) return FormValidation.error("Please set a value"); if (value.length() < 4) return FormValidation.warning("Isn't the value too short?"); @@ -234,6 +233,7 @@ public boolean isApplicable(Class<? extends AbstractProject> aClass) { /** * @return Display Name of the plugin */ + @NonNull @Override public String getDisplayName() { return "Scan build using AppSpider"; @@ -307,8 +307,8 @@ public void setAppSpiderClientName(String appSpiderClientName) { public AuthenticationModel buildAuthenticationModel() { return appSpiderClientId != null && !appSpiderClientId.isEmpty() && appSpiderEnableMultiClientOrSysAdmin - ? new AuthenticationModel(appSpiderUsername, Secret.toString(appSpiderPassword), Optional.of(appSpiderClientId)) - : new AuthenticationModel(appSpiderUsername, Secret.toString(appSpiderPassword), Optional.empty()); + ? new AuthenticationModel(appSpiderUsername, Secret.toString(appSpiderPassword), appSpiderClientId) + : new AuthenticationModel(appSpiderUsername, Secret.toString(appSpiderPassword), null); } private LoggerFacade buildLoggerFacade() { @@ -359,11 +359,11 @@ public boolean isVerboseEnabled() { private EnterpriseRestClient buildEnterpriseClient(CloseableHttpClient httpClient, String endpoint) { LoggerFacade logger = buildLoggerFacade(); - ContentHelper contentHelper = new ContentHelper(logger); + ContentHelper contentHelper = ContentHelper.createInstanceOrThrow(logger); return new EnterpriseRestClient( - new HttpClientService(httpClient, contentHelper, logger), + HttpClientService.createInstanceOrThrow(httpClient, contentHelper, logger), endpoint, - new ApiSerializer(logger), + ApiSerializer.createInstanceOrThrow(logger), contentHelper, logger); } @@ -381,8 +381,16 @@ public boolean configure(StaplerRequest req, net.sf.json.JSONObject formData) th * all the available scan configs * @return ListBoxModel containing the scan config names */ - public ListBoxModel doFillClientNameItems() throws InterruptedException { - Map<String, String> idToNames = getClientIdNamePairsWithRetry(NUMBER_OF_GET_CLIENT_ATTEMPTS, DELAY_BETWEEN_GET_CLIENT_ATTEMPTS) + public ListBoxModel doFillClientNameItems(@AncestorInPath Item item) throws InterruptedException { + if (item == null) { // no context + return emptyListBoxModel("[Select a client name]"); + } + boolean hasConfigurePermission = item.hasPermission(Item.CONFIGURE); + if (!hasConfigurePermission) { + return emptyListBoxModel("[Select a client name]"); + } + + Map<String, String> idToNames = getClientIdNamePairsWithRetry() .stream() .collect(Collectors.toMap(ClientIdNamePair::getName, ClientIdNamePair::getId)); this.clientIdToNames = Optional.of(idToNames); @@ -405,10 +413,15 @@ public ListBoxModel doFillClientNameItems() throws InterruptedException { * all the available scan configs * @return ListBoxModel containing the scan config names */ - public ListBoxModel doFillConfigNameItems(@QueryParameter String clientName) { + public ListBoxModel doFillConfigNameItems(@QueryParameter String clientName, @AncestorInPath Item item) { + if (item == null) { // no context + return emptyListBoxModel("[Select a scan config name]"); + } + boolean hasConfigurePermission = item.hasPermission(Item.CONFIGURE); - if (clientName == null || clientName.equals(CLIENT_NAME_PLACEHOLDER_TEXT) || !clientIdToNames.isPresent()){ - return buildListBoxModel("[Select an engine group name]", new String[0]); + if (!hasConfigurePermission || clientName == null || clientName.equals(CLIENT_NAME_PLACEHOLDER_TEXT) || + clientIdToNames.isEmpty()){ + return emptyListBoxModel("[Select a scan config name]"); } appSpiderClientId = ""; @@ -424,11 +437,21 @@ public ListBoxModel doFillConfigNameItems(@QueryParameter String clientName) { * all the available scan engine groups * @return ListBoxModel containing engine details */ - public ListBoxModel doFillScanConfigEngineGroupNameItems() { + public ListBoxModel doFillScanConfigEngineGroupNameItems(@AncestorInPath Item item) { + if (item == null) { // no context + return emptyListBoxModel("[Select an engine group name]"); + } + boolean hasConfigurePermission = item.hasPermission(Item.CONFIGURE); + if (!hasConfigurePermission) { + return emptyListBoxModel("[Select an engine group name]"); + } scanConfigEngines = getEngineGroups(); return buildListBoxModel("[Select an engine group name]", scanConfigEngines); } + private static ListBoxModel emptyListBoxModel(String introduction) { + return buildListBoxModel(introduction, new String[0]); + } private static ListBoxModel buildListBoxModel(String introduction, String[] items) { ListBoxModel model = new ListBoxModel(); model.add(introduction); // Adding a default "Pick a scan configuration" entry @@ -468,7 +491,7 @@ public FormValidation doTestCredentials(@QueryParameter("appSpiderAllowSelfSigne public FormValidation doValidateNewScanConfig(@QueryParameter("scanConfigName") final String scanConfigName, @QueryParameter("scanConfigUrl") final String scanConfigUrl) { try { - final String ALPHANUMERIC_REGEX = "^[a-zA-Z0_\\-\\.]*$"; + final String ALPHANUMERIC_REGEX = "^[a-zA-Z0_\\-.]*$"; if (!scanConfigName.matches(ALPHANUMERIC_REGEX) || scanConfigName.contains(" ") || scanConfigName.isEmpty()) { @@ -486,8 +509,8 @@ public FormValidation doValidateNewScanConfig(@QueryParameter("scanConfigName") return FormValidation.ok("Valid scan configuration name and url."); } catch (IOException /* | MalformedURLException */ e) { buildLoggerFacade().println(e.getMessage() + " from doValidateNewScanConfig"); - return FormValidation.error("Unable to connect to \"" + scanConfigUrl +"\". Try again in a few mins or " + - "try another url"); + return FormValidation.error("Unable to connect to \"" + scanConfigUrl + + "\". Try again in a few minutes or try another url"); } } @@ -506,14 +529,14 @@ private String[] getConfigNames() { new String[0]); } - private List<ClientIdNamePair> getClientIdNamePairsWithRetry(int attempts, long delayInMilliseconds) throws InterruptedException { + private List<ClientIdNamePair> getClientIdNamePairsWithRetry() throws InterruptedException { - for (int i = 0; i< attempts; i++) { + for (int i = 0; i< DescriptorImp.NUMBER_OF_GET_CLIENT_ATTEMPTS; i++) { List<ClientIdNamePair> pairs = getClientIdNamePairs(); if (!pairs.isEmpty()) { return pairs; } - Thread.sleep(delayInMilliseconds); + Thread.sleep(DescriptorImp.DELAY_BETWEEN_GET_CLIENT_ATTEMPTS); } return Collections.emptyList(); @@ -541,7 +564,7 @@ private <T> T executeRequest(String endpoint, boolean appSpiderAllowSelfSignedCe if (Objects.isNull(supplier)) return errorResult; - try (CloseableHttpClient httpClient = new HttpClientFactory(appSpiderAllowSelfSignedCertificate).getClient()) { + try (CloseableHttpClient httpClient = HttpClientFactory.createInstanceOrThrow(appSpiderAllowSelfSignedCertificate).getClient()) { EnterpriseClient client = buildEnterpriseClient(httpClient, endpoint); return supplier.apply(client); } catch (IOException | SslContextCreationException e) { @@ -554,15 +577,14 @@ private <T> T executeRequestWithAuthorization(AuthorizedRequest<T> request, T er if (Objects.isNull(request)) return errorResult; - try (CloseableHttpClient httpClient = new HttpClientFactory(appSpiderAllowSelfSignedCertificate).getClient()) { + try (CloseableHttpClient httpClient = HttpClientFactory.createInstanceOrThrow(appSpiderAllowSelfSignedCertificate).getClient()) { EnterpriseClient client = buildEnterpriseClient(httpClient, appSpiderEntUrl); if (Objects.isNull(appSpiderPassword)) { return errorResult; } Optional<String> maybeAuthKey = client.login(buildAuthenticationModel()); - if (!maybeAuthKey.isPresent()) { - FormValidation.error("Unauthorized"); + if (maybeAuthKey.isEmpty()) { return errorResult; } return request.executeRequest(client, maybeAuthKey.get());
src/test/java/com/rapid7/appspider/datatransferobjects/ScanResultTests.java+9 −13 modified@@ -6,6 +6,7 @@ import static org.mockito.Mockito.when; import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -19,43 +20,38 @@ class ScanResultTests { @Mock private JSONObject jsonObject; - @Test - void ctor_throwsIllegalArgumentException_whenJsonObjectIsNull() { - assertThrows(IllegalArgumentException.class, () -> new ScanResult((JSONObject) null)); - } - @Test void ctor_throwsIllegalArgumentException_whenJsonObjectThrowsJSONException() { when(jsonObject.getBoolean(any(String.class))).thenThrow(new JSONException("test")); - assertThrows(IllegalArgumentException.class, () -> new ScanResult(jsonObject)); + assertThrows(IllegalArgumentException.class, () -> ScanResult.createInstanceFromJsonOrThrow(jsonObject)); } @Test void ctor_throwsIllegalArgumentException_whenReturnsEmptyObject() { when(jsonObject.getJSONObject("Scan")).thenReturn(new JSONObject()); - assertThrows(IllegalArgumentException.class, () -> new ScanResult(jsonObject)); + assertThrows(IllegalArgumentException.class, () -> ScanResult.createInstanceFromJsonOrThrow(jsonObject)); } @Test void getScanId_returnsGivenScanId() { ScanResult result = new ScanResult(true, "id"); - assertEquals("id", result.getScanId()); + Assertions.assertEquals("id", result.getScanId()); } @Test void isSuccess_returnsGivenIsSuccess() { ScanResult result = new ScanResult(true, "id"); - assertEquals(true, result.isSuccess()); + Assertions.assertTrue(result.isSuccess()); } @Test void getScanId_returnsGivenScanIdInJSON() { when(jsonObject.getJSONObject("Scan")).thenReturn(jsonObject); when(jsonObject.getString("Id")).thenReturn("id"); - ScanResult result = new ScanResult(jsonObject); + ScanResult result = ScanResult.createInstanceFromJsonOrThrow(jsonObject); - assertEquals("id", result.getScanId()); + Assertions.assertEquals("id", result.getScanId()); } @Test @@ -64,8 +60,8 @@ void isSuccess_returnsGivenIsSuccessInJSON() { when(jsonObject.getString("Id")).thenReturn("id"); when(jsonObject.getBoolean("IsSuccess")).thenReturn(true); - ScanResult result = new ScanResult(jsonObject); + ScanResult result = ScanResult.createInstanceFromJsonOrThrow(jsonObject); - assertEquals(true, result.isSuccess()); + Assertions.assertTrue(result.isSuccess()); } }
src/test/java/com/rapid7/appspider/EnterpriseClientTestContext.java+9 −9 modified@@ -22,7 +22,7 @@ import java.util.*; import java.util.stream.Collectors; -import static com.rapid7.appspider.Utility.toStringArray; +import static com.rapid7.appspider.FunctionalUtility.toStringArray; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -47,22 +47,22 @@ public class EnterpriseClientTestContext implements AutoCloseable { private final HttpClient mockHttpClient; private String expectedAuthToken; - private String configId; - private String configName; - private String clientId; - private String clientName; + private final String configId; + private final String configName; + private final String clientId; + private final String clientName; private String expectedScanId; private EnterpriseClient enterpriseClient; - private String url; + private final String url; private Map<String, String> expectedEngineGroupsIdsByName; private Map<String, String> expectedEngineGroupsNamesForClient; private final List<EngineStub> engineGroupDetails; EnterpriseClientTestContext(String url) { this.url = url; mockLogger = mock(LoggerFacade.class); - mockContentHelper = new ContentHelper(mockLogger); - mockApiSerializer = new ApiSerializer(mockLogger); + mockContentHelper = ContentHelper.createInstanceOrThrow(mockLogger); + mockApiSerializer = ApiSerializer.createInstanceOrThrow(mockLogger); expectedAuthToken = ""; // set by isSuccess state of each test, just being reset here expectedScanId = ""; configId = "3249E3F6-3B33-4D4E-93EB-2F464AB424A8"; @@ -157,7 +157,7 @@ public EnterpriseClientTestContext arrangeExpectedValues(boolean isSuccess) { } public EnterpriseClientTestContext configureEnterpriseClient() { - enterpriseClient = new EnterpriseRestClient(new HttpClientService(mockHttpClient, mockContentHelper, mockLogger), url, mockApiSerializer, mockContentHelper, mockLogger); + enterpriseClient = new EnterpriseRestClient(HttpClientService.createInstanceOrThrow(mockHttpClient, mockContentHelper, mockLogger), url, mockApiSerializer, mockContentHelper, mockLogger); return this; }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-xxv9-w5hm-328jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-28155ghsaADVISORY
- www.jenkins.io/security/advisory/2024-03-06/ghsavendor-advisoryWEB
- www.openwall.com/lists/oss-security/2024/03/06/3ghsaWEB
- github.com/jenkinsci/appspider-build-scanner-plugin/commit/1677f098fbe4c71d782fc4c7bab5f972c575a86dghsaWEB
News mentions
1- Jenkins Security Advisory 2024-03-06Jenkins Security Advisories · Mar 6, 2024