Skip to content

Commit

Permalink
feat: Add support for Nexus v3 to NexusAnalyzer (#5849)
Browse files Browse the repository at this point in the history
  • Loading branch information
aikebah authored Aug 19, 2023
1 parent a29afc4 commit e685b80
Show file tree
Hide file tree
Showing 7 changed files with 608 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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).
* <p>
* 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>MavenArtifact</code> is populated with the
Expand All @@ -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();
}
Loading

0 comments on commit e685b80

Please sign in to comment.