From e685b8050531228e5826bc803b41a6fedc0c3685 Mon Sep 17 00:00:00 2001 From: Hans Aikema Date: Sat, 19 Aug 2023 13:58:53 +0200 Subject: [PATCH] feat: Add support for Nexus v3 to NexusAnalyzer (#5849) --- .../analyzer/NexusAnalyzer.java | 40 ++- .../data/nexus/NexusSearch.java | 196 +------------ .../data/nexus/NexusV2Search.java | 211 ++++++++++++++ .../data/nexus/NexusV3Search.java | 259 ++++++++++++++++++ .../data/nexus/NexusV2SearchTest.java | 84 ++++++ ...SearchTest.java => NexusV3SearchTest.java} | 23 +- core/src/test/resources/logback-test.xml | 2 +- 7 files changed, 608 insertions(+), 207 deletions(-) create mode 100644 core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV2Search.java create mode 100644 core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV3Search.java create mode 100644 core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV2SearchTest.java rename core/src/test/java/org/owasp/dependencycheck/data/nexus/{NexusSearchTest.java => NexusV3SearchTest.java} (83%) diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/NexusAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/NexusAnalyzer.java index b5828678402..6a657cf916c 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/NexusAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/NexusAnalyzer.java @@ -22,6 +22,8 @@ import org.owasp.dependencycheck.analyzer.exception.AnalysisException; import org.owasp.dependencycheck.data.nexus.MavenArtifact; import org.owasp.dependencycheck.data.nexus.NexusSearch; +import org.owasp.dependencycheck.data.nexus.NexusV2Search; +import org.owasp.dependencycheck.data.nexus.NexusV3Search; import org.owasp.dependencycheck.dependency.Confidence; import org.owasp.dependencycheck.dependency.Dependency; import org.owasp.dependencycheck.dependency.Evidence; @@ -35,6 +37,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Locale; import javax.annotation.concurrent.ThreadSafe; import org.owasp.dependencycheck.dependency.EvidenceType; import org.owasp.dependencycheck.exception.InitializationException; @@ -169,17 +172,38 @@ public void prepareFileTypeAnalyzer(Engine engine) throws InitializationExceptio if (isEnabled()) { final boolean useProxy = useProxy(); LOGGER.debug("Using proxy: {}", useProxy); - try { - searcher = new NexusSearch(getSettings(), useProxy); - if (!searcher.preflightRequest()) { - setEnabled(false); - throw new InitializationException("There was an issue getting Nexus status. Disabling analyzer."); - } - } catch (MalformedURLException mue) { + searcher = createNexusSearchOrDisable(useProxy); + } + } + + /** + * Creates a NexusSearch for the appropriate Nexus version (Nexus V2 and V3 supported). + *

+ * If errors are encountered creating or validating the NexusSearch it disables this analyzer. + * + * @param useProxy Whether a proxy is to be used + * @return A NexusSearch appropriate for the configured ANALYZER_NEXUS_URL + * @throws InitializationException Upon errors creating of validating the ANALYZER_NEXUS_URL + */ + private NexusSearch createNexusSearchOrDisable(boolean useProxy) throws InitializationException { + final Settings settings = getSettings(); + final String nexusRootURL = settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL); + final NexusSearch result; + try { + if (nexusRootURL.toLowerCase(Locale.ROOT).contains("service/local/")) { + result = new NexusV2Search(settings, useProxy); + } else { + result = new NexusV3Search(settings, useProxy); + } + if (!result.preflightRequest()) { setEnabled(false); - throw new InitializationException("Malformed URL to Nexus", mue); + throw new InitializationException("There was an error getting Nexus status. Disabling NexusAnalyzer."); } + } catch (MalformedURLException mue) { + setEnabled(false); + throw new InitializationException("Malformed URL to Nexus. Disabling NexusAnalyzer", mue); } + return result; } /** diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusSearch.java b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusSearch.java index 4670966fe19..264d0a84c09 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusSearch.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusSearch.java @@ -13,76 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * Copyright (c) 2014 Jeremy Long. All Rights Reserved. + * Copyright (c) 2023 Jeremy Long. All Rights Reserved. */ package org.owasp.dependencycheck.data.nexus; -import java.io.FileNotFoundException; import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import javax.annotation.concurrent.ThreadSafe; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.owasp.dependencycheck.utils.Settings; - -import org.owasp.dependencycheck.utils.URLConnectionFactory; -import org.owasp.dependencycheck.utils.XmlUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.xml.sax.SAXException; - -/** - * Class of methods to search Nexus repositories. - * - * @author colezlaw - */ -@ThreadSafe -public class NexusSearch { - - /** - * The root URL for the Nexus repository service. - */ - private final URL rootURL; - - /** - * Whether to use the Proxy when making requests. - */ - private final boolean useProxy; - /** - * The configured settings. - */ - private final Settings settings; - /** - * Used for logging. - */ - private static final Logger LOGGER = LoggerFactory.getLogger(NexusSearch.class); - - /** - * Creates a NexusSearch for the given repository URL. - * - * @param settings the configured settings - * @param useProxy flag indicating if the proxy settings should be used - * @throws java.net.MalformedURLException thrown if the configured URL is - * invalid - */ - public NexusSearch(Settings settings, boolean useProxy) throws MalformedURLException { - this.settings = settings; - this.useProxy = useProxy; - - final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL); - LOGGER.debug("Nexus Search URL: {}", searchUrl); - this.rootURL = new URL(searchUrl); - - } +public interface NexusSearch { /** * Searches the configured Nexus repository for the given sha1 hash. If the * artifact is found, a MavenArtifact is populated with the @@ -91,135 +28,14 @@ public NexusSearch(Settings settings, boolean useProxy) throws MalformedURLExcep * @param sha1 The SHA-1 hash string for which to search * @return the populated Maven coordinates * @throws IOException if it's unable to connect to the specified repository - * or if the specified artifact is not found. + * or if the specified artifact is not found. */ - public MavenArtifact searchSha1(String sha1) throws IOException { - if (null == sha1 || !sha1.matches("^[0-9A-Fa-f]{40}$")) { - throw new IllegalArgumentException("Invalid SHA1 format"); - } - - final URL url = new URL(rootURL, String.format("identify/sha1/%s", - sha1.toLowerCase())); - - LOGGER.debug("Searching Nexus url {}", url); - - // Determine if we need to use a proxy. The rules: - // 1) If the proxy is set, AND the setting is set to true, use the proxy - // 2) Otherwise, don't use the proxy (either the proxy isn't configured, - // or proxy is specifically set to false - final HttpURLConnection conn; - final URLConnectionFactory factory = new URLConnectionFactory(settings); - conn = factory.createHttpURLConnection(url, useProxy); - conn.setDoOutput(true); - final String authHeader = buildHttpAuthHeaderValue(); - if (!authHeader.isEmpty()) { - conn.addRequestProperty("Authorization", authHeader); - } - - // JSON would be more elegant, but there's not currently a dependency - // on JSON, so don't want to add one just for this - conn.addRequestProperty("Accept", "application/xml"); - conn.connect(); - - switch (conn.getResponseCode()) { - case 200: - try { - final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder(); - final Document doc = builder.parse(conn.getInputStream()); - final XPath xpath = XPathFactory.newInstance().newXPath(); - final String groupId = xpath - .evaluate( - "/org.sonatype.nexus.rest.model.NexusArtifact/groupId", - doc); - final String artifactId = xpath.evaluate( - "/org.sonatype.nexus.rest.model.NexusArtifact/artifactId", - doc); - final String version = xpath - .evaluate( - "/org.sonatype.nexus.rest.model.NexusArtifact/version", - doc); - final String link = xpath - .evaluate( - "/org.sonatype.nexus.rest.model.NexusArtifact/artifactLink", - doc); - final String pomLink = xpath - .evaluate( - "/org.sonatype.nexus.rest.model.NexusArtifact/pomLink", - doc); - final MavenArtifact ma = new MavenArtifact(groupId, artifactId, version); - if (link != null && !link.isEmpty()) { - ma.setArtifactUrl(link); - } - if (pomLink != null && !pomLink.isEmpty()) { - ma.setPomUrl(pomLink); - } - return ma; - } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) { - // Anything else is jacked-up XML stuff that we really can't recover - // from well - throw new IOException(e.getMessage(), e); - } - case 404: - throw new FileNotFoundException("Artifact not found in Nexus"); - default: - LOGGER.debug("Could not connect to Nexus received response code: {} {}", - conn.getResponseCode(), conn.getResponseMessage()); - throw new IOException("Could not connect to Nexus"); - } - } + MavenArtifact searchSha1(String sha1) throws IOException; /** * Do a preflight request to see if the repository is actually working. * - * @return whether the repository is listening and returns the /status URL - * correctly - */ - public boolean preflightRequest() { - final HttpURLConnection conn; - try { - final URL url = new URL(rootURL, "status"); - final URLConnectionFactory factory = new URLConnectionFactory(settings); - conn = factory.createHttpURLConnection(url, useProxy); - conn.addRequestProperty("Accept", "application/xml"); - final String authHeader = buildHttpAuthHeaderValue(); - if (!authHeader.isEmpty()) { - conn.addRequestProperty("Authorization", authHeader); - } - conn.connect(); - if (conn.getResponseCode() != 200) { - LOGGER.warn("Expected 200 result from Nexus, got {}", conn.getResponseCode()); - return false; - } - final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder(); - - final Document doc = builder.parse(conn.getInputStream()); - if (!"status".equals(doc.getDocumentElement().getNodeName())) { - LOGGER.warn("Expected root node name of status, got {}", doc.getDocumentElement().getNodeName()); - return false; - } - } catch (IOException | ParserConfigurationException | SAXException e) { - LOGGER.warn("Pre-flight request to Nexus failed: ", e); - return false; - } - return true; - } - - /** - * Constructs the base64 encoded basic authentication header value. - * - * @return the base64 encoded basic authentication header value + * @return whether the repository is listening and returns the expected status response */ - private String buildHttpAuthHeaderValue() { - final String user = settings.getString(Settings.KEYS.ANALYZER_NEXUS_USER, ""); - final String pass = settings.getString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD, ""); - String result = ""; - if (user.isEmpty() || pass.isEmpty()) { - LOGGER.debug("Skip authentication as user and/or password for nexus is empty"); - } else { - final String auth = user + ':' + pass; - final String base64Auth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - result = "Basic " + base64Auth; - } - return result; - } + boolean preflightRequest(); } diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV2Search.java b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV2Search.java new file mode 100644 index 00000000000..d1242a77dbb --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV2Search.java @@ -0,0 +1,211 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2014 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.nexus; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import javax.annotation.concurrent.ThreadSafe; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.owasp.dependencycheck.utils.Settings; + +import org.owasp.dependencycheck.utils.URLConnectionFactory; +import org.owasp.dependencycheck.utils.XmlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Class of methods to search Nexus repositories. + * + * @author colezlaw + */ +@ThreadSafe +public class NexusV2Search implements NexusSearch { + + /** + * The root URL for the Nexus repository service. + */ + private final URL rootURL; + + /** + * Whether to use the Proxy when making requests. + */ + private final boolean useProxy; + /** + * The configured settings. + */ + private final Settings settings; + /** + * Used for logging. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(NexusV2Search.class); + + /** + * Creates a NexusSearch for the given repository URL. + * + * @param settings the configured settings + * @param useProxy flag indicating if the proxy settings should be used + * @throws java.net.MalformedURLException thrown if the configured URL is + * invalid + */ + public NexusV2Search(Settings settings, boolean useProxy) throws MalformedURLException { + this.settings = settings; + this.useProxy = useProxy; + + final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL); + LOGGER.debug("Nexus Search URL: {}", searchUrl); + this.rootURL = new URL(searchUrl); + + } + + @Override + public MavenArtifact searchSha1(String sha1) throws IOException { + if (null == sha1 || !sha1.matches("^[0-9A-Fa-f]{40}$")) { + throw new IllegalArgumentException("Invalid SHA1 format"); + } + + final URL url = new URL(rootURL, String.format("identify/sha1/%s", + sha1.toLowerCase())); + + LOGGER.debug("Searching Nexus url {}", url); + + // Determine if we need to use a proxy. The rules: + // 1) If the proxy is set, AND the setting is set to true, use the proxy + // 2) Otherwise, don't use the proxy (either the proxy isn't configured, + // or proxy is specifically set to false + final HttpURLConnection conn; + final URLConnectionFactory factory = new URLConnectionFactory(settings); + conn = factory.createHttpURLConnection(url, useProxy); + conn.setDoOutput(true); + final String authHeader = buildHttpAuthHeaderValue(); + if (!authHeader.isEmpty()) { + conn.addRequestProperty("Authorization", authHeader); + } + + // JSON would be more elegant, but there's not currently a dependency + // on JSON, so don't want to add one just for this + conn.addRequestProperty("Accept", "application/xml"); + conn.connect(); + + switch (conn.getResponseCode()) { + case 200: + try { + final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder(); + final Document doc = builder.parse(conn.getInputStream()); + final XPath xpath = XPathFactory.newInstance().newXPath(); + final String groupId = xpath + .evaluate( + "/org.sonatype.nexus.rest.model.NexusArtifact/groupId", + doc); + final String artifactId = xpath.evaluate( + "/org.sonatype.nexus.rest.model.NexusArtifact/artifactId", + doc); + final String version = xpath + .evaluate( + "/org.sonatype.nexus.rest.model.NexusArtifact/version", + doc); + final String link = xpath + .evaluate( + "/org.sonatype.nexus.rest.model.NexusArtifact/artifactLink", + doc); + final String pomLink = xpath + .evaluate( + "/org.sonatype.nexus.rest.model.NexusArtifact/pomLink", + doc); + final MavenArtifact ma = new MavenArtifact(groupId, artifactId, version); + if (link != null && !link.isEmpty()) { + ma.setArtifactUrl(link); + } + if (pomLink != null && !pomLink.isEmpty()) { + ma.setPomUrl(pomLink); + } + return ma; + } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) { + // Anything else is jacked-up XML stuff that we really can't recover + // from well + throw new IOException(e.getMessage(), e); + } + case 404: + throw new FileNotFoundException("Artifact not found in Nexus"); + default: + LOGGER.debug("Could not connect to Nexus received response code: {} {}", + conn.getResponseCode(), conn.getResponseMessage()); + throw new IOException("Could not connect to Nexus"); + } + } + + @Override + public boolean preflightRequest() { + final HttpURLConnection conn; + try { + final URL url = new URL(rootURL, "status"); + final URLConnectionFactory factory = new URLConnectionFactory(settings); + conn = factory.createHttpURLConnection(url, useProxy); + conn.addRequestProperty("Accept", "application/xml"); + final String authHeader = buildHttpAuthHeaderValue(); + if (!authHeader.isEmpty()) { + conn.addRequestProperty("Authorization", authHeader); + } + conn.connect(); + if (conn.getResponseCode() != 200) { + LOGGER.warn("Expected 200 result from Nexus, got {}", conn.getResponseCode()); + return false; + } + final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder(); + + final Document doc = builder.parse(conn.getInputStream()); + if (!"status".equals(doc.getDocumentElement().getNodeName())) { + LOGGER.warn("Expected root node name of status, got {}", doc.getDocumentElement().getNodeName()); + return false; + } + } catch (IOException | ParserConfigurationException | SAXException e) { + LOGGER.warn("Pre-flight request to Nexus failed: ", e); + return false; + } + return true; + } + + /** + * Constructs the base64 encoded basic authentication header value. + * + * @return the base64 encoded basic authentication header value + */ + private String buildHttpAuthHeaderValue() { + final String user = settings.getString(Settings.KEYS.ANALYZER_NEXUS_USER, ""); + final String pass = settings.getString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD, ""); + String result = ""; + if (user.isEmpty() || pass.isEmpty()) { + LOGGER.debug("Skip authentication as user and/or password for nexus is empty"); + } else { + final String auth = user + ':' + pass; + final String base64Auth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + result = "Basic " + base64Auth; + } + return result; + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV3Search.java b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV3Search.java new file mode 100644 index 00000000000..058b5329a38 --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/data/nexus/NexusV3Search.java @@ -0,0 +1,259 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2023 Hans Aikema. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.nexus; + +import org.owasp.dependencycheck.utils.Settings; +import org.owasp.dependencycheck.utils.URLConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.concurrent.ThreadSafe; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class of methods to search Nexus v3 repositories. + * + * @author Hans Aikema + */ +@ThreadSafe +public class NexusV3Search implements NexusSearch { + + /** + * By default, NexusV3Search accepts only classifier-less artifacts. + *

+ * This prevents, among others, sha1-collisions for empty jars on empty javadoc/sources jars. + * See e.g. issues #5559 and #5118 + */ + private final Set acceptedClassifiers = new HashSet<>(); + + /** + * The root URL for the Nexus repository service. + */ + private final URL rootURL; + + /** + * Whether to use the Proxy when making requests. + */ + private final boolean useProxy; + /** + * The configured settings. + */ + private final Settings settings; + /** + * Used for logging. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(NexusV3Search.class); + + /** + * Creates a NexusV3Search for the given repository URL. + * + * @param settings the configured settings + * @param useProxy flag indicating if the proxy settings should be used + * @throws MalformedURLException thrown if the configured URL is + * invalid + */ + public NexusV3Search(Settings settings, boolean useProxy) throws MalformedURLException { + this.settings = settings; + this.useProxy = useProxy; + this.acceptedClassifiers.add(null); + final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL); + LOGGER.debug("Nexus Search URL: {}", searchUrl); + this.rootURL = new URL(searchUrl); + + } + + @Override + public MavenArtifact searchSha1(String sha1) throws IOException { + if (null == sha1 || !sha1.matches("^[0-9A-Fa-f]{40}$")) { + throw new IllegalArgumentException("Invalid SHA1 format"); + } + + final List collectedMatchingArtifacts = new ArrayList<>(1); + + String continuationToken = retrievePageAndAddMatchingArtifact(collectedMatchingArtifacts, sha1, null); + while (continuationToken != null && collectedMatchingArtifacts.isEmpty()) { + continuationToken = retrievePageAndAddMatchingArtifact(collectedMatchingArtifacts, sha1, continuationToken); + } + if (collectedMatchingArtifacts.isEmpty()) { + throw new FileNotFoundException("Artifact not found in Nexus"); + } else { + return collectedMatchingArtifacts.get(0); + } + } + + private String retrievePageAndAddMatchingArtifact(List collectedMatchingArtifacts, String sha1, String continuationToken) + throws IOException { + final URL url; + LOGGER.debug("Search with continuation token {}", continuationToken); + if (continuationToken == null) { + url = new URL(rootURL, String.format("v1/search/?sha1=%s", + sha1.toLowerCase())); + } else { + url = new URL(rootURL, String.format("v1/search/?sha1=%s&continuationToken=%s", + sha1.toLowerCase(), continuationToken)); + } + + LOGGER.debug("Searching Nexus url {}", url); + // Determine if we need to use a proxy. The rules: + // 1) If the proxy is set, AND the setting is set to true, use the proxy + // 2) Otherwise, don't use the proxy (either the proxy isn't configured, + // or proxy is specifically set to false + final HttpURLConnection conn; + final URLConnectionFactory factory = new URLConnectionFactory(settings); + conn = factory.createHttpURLConnection(url, useProxy); + conn.setDoOutput(true); + final String authHeader = buildHttpAuthHeaderValue(); + if (!authHeader.isEmpty()) { + conn.addRequestProperty("Authorization", authHeader); + } + + conn.addRequestProperty("Accept", "application/json"); + conn.connect(); + final String nextContinuationToken; + if (conn.getResponseCode() == 200) { + nextContinuationToken = parseResponse(conn, sha1, collectedMatchingArtifacts); + } else { + LOGGER.debug("Could not connect to Nexus received response code: {} {}", + conn.getResponseCode(), conn.getResponseMessage()); + throw new IOException(String.format("Could not connect to Nexus, HTTP response code %d", conn.getResponseCode())); + } + return nextContinuationToken; + } + + private String parseResponse(HttpURLConnection conn, String sha1, List matchingArtifacts) throws IOException { + try (InputStream in = new BufferedInputStream(conn.getInputStream()); + JsonReader jsonReader = Json.createReader(in)) { + final JsonObject jsonResponse = jsonReader.readObject(); + final String continuationToken = jsonResponse.getString("continuationToken", null); + final JsonArray components = jsonResponse.getJsonArray("items"); + boolean found = false; + for (int i = 0; i < components.size() && !found; i++) { + boolean jarFound = false; + boolean pomFound = false; + String downloadUrl = null; + String groupId = null; + String artifactId = null; + String version = null; + String pomUrl = null; + + final JsonObject component = components.getJsonObject(i); + + final String format = components.getJsonObject(0).getString("format", "unknown"); + if ("maven2".equals(format)) { + final JsonArray assets = component.getJsonArray("assets"); + for (int j = 0; !found && j < assets.size(); j++) { + final JsonObject asset = assets.getJsonObject(j); + final JsonObject checksums = asset.getJsonObject("checksum"); + final JsonObject maven2 = asset.getJsonObject("maven2"); + if (maven2 != null + && "jar".equals(maven2.getString("extension", null)) + && acceptedClassifiers.contains(maven2.getString("classifier", null)) + && checksums != null && sha1.equals(checksums.getString("sha1", null)) + ) { + downloadUrl = asset.getString("downloadUrl"); + groupId = maven2.getString("groupId"); + artifactId = maven2.getString("artifactId"); + version = maven2.getString("version"); + + jarFound = true; + } else if (maven2 != null && "pom".equals(maven2.getString("extension"))) { + pomFound = true; + pomUrl = asset.getString("downloadUrl"); + } + if (pomFound && jarFound) { + found = true; + } + } + if (found) { + matchingArtifacts.add(new MavenArtifact(groupId, artifactId, version, downloadUrl, pomUrl)); + } else if (jarFound) { + final MavenArtifact ma = new MavenArtifact(groupId, artifactId, version, downloadUrl); + ma.setPomUrl(MavenArtifact.derivePomUrl(artifactId, version, downloadUrl)); + matchingArtifacts.add(ma); + found = true; + } + } + } + return continuationToken; + } + } + + @Override + public boolean preflightRequest() { + final HttpURLConnection conn; + try { + final URL url = new URL(rootURL, "v1/status"); + final URLConnectionFactory factory = new URLConnectionFactory(settings); + conn = factory.createHttpURLConnection(url, useProxy); + conn.addRequestProperty("Accept", "application/json"); + final String authHeader = buildHttpAuthHeaderValue(); + if (!authHeader.isEmpty()) { + conn.addRequestProperty("Authorization", authHeader); + } + conn.connect(); + if (conn.getResponseCode() != 200) { + LOGGER.warn("Expected 200 result from Nexus, got {}", conn.getResponseCode()); + return false; + } + if (conn.getContentLength() != 0) { + LOGGER.warn("Expected empty OK response (content-length 0), got content-length {}", conn.getContentLength()); + return false; + } + } catch (IOException e) { + LOGGER.warn("Pre-flight request to Nexus failed: ", e); + return false; + } + return true; + } + + /** + * Constructs the base64 encoded basic authentication header value. + * + * @return the base64 encoded basic authentication header value + */ + private String buildHttpAuthHeaderValue() { + final String user = settings.getString(Settings.KEYS.ANALYZER_NEXUS_USER, ""); + final String pass = settings.getString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD, ""); + String result = ""; + if (user.isEmpty() || pass.isEmpty()) { + LOGGER.debug("Skip authentication as user and/or password for nexus is empty"); + } else { + final String auth = user + ':' + pass; + final String base64Auth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + result = "Basic " + base64Auth; + } + return result; + } + +} diff --git a/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV2SearchTest.java b/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV2SearchTest.java new file mode 100644 index 00000000000..8bc0a9fde81 --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV2SearchTest.java @@ -0,0 +1,84 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2014 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.nexus; + +import java.io.FileNotFoundException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.utils.Settings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NexusV2SearchTest extends BaseTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(NexusV2SearchTest.class); + private NexusV2Search searcher; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + Settings sett = getSettings(); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_USER, "demo"); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD, "demo"); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_URL, "https://localhost/nexus/service/local/"); + String nexusUrl = sett.getString(Settings.KEYS.ANALYZER_NEXUS_URL); + LOGGER.debug(nexusUrl); + searcher = new NexusV2Search(sett, false); + Assume.assumeTrue(searcher.preflightRequest()); + } + + @Test(expected = IllegalArgumentException.class) + @Ignore + public void testNullSha1() throws Exception { + searcher.searchSha1(null); + } + + @Test(expected = IllegalArgumentException.class) + @Ignore + public void testMalformedSha1() throws Exception { + searcher.searchSha1("invalid"); + } + + // This test does generate network traffic and communicates with a host + // you may not be able to reach. Remove the @Ignore annotation if you want to + // test it anyway + @Test + @Ignore + public void testValidSha1() throws Exception { + MavenArtifact ma = searcher.searchSha1("9977a8d04e75609cf01badc4eb6a9c7198c4c5ea"); + assertEquals("Incorrect group", "org.apache.maven.plugins", ma.getGroupId()); + assertEquals("Incorrect artifact", "maven-compiler-plugin", ma.getArtifactId()); + assertEquals("Incorrect version", "3.1", ma.getVersion()); + assertNotNull("URL Should not be null", ma.getArtifactUrl()); + } + + // This test does generate network traffic and communicates with a host + // you may not be able to reach. Remove the @Ignore annotation if you want to + // test it anyway + @Test(expected = FileNotFoundException.class) + @Ignore + public void testMissingSha1() throws Exception { + searcher.searchSha1("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + } +} diff --git a/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusSearchTest.java b/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV3SearchTest.java similarity index 83% rename from core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusSearchTest.java rename to core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV3SearchTest.java index c77c9005108..166748899fc 100644 --- a/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusSearchTest.java +++ b/core/src/test/java/org/owasp/dependencycheck/data/nexus/NexusV3SearchTest.java @@ -17,9 +17,6 @@ */ package org.owasp.dependencycheck.data.nexus; -import java.io.FileNotFoundException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import org.junit.Assume; import org.junit.Before; import org.junit.Ignore; @@ -29,18 +26,28 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class NexusSearchTest extends BaseTest { +import java.io.FileNotFoundException; - private static final Logger LOGGER = LoggerFactory.getLogger(NexusSearchTest.class); - private NexusSearch searcher; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class NexusV3SearchTest extends BaseTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(NexusV3SearchTest.class); + private NexusV3Search searcher; @Before @Override public void setUp() throws Exception { super.setUp(); - String nexusUrl = getSettings().getString(Settings.KEYS.ANALYZER_NEXUS_URL); + Settings sett = getSettings(); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_USER, "demo"); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD, "demo"); +// sett.setString(Settings.KEYS.ANALYZER_NEXUS_URL, "http://localhost/service/rest/"); + String nexusUrl = sett.getString(Settings.KEYS.ANALYZER_NEXUS_URL); + LOGGER.debug(nexusUrl); - searcher = new NexusSearch(getSettings(), false); + searcher = new NexusV3Search(sett, false); Assume.assumeTrue(searcher.preflightRequest()); } diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml index 69e9d03f2f0..3bf2ce1769d 100644 --- a/core/src/test/resources/logback-test.xml +++ b/core/src/test/resources/logback-test.xml @@ -24,7 +24,7 @@ - +