diff --git a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Purge.java b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Purge.java index 1599a6372b4..1fbdf9e8276 100644 --- a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Purge.java +++ b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Purge.java @@ -62,6 +62,11 @@ public class Purge extends Task { */ private String hostedSuppressionsUrl = null; + /** + * The authorization header to hosted suppressions file with base FP suppressions. + */ + private String hostedSuppressionsAuthHeader = null; + /** * Construct a new DependencyCheckTask. */ @@ -131,6 +136,24 @@ public void setHostedSuppressionsUrl(final String hostedSuppressionsUrl) { this.hostedSuppressionsUrl = hostedSuppressionsUrl; } + /** + * Get the value of hostedSuppressionsAuthHeader. + * + * @return the value of hostedSuppressionsAuthHeader + */ + public String getHostedSuppressionsAuthHeader() { + return hostedSuppressionsAuthHeader; + } + + /** + * Set the value of hostedSuppressionsAuthHeader. + * + * @param hostedSuppressionsUrl new value of hostedSuppressionsAuthHeader + */ + public void setHostedSuppressionsAuthHeader(final String hostedSuppressionsAuthHeader) { + this.hostedSuppressionsAuthHeader = hostedSuppressionsAuthHeader; + } + /** * Sets the * {@link Thread#getContextClassLoader() Thread Context Class Loader} to the @@ -214,6 +237,7 @@ protected void populateSettings() throws BuildException { log(msg, ex, Project.MSG_WARN); } settings.setStringIfNotEmpty(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, hostedSuppressionsUrl); + settings.setStringIfNotEmpty(Settings.KEYS.HOSTED_SUPPRESSIONS_AUTH_HEADER, hostedSuppressionsAuthHeader); if (dataDirectory != null) { settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDirectory); } else { diff --git a/ant/src/site/markdown/config-purge.md b/ant/src/site/markdown/config-purge.md index 5006865fe81..2173d6d6947 100644 --- a/ant/src/site/markdown/config-purge.md +++ b/ant/src/site/markdown/config-purge.md @@ -23,6 +23,7 @@ Advanced Configuration ==================== The following properties can be configured in the plugin. However, they are less frequently changed. -Property | Description | Default Value -----------------------|--------------------------------------------------------------------------------------------------|------------------ -hostedSuppressionsUrl | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml +Property | Description | Default Value +-----------------------------|--------------------------------------------------------------------------------------------------|------------------ +hostedSuppressionsUrl | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml +hostedSuppressionsAuthHeader | The authorization header to a mirrored copy of the hosted suppressions file for internet-constrained environments | diff --git a/ant/src/site/markdown/config-update.md b/ant/src/site/markdown/config-update.md index 11b7481da69..72d9663b0b2 100644 --- a/ant/src/site/markdown/config-update.md +++ b/ant/src/site/markdown/config-update.md @@ -51,5 +51,6 @@ databaseUser | The username used when connecting to the database. databasePassword | The password used when connecting to the database. |   hostedSuppressionsEnabled | Whether the hosted suppression file will be used. | true hostedSuppressionsUrl | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml +hostedSuppressionsAuthHeader | The authorization header to a mirrored copy of the hosted suppressions file for internet-constrained environments | hostedSuppressionsValidForHours | Sets the number of hours to wait before checking for new updates of the hosted suppressions file | 2 hostedSuppressionsForceUpdate | Sets whether the hosted suppressions file should update regardless of the `autoupdate` and validForHours settings | false \ No newline at end of file diff --git a/ant/src/site/markdown/configuration.md b/ant/src/site/markdown/configuration.md index 6d1fe3a0b75..20c0a948215 100644 --- a/ant/src/site/markdown/configuration.md +++ b/ant/src/site/markdown/configuration.md @@ -164,5 +164,6 @@ databaseUser | The username used when connecting to the database. databasePassword | The password used when connecting to the database. |   hostedSuppressionsEnabled | Whether the hosted suppression file will be used. | true hostedSuppressionsUrl | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml +hostedSuppressionsAuthHeader | The authorization header to a mirrored copy of the hosted suppressions file for internet-constrained environments | hostedSuppressionsValidForHours | Sets the number of hours to wait before checking for new updates of the hosted suppressions file | 2 hostedSuppressionsForceUpdate | Sets whether the hosted suppressions file should update regardless of the `autoupdate` and validForHours settings | false \ No newline at end of file diff --git a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java index 4afa1c479ab..356398084f9 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java +++ b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java @@ -530,7 +530,10 @@ private void addAdvancedOptions(final Options options) { .addOption(newOptionWithArg(ARGUMENT.HOSTED_SUPPRESSIONS_VALID_FOR_HOURS, "hours", "The number of hours to wait before checking for new updates of the the hosted suppressions file.")) .addOption(newOptionWithArg(ARGUMENT.HOSTED_SUPPRESSIONS_URL, "url", - "The URL for a mirrored hosted suppressions file")); + "The URL for a mirrored hosted suppressions file")) + .addOption(newOptionWithArg(ARGUMENT.HOSTED_SUPPRESSIONS_AUTH_HEADER, "authorization header", + "The authorization header for a mirrored hosted suppressions file")) + ; } @@ -1600,5 +1603,10 @@ public static class ARGUMENT { * suppressions file . */ public static final String HOSTED_SUPPRESSIONS_URL = "hostedSuppressionsUrl"; + /** + * The CLI argument to set the location of a mirrored hosted + * suppressions file authorization header. + */ + public static final String HOSTED_SUPPRESSIONS_AUTH_HEADER = "hostedSuppressionsAuthHeader"; } } diff --git a/cli/src/main/resources/completion-for-dependency-check.sh b/cli/src/main/resources/completion-for-dependency-check.sh index 09ffff27370..34a0140bbb4 100755 --- a/cli/src/main/resources/completion-for-dependency-check.sh +++ b/cli/src/main/resources/completion-for-dependency-check.sh @@ -78,6 +78,7 @@ _odc_completions() --hostedSuppressionsForceUpdate --hostedSuppressionsValidForHours --hostedSuppressionsUrl + --hostedSuppressionsAuthHeader --junitFailOnCVSS -l --log -n --noupdate diff --git a/cli/src/site/markdown/arguments.md b/cli/src/site/markdown/arguments.md index a60acf72a5f..2376ab59aa0 100644 --- a/cli/src/site/markdown/arguments.md +++ b/cli/src/site/markdown/arguments.md @@ -130,3 +130,4 @@ Advanced Options | | \-\-hostedSuppressionsForceUpdate | | Whether the hosted suppressions file will update regardless of the `noupdate` argument. | false | | | \-\-hostedSuppressionsValidForHours | \ | The number of hours to wait before checking for new updates of the hosted suppressions file | 2 | | | \-\-hostedSuppressionsUrl | \ | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml | +| | \-\-hostedSuppressionsAuthHeader | \ | The authorization header to a mirrored copy of the hosted suppressions file for internet-constrained environments | | diff --git a/core/src/main/java/org/owasp/dependencycheck/data/update/HostedSuppressionsDataSource.java b/core/src/main/java/org/owasp/dependencycheck/data/update/HostedSuppressionsDataSource.java index e50d12af9ad..71bfaf9417a 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/update/HostedSuppressionsDataSource.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/update/HostedSuppressionsDataSource.java @@ -132,7 +132,11 @@ private void fetchHostedSuppressions(Settings settings, URL repoUrl, File repoFi if (LOGGER.isDebugEnabled()) { LOGGER.debug("Hosted Suppressions URL: {}", repoUrl.toExternalForm()); } - Downloader.getInstance().fetchFile(repoUrl, repoFile); + LOGGER.trace("Downloading Hosted Suppressions file from '{}'", repoUrl); + Downloader.getInstance().fetchFile(repoUrl, repoFile, + settings.useProxy(), + Settings.KEYS.HOSTED_SUPPRESSIONS_USER, Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD, + Downloader.NO_PROPERTY_DEFINED, Settings.KEYS.HOSTED_SUPPRESSIONS_AUTH_HEADER); } catch (IOException | TooManyRequestsException | ResourceNotFoundException | WriteLockException ex) { throw new UpdateException("Failed to update the hosted suppressions file", ex); } diff --git a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java index 2e05a2c2fc8..195451e3dbd 100644 --- a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java +++ b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java @@ -1026,6 +1026,12 @@ public abstract class BaseDependencyCheckMojo extends AbstractMojo implements Ma @SuppressWarnings("CanBeFinal") @Parameter(property = "hostedSuppressionsUrl") private String hostedSuppressionsUrl; + /** + * The hosted suppressions authorization header. + */ + @SuppressWarnings("CanBeFinal") + @Parameter(property = "hostedSuppressionsAuthHeader") + private String hostedSuppressionsAuthHeader; /** * Whether the hosted suppressions file will be updated regardless of the * `autoupdate` settings. @@ -2379,6 +2385,7 @@ protected void populateSettings() { } settings.setIntIfNotNull(Settings.KEYS.HOSTED_SUPPRESSIONS_VALID_FOR_HOURS, hostedSuppressionsValidForHours); settings.setStringIfNotNull(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, hostedSuppressionsUrl); + settings.setStringIfNotNull(Settings.KEYS.HOSTED_SUPPRESSIONS_AUTH_HEADER, hostedSuppressionsAuthHeader); settings.setBooleanIfNotNull(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, hostedSuppressionsForceUpdate); settings.setBooleanIfNotNull(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, hostedSuppressionsEnabled); } diff --git a/maven/src/site/markdown/configuration.md b/maven/src/site/markdown/configuration.md index 1b3caeaec02..c1a47e44ce0 100644 --- a/maven/src/site/markdown/configuration.md +++ b/maven/src/site/markdown/configuration.md @@ -173,6 +173,7 @@ databasePassword | The password used when connecting to the database. hostedSuppressionsEnabled | Whether the hosted suppressions file will be used. | true hostedSuppressionsForceUpdate | Whether the hosted suppressions file will update regardless of the `autoupdate` setting. | false hostedSuppressionsUrl | The URL to a mirrored copy of the hosted suppressions file for internet-constrained environments. | https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml +hostedSuppressionsAuthHeader | The authorization header to a mirrored copy of the hosted suppressions file for internet-constrained environments. | hostedSuppressionsValidForHours| Sets the number of hours to wait before checking for new updates from the NVD. | 2 retireJsUrlServerId | The id of a server defined in the settings.xml to retrieve the credentials (username and password) to connect to RetireJS instance. |   retireJsUser | If you don't want register user/password in settings.xml, you can specify user. |   diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/CredentialHelper.java b/utils/src/main/java/org/owasp/dependencycheck/utils/CredentialHelper.java new file mode 100644 index 00000000000..d8fb45aa72b --- /dev/null +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/CredentialHelper.java @@ -0,0 +1,202 @@ +package org.owasp.dependencycheck.utils; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Pattern; + +import org.apache.hc.client5.http.auth.BearerToken; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; + +public class CredentialHelper { + /** + * Get credentials from the provided settings + * @param theUser Username for a Basic auth + * @param thePass Password for a Basic auth + * @param theToken Token for a Bearer auth, takes precedence over user/password + * @param theAuth Authorization header value, e.g. 'Basic dXNlcjpwYXNzd29yZA==', takes precedence over user/password or token + * + * @return credentials + * @throws InvalidSettingException + */ + public static Credentials getCredentials(String theUser, char[] thePass, char[] theToken, char[] theAuth) throws InvalidSettingException { + Credentials _creds = null; + + // create a basic auth for user and password, if provided + if (theUser != null && !theUser.isBlank()) { + if(thePass == null || thePass.length == 0) + throw new InvalidSettingException("no password provided for user " + theUser); + try { + _creds = new UsernamePasswordCredentials(theUser, thePass); + } catch (Exception e) { + throw new InvalidSettingException("invalid user or password"); + } + } + + // create a bearer token, if provided, takes precedence over user/password + if (theToken != null && theToken.length > 0) { + _creds = new BearerToken(new String(theToken)); + } + + // if an auth has been passed, takes precedence over user/password/token + if (theAuth != null && theAuth.length > 0) { + if(startsWith(theAuth,StandardAuthScheme.BASIC)) { + _creds = getBasicCredentialsFromAuthHeader(theAuth); + } else if(startsWith(theAuth, StandardAuthScheme.BEARER)) { + _creds = getBearerCredentialsFromAuthHeader(theAuth); + } else + throw new InvalidSettingException("unknown authentication scheme. " + + "Supported authentication schemes are " + + StandardAuthScheme.BASIC + " and " + StandardAuthScheme.BEARER); + } + return _creds; + } + + /** + * Create a bearer token credentials from the auth header + * @param authHeader + * @return credentials + * @throws InvalidSettingException + */ + protected static Credentials getBearerCredentialsFromAuthHeader(char[] authHeader) throws InvalidSettingException { + if (authHeader == null || authHeader.length == 0) + throw new InvalidSettingException("empty authentication header"); + try { + // get token + String token = new String(authHeader); + if(!token.startsWith(StandardAuthScheme.BEARER + " ")) + throw new InvalidSettingException("auth header should start with [" + StandardAuthScheme.BEARER + " ]"); + token = token.replaceAll("^" + Pattern.quote(StandardAuthScheme.BEARER),"").trim(); + if(token.isBlank()) + throw new InvalidSettingException("empty bearer token"); + return new BearerToken(token); + } catch (Exception e) { + throw new InvalidSettingException(e.getMessage()); + } + } + + /** + * Create a basic credentials from the basic auth header + * @param authHeader + * @return credentials + * @throws InvalidSettingException + */ + protected static Credentials getBasicCredentialsFromAuthHeader(char[] authHeader) throws InvalidSettingException { + if (authHeader == null || authHeader.length == 0) + throw new InvalidSettingException("empty authentication header"); + try { + // decode B64 to get user and password + String user = getBasicUser(authHeader, StandardAuthScheme.BASIC.length() + 1); + return new UsernamePasswordCredentials( + user, + getBasicPassword(authHeader, StandardAuthScheme.BASIC.length() + 1) + ); + } catch (Exception e) { + throw new InvalidSettingException(e.getMessage()); + } + } + + + + /** + * copy a char array into an array of bytes without using a String + * @param in + * @param start where to start in the array if chars + * @return byte array + * @throws InvalidSettingException + */ + protected static byte[] toBytes(char[] in, int start) throws InvalidSettingException { + if (in == null || in.length == 0) + throw new InvalidSettingException("empty authentication provided"); + if(start >= in.length) + throw new InvalidSettingException("invalid authentication provided - too short"); + char[] chars = new char[in.length - start]; + for(int i = start; i < in.length; i++) { + chars[i-start] = in[i]; + } + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), + byteBuffer.position(), byteBuffer.limit()); + Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + return bytes; + } + + /** + * get the user out of a b64 string (Basic auth) + * @param in + * @param start + * @return + * @throws InvalidSettingException + */ + protected static String getBasicUser(char[] in, int start) throws InvalidSettingException { + if (in == null || in.length == 0) + throw new InvalidSettingException("invalid authentication string for the username"); + if(start >= in.length) + throw new InvalidSettingException("authentication string too short for the username"); + byte[] src = toBytes(in, start); + if(src == null || src.length ==0) + return null; + byte[] decoded = Base64.getDecoder().decode(src); + String user = ""; + for(int i = 0; i < decoded.length; i++) { + if(decoded[i]!=':') user += (char) decoded[i]; + else return user; + } + throw new InvalidSettingException("unable to find the username"); + } + + /** + * get the password out of a B64 string (Basic auth) + * @param in + * @param start + * @return + * @throws InvalidSettingException + */ + protected static char[] getBasicPassword(char[] in, int start) throws InvalidSettingException { + if (in == null || in.length == 0) + throw new InvalidSettingException("invalid authentication string for the pasword"); + if(start >= in.length) + throw new InvalidSettingException("authentication string too short for the password"); + byte[] src = toBytes(in, start); + if(src == null || src.length ==0) + return null; + byte[] decoded = Base64.getDecoder().decode(src); + start = 0; + for(int i = 0; i < decoded.length && start == 0; i++) { + if(decoded[i] == ':') start = i + 1; + } + if(start == 0 || start >= decoded.length) + throw new InvalidSettingException("unable to find the password"); + + char[] password = new char[decoded.length - start]; + for(int i = start; i < decoded.length; i++) { + password[i - start] = (char) (decoded[i] & 0xFF) ; + } + return password; + } + + /** + * Utility function to perform startsWith on a char array + * @param in + * @param start + * @return + */ + protected static boolean startsWith(char[] in, String start) { + if (in == null || in.length == 0) + return false; + if (start == null || start.isBlank()) + return false; + if(start.length() > in.length) + return false; + for(int i = 0; i < start.length(); i++) { + if(start.charAt(i) != in[i]) return false; + } + return true; + } + +} diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/Downloader.java b/utils/src/main/java/org/owasp/dependencycheck/utils/Downloader.java index de2f955cec9..92723e48364 100644 --- a/utils/src/main/java/org/owasp/dependencycheck/utils/Downloader.java +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/Downloader.java @@ -66,6 +66,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.UUID; import static java.lang.String.format; @@ -74,6 +75,10 @@ * @author Jeremy Long, Hans Aikema */ public final class Downloader { + /** + * No defined key for this property + */ + public static final String NO_PROPERTY_DEFINED = UUID.randomUUID().toString(); /** * The builder to use for a HTTP Client that uses the configured proxy-settings @@ -164,7 +169,7 @@ public void configure(Settings settings) throws InvalidSettingException { } if (settings.getString(Settings.KEYS.PROXY_USERNAME) != null) { final String proxyuser = settings.getString(Settings.KEYS.PROXY_USERNAME); - final char[] proxypass = settings.getString(Settings.KEYS.PROXY_PASSWORD).toCharArray(); + final char[] proxypass = settings.getString(Settings.KEYS.PROXY_PASSWORD, "").toCharArray(); credentialsProvider.setCredentials( new AuthScope(null, proxyHost, proxyPort, null, null), new UsernamePasswordCredentials(proxyuser, proxypass) @@ -184,7 +189,7 @@ public void configure(Settings settings) throws InvalidSettingException { private void tryAddRetireJSCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_USER, Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_PASSWORD, @@ -193,18 +198,21 @@ private void tryAddRetireJSCredentials(Settings settings, CredentialsStore crede } private void tryAddHostedSuppressionCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { - if (settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + if (settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD) != null + || settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_AUTH_HEADER) != null) { + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.HOSTED_SUPPRESSIONS_USER, Settings.KEYS.HOSTED_SUPPRESSIONS_URL, Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD, + NO_PROPERTY_DEFINED, + Settings.KEYS.HOSTED_SUPPRESSIONS_AUTH_HEADER, "Hosted suppressions"); } } private void tryAddKEVCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.KEV_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.KEV_USER, Settings.KEYS.KEV_URL, Settings.KEYS.KEV_PASSWORD, @@ -214,7 +222,7 @@ private void tryAddKEVCredentials(Settings settings, CredentialsStore credential private void tryAddNexusAnalyzerCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.ANALYZER_NEXUS_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.ANALYZER_NEXUS_USER, Settings.KEYS.ANALYZER_NEXUS_URL, Settings.KEYS.ANALYZER_NEXUS_PASSWORD, @@ -224,7 +232,7 @@ private void tryAddNexusAnalyzerCredentials(Settings settings, CredentialsStore private void tryAddCentralAnalyzerCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.ANALYZER_CENTRAL_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.ANALYZER_CENTRAL_USER, Settings.KEYS.ANALYZER_CENTRAL_URL, Settings.KEYS.ANALYZER_CENTRAL_PASSWORD, @@ -234,7 +242,7 @@ private void tryAddCentralAnalyzerCredentials(Settings settings, CredentialsStor private void tryAddCentralContentCredentials(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.CENTRAL_CONTENT_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.CENTRAL_CONTENT_USER, Settings.KEYS.CENTRAL_CONTENT_URL, Settings.KEYS.CENTRAL_CONTENT_PASSWORD, @@ -244,7 +252,7 @@ private void tryAddCentralContentCredentials(Settings settings, CredentialsStore private void tryAddNVDApiDatafeed(Settings settings, CredentialsStore credentialsStore) throws InvalidSettingException { if (settings.getString(Settings.KEYS.NVD_API_DATAFEED_PASSWORD) != null) { - addUserPasswordCreds(settings, credentialsStore, + addConfiguredCredentials(settings, credentialsStore, Settings.KEYS.NVD_API_DATAFEED_USER, Settings.KEYS.NVD_API_DATAFEED_URL, Settings.KEYS.NVD_API_DATAFEED_PASSWORD, @@ -253,48 +261,97 @@ private void tryAddNVDApiDatafeed(Settings settings, CredentialsStore credential } /** - * Add user/password credentials for the host/port of the URL, all configured in the settings, to the credential-store. + * Add credentials for the host/port of the URL, all configured in the settings, to the credential-store. * * @param settings The settings to retrieve the values from * @param store The credentialStore * @param userKey The key for a configured username credential part + * @param urlKey The key for a configured url for which the credentials hold * @param passwordKey The key for a configured password credential part + * @param tokenKey The key for a configured token credential part + * @param authKey The key for a configured auth header credential part + * @param desc A descriptive text for use in error messages for this credential + * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings. + */ + private void addConfiguredCredentials(Settings settings, CredentialsStore store, + String userKey, String urlKey, String passwordKey, + String desc) + throws InvalidSettingException { + addConfiguredCredentials(settings, store, + userKey, urlKey, passwordKey, + NO_PROPERTY_DEFINED, NO_PROPERTY_DEFINED, + desc); + } + + /** + * Add credentials for the host/port of the URL, all configured in the settings, to the credential-store. + * + * @param settings The settings to retrieve the values from + * @param store The credentialStore + * @param userKey The key for a configured username credential part * @param urlKey The key for a configured url for which the credentials hold + * @param passwordKey The key for a configured password credential part + * @param tokenKey The key for a configured token credential part + * @param authKey The key for a configured auth header credential part * @param desc A descriptive text for use in error messages for this credential * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings. */ - private void addUserPasswordCreds(Settings settings, CredentialsStore store, String userKey, String urlKey, String passwordKey, String desc) + private void addConfiguredCredentials(Settings settings, CredentialsStore store, + String userKey, String urlKey, String passwordKey, + String tokenKey, String authKey, + String desc) throws InvalidSettingException { final String theUser = settings.getString(userKey); final String theURL = settings.getString(urlKey); final char[] thePass = settings.getString(passwordKey, "").toCharArray(); - if (theUser == null || theURL == null || thePass.length == 0) { + final char[] theToken = settings.getString(tokenKey, "").toCharArray(); + final char[] theAuth = settings.getString(authKey, "").toCharArray(); + + if (thePass.length > 0 && (theUser == null || theURL == null)) { throw new InvalidSettingException(desc + " URL and username are required when setting " + desc + " password"); } + if (theToken.length > 0 && theURL == null) { + throw new InvalidSettingException(desc + " URL is required when setting " + desc + " token"); + } + if (theAuth.length > 0 && theURL == null) { + throw new InvalidSettingException(desc + " URL is required when setting " + desc + " authorization header"); + } try { final URL parsedURL = new URL(theURL); - addCredentials(store, desc, parsedURL, theUser, thePass); + Credentials creds = CredentialHelper.getCredentials(theUser, thePass, theToken, theAuth); + addCredentials(store, desc, parsedURL, creds); } catch (MalformedURLException e) { throw new InvalidSettingException(desc + " URL must be a valid URL", e); + } catch (InvalidSettingException e) { + throw new InvalidSettingException("Invalid configuration for " + desc, e); } } - private static void addCredentials(CredentialsStore credentialsStore, String messageScope, URL parsedURL, String theUser, char[] thePass) - throws InvalidSettingException { + + protected static void addCredentials(CredentialsStore credentialsStore, String messageScope, + URL parsedURL, Credentials creds) + throws InvalidSettingException { final String theProtocol = parsedURL.getProtocol(); if ("file".equals(theProtocol)) { LOGGER.warn("Credentials are not supported for file-protocol, double-check your configuration options for {}.", messageScope); return; } else if ("http".equals(theProtocol)) { - LOGGER.warn("Insecure configuration: Basic Credentials are configured to be used over a plain http connection for {}. " - + "Consider migrating to https to guard the credentials.", messageScope); + LOGGER.warn("Insecure configuration: Credentials are configured to be used over a plain http connection for {}. " + + "Consider migrating to https to guard the credentials.", messageScope); } else if (!"https".equals(theProtocol)) { throw new InvalidSettingException("Unsupported protocol in the " + messageScope - + " URL; only file, http and https are supported"); + + " URL; only file, http and https are supported"); } + final String theHost = parsedURL.getHost(); final int thePort = parsedURL.getPort(); - final Credentials creds = new UsernamePasswordCredentials(theUser, thePass); + + // add credentials to store + if(creds == null) { + LOGGER.info("No credentials passed for {}", messageScope); + return; + } + LOGGER.info("Adding {} credentials for {}", creds.getClass().getSimpleName(), messageScope); final AuthScope scope = new AuthScope(theProtocol, theHost, thePort, null, null); credentialsStore.setCredentials(scope, creds); } @@ -328,6 +385,7 @@ public void fetchFile(URL url, File outputPath) */ public void fetchFile(URL url, File outputPath, boolean useProxy) throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException { + LOGGER.trace("Fetching {}",url); try { if ("file".equals(url.getProtocol())) { final Path p = Paths.get(url.toURI()); @@ -368,6 +426,31 @@ private static void wrapAndThrowHttpResponseException(String url, HttpResponseEx throw new DownloadFailedException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre); } } + + /** + * Retrieves a file from a given URL using an ad-hoc created CredentialsProvider if needed + * and saves it to the outputPath. + * + * @param url the URL of the file to download + * @param outputPath the path to the save the file to + * @param useProxy whether to use the configured proxy when downloading + * files + * @param userKey the settings key for the username to be used + * @param passwordKey the settings key for the password to be used + * @throws DownloadFailedException is thrown if there is an error downloading the file + * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file + * @throws TooManyRequestsException thrown when a 429 is received + * @throws ResourceNotFoundException thrown when a 404 is received + * @implNote This method should only be used in cases where the target host cannot be determined beforehand from settings, so that ad-hoc + * Credentials needs to be constructed for the target URL when the user/password keys point to configured credentials. The method delegates to + * {@link #fetchFile(URL, File, boolean)} when credentials are not configured for the given keys or the resource points to a file. + */ + public void fetchFile(URL url, File outputPath, boolean useProxy, + String userKey, String passwordKey) throws DownloadFailedException, + TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException { + fetchFile(url, outputPath, useProxy, userKey, passwordKey, + NO_PROPERTY_DEFINED, NO_PROPERTY_DEFINED); + } /** * Retrieves a file from a given URL using an ad-hoc created CredentialsProvider if needed @@ -379,6 +462,8 @@ private static void wrapAndThrowHttpResponseException(String url, HttpResponseEx * files * @param userKey the settings key for the username to be used * @param passwordKey the settings key for the password to be used + * @param tokenKey the settings key for the token to be used + * @param authKey the settings key for the authorization header to be used * @throws DownloadFailedException is thrown if there is an error downloading the file * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file * @throws TooManyRequestsException thrown when a 429 is received @@ -387,16 +472,22 @@ private static void wrapAndThrowHttpResponseException(String url, HttpResponseEx * Credentials needs to be constructed for the target URL when the user/password keys point to configured credentials. The method delegates to * {@link #fetchFile(URL, File, boolean)} when credentials are not configured for the given keys or the resource points to a file. */ - public void fetchFile(URL url, File outputPath, boolean useProxy, String userKey, String passwordKey) throws DownloadFailedException, + public void fetchFile(URL url, File outputPath, boolean useProxy, + String userKey, String passwordKey, + String tokenKey, String authKey) throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException { - if ("file".equals(url.getProtocol()) - || userKey == null || settings.getString(userKey) == null - || passwordKey == null || settings.getString(passwordKey) == null - ) { + boolean hasCredentials = settings != null && ( + !settings.getString(passwordKey, "").isBlank() + || !settings.getString(tokenKey, "").isBlank() + || !settings.getString(authKey, "").isBlank()); + LOGGER.debug("credentials defined for {}: {}", url, hasCredentials); + + if ("file".equals(url.getProtocol()) || !hasCredentials) { // no credentials configured, so use the default fetchFile fetchFile(url, outputPath, useProxy); return; } + LOGGER.trace("Fetching {} userkey={}, passwordKey={}, tokenKey={}, authKey={}",url, userKey, passwordKey, tokenKey, authKey); final String theProtocol = url.getProtocol(); if (!("http".equals(theProtocol) || "https".equals(theProtocol))) { throw new DownloadFailedException("Unsupported protocol in the URL; only file, http and https are supported"); @@ -404,7 +495,9 @@ public void fetchFile(URL url, File outputPath, boolean useProxy, String userKey try { final HttpClientContext context = HttpClientContext.create(); final BasicCredentialsProvider localCredentials = new BasicCredentialsProvider(); - addCredentials(localCredentials, url.toString(), url, settings.getString(userKey), settings.getString(passwordKey).toCharArray()); + Credentials creds = CredentialHelper.getCredentials(settings.getString(userKey), settings.getString(passwordKey, "").toCharArray(), + settings.getString(tokenKey, "").toCharArray(), settings.getString(authKey, "").toCharArray()); + addCredentials(localCredentials, url.toString(), url, creds); context.setCredentialsProvider(localCredentials); try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) { final BasicClassicHttpRequest req = new BasicClassicHttpRequest(Method.GET, url.toURI()); diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java index fa398a87411..245bc3f5811 100644 --- a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java @@ -327,6 +327,12 @@ public static final class KEYS { */ public static final String HOSTED_SUPPRESSIONS_PASSWORD = "hosted.suppressions.password"; + /** + * The properties key for the hosted suppressions authorization header value. + * For use when hosted suppressions are mirrored locally on a site requiring authentication + */ + public static final String HOSTED_SUPPRESSIONS_AUTH_HEADER = "hosted.suppressions.auth.header"; + /** * The properties key for defining whether the hosted suppressions file * will be updated regardless of the autoupdate settings. @@ -1505,6 +1511,15 @@ public String getConnectionString(String connectionStringKey, String dbFileNameK return connStr; } + /** + * @return whether the proxy should be used + */ + public boolean useProxy() { + String proxyServer = getString(Settings.KEYS.PROXY_SERVER, ""); + return proxyServer!=null && !proxyServer.isEmpty(); + } + + /** * Retrieves the primary data directory that is used for caching web * content. diff --git a/utils/src/test/java/org/owasp/dependencycheck/utils/CredentialHelperTest.java b/utils/src/test/java/org/owasp/dependencycheck/utils/CredentialHelperTest.java new file mode 100644 index 00000000000..ca3e00b1adb --- /dev/null +++ b/utils/src/test/java/org/owasp/dependencycheck/utils/CredentialHelperTest.java @@ -0,0 +1,240 @@ +package org.owasp.dependencycheck.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Base64; +import java.util.UUID; + +import org.apache.hc.client5.http.auth.BearerToken; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.junit.Test; + +public class CredentialHelperTest { + + // make sure that getting a string from settings for a non defined property key doesn't throw an exception and returns the default value + @Test + public void test_settings_null() throws Exception { + Settings settings = new Settings(); + String expected = UUID.randomUUID().toString(); + assertNotNull(settings.getString(Downloader.NO_PROPERTY_DEFINED, expected)); + assertEquals(expected, settings.getString(Downloader.NO_PROPERTY_DEFINED, expected)); + } + + @Test + public void testBaseFunctions() throws Exception { + String user = UUID.randomUUID().toString(); + String password = UUID.randomUUID().toString(); + String b64 = StandardAuthScheme.BASIC + " " + Base64.getEncoder().encodeToString((user+":"+password).getBytes()); + int start = StandardAuthScheme.BASIC.length() + 1; + + // starts with + assertTrue(CredentialHelper.startsWith(b64.toCharArray(), StandardAuthScheme.BASIC)); + assertFalse(CredentialHelper.startsWith(b64.toCharArray(), StandardAuthScheme.BEARER)); + + // Basic auth + assertEquals(user, CredentialHelper.getBasicUser(b64.toCharArray(), start)); + assertEquals(password, new String(CredentialHelper.getBasicPassword(b64.toCharArray(), start))); + } + + + + /////////////////////////////////////////////////// + // test basic auth methods + /////////////////////////////////////////////////// + + @Test + public void testGetBasicCredentials() throws Exception { + String user = "U-" + UUID.randomUUID().toString(); + String pass = "P-" + UUID.randomUUID().toString(); + String auth = StandardAuthScheme.BASIC + " " + Base64.getEncoder().encodeToString(((user + ":" + pass).getBytes())); + + + Credentials credentials = CredentialHelper.getBasicCredentialsFromAuthHeader(auth.toCharArray()); + assertNotNull(credentials); + assertTrue(credentials instanceof UsernamePasswordCredentials); + assertEquals(user, ((UsernamePasswordCredentials) credentials).getUserName()); + assertEquals(pass, new String(((UsernamePasswordCredentials) credentials).getUserPassword())); + + checkBasicException(null); + checkBasicException("".toCharArray()); + checkBasicException(" ".toCharArray()); + checkBasicException("12323333333333 33".toCharArray()); + auth = StandardAuthScheme.BASIC + " "; + checkBasicException(auth.toCharArray()); + auth = StandardAuthScheme.BASIC + " !!!!!!!!!!!!!!!!!!!!!!!!"; + checkBasicException(auth.toCharArray()); + auth = StandardAuthScheme.BASIC + " " + Base64.getEncoder().encodeToString(((user + ":" + pass).getBytes()))+"1"; + checkBasicException(auth.toCharArray()); + } + + @Test + public void testCredsBasic() throws Exception { + String user = "U1-" + UUID.randomUUID().toString(); + String password = "P1-" + UUID.randomUUID().toString(); + + // no auth + checkBasicCreds(user, password, "", "", user, password); + checkBasicCreds(user, password, "", "", user, password); + + // user, password and auth + String user2 = "U2-" + UUID.randomUUID().toString(); + String password2 = "P2-" + UUID.randomUUID().toString(); + String b64 = StandardAuthScheme.BASIC + " " + + Base64.getEncoder().encodeToString((user2+":"+password2).getBytes()); + checkBasicCreds(user, password, "", b64, user2, password2); + checkBasicCreds(user, password, "", b64, user2, password2); + + // only auth + checkBasicCreds(null, null, "", b64, user2, password2); + checkBasicCreds(null, null, "", b64, user2, password2); + } + + @Test + public void testCredsBasicException() throws Exception { + // no password + String pfx = "U-"; + checkException(pfx+UUID.randomUUID().toString(), null, null, "no password", pfx); + checkException(null, null, UUID.randomUUID().toString(), + "supported", StandardAuthScheme.BASIC, StandardAuthScheme.BEARER); + } + + + /////////////////////////////////////////////////// + // test bearer auth methods + /////////////////////////////////////////////////// + + @Test + public void testGetBearerCredentials() throws Exception { + String token = "token-" + UUID.randomUUID(); + String auth = StandardAuthScheme.BEARER + " " + token; + + Credentials credentials = CredentialHelper.getBearerCredentialsFromAuthHeader(auth.toCharArray()); + assertNotNull(credentials); + assertTrue(credentials instanceof BearerToken); + assertEquals(token, ((BearerToken) credentials).getToken()); + + checkBearerException(null); + checkBearerException(new char[0]); + auth = UUID.randomUUID().toString(); + checkBearerException(auth.toCharArray()); + auth = StandardAuthScheme.BEARER + UUID.randomUUID().toString(); + checkBearerException(auth.toCharArray()); + auth = StandardAuthScheme.BEARER + " "; + checkBearerException(auth.toCharArray()); + } + + @Test + public void testCredsTokenAuth() throws Exception { + String user = "U1-" + UUID.randomUUID().toString(); + String password = "P1-" + UUID.randomUUID().toString(); + String token = "token-" + UUID.randomUUID(); + String auth = StandardAuthScheme.BEARER + " " + token; + + // with user / password + checkTokenCreds(user, password, "", auth, token); + checkTokenCreds(user, password, "", auth, token); + + // without user / password + checkTokenCreds(null, null, "", auth, token); + checkTokenCreds(null, null, "", auth, token); + } + + + @Test + public void testCredsToken() throws Exception { + String token = "token-" + UUID.randomUUID(); + + // without user / password + checkTokenCreds(null, null, token, "", token); + checkTokenCreds(null, null, token, "", token); + } + + @Test + public void testCredsTokenException() throws Exception { + String auth = StandardAuthScheme.BEARER + " "; + checkException(null, null, null, auth, "empty bearer token"); + checkException(null, null, null, StandardAuthScheme.BEARER, "should start with"); + } + + + + + /////////////////////////////////////////////////// + // private test methods + /////////////////////////////////////////////////// + + private void checkException(String user, String password, String token, + String auth, String ... messages) throws Exception { + if(password == null) password = ""; + if(auth == null) auth = ""; + if(token == null) token = ""; + try { + CredentialHelper.getCredentials(user, password.toCharArray(), + token.toCharArray(), auth.toCharArray()); + throw new Exception("should have thrown an InvalidSettingException"); + } catch(InvalidSettingException ok) { + assertNotNull(ok); + if(messages==null || messages.length == 0) + return; + assertNotNull(ok.getMessage()); + for(String message : messages) { + assertTrue(ok.getMessage().toLowerCase().contains(message.toLowerCase())); + } + } + } + + + private void checkTokenCreds(String user, String password, String token, String auth, + String expectedToken) throws Exception { + if(password == null) password = ""; + if(auth == null) auth = ""; + if(token == null) token = ""; + Credentials creds = CredentialHelper.getCredentials(user, password.toCharArray(), + token.toCharArray(), auth.toCharArray()); + + assertTrue(creds instanceof BearerToken); + BearerToken bearer = (BearerToken) creds; + assertEquals(expectedToken, bearer.getToken()); + } + + + private void checkBasicCreds(String user, String password, + String token, String auth, + String expectedUser, String expectedPassword) throws Exception { + if(password == null) password = ""; + if(token == null) token = ""; + Credentials creds = CredentialHelper.getCredentials(user, password.toCharArray(), + token.toCharArray(), auth.toCharArray()); + + assertTrue(creds instanceof UsernamePasswordCredentials); + UsernamePasswordCredentials basicCreds = (UsernamePasswordCredentials) creds; + assertEquals(expectedUser, basicCreds.getUserName()); + assertEquals(expectedPassword, new String(basicCreds.getUserPassword())); + } + + private void checkBasicException(char[] auth) throws Exception { + try { + CredentialHelper.getBasicCredentialsFromAuthHeader(auth); + throw new Exception("should have thrown an InvalidSettingException"); + } catch(InvalidSettingException ok) { + assertNotNull(ok); + assertNotNull(ok.getMessage()); + } + } + + private void checkBearerException(char[] auth) throws Exception { + try { + CredentialHelper.getBearerCredentialsFromAuthHeader(auth); + throw new Exception("should have thrown an InvalidSettingException"); + } catch(InvalidSettingException ok) { + assertNotNull(ok); + assertNotNull(ok.getMessage()); + } + } + +}