You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by kl...@apache.org on 2021/08/12 21:09:22 UTC

[geode] branch support/1.14 updated: GEODE-9354: Extract and test ArgumentRedactorRegex (#6641) (#6747)

This is an automated email from the ASF dual-hosted git repository.

klund pushed a commit to branch support/1.14
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/support/1.14 by this push:
     new b82e282  GEODE-9354: Extract and test ArgumentRedactorRegex (#6641) (#6747)
b82e282 is described below

commit b82e282b80ef27d9018509be0f2e62173a5a35d5
Author: Kirk Lund <kl...@apache.org>
AuthorDate: Thu Aug 12 14:08:31 2021 -0700

    GEODE-9354: Extract and test ArgumentRedactorRegex (#6641) (#6747)
    
    * Rename ArgumentRedactorJUnitTest to ArgumentRedactorTest.
    * Reorganize and reformat ArgumentRedactor and its tests.
    * Fix issues in regex found by new tests.
    
    (cherry picked from commit 693e18c48d1c0e3601c517cb7c5493b54649dc10)
---
 ...scribeConfigAreFullyRedactedAcceptanceTest.java |  111 +--
 .../redaction/ArgumentRedactorIntegrationTest.java |   52 +
 .../apache/geode/codeAnalysis/excludedClasses.txt  |    1 +
 .../org/apache/geode/internal/AbstractConfig.java  |    2 +-
 .../geode/internal/util/ArgumentRedactor.java      |  216 +----
 .../redaction/CombinedSensitiveDictionary.java     |   41 +
 .../geode/internal/util/redaction/ParserRegex.java |   93 ++
 .../internal/util/redaction/RedactionDefaults.java |   58 ++
 .../internal/util/redaction/RedactionStrategy.java |   51 +
 .../util/redaction/RegexRedactionStrategy.java     |   50 +
 .../util/redaction/SensitiveDataDictionary.java    |   32 +
 .../util/redaction/SensitivePrefixDictionary.java  |   37 +
 .../redaction/SensitiveSubstringDictionary.java    |   37 +
 .../internal/util/redaction/StringRedaction.java   |  123 +++
 .../internal/util/ArgumentRedactorJUnitTest.java   |  221 -----
 .../geode/internal/util/ArgumentRedactorTest.java  |  675 +++++++++++++
 .../redaction/CombinedSensitiveDictionaryTest.java |  130 +++
 .../internal/util/redaction/ParserRegexTest.java   | 1006 ++++++++++++++++++++
 .../util/redaction/RedactionDefaultsTest.java      |   80 ++
 .../util/redaction/RegexRedactionStrategyTest.java |  396 ++++++++
 .../redaction/SensitivePrefixDictionaryTest.java   |  160 ++++
 .../SensitiveSubstringDictionaryTest.java          |  160 ++++
 .../util/redaction/StringRedactionTest.java        |  254 +++++
 .../geode/test/junit/rules/RequiresGeodeHome.java  |    9 +-
 24 files changed, 3540 insertions(+), 455 deletions(-)

diff --git a/geode-assembly/src/acceptanceTest/java/org/apache/geode/management/internal/cli/commands/LogsAndDescribeConfigAreFullyRedactedAcceptanceTest.java b/geode-assembly/src/acceptanceTest/java/org/apache/geode/management/internal/cli/commands/LogsAndDescribeConfigAreFullyRedactedAcceptanceTest.java
index 1c0cc17..e569709 100644
--- a/geode-assembly/src/acceptanceTest/java/org/apache/geode/management/internal/cli/commands/LogsAndDescribeConfigAreFullyRedactedAcceptanceTest.java
+++ b/geode-assembly/src/acceptanceTest/java/org/apache/geode/management/internal/cli/commands/LogsAndDescribeConfigAreFullyRedactedAcceptanceTest.java
@@ -16,25 +16,24 @@ package org.apache.geode.management.internal.cli.commands;
 
 import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
 import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_MANAGER;
+import static org.apache.geode.examples.security.ExampleSecurityManager.SECURITY_JSON;
+import static org.apache.geode.test.util.ResourceUtils.createFileFromResource;
 import static org.apache.geode.test.util.ResourceUtils.getResource;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.util.Collection;
 import java.util.Properties;
-import java.util.Scanner;
 
-import org.apache.commons.io.FileUtils;
-import org.assertj.core.api.SoftAssertions;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
+import org.junit.rules.TemporaryFolder;
 
 import org.apache.geode.examples.security.ExampleSecurityManager;
 import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.assertj.LogFileAssert;
 import org.apache.geode.test.junit.categories.LoggingTest;
 import org.apache.geode.test.junit.categories.SecurityTest;
 import org.apache.geode.test.junit.rules.RequiresGeodeHome;
@@ -53,94 +52,82 @@ import org.apache.geode.test.junit.rules.gfsh.GfshRule;
 @Category({SecurityTest.class, LoggingTest.class})
 public class LogsAndDescribeConfigAreFullyRedactedAcceptanceTest {
 
-  private static final String sharedPasswordString = "abcdefg";
-
-  private File propertyFile;
-  private File securityPropertyFile;
+  private static final String PASSWORD = "abcdefg";
 
   @Rule
+  public RequiresGeodeHome geodeHome = new RequiresGeodeHome();
+  @Rule
   public GfshRule gfsh = new GfshRule();
-
   @Rule
-  public RequiresGeodeHome geodeHome = new RequiresGeodeHome();
+  public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
   @Before
   public void createDirectoriesAndFiles() throws Exception {
-    propertyFile = gfsh.getTemporaryFolder().newFile("geode.properties");
-    securityPropertyFile = gfsh.getTemporaryFolder().newFile("security.properties");
-
-    Properties properties = new Properties();
-    properties.setProperty(LOG_LEVEL, "debug");
-    properties.setProperty("security-username", "propertyFileUser");
-    properties.setProperty("security-password", sharedPasswordString + "-propertyFile");
-    try (FileOutputStream fileOutputStream = new FileOutputStream(propertyFile)) {
-      properties.store(fileOutputStream, null);
+    File geodePropertiesFile = temporaryFolder.newFile("geode.properties");
+    File securityPropertiesFile = temporaryFolder.newFile("security.properties");
+
+    Properties geodeProperties = new Properties();
+    geodeProperties.setProperty(LOG_LEVEL, "debug");
+    geodeProperties.setProperty("security-username", "propertyFileUser");
+    geodeProperties.setProperty("security-password", PASSWORD + "-propertyFile");
+
+    try (FileOutputStream fileOutputStream = new FileOutputStream(geodePropertiesFile)) {
+      geodeProperties.store(fileOutputStream, null);
     }
 
     Properties securityProperties = new Properties();
     securityProperties.setProperty(SECURITY_MANAGER, ExampleSecurityManager.class.getName());
-    securityProperties.setProperty(ExampleSecurityManager.SECURITY_JSON, "security.json");
+    securityProperties.setProperty(SECURITY_JSON, "security.json");
     securityProperties.setProperty("security-file-username", "securityPropertyFileUser");
-    securityProperties.setProperty("security-file-password",
-        sharedPasswordString + "-securityPropertyFile");
-    try (FileOutputStream fileOutputStream = new FileOutputStream(securityPropertyFile)) {
+    securityProperties.setProperty("security-file-password", PASSWORD + "-securityPropertyFile");
+
+    try (FileOutputStream fileOutputStream = new FileOutputStream(securityPropertiesFile)) {
       securityProperties.store(fileOutputStream, null);
     }
 
-    startSecureLocatorAndServer();
-  }
-
-  private void startSecureLocatorAndServer() {
     // The json is in the root resource directory.
-    String securityJson =
-        getResource(LogsAndDescribeConfigAreFullyRedactedAcceptanceTest.class,
-            "/security.json").getPath();
-    // We want to add the folder to the classpath, so we strip off the filename.
-    securityJson = securityJson.substring(0, securityJson.length() - "security.json".length());
-    String startLocatorCmd =
-        new CommandStringBuilder("start locator").addOption("name", "test-locator")
-            .addOption("properties-file", propertyFile.getAbsolutePath())
-            .addOption("security-properties-file", securityPropertyFile.getAbsolutePath())
-            .addOption("J", "-Dsecure-username-jd=user-jd")
-            .addOption("J", "-Dsecure-password-jd=password-jd")
-            .addOption("classpath", securityJson).getCommandString();
+    createFileFromResource(getResource("/security.json"), temporaryFolder.getRoot(),
+        "security.json");
+
+    String startLocatorCmd = new CommandStringBuilder("start locator")
+        .addOption("name", "test-locator")
+        .addOption("properties-file", geodePropertiesFile.getAbsolutePath())
+        .addOption("security-properties-file", securityPropertiesFile.getAbsolutePath())
+        .addOption("J", "-Dsecure-username-jd=user-jd")
+        .addOption("J", "-Dsecure-password-jd=password-jd")
+        .addOption("classpath", temporaryFolder.getRoot().getAbsolutePath())
+        .getCommandString();
 
     String startServerCmd = new CommandStringBuilder("start server")
-        .addOption("name", "test-server").addOption("user", "viaStartMemberOptions")
-        .addOption("password", sharedPasswordString + "-viaStartMemberOptions")
-        .addOption("properties-file", propertyFile.getAbsolutePath())
-        .addOption("security-properties-file", securityPropertyFile.getAbsolutePath())
+        .addOption("name", "test-server")
+        .addOption("user", "viaStartMemberOptions")
+        .addOption("password", PASSWORD + "-viaStartMemberOptions")
+        .addOption("properties-file", geodePropertiesFile.getAbsolutePath())
+        .addOption("security-properties-file", securityPropertiesFile.getAbsolutePath())
         .addOption("J", "-Dsecure-username-jd=user-jd")
-        .addOption("J", "-Dsecure-password-jd=" + sharedPasswordString + "-password-jd")
+        .addOption("J", "-Dsecure-password-jd=" + PASSWORD + "-password-jd")
         .addOption("server-port", "0")
-        .addOption("classpath", securityJson).getCommandString();
+        .addOption("classpath", temporaryFolder.getRoot().getAbsolutePath())
+        .getCommandString();
 
     gfsh.execute(startLocatorCmd, startServerCmd);
   }
 
   @Test
-  public void logsDoNotContainStringThatShouldBeRedacted() throws FileNotFoundException {
-    Collection<File> logs =
-        FileUtils.listFiles(gfsh.getTemporaryFolder().getRoot(), new String[] {"log"}, true);
-
-    // Use soft assertions to report all redaction failures, not just the first one.
-    SoftAssertions softly = new SoftAssertions();
-    for (File logFile : logs) {
-      Scanner scanner = new Scanner(logFile);
-      while (scanner.hasNextLine()) {
-        String line = scanner.nextLine();
-        softly.assertThat(line).describedAs("File: %s, Line: %s", logFile.getAbsolutePath(), line)
-            .doesNotContain(sharedPasswordString);
-      }
+  public void logsDoNotContainStringThatShouldBeRedacted() {
+    File dir = gfsh.getTemporaryFolder().getRoot();
+    File[] logFiles = dir.listFiles((d, name) -> name.endsWith(".log"));
+
+    for (File logFile : logFiles) {
+      LogFileAssert.assertThat(logFile).doesNotContain(PASSWORD);
     }
-    softly.assertAll();
   }
 
   @Test
   public void describeConfigRedactsJvmArguments() {
     String connectCommand = new CommandStringBuilder("connect")
         .addOption("user", "viaStartMemberOptions")
-        .addOption("password", sharedPasswordString + "-viaStartMemberOptions").getCommandString();
+        .addOption("password", PASSWORD + "-viaStartMemberOptions").getCommandString();
 
     String describeLocatorConfigCommand = new CommandStringBuilder("describe config")
         .addOption("hide-defaults", "false").addOption("member", "test-locator").getCommandString();
@@ -150,6 +137,6 @@ public class LogsAndDescribeConfigAreFullyRedactedAcceptanceTest {
 
     GfshExecution execution =
         gfsh.execute(connectCommand, describeLocatorConfigCommand, describeServerConfigCommand);
-    assertThat(execution.getOutputText()).doesNotContain(sharedPasswordString);
+    assertThat(execution.getOutputText()).doesNotContain(PASSWORD);
   }
 }
diff --git a/geode-core/src/integrationTest/java/org/apache/geode/internal/util/redaction/ArgumentRedactorIntegrationTest.java b/geode-core/src/integrationTest/java/org/apache/geode/internal/util/redaction/ArgumentRedactorIntegrationTest.java
new file mode 100644
index 0000000..be358e3
--- /dev/null
+++ b/geode-core/src/integrationTest/java/org/apache/geode/internal/util/redaction/ArgumentRedactorIntegrationTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.contrib.java.lang.system.RestoreSystemProperties;
+
+import org.apache.geode.internal.logging.Banner;
+
+public class ArgumentRedactorIntegrationTest {
+
+  private static final String someProperty = "redactorTest.someProperty";
+  private static final String somePasswordProperty = "redactorTest.aPassword";
+  private static final String someOtherPasswordProperty =
+      "redactorTest.aPassword-withCharactersAfterward";
+
+  @Rule
+  public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
+
+  @Test
+  public void systemPropertiesGetRedactedInBanner() {
+    System.setProperty(someProperty, "isNotRedacted");
+    System.setProperty(somePasswordProperty, "isRedacted");
+    System.setProperty(someOtherPasswordProperty, "isRedacted");
+
+    List<String> args = asList("--user=me", "--password=isRedacted",
+        "--another-password-for-some-reason =isRedacted", "--yet-another-password = isRedacted",
+        "--one-more-password isRedacted");
+
+    String banner = new Banner().getString(args.toArray(new String[0]));
+
+    assertThat(banner).doesNotContain("isRedacted");
+  }
+}
diff --git a/geode-core/src/integrationTest/resources/org/apache/geode/codeAnalysis/excludedClasses.txt b/geode-core/src/integrationTest/resources/org/apache/geode/codeAnalysis/excludedClasses.txt
index a96907f..99ab950 100644
--- a/geode-core/src/integrationTest/resources/org/apache/geode/codeAnalysis/excludedClasses.txt
+++ b/geode-core/src/integrationTest/resources/org/apache/geode/codeAnalysis/excludedClasses.txt
@@ -60,6 +60,7 @@ org/apache/geode/internal/shared/TCPSocketOptions
 org/apache/geode/internal/statistics/platform/LinuxProcFsStatistics$CPU
 org/apache/geode/internal/tcp/VersionedByteBufferInputStream
 org/apache/geode/internal/util/concurrent/StoppableReadWriteLock
+org/apache/geode/internal/util/redaction/ParserRegex$Group
 org/apache/geode/logging/internal/LogMessageRegex$Group
 org/apache/geode/logging/internal/log4j/LogWriterLogger
 org/apache/geode/logging/internal/spi/LogLevelUpdateOccurs
diff --git a/geode-core/src/main/java/org/apache/geode/internal/AbstractConfig.java b/geode-core/src/main/java/org/apache/geode/internal/AbstractConfig.java
index 4daeb4e..03c4d6d 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/AbstractConfig.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/AbstractConfig.java
@@ -384,7 +384,7 @@ public abstract class AbstractConfig implements Config {
         attributeValueToPrint = getAttribute(name);
       } else if (sourceIsSecured) {
         // Never show secure sources
-        attributeValueToPrint = ArgumentRedactor.redacted;
+        attributeValueToPrint = ArgumentRedactor.getRedacted();
       } else {
         // Otherwise, redact based on the key string
         attributeValueToPrint =
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/ArgumentRedactor.java b/geode-core/src/main/java/org/apache/geode/internal/util/ArgumentRedactor.java
index 172d449..9df1a4e 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/util/ArgumentRedactor.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/ArgumentRedactor.java
@@ -12,209 +12,89 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
-
 package org.apache.geode.internal.util;
 
-import java.util.Collections;
+import java.util.Collection;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
 import org.apache.geode.annotations.Immutable;
-import org.apache.geode.distributed.ConfigurationProperties;
-import org.apache.geode.distributed.internal.DistributionConfig;
+import org.apache.geode.internal.util.redaction.StringRedaction;
 
 public class ArgumentRedactor {
-  public static final String redacted = "********";
 
   @Immutable
-  private static final List<String> tabooToContain =
-      Collections.unmodifiableList(ArrayUtils.asList("password"));
-  @Immutable
-  private static final List<String> tabooForOptionToStartWith =
-      Collections.unmodifiableList(ArrayUtils.asList(DistributionConfig.SYS_PROP_NAME,
-          DistributionConfig.SSL_SYSTEM_PROPS_NAME,
-          ConfigurationProperties.SECURITY_PREFIX));
-
-  private static final Pattern optionWithArgumentPattern = getOptionWithArgumentPattern();
+  private static final StringRedaction DELEGATE = new StringRedaction();
 
+  private ArgumentRedactor() {
+    // do not instantiate
+  }
 
   /**
-   * This method returns the {@link java.util.regex.Pattern} given below, used to capture
-   * command-line options that accept an argument. For clarity, the regex is given here without
-   * the escape characters required by Java's string handling.
-   * <p>
-   *
-   * {@code ((?:^| )(?:--J=)?--?)([^\s=]+)(?=[ =])( *[ =] *)(?! *-)((?:"[^"]*"|\S+))}
+   * Parse a string to find key/value pairs and redact the values if identified as sensitive.
    *
    * <p>
-   * This pattern consists of one captured boundary,
-   * three additional capture groups, and two look-ahead boundaries.
+   * The following format is expected:<br>
+   * - Each key/value pair should be separated by spaces.<br>
+   * - The key must be preceded by '--', '-D', or '--J=-D'.<br>
+   * - The value may optionally be wrapped in quotation marks.<br>
+   * - The value is assigned to a key with '=', '=' padded with any number of optional spaces, or
+   * any number of spaces without '='.<br>
+   * - The value must not contain spaces without being wrapped in quotation marks.<br>
+   * - The value may contain spaces or any other symbols when wrapped in quotation marks.
    *
    * <p>
-   * The four capture groups are:
-   * <ul>
-   * <li>[1] The beginning boundary, including at most one leading space,
-   * possibly including "--J=", and including the option's leading "-" or "--"</li>
-   * <li>[2] The option, which cannot include spaces</li>
-   * <li>[3] The option / argument separator, consisting of at least one character
-   * made of spaces and/or at most one "="</li>
-   * <li>[4] The argument, which terminates at the next space unless it is encapsulated by
-   * quotation-marks, in which case it terminates at the next quotation mark.</li>
-   * </ul>
+   * Examples:
+   * <ol>
+   * <li>"--password=secret"
+   * <li>"--user me --password secret"
+   * <li>"-Dflag -Dopt=arg"
+   * <li>"--classpath=."
+   * <li>"password=secret"
+   * </ol>
    *
-   * Look-ahead groups avoid falsely identifying two flag options (e.g. `{@code --help --all}`) from
-   * interpreting the second flag as the argument to the first option
-   * (here, misinterpreting as `{@code --help="--all"}`).
-   * <p>
+   * @param string The string input to be parsed
    *
-   * Note that at time of writing, the argument (capture group 4) is not consumed by this class's
-   * logic, but its capture has proven repeatedly useful during iteration and testing.
+   * @return A string that has sensitive data redacted.
    */
-  private static Pattern getOptionWithArgumentPattern() {
-    String capture_beginningBoundary;
-    {
-      String spaceOrBeginningAnchor = "(?:^| )";
-      String maybeLeadingWithDashDashJEquals = "(?:--J=)?";
-      String oneOrTwoDashes = "--?";
-      capture_beginningBoundary =
-          "(" + spaceOrBeginningAnchor + maybeLeadingWithDashDashJEquals + oneOrTwoDashes + ")";
-    }
-
-    String capture_optionNameHasNoSpaces = "([^\\s=]+)";
-
-    String boundary_lookAheadForSpaceOrEquals = "(?=[ =])";
-
-    String capture_optionArgumentSeparator = "( *[ =] *)";
-
-    String boundary_negativeLookAheadToPreventNextOptionAsThisArgument = "(?! *-)";
-
-    String capture_Argument;
-    {
-      String argumentCanBeAnythingBetweenQuotes = "\"[^\"]*\"";
-      String argumentCanHaveNoSpacesWithoutQuotes = "\\S+";
-      String argumentCanBeEitherOfTheAbove = "(?:" + argumentCanBeAnythingBetweenQuotes + "|"
-          + argumentCanHaveNoSpacesWithoutQuotes + ")";
-      capture_Argument = "(" + argumentCanBeEitherOfTheAbove + ")";
-    }
-
-    String fullPattern = capture_beginningBoundary + capture_optionNameHasNoSpaces
-        + boundary_lookAheadForSpaceOrEquals + capture_optionArgumentSeparator
-        + boundary_negativeLookAheadToPreventNextOptionAsThisArgument + capture_Argument;
-    return Pattern.compile(fullPattern);
+  public static String redact(String string) {
+    return DELEGATE.redact(string);
   }
 
-  private ArgumentRedactor() {}
+  public static String redact(Iterable<String> strings) {
+    return DELEGATE.redact(strings);
+  }
 
   /**
-   * Parse a string to find option/argument pairs and redact the arguments if necessary.<br>
-   *
-   * The following format is expected:<br>
-   * - Each option/argument pair should be separated by spaces.<br>
-   * - The option of each pair must be preceded by at least one hyphen '-'.<br>
-   * - Arguments may or may not be wrapped in quotation marks.<br>
-   * - Options and arguments may be separated by an equals sign '=' or any number of spaces.<br>
-   * <br>
-   * Examples:<br>
-   * "--password=secret"<br>
-   * "--user me --password secret"<br>
-   * "-Dflag -Dopt=arg"<br>
-   * "--classpath=."<br>
-   *
-   * See {@link #getOptionWithArgumentPattern()} for more information on
-   * the regular expression used.
+   * Return the redacted value string if the provided key is identified as sensitive, otherwise
+   * return the original value.
    *
-   * @param line The argument input to be parsed
-   * @param permitFirstPairWithoutHyphen When true, prepends the line with a "-", which is later
-   *        removed. This allows the use on, e.g., "password=secret" rather than "--password=secret"
+   * @param key A string such as a system property, java option, or command-line key.
+   * @param value The string value for the key.
    *
-   * @return A redacted string that has sensitive information obscured.
-   */
-  public static String redact(String line, boolean permitFirstPairWithoutHyphen) {
-
-    boolean wasPaddedWithHyphen = false;
-    if (!line.trim().startsWith("-") && permitFirstPairWithoutHyphen) {
-      line = "-" + line.trim();
-      wasPaddedWithHyphen = true;
-    }
-
-    Matcher matcher = optionWithArgumentPattern.matcher(line);
-    while (matcher.find()) {
-      String option = matcher.group(2);
-      if (!isTaboo(option)) {
-        continue;
-      }
-
-      String leadingBoundary = matcher.group(1);
-      String separator = matcher.group(3);
-      String withRedaction = leadingBoundary + option + separator + redacted;
-      line = line.replace(matcher.group(), withRedaction);
-    }
-
-    if (wasPaddedWithHyphen) {
-      line = line.substring(1);
-    }
-    return line;
-  }
-
-  /**
-   * Alias for {@code redact(line, true)}. See
-   * {@link org.apache.geode.internal.util.ArgumentRedactor#redact(java.lang.String, boolean)}
+   * @return The redacted string if the key is identified as sensitive, otherwise the original
+   *         value.
    */
-  public static String redact(String line) {
-    return redact(line, true);
-  }
-
-  public static String redact(final List<String> args) {
-    return redact(String.join(" ", args));
+  public static String redactArgumentIfNecessary(String key, String value) {
+    return DELEGATE.redactArgumentIfNecessary(key, value);
   }
 
-  /**
-   * Return the redaction string if the provided option's argument should be redacted.
-   * Otherwise, return the provided argument unchanged.
-   *
-   * @param option A string such as a system property, jvm parameter or command-line option.
-   * @param argument A string that is the argument assigned to the option.
-   *
-   * @return A redacted string if the option indicates it should be redacted, otherwise the
-   *         provided argument.
-   */
-  public static String redactArgumentIfNecessary(String option, String argument) {
-    if (isTaboo(option)) {
-      return redacted;
-    }
-    return argument;
+  public static List<String> redactEachInList(Collection<String> strings) {
+    return DELEGATE.redactEachInList(strings);
   }
 
   /**
-   * Determine whether a option's argument should be redacted.
+   * Returns true if a string identifies sensitive data. For example, a string containing
+   * the word "password" identifies data that is sensitive and should be secured.
    *
-   * @param option The option in question.
+   * @param key The string to be evaluated.
    *
-   * @return true if the value should be redacted, otherwise false.
+   * @return true if the string identifies sensitive data.
    */
-  static boolean isTaboo(String option) {
-    if (option == null) {
-      return false;
-    }
-    for (String taboo : tabooForOptionToStartWith) {
-      // If a parameter is passed with -Dsecurity-option=argument, the option option is
-      // "Dsecurity-option".
-      // With respect to taboo words, also check for the addition of the extra D
-      if (option.toLowerCase().startsWith(taboo) || option.toLowerCase().startsWith("d" + taboo)) {
-        return true;
-      }
-    }
-    for (String taboo : tabooToContain) {
-      if (option.toLowerCase().contains(taboo)) {
-        return true;
-      }
-    }
-    return false;
+  public static boolean isSensitive(String key) {
+    return DELEGATE.isSensitive(key);
   }
 
-  public static List<String> redactEachInList(List<String> argList) {
-    return argList.stream().map(ArgumentRedactor::redact).collect(Collectors.toList());
+  public static String getRedacted() {
+    return DELEGATE.getRedacted();
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionary.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionary.java
new file mode 100644
index 0000000..b63efce
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionary.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static java.util.Arrays.stream;
+
+import java.util.stream.Collectors;
+
+/**
+ * Delegates to multiple instances of SensitiveDataDictionary.
+ */
+class CombinedSensitiveDictionary implements SensitiveDataDictionary {
+
+  private final Iterable<SensitiveDataDictionary> dictionaries;
+
+  CombinedSensitiveDictionary(SensitiveDataDictionary... dictionaries) {
+    this.dictionaries = stream(dictionaries).collect(Collectors.toSet());
+  }
+
+  @Override
+  public boolean isSensitive(String string) {
+    for (SensitiveDataDictionary dictionary : dictionaries) {
+      if (dictionary.isSensitive(string)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/ParserRegex.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/ParserRegex.java
new file mode 100644
index 0000000..0bfe6d8
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/ParserRegex.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.apache.geode.internal.util.redaction.ParserRegex.Group.ASSIGN;
+import static org.apache.geode.internal.util.redaction.ParserRegex.Group.KEY;
+import static org.apache.geode.internal.util.redaction.ParserRegex.Group.PREFIX;
+import static org.apache.geode.internal.util.redaction.ParserRegex.Group.VALUE;
+
+import java.util.regex.Pattern;
+
+/**
+ * Regex with named capture groups that can be used to match strings containing a key with value or
+ * a flag.
+ *
+ * <p>
+ * The raw regex string is:
+ *
+ * {@code (?<prefix>--J=-D|-D|--)(?<key>[^\s=]+)(?:(?! (?:--J=-D|-D|--))(?<assign> *[ =] *)(?! (?:--J=-D|-D|--))(?<value>"[^"]*"|\S+))?}
+ */
+class ParserRegex {
+
+  private static final String REGEX =
+      String.valueOf(PREFIX) +
+          KEY +
+          "(?:(?! (?:--J=-D|-D|--))" +
+          ASSIGN + "(?! (?:--J=-D|-D|--))" +
+          VALUE +
+          ")?";
+
+  private static final Pattern PATTERN = Pattern.compile(REGEX);
+
+  static String getRegex() {
+    return REGEX;
+  }
+
+  static Pattern getPattern() {
+    return PATTERN;
+  }
+
+  enum Group {
+    /** Prefix precedes each key and may be --, -D, or --J=-D */
+    PREFIX(1, "prefix", "(?<prefix>--J=-D|-D|--)"),
+
+    /** Key has a value or represents a flag when no value is assigned */
+    KEY(2, "key", "(?<key>[^\\s=]+)"),
+
+    /** Assign is an operator for assigning a value to a key and may be = or space */
+    ASSIGN(3, "assign", "(?<assign> *[ =] *)"),
+
+    /** Value is assigned to a key following an assign operator */
+    VALUE(4, "value", "(?<value>\"[^\"]*\"|\\S+)");
+
+    private final int index;
+    private final String name;
+    private final String regex;
+
+    Group(final int index, final String name, final String regex) {
+      this.index = index;
+      this.name = name;
+      this.regex = regex;
+    }
+
+    int getIndex() {
+      return index;
+    }
+
+    String getName() {
+      return name;
+    }
+
+    String getRegex() {
+      return regex;
+    }
+
+    @Override
+    public String toString() {
+      return regex;
+    }
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionDefaults.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionDefaults.java
new file mode 100644
index 0000000..ced2b84
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionDefaults.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static java.util.Collections.unmodifiableList;
+import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_PREFIX;
+import static org.apache.geode.distributed.internal.DistributionConfig.SSL_SYSTEM_PROPS_NAME;
+import static org.apache.geode.distributed.internal.DistributionConfig.SYS_PROP_NAME;
+import static org.apache.geode.internal.util.ArrayUtils.asList;
+
+import java.util.List;
+
+import org.apache.geode.annotations.Immutable;
+
+/**
+ * Default strings that indicate sensitive data requiring redaction.
+ */
+class RedactionDefaults {
+
+  static final String REDACTED = "********";
+
+  private static final String JAVA_OPTION_D = "-D";
+  private static final String GFSH_OPTION_JD = "--J=-D";
+
+  /**
+   * Strings containing these substrings are flagged as sensitive.
+   */
+  @Immutable
+  static final List<String> SENSITIVE_SUBSTRINGS =
+      unmodifiableList(asList("password"));
+
+  /**
+   * Strings starting with these prefixes are flagged as sensitive.
+   */
+  @Immutable
+  static final List<String> SENSITIVE_PREFIXES =
+      unmodifiableList(asList(SYS_PROP_NAME,
+          SSL_SYSTEM_PROPS_NAME,
+          SECURITY_PREFIX,
+          JAVA_OPTION_D + SYS_PROP_NAME,
+          JAVA_OPTION_D + SSL_SYSTEM_PROPS_NAME,
+          JAVA_OPTION_D + SECURITY_PREFIX,
+          GFSH_OPTION_JD + SYS_PROP_NAME,
+          GFSH_OPTION_JD + SSL_SYSTEM_PROPS_NAME,
+          GFSH_OPTION_JD + SECURITY_PREFIX));
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionStrategy.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionStrategy.java
new file mode 100644
index 0000000..c305822
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RedactionStrategy.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+/**
+ * Defines the algorithm for scanning input strings for redaction of sensitive strings.
+ */
+@FunctionalInterface
+interface RedactionStrategy {
+
+  /**
+   * Parse a string to find key/value pairs and redact the values if identified as sensitive.
+   *
+   * <p>
+   * The following format is expected:<br>
+   * - Each key/value pair should be separated by spaces.<br>
+   * - The key must be preceded by '--', '-D', or '--J=-D'.<br>
+   * - The value may optionally be wrapped in quotation marks.<br>
+   * - The value is assigned to a key with '=', '=' padded with any number of optional spaces, or
+   * any number of spaces without '='.<br>
+   * - The value must not contain spaces without being wrapped in quotation marks.<br>
+   * - The value may contain spaces or any other symbols when wrapped in quotation marks.
+   *
+   * <p>
+   * Examples:
+   * <ol>
+   * <li>"--password=secret"
+   * <li>"--user me --password secret"
+   * <li>"-Dflag -Dopt=arg"
+   * <li>"--classpath=."
+   * <li>"password=secret"
+   * </ol>
+   *
+   * @param string The string input to be parsed
+   *
+   * @return A string that has sensitive data redacted.
+   */
+  String redact(String string);
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategy.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategy.java
new file mode 100644
index 0000000..1f758eb
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import java.util.function.Function;
+import java.util.regex.Matcher;
+
+/**
+ * Simple redaction strategy using regex to parse options.
+ */
+class RegexRedactionStrategy implements RedactionStrategy {
+
+  private final Function<String, Boolean> isSensitive;
+  private final String redacted;
+
+  RegexRedactionStrategy(Function<String, Boolean> isSensitive, String redacted) {
+    this.isSensitive = isSensitive;
+    this.redacted = redacted;
+  }
+
+  @Override
+  public String redact(String string) {
+    Matcher matcher = ParserRegex.getPattern().matcher(string);
+    while (matcher.find()) {
+      String option = matcher.group(2);
+      if (!isSensitive.apply(option)) {
+        continue;
+      }
+
+      String leadingBoundary = matcher.group(1);
+      String separator = matcher.group(3);
+      String withRedaction = leadingBoundary + option + separator + redacted;
+      string = string.replace(matcher.group(), withRedaction);
+    }
+
+    return string;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveDataDictionary.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveDataDictionary.java
new file mode 100644
index 0000000..deb73e4
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveDataDictionary.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+/**
+ * Evaluates strings to determine if they identify sensitive data.
+ */
+@FunctionalInterface
+interface SensitiveDataDictionary {
+
+  /**
+   * Returns true if a string identifies sensitive data. For example, a string containing
+   * the word "password" identifies data that is sensitive and should be secured.
+   *
+   * @param string The string to be evaluated.
+   *
+   * @return true if the string identifies sensitive data.
+   */
+  boolean isSensitive(String string);
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionary.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionary.java
new file mode 100644
index 0000000..9d08670
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionary.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+class SensitivePrefixDictionary implements SensitiveDataDictionary {
+
+  private final Iterable<String> sensitivePrefixes;
+
+  SensitivePrefixDictionary(Iterable<String> sensitivePrefixes) {
+    this.sensitivePrefixes = sensitivePrefixes;
+  }
+
+  @Override
+  public boolean isSensitive(String string) {
+    if (string == null) {
+      return false;
+    }
+    for (String prefix : sensitivePrefixes) {
+      if (string.toLowerCase().startsWith(prefix.toLowerCase())) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionary.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionary.java
new file mode 100644
index 0000000..5cd4342
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionary.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+class SensitiveSubstringDictionary implements SensitiveDataDictionary {
+
+  private final Iterable<String> sensitiveSubstrings;
+
+  SensitiveSubstringDictionary(Iterable<String> sensitiveSubstrings) {
+    this.sensitiveSubstrings = sensitiveSubstrings;
+  }
+
+  @Override
+  public boolean isSensitive(String string) {
+    if (string == null) {
+      return false;
+    }
+    for (String substring : sensitiveSubstrings) {
+      if (string.toLowerCase().contains(substring)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/geode-core/src/main/java/org/apache/geode/internal/util/redaction/StringRedaction.java b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/StringRedaction.java
new file mode 100644
index 0000000..ea514fc
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/internal/util/redaction/StringRedaction.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.geode.internal.util.redaction.RedactionDefaults.REDACTED;
+import static org.apache.geode.internal.util.redaction.RedactionDefaults.SENSITIVE_PREFIXES;
+import static org.apache.geode.internal.util.redaction.RedactionDefaults.SENSITIVE_SUBSTRINGS;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.geode.annotations.VisibleForTesting;
+
+/**
+ * Redacts value strings for keys that are identified as sensitive data.
+ */
+public class StringRedaction implements SensitiveDataDictionary {
+
+  private final String redacted;
+  private final SensitiveDataDictionary sensitiveDataDictionary;
+  private final RedactionStrategy redactionStrategy;
+
+  public StringRedaction() {
+    this(REDACTED,
+        new CombinedSensitiveDictionary(
+            new SensitivePrefixDictionary(SENSITIVE_PREFIXES),
+            new SensitiveSubstringDictionary(SENSITIVE_SUBSTRINGS)));
+  }
+
+  private StringRedaction(String redacted, SensitiveDataDictionary sensitiveDataDictionary) {
+    this(redacted,
+        sensitiveDataDictionary,
+        new RegexRedactionStrategy(sensitiveDataDictionary::isSensitive, redacted));
+  }
+
+  @VisibleForTesting
+  StringRedaction(String redacted, SensitiveDataDictionary sensitiveDataDictionary,
+      RedactionStrategy redactionStrategy) {
+    this.redacted = redacted;
+    this.sensitiveDataDictionary = sensitiveDataDictionary;
+    this.redactionStrategy = redactionStrategy;
+  }
+
+  /**
+   * Parse a string to find key/value pairs and redact the values if identified as sensitive.
+   *
+   * <p>
+   * The following format is expected:<br>
+   * - Each key/value pair should be separated by spaces.<br>
+   * - The key must be preceded by '--', '-D', or '--J=-D'.<br>
+   * - The value may optionally be wrapped in quotation marks.<br>
+   * - The value is assigned to a key with '=', '=' padded with any number of optional spaces, or
+   * any number of spaces without '='.<br>
+   * - The value must not contain spaces without being wrapped in quotation marks.<br>
+   * - The value may contain spaces or any other symbols when wrapped in quotation marks.
+   *
+   * <p>
+   * Examples:
+   * <ol>
+   * <li>"--password=secret"
+   * <li>"--user me --password secret"
+   * <li>"-Dflag -Dopt=arg"
+   * <li>"--classpath=."
+   * <li>"password=secret"
+   * </ol>
+   *
+   * @param string The string input to be parsed
+   *
+   * @return A string that has sensitive data redacted.
+   */
+  public String redact(String string) {
+    return redactionStrategy.redact(string);
+  }
+
+  public String redact(Iterable<String> strings) {
+    return redact(String.join(" ", strings));
+  }
+
+  /**
+   * Return the redacted value string if the provided key is identified as sensitive, otherwise
+   * return the original value.
+   *
+   * @param key A string such as a system property, java option, or command-line key.
+   * @param value The string value for the key.
+   *
+   * @return The redacted string if the key is identified as sensitive, otherwise the original
+   *         value.
+   */
+  public String redactArgumentIfNecessary(String key, String value) {
+    if (isSensitive(key)) {
+      return redacted;
+    }
+    return value;
+  }
+
+  public List<String> redactEachInList(Collection<String> strings) {
+    return strings.stream()
+        .map(this::redact)
+        .collect(toList());
+  }
+
+  @Override
+  public boolean isSensitive(String key) {
+    return sensitiveDataDictionary.isSensitive(key);
+  }
+
+  public String getRedacted() {
+    return redacted;
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorJUnitTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorJUnitTest.java
deleted file mode 100644
index 129627c..0000000
--- a/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorJUnitTest.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
- * agreements. See the NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The ASF licenses this file to You 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.
- */
-
-package org.apache.geode.internal.util;
-
-import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_ENABLED;
-import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_TRUSTSTORE_PASSWORD;
-import static org.apache.geode.distributed.ConfigurationProperties.CONSERVE_SOCKETS;
-import static org.apache.geode.distributed.ConfigurationProperties.GATEWAY_SSL_TRUSTSTORE_PASSWORD;
-import static org.apache.geode.distributed.ConfigurationProperties.SERVER_SSL_KEYSTORE_PASSWORD;
-import static org.apache.geode.internal.util.ArgumentRedactor.isTaboo;
-import static org.apache.geode.internal.util.ArgumentRedactor.redact;
-import static org.apache.geode.internal.util.ArgumentRedactor.redactEachInList;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import org.junit.Test;
-
-import org.apache.geode.internal.logging.Banner;
-
-public class ArgumentRedactorJUnitTest {
-  private static final String someProperty = "redactorTest.someProperty";
-  private static final String somePasswordProperty = "redactorTest.aPassword";
-  private static final String someOtherPasswordProperty =
-      "redactorTest.aPassword-withCharactersAfterward";
-
-  @Test
-  public void theseLinesShouldRedact() {
-    String argumentThatShouldBeRedacted = "__this_should_be_redacted__";
-    List<String> someTabooOptions =
-        Arrays.asList("-Dgemfire.password=" + argumentThatShouldBeRedacted,
-            "--password=" + argumentThatShouldBeRedacted,
-            "--J=-Dgemfire.some.very.qualified.item.password=" + argumentThatShouldBeRedacted,
-            "--J=-Dsysprop-secret.information=" + argumentThatShouldBeRedacted);
-
-    List<String> fullyRedacted = redactEachInList(someTabooOptions);
-    assertThat(fullyRedacted).doesNotContainAnyElementsOf(someTabooOptions);
-  }
-
-  @Test
-  public void redactorWillIdentifySampleTabooProperties() {
-    List<String> shouldBeRedacted = Arrays.asList("gemfire.security-password", "password",
-        "other-password-option", CLUSTER_SSL_TRUSTSTORE_PASSWORD, GATEWAY_SSL_TRUSTSTORE_PASSWORD,
-        SERVER_SSL_KEYSTORE_PASSWORD, "security-username", "security-manager",
-        "security-important-property", "javax.net.ssl.keyStorePassword",
-        "javax.net.ssl.some.security.item", "javax.net.ssl.keyStoreType", "sysprop-secret-prop");
-    for (String option : shouldBeRedacted) {
-      assertThat(isTaboo(option))
-          .describedAs("This option should be identified as taboo: " + option).isTrue();
-    }
-  }
-
-  @Test
-  public void redactorWillAllowSampleMiscProperties() {
-    List<String> shouldNotBeRedacted = Arrays.asList("gemfire.security-manager",
-        CLUSTER_SSL_ENABLED, CONSERVE_SOCKETS, "username", "just-an-option");
-    for (String option : shouldNotBeRedacted) {
-      assertThat(isTaboo(option))
-          .describedAs("This option should not be identified as taboo: " + option).isFalse();
-    }
-  }
-
-  @Test
-  public void argListOfPasswordsAllRedact() {
-    List<String> argList = new ArrayList<>();
-    argList.add("--gemfire.security-password=secret");
-    argList.add("--login-password=secret");
-    argList.add("--gemfire-password = super-secret");
-    argList.add("--geode-password= confidential");
-    argList.add("--some-other-password =shhhh");
-    argList.add("--justapassword =failed");
-    String redacted = redact(argList);
-    assertThat(redacted).contains("--gemfire.security-password=********");
-    assertThat(redacted).contains("--login-password=********");
-    assertThat(redacted).contains("--gemfire-password = ********");
-    assertThat(redacted).contains("--geode-password= ********");
-    assertThat(redacted).contains("--some-other-password =********");
-    assertThat(redacted).contains("--justapassword =********");
-  }
-
-  @Test
-  public void argListOfPasswordsAllRedactViaRedactEachInList() {
-    List<String> argList = new ArrayList<>();
-    argList.add("--gemfire.security-password=secret");
-    argList.add("--login-password=secret");
-    argList.add("--gemfire-password = super-secret");
-    argList.add("--geode-password= confidential");
-    argList.add("--some-other-password =shhhh");
-    argList.add("--justapassword =failed");
-    List<String> redacted = redactEachInList(argList);
-    assertThat(redacted).contains("--gemfire.security-password=********");
-    assertThat(redacted).contains("--login-password=********");
-    assertThat(redacted).contains("--gemfire-password = ********");
-    assertThat(redacted).contains("--geode-password= ********");
-    assertThat(redacted).contains("--some-other-password =********");
-    assertThat(redacted).contains("--justapassword =********");
-  }
-
-
-  @Test
-  public void argListOfMiscOptionsDoNotRedact() {
-    List<String> argList = new ArrayList<>();
-    argList.add("--gemfire.security-properties=./security.properties");
-    argList.add("--gemfire.sys.security-option=someArg");
-    argList.add("--gemfire.use-cluster-configuration=true");
-    argList.add("--someotherstringoption");
-    argList.add("--login-name=admin");
-    argList.add("--myArg --myArg2 --myArg3=-arg4");
-    argList.add("--myArg --myArg2 --myArg3=\"-arg4\"");
-    String redacted = redact(argList);
-    assertThat(redacted).contains("--gemfire.security-properties=./security.properties");
-    assertThat(redacted).contains("--gemfire.sys.security-option=someArg");
-    assertThat(redacted).contains("--gemfire.use-cluster-configuration=true");
-    assertThat(redacted).contains("--someotherstringoption");
-    assertThat(redacted).contains("--login-name=admin");
-    assertThat(redacted).contains("--myArg --myArg2 --myArg3=-arg4");
-    assertThat(redacted).contains("--myArg --myArg2 --myArg3=\"-arg4\"");
-  }
-
-  @Test
-  public void protectedIndividualOptionsRedact() {
-    String arg;
-
-    arg = "-Dgemfire.security-password=secret";
-    assertThat(redact(arg)).endsWith("password=********");
-
-    arg = "--J=-Dsome.highly.qualified.password=secret";
-    assertThat(redact(arg)).endsWith("password=********");
-
-    arg = "--password=foo";
-    assertThat(redact(arg)).isEqualToIgnoringWhitespace("--password=********");
-
-    arg = "-Dgemfire.security-properties=\"c:\\Program Files (x86)\\My Folder\"";
-    assertThat(redact(arg)).isEqualTo(arg);
-  }
-
-  @Test
-  public void miscIndividualOptionsDoNotRedact() {
-    String arg;
-
-    arg = "-Dgemfire.security-properties=./security-properties";
-    assertThat(redact(arg)).isEqualTo(arg);
-
-    arg = "-J-Dgemfire.sys.security-option=someArg";
-    assertThat(redact(arg)).isEqualTo(arg);
-
-    arg = "-Dgemfire.sys.option=printable";
-    assertThat(redact(arg)).isEqualTo(arg);
-
-    arg = "-Dgemfire.use-cluster-configuration=true";
-    assertThat(redact(arg)).isEqualTo(arg);
-
-    arg = "someotherstringoption";
-    assertThat(redact(arg)).isEqualTo(arg);
-
-    arg = "--classpath=.";
-    assertThat(redact(arg)).isEqualTo(arg);
-  }
-
-  @Test
-  public void wholeLinesAreProperlyRedacted() {
-    String arg;
-    arg = "-DmyArg -Duser-password=foo --classpath=.";
-    assertThat(redact(arg)).isEqualTo("-DmyArg -Duser-password=******** --classpath=.");
-
-    arg = "-DmyArg -Duser-password=foo -DOtherArg -Dsystem-password=bar";
-    assertThat(redact(arg))
-        .isEqualTo("-DmyArg -Duser-password=******** -DOtherArg -Dsystem-password=********");
-
-    arg =
-        "-Dlogin-password=secret -Dlogin-name=admin -Dgemfire-password = super-secret --geode-password= confidential -J-Dsome-other-password =shhhh";
-    String redacted = redact(arg);
-    assertThat(redacted).contains("login-password=********");
-    assertThat(redacted).contains("login-name=admin");
-    assertThat(redacted).contains("gemfire-password = ********");
-    assertThat(redacted).contains("geode-password= ********");
-    assertThat(redacted).contains("some-other-password =********");
-  }
-
-  @Test
-  public void redactScriptLine() {
-    assertThat(redact("connect --password=test --user=test"))
-        .isEqualTo("connect --password=******** --user=test");
-
-    assertThat(redact("connect --test-password=test --product-password=test1"))
-        .isEqualTo("connect --test-password=******** --product-password=********");
-  }
-
-  @Test
-  public void systemPropertiesGetRedactedInBanner() {
-    try {
-      System.setProperty(someProperty, "isNotRedacted");
-      System.setProperty(somePasswordProperty, "isRedacted");
-      System.setProperty(someOtherPasswordProperty, "isRedacted");
-
-      List<String> args = ArrayUtils.asList("--user=me", "--password=isRedacted",
-          "--another-password-for-some-reason =isRedacted", "--yet-another-password = isRedacted");
-      String banner = new Banner().getString(args.toArray(new String[0]));
-      assertThat(banner).doesNotContain("isRedacted");
-    } finally {
-      System.clearProperty(someProperty);
-      System.clearProperty(somePasswordProperty);
-      System.clearProperty(someOtherPasswordProperty);
-    }
-  }
-}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorTest.java
new file mode 100644
index 0000000..6a60c95
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/ArgumentRedactorTest.java
@@ -0,0 +1,675 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util;
+
+import static org.apache.geode.internal.util.ArgumentRedactor.getRedacted;
+import static org.apache.geode.internal.util.ArgumentRedactor.isSensitive;
+import static org.apache.geode.internal.util.ArgumentRedactor.redact;
+import static org.apache.geode.internal.util.ArgumentRedactor.redactEachInList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+
+public class ArgumentRedactorTest {
+
+  @Test
+  public void isSensitive_isTrueForGemfireSecurityPassword() {
+    String input = "gemfire.security-password";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForPassword() {
+    String input = "password";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForOptionContainingPassword() {
+    String input = "other-password-option";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForClusterSslTruststorePassword() {
+    String input = "cluster-ssl-truststore-password";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForGatewaySslTruststorePassword() {
+    String input = "gateway-ssl-truststore-password";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForServerSslKeystorePassword() {
+    String input = "server-ssl-keystore-password";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForSecurityUsername() {
+    String input = "security-username";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForSecurityManager() {
+    String input = "security-manager";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForOptionStartingWithSecurityHyphen() {
+    String input = "security-important-property";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForJavaxNetSslKeyStorePassword() {
+    String input = "javax.net.ssl.keyStorePassword";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForOptionStartingWithJavaxNetSsl() {
+    String input = "javax.net.ssl.some.security.item";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForJavaxNetSslKeyStoreType() {
+    String input = "javax.net.ssl.keyStoreType";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isTrueForOptionStartingWithSyspropHyphen() {
+    String input = "sysprop-secret-prop";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isTrue();
+  }
+
+  @Test
+  public void isSensitive_isFalseForGemfireSecurityManager() {
+    String input = "gemfire.security-manager";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isFalse();
+  }
+
+  @Test
+  public void isSensitive_isFalseForClusterSslEnabled() {
+    String input = "cluster-ssl-enabled";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isFalse();
+  }
+
+  @Test
+  public void isSensitive_isFalseForConserveSockets() {
+    String input = "conserve-sockets";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isFalse();
+  }
+
+  @Test
+  public void isSensitive_isFalseForUsername() {
+    String input = "username";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isFalse();
+  }
+
+  @Test
+  public void isSensitive_isFalseForNonMatchingStringContainingHyphens() {
+    String input = "just-an-option";
+
+    boolean output = isSensitive(input);
+
+    assertThat(output)
+        .as("output of isSensitive(" + input + ")")
+        .isFalse();
+  }
+
+  @Test
+  public void redactString_redactsGemfirePasswordWithHyphenD() {
+    String string = "-Dgemfire.password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsPasswordWithHyphens() {
+    String string = "--password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsOptionEndingWithPasswordWithHyphensJDd() {
+    String string = "--J=-Dgemfire.some.very.qualified.item.password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsOptionStartingWithSyspropHyphenWithHyphensJD() {
+    String string = "--J=-Dsysprop-secret.information=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsGemfireSecurityPasswordWithHyphenD() {
+    String string = "-Dgemfire.security-password=%s";
+    String sensitive = "secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD1() {
+    String input = "-Dgemfire.security-properties=argument-value";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD2() {
+    String input = "-Dgemfire.security-properties=\"c:\\Program Files (x86)\\My Folder\"";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD3() {
+    String input = "-Dgemfire.security-properties=./security-properties";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_doesNotRedactOptionContainingSecurityHyphenWithHyphensJD() {
+    String input = "--J=-Dgemfire.sys.security-option=someArg";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_doesNotRedactNonMatchingGemfireOptionWithHyphenD() {
+    String input = "-Dgemfire.sys.option=printable";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_redactsGemfireUseClusterConfigurationWithHyphenD() {
+    String input = "-Dgemfire.use-cluster-configuration=true";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_returnsNonMatchingString() {
+    String input = "someotherstringoption";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_doesNotRedactClasspathWithHyphens() {
+    String input = "--classpath=.";
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactString_redactsMatchingOptionWithNonMatchingOptionAndFlagAndMultiplePrefixes() {
+    String string = "--J=-Dflag -Duser-password=%s --classpath=.";
+    String sensitive = "foo";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsMultipleMatchingOptionsWithFlags() {
+    String string = "-DmyArg -Duser-password=%s -DOtherArg -Dsystem-password=%s";
+    String sensitive1 = "foo";
+    String sensitive2 = "bar";
+    String input = String.format(string, sensitive1, sensitive2);
+    String expected = String.format(string, getRedacted(), getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive1)
+        .doesNotContain(sensitive2)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsMultipleMatchingOptionsWithMultipleNonMatchingOptionsAndMultiplePrefixes() {
+    String string =
+        "-Dlogin-password=%s -Dlogin-name=%s -Dgemfire-password = %s --geode-password= %s --J=-Dsome-other-password =%s";
+    String sensitive1 = "secret";
+    String nonSensitive = "admin";
+    String sensitive2 = "super-secret";
+    String sensitive3 = "confidential";
+    String sensitive4 = "shhhh";
+    String input = String.format(
+        string, sensitive1, nonSensitive, sensitive2, sensitive3, sensitive4);
+    String expected = String.format(
+        string, getRedacted(), nonSensitive, getRedacted(), getRedacted(), getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive1)
+        .contains(nonSensitive)
+        .doesNotContain(sensitive2)
+        .doesNotContain(sensitive3)
+        .doesNotContain(sensitive4)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsMatchingOptionWithNonMatchingOptionAfterCommand() {
+    String string = "connect --password=%s --user=%s";
+    String reusedSensitive = "test";
+    String input = String.format(string, reusedSensitive, reusedSensitive);
+    String expected = String.format(string, getRedacted(), reusedSensitive);
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .contains(reusedSensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsMultipleMatchingOptionsButNotKeyUsingSameStringAsValue() {
+    String string = "connect --%s-password=%s --product-password=%s";
+    String reusedSensitive = "test";
+    String sensitive = "test1";
+    String input = String.format(string, reusedSensitive, reusedSensitive, sensitive);
+    String expected = String.format(string, reusedSensitive, getRedacted(), getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .contains(reusedSensitive)
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactRedactsGemfireSslTruststorePassword() {
+    String string = "-Dgemfire.ssl-truststore-password=%s";
+    String sensitive = "gibberish";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsGemfireSslKeystorePassword() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "gibberish";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsValueEndingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "supersecret-";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsValueContainingHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "super-secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsValueContainingManyHyphens() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "this-is-super-secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsValueStartingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "-supersecret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactString_redactsQuotedValueStartingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "\"-supersecret\"";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, getRedacted());
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactIterable_redactsMultipleMatchingOptions() {
+    String sensitive1 = "secret";
+    String sensitive2 = "super-secret";
+    String sensitive3 = "confidential";
+    String sensitive4 = "shhhh";
+    String sensitive5 = "failed";
+
+    Collection<String> input = new ArrayList<>();
+    input.add("--gemfire.security-password=" + sensitive1);
+    input.add("--login-password=" + sensitive1);
+    input.add("--gemfire-password = " + sensitive2);
+    input.add("--geode-password= " + sensitive3);
+    input.add("--some-other-password =" + sensitive4);
+    input.add("--justapassword =" + sensitive5);
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive1)
+        .doesNotContain(sensitive2)
+        .doesNotContain(sensitive3)
+        .doesNotContain(sensitive4)
+        .doesNotContain(sensitive5)
+        .contains("--gemfire.security-password=" + getRedacted())
+        .contains("--login-password=" + getRedacted())
+        .contains("--gemfire-password = " + getRedacted())
+        .contains("--geode-password= " + getRedacted())
+        .contains("--some-other-password =" + getRedacted())
+        .contains("--justapassword =" + getRedacted());
+  }
+
+  @Test
+  public void redactIterable_doesNotRedactMultipleNonMatchingOptions() {
+    Collection<String> input = new ArrayList<>();
+    input.add("--gemfire.security-properties=./security.properties");
+    input.add("--gemfire.sys.security-option=someArg");
+    input.add("--gemfire.use-cluster-configuration=true");
+    input.add("--someotherstringoption");
+    input.add("--login-name=admin");
+    input.add("--myArg --myArg2 --myArg3=-arg4");
+    input.add("--myArg --myArg2 --myArg3=\"-arg4\"");
+
+    String output = redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .contains("--gemfire.security-properties=./security.properties")
+        .contains("--gemfire.sys.security-option=someArg")
+        .contains("--gemfire.use-cluster-configuration=true")
+        .contains("--someotherstringoption")
+        .contains("--login-name=admin")
+        .contains("--myArg --myArg2 --myArg3=-arg4")
+        .contains("--myArg --myArg2 --myArg3=\"-arg4\"");
+  }
+
+  @Test
+  public void redactEachInList_redactsCollectionOfMatchingOptions() {
+    String sensitive1 = "secret";
+    String sensitive2 = "super-secret";
+    String sensitive3 = "confidential";
+    String sensitive4 = "shhhh";
+    String sensitive5 = "failed";
+
+    Collection<String> input = new ArrayList<>();
+    input.add("--gemfire.security-password=" + sensitive1);
+    input.add("--login-password=" + sensitive1);
+    input.add("--gemfire-password = " + sensitive2);
+    input.add("--geode-password= " + sensitive3);
+    input.add("--some-other-password =" + sensitive4);
+    input.add("--justapassword =" + sensitive5);
+
+    List<String> output = redactEachInList(input);
+
+    assertThat(output)
+        .as("output of redactEachInList(" + input + ")")
+        .doesNotContain(sensitive1)
+        .doesNotContain(sensitive2)
+        .doesNotContain(sensitive3)
+        .doesNotContain(sensitive4)
+        .doesNotContain(sensitive5)
+        .contains("--gemfire.security-password=" + getRedacted())
+        .contains("--login-password=" + getRedacted())
+        .contains("--gemfire-password = " + getRedacted())
+        .contains("--geode-password= " + getRedacted())
+        .contains("--some-other-password =" + getRedacted())
+        .contains("--justapassword =" + getRedacted());
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionaryTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionaryTest.java
new file mode 100644
index 0000000..ab691da
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/CombinedSensitiveDictionaryTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+
+public class CombinedSensitiveDictionaryTest {
+
+  @Test
+  public void isFalseWhenZeroDelegates() {
+    CombinedSensitiveDictionary combined = new CombinedSensitiveDictionary();
+
+    boolean result = combined.isSensitive("string");
+
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void delegatesInputToSingleDictionary() {
+    String input = "string";
+    SensitiveDataDictionary dictionary = mock(SensitiveDataDictionary.class);
+    CombinedSensitiveDictionary combined = new CombinedSensitiveDictionary(dictionary);
+
+    combined.isSensitive(input);
+
+    verify(dictionary).isSensitive(same(input));
+  }
+
+  @Test
+  public void delegatesInputToTwoDictionaries() {
+    String input = "string";
+    SensitiveDataDictionary dictionary1 = mock(SensitiveDataDictionary.class);
+    SensitiveDataDictionary dictionary2 = mock(SensitiveDataDictionary.class);
+    CombinedSensitiveDictionary combined =
+        new CombinedSensitiveDictionary(dictionary1, dictionary2);
+
+    combined.isSensitive(input);
+
+    verify(dictionary1).isSensitive(same(input));
+    verify(dictionary2).isSensitive(same(input));
+  }
+
+  @Test
+  public void delegatesInputToManyDictionaries() {
+    String input = "string";
+    SensitiveDataDictionary dictionary1 = mock(SensitiveDataDictionary.class);
+    SensitiveDataDictionary dictionary2 = mock(SensitiveDataDictionary.class);
+    SensitiveDataDictionary dictionary3 = mock(SensitiveDataDictionary.class);
+    SensitiveDataDictionary dictionary4 = mock(SensitiveDataDictionary.class);
+    CombinedSensitiveDictionary combined =
+        new CombinedSensitiveDictionary(dictionary1, dictionary2, dictionary3, dictionary4);
+
+    combined.isSensitive(input);
+
+    verify(dictionary1).isSensitive(same(input));
+    verify(dictionary2).isSensitive(same(input));
+    verify(dictionary3).isSensitive(same(input));
+    verify(dictionary4).isSensitive(same(input));
+  }
+
+  @Test
+  public void isFalseWhenManyDictionariesAreFalse() {
+    String input = "string";
+    SensitiveDataDictionary dictionary1 = createDictionary(false);
+    SensitiveDataDictionary dictionary2 = createDictionary(false);
+    SensitiveDataDictionary dictionary3 = createDictionary(false);
+    SensitiveDataDictionary dictionary4 = createDictionary(false);
+    CombinedSensitiveDictionary combined =
+        new CombinedSensitiveDictionary(dictionary1, dictionary2, dictionary3, dictionary4);
+
+    boolean result = combined.isSensitive(input);
+
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void isTrueWhenManyDictionariesAreTrue() {
+    String input = "string";
+    SensitiveDataDictionary dictionary1 = createDictionary(true);
+    SensitiveDataDictionary dictionary2 = createDictionary(true);
+    SensitiveDataDictionary dictionary3 = createDictionary(true);
+    SensitiveDataDictionary dictionary4 = createDictionary(true);
+    CombinedSensitiveDictionary combined =
+        new CombinedSensitiveDictionary(dictionary1, dictionary2, dictionary3, dictionary4);
+
+    boolean result = combined.isSensitive(input);
+
+    assertThat(result).isTrue();
+  }
+
+  @Test
+  public void isTrueWhenOneOfManyDictionariesIsTrue() {
+    String input = "string";
+    SensitiveDataDictionary dictionary1 = createDictionary(false);
+    SensitiveDataDictionary dictionary2 = createDictionary(false);
+    SensitiveDataDictionary dictionary3 = createDictionary(false);
+    SensitiveDataDictionary dictionary4 = createDictionary(true);
+    CombinedSensitiveDictionary combined =
+        new CombinedSensitiveDictionary(dictionary1, dictionary2, dictionary3, dictionary4);
+
+    boolean result = combined.isSensitive(input);
+
+    assertThat(result).isTrue();
+  }
+
+  private SensitiveDataDictionary createDictionary(boolean isSensitive) {
+    SensitiveDataDictionary dictionary = mock(SensitiveDataDictionary.class);
+    when(dictionary.isSensitive(anyString())).thenReturn(isSensitive);
+    return dictionary;
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/ParserRegexTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/ParserRegexTest.java
new file mode 100644
index 0000000..bc14929
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/ParserRegexTest.java
@@ -0,0 +1,1006 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.apache.geode.internal.util.redaction.ParserRegex.getPattern;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+
+import org.apache.geode.internal.util.redaction.ParserRegex.Group;
+
+public class ParserRegexTest {
+
+  @Test
+  public void capturesOption() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenPrefixIsHyphenD() {
+    String input = "-Doption=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenPrefixIsHyphensJD() {
+    String input = "--J=-Doption=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignIsSpace() {
+    String input = "--option argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignIsSpaceEquals() {
+    String input = "--option =argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" =");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignIsEqualsSpace() {
+    String input = "--option= argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("= ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignIsSpaceEqualsSpace() {
+    String input = "--option = argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" = ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenKeyContainsHyphens() {
+    String input = "--this-is-the-option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("this-is-the-option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueContainsHyphens() {
+    String input = "--option=this-is-the-argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("this-is-the-argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueIsQuoted() {
+    String input = "--option=\"argument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"argument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenValueIsQuotedAndAssignIsSpace() {
+    String input = "--option \"argument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"argument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignContainsTwoSpaces() {
+    String input = "--option  argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("  ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenAssignContainsManySpaces() {
+    String input = "--option   argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("   ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithHyphen() {
+    String input = "--option=-argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("-argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithHyphenAndAssignIsSpace() {
+    String input = "--option -argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("-argument");
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueBeginsWithHyphen() {
+    String input = "--option=\"-argument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"-argument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithTwoHyphens() {
+    String input = "--option=--argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("--argument");
+  }
+
+  @Test
+  public void capturesFlag() {
+    String input = "--flag";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesFlagWhenPrefixIsHyphenD() {
+    String input = "-Dflag";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesFlagWhenPrefixIsHyphensJD() {
+    String input = "--J=-Dflag";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesTwoFlags() {
+    String input = "--option --argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("argument");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesTwoFlagsWhenPrefixIsHyphenD() {
+    String input = "-Doption -Dargument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("argument");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesTwoFlagsWhenPrefixIsHyphensJD() {
+    String input = "--J=-Doption --J=-Dargument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("argument");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueBeginsWithTwoHyphens() {
+    String input = "--option=\"--argument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"--argument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueBeginsWithHyphenD() {
+    String input = "--option=\"-Dargument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"-Dargument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueBeginsWithHyphensJD() {
+    String input = "--option=\"--J=-Dargument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"--J=-Dargument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithManyHyphens() {
+    String input = "--option=---argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("---argument");
+  }
+
+  @Test
+  public void capturesTwoFlagsWhenValueBeginsWithManyHyphensAndAssignIsSpace() {
+    String input = "--option ---argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("-argument");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueBeginsWithManyHyphens() {
+    String input = "--option=\"---argument\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"---argument\"");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithHyphenD() {
+    String input = "--option=-Dargument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("-Dargument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithHyphensJD() {
+    String input = "--option=--J=-Dargument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("--J=-Dargument");
+  }
+
+  @Test
+  public void capturesOptionWithPartialValueWhenValueContainsSpace() {
+    String input = "--option=foo bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("foo");
+
+    assertThat(matcher.find()).isFalse();
+  }
+
+  @Test
+  public void capturesOptionWithPartialValueWhenValueContainsSpaceAndSingleHyphens() {
+    String input = "--option=-foo -bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("-foo");
+
+    assertThat(matcher.find()).isFalse();
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueContainsSpaceAndSingleHyphens() {
+    String input = "--option=\"-foo -bar\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"-foo -bar\"");
+  }
+
+  @Test
+  public void capturesOptionAndFlagWhenValueContainsSpaceAndDoubleHyphens() {
+    String input = "--option=--foo --bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("--foo");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("bar");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesOptionWhenQuotedValueContainsSpaceAndDoubleHyphens() {
+    String input = "--option=\"--foo --bar\"";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("\"--foo --bar\"");
+  }
+
+  @Test
+  public void capturesOptionWhenKeyContainsUnderscores() {
+    String input = "--this_is_the_option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("this_is_the_option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueContainsUnderscores() {
+    String input = "--option=this_is_the_argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("this_is_the_argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithUnderscore() {
+    String input = "--option=_argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("_argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithUnderscoreAndAssignIsSpace() {
+    String input = "--option _argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("_argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithManyUnderscores() {
+    String input = "--option=___argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("___argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithManyUnderscoresAndAssignIsSpace() {
+    String input = "--option ___argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("___argument");
+  }
+
+  @Test
+  public void capturesOptionWhenKeyContainsPeriods() {
+    String input = "--this.is.the.option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("this.is.the.option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueContainsPeriods() {
+    String input = "--option=this.is.the.argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("this.is.the.argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithPeriod() {
+    String input = "--option=.argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo(".argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithPeriodAndAssignIsSpace() {
+    String input = "--option .argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo(".argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithManyPeriods() {
+    String input = "--option=...argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("...argument");
+  }
+
+  @Test
+  public void capturesOptionWhenValueBeginsWithManyPeriodsAndAssignIsSpace() {
+    String input = "--option ...argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("...argument");
+  }
+
+  @Test
+  public void doesNotMatchWhenPrefixIsSingleHyphen() {
+    String input = "-option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isFalse();
+  }
+
+  @Test
+  public void doesNotMatchWhenPrefixIsMissing() {
+    String input = "option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isFalse();
+  }
+
+  @Test
+  public void groupZeroCapturesFullInputWhenValid() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(0)).isEqualTo(input);
+  }
+
+  @Test
+  public void groupValuesHasSizeEqualToGroupCount() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(Group.values()).hasSize(matcher.groupCount());
+  }
+
+  @Test
+  public void groupPrefixCapturesHyphens() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+  }
+
+  @Test
+  public void groupPrefixCapturesHyphenD() {
+    String input = "-Doption=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+  }
+
+  @Test
+  public void groupPrefixCapturesHyphensJD() {
+    String input = "--J=-Doption=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+  }
+
+  @Test
+  public void groupPrefixCapturesIsolatedHyphens() {
+    String prefix = "--";
+    Matcher matcher = Pattern.compile(Group.PREFIX.getRegex()).matcher(prefix);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group()).isEqualTo(prefix);
+  }
+
+  @Test
+  public void groupPrefixCapturesIsolatedHyphenD() {
+    String prefix = "-D";
+    Matcher matcher = Pattern.compile(Group.PREFIX.getRegex()).matcher(prefix);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group()).isEqualTo(prefix);
+  }
+
+  @Test
+  public void groupPrefixCapturesIsolatedHyphensJD() {
+    String prefix = "--J=-D";
+    Matcher matcher = Pattern.compile(Group.PREFIX.getRegex()).matcher(prefix);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group()).isEqualTo(prefix);
+  }
+
+  @Test
+  public void groupKeyCapturesKey() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option");
+  }
+
+  @Test
+  public void groupKeyCapturesIsolatedKey() {
+    String option = "option";
+    Matcher matcher = Pattern.compile(Group.KEY.getRegex()).matcher(option);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group()).isEqualTo(option);
+  }
+
+  @Test
+  public void groupAssignCapturesEquals() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+  }
+
+  @Test
+  public void groupAssignCapturesSpace() {
+    String input = "--option argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo(" ");
+  }
+
+  @Test
+  public void groupAssignCapturesIsolatedEqualsSurroundedBySpaces() {
+    String assignment = " = ";
+    Matcher matcher = Pattern.compile(Group.ASSIGN.getRegex()).matcher(assignment);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(assignment);
+  }
+
+  @Test
+  public void groupValueCapturesValue() {
+    String input = "--option=argument";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.matches()).isTrue();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument");
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedValue() {
+    String argument = "argument";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedValueStartingWithManyHyphens() {
+    String argument = "---argument";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void groupValueDoesNotMatchIsolatedValueContainingSpaces() {
+    String argument = "foo bar oi vey";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+
+    assertThat(matcher.matches()).isFalse();
+  }
+
+  @Test
+  public void groupValueDoesNotMatchIsolatedValueContainingDoubleHyphensAndSpaces() {
+    String argument = "--foo --bar --oi --vey";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+
+    assertThat(matcher.matches()).isFalse();
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedQuotedValueContainingDoubleHyphensAndSpaces() {
+    String argument = "\"--foo --bar --oi --vey\"";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedValueEndingWithHyphen() {
+    String argument = "value-";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedValueEndingWithQuote() {
+    String argument = "value\"";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void groupValueCapturesIsolatedValueContainingSymbols() {
+    String argument = "'v@lu!\"t";
+    Matcher matcher = Pattern.compile(Group.VALUE.getRegex()).matcher(argument);
+    assertThat(matcher.matches()).isTrue();
+
+    assertThat(matcher.group()).isEqualTo(argument);
+  }
+
+  @Test
+  public void capturesMultipleOptions() {
+    String input = "--option1=argument1 --option2=argument2";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option1");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument1");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option2");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument2");
+  }
+
+  @Test
+  public void capturesFlagAfterMultipleOptions() {
+    String input = "--option1=argument1 --option2=argument2 --flag";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option1");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument1");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option2");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("argument2");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+  }
+
+  @Test
+  public void capturesMultipleOptionsAfterFlag() {
+    String input = "--flag --option1=foo --option2=.";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option1");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("foo");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option2");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo(".");
+  }
+
+  @Test
+  public void capturesMultipleOptionsSurroundingFlag() {
+    String input = "--option1=foo --flag --option2=.";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option1");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("foo");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("option2");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo(".");
+  }
+
+  @Test
+  public void capturesMultipleOptionsAfterCommand() {
+    String input = "command --key=value --foo=bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("key");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("value");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("foo");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("bar");
+  }
+
+  @Test
+  public void capturesMultipleOptionsSurroundingFlagAfterCommand() {
+    String input = "command --key=value --flag --foo=bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("key");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("value");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("foo");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("bar");
+  }
+
+  @Test
+  public void capturesMultipleOptionsWithVariousPrefixes() {
+    String input = "--key=value -Dflag --J=-Dfoo=bar";
+    Matcher matcher = getPattern().matcher(input);
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("key");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("value");
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("flag");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isNull();
+    assertThat(matcher.group(Group.VALUE.getIndex())).isNull();
+
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group(Group.PREFIX.getIndex())).isEqualTo("--J=-D");
+    assertThat(matcher.group(Group.KEY.getIndex())).isEqualTo("foo");
+    assertThat(matcher.group(Group.ASSIGN.getIndex())).isEqualTo("=");
+    assertThat(matcher.group(Group.VALUE.getIndex())).isEqualTo("bar");
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RedactionDefaultsTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RedactionDefaultsTest.java
new file mode 100644
index 0000000..de75a4c
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RedactionDefaultsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+public class RedactionDefaultsTest {
+
+  @Test
+  public void sensitiveSubstringsContainsOnlyPassword() {
+    assertThat(RedactionDefaults.SENSITIVE_SUBSTRINGS).containsOnly("password");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsSyspropHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("sysprop-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphenDSyspropHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("-Dsysprop-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphensJDSyspropHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("--J=-Dsysprop-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsJavaxDotNetDotSsl() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("javax.net.ssl");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphenDJavaxDotNetDotSsl() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("-Djavax.net.ssl");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphensJDJavaxDotNetDotSsl() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("--J=-Djavax.net.ssl");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsSecurityHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("security-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphenDSecurityHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("-Dsecurity-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsHyphensJDSecurityHyphen() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES).contains("--J=-Dsecurity-");
+  }
+
+  @Test
+  public void sensitivePrefixesContainsOnlyExpectedStrings() {
+    assertThat(RedactionDefaults.SENSITIVE_PREFIXES)
+        .containsOnly("sysprop-", "javax.net.ssl", "security-",
+            "-Dsysprop-", "-Djavax.net.ssl", "-Dsecurity-",
+            "--J=-Dsysprop-", "--J=-Djavax.net.ssl", "--J=-Dsecurity-");
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategyTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategyTest.java
new file mode 100644
index 0000000..ba50c6c
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/RegexRedactionStrategyTest.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.apache.geode.internal.util.redaction.RedactionDefaults.SENSITIVE_PREFIXES;
+import static org.apache.geode.internal.util.redaction.RedactionDefaults.SENSITIVE_SUBSTRINGS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class RegexRedactionStrategyTest {
+
+  private static final String REDACTED = "redacted";
+
+  private RegexRedactionStrategy regexRedactionStrategy;
+
+  @Before
+  public void setUp() {
+    SensitiveDataDictionary sensitiveDataDictionary = new CombinedSensitiveDictionary(
+        new SensitivePrefixDictionary(SENSITIVE_PREFIXES),
+        new SensitiveSubstringDictionary(SENSITIVE_SUBSTRINGS));
+
+    regexRedactionStrategy =
+        new RegexRedactionStrategy(sensitiveDataDictionary::isSensitive, REDACTED);
+  }
+
+  @Test
+  public void redactsGemfirePasswordWithHyphenD() {
+    String string = "-Dgemfire.password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsPasswordWithHyphens() {
+    String string = "--password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsOptionEndingWithPasswordWithHyphensJDd() {
+    String string = "--J=-Dgemfire.some.very.qualified.item.password=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsOptionStartingWithSyspropHyphenWithHyphensJD() {
+    String string = "--J=-Dsysprop-secret.information=%s";
+    String sensitive = "__this_should_be_redacted__";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsGemfireSecurityPasswordWithHyphenD() {
+    String string = "-Dgemfire.security-password=%s";
+    String sensitive = "secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD1() {
+    String input = "-Dgemfire.security-properties=argument-value";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD2() {
+    String input = "-Dgemfire.security-properties=\"c:\\Program Files (x86)\\My Folder\"";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void doesNotRedactOptionEndingWithSecurityPropertiesWithHyphenD3() {
+    String input = "-Dgemfire.security-properties=./security-properties";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void doesNotRedactOptionContainingSecurityHyphenWithHyphensJD() {
+    String input = "--J=-Dgemfire.sys.security-option=someArg";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void doesNotRedactNonMatchingGemfireOptionWithHyphenD() {
+    String input = "-Dgemfire.sys.option=printable";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactsGemfireUseClusterConfigurationWithHyphenD() {
+    String input = "-Dgemfire.use-cluster-configuration=true";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void returnsNonMatchingString() {
+    String input = "someotherstringoption";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void doesNotRedactClasspathWithHyphens() {
+    String input = "--classpath=.";
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .isEqualTo(input);
+  }
+
+  @Test
+  public void redactsMatchingOptionWithNonMatchingOptionAndFlagAndMultiplePrefixes() {
+    String string = "--J=-Dflag -Duser-password=%s --classpath=.";
+    String sensitive = "foo";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsMultipleMatchingOptionsWithFlags() {
+    String string = "-DmyArg -Duser-password=%s -DOtherArg -Dsystem-password=%s";
+    String sensitive1 = "foo";
+    String sensitive2 = "bar";
+    String input = String.format(string, sensitive1, sensitive2);
+    String expected = String.format(string, REDACTED, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive1)
+        .doesNotContain(sensitive2)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsMultipleMatchingOptionsWithMultipleNonMatchingOptionsAndMultiplePrefixes() {
+    String string =
+        "-Dlogin-password=%s -Dlogin-name=%s -Dgemfire-password = %s --geode-password= %s --J=-Dsome-other-password =%s";
+    String sensitive1 = "secret";
+    String nonSensitive = "admin";
+    String sensitive2 = "super-secret";
+    String sensitive3 = "confidential";
+    String sensitive4 = "shhhh";
+    String input = String.format(
+        string, sensitive1, nonSensitive, sensitive2, sensitive3, sensitive4);
+    String expected = String.format(
+        string, REDACTED, nonSensitive, REDACTED, REDACTED, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive1)
+        .contains(nonSensitive)
+        .doesNotContain(sensitive2)
+        .doesNotContain(sensitive3)
+        .doesNotContain(sensitive4)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsMatchingOptionWithNonMatchingOptionAfterCommand() {
+    String string = "connect --password=%s --user=%s";
+    String reusedSensitive = "test";
+    String input = String.format(string, reusedSensitive, reusedSensitive);
+    String expected = String.format(string, REDACTED, reusedSensitive);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .contains(reusedSensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsMultipleMatchingOptionsButNotKeyUsingSameStringAsValue() {
+    String string = "connect --%s-password=%s --product-password=%s";
+    String reusedSensitive = "test";
+    String sensitive = "test1";
+    String input = String.format(string, reusedSensitive, reusedSensitive, sensitive);
+    String expected = String.format(string, reusedSensitive, REDACTED, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .contains(reusedSensitive)
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactRedactsGemfireSslTruststorePassword() {
+    String string = "-Dgemfire.ssl-truststore-password=%s";
+    String sensitive = "gibberish";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsGemfireSslKeystorePassword() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "gibberish";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsValueEndingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "supersecret-";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsValueContainingHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "super-secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsValueContainingManyHyphens() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "this-is-super-secret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsValueStartingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "-supersecret";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void redactsQuotedValueStartingWithHyphen() {
+    String string = "-Dgemfire.ssl-keystore-password=%s";
+    String sensitive = "\"-supersecret\"";
+    String input = String.format(string, sensitive);
+    String expected = String.format(string, REDACTED);
+
+    String output = regexRedactionStrategy.redact(input);
+
+    assertThat(output)
+        .as("output of redact(" + input + ")")
+        .doesNotContain(sensitive)
+        .isEqualTo(expected);
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionaryTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionaryTest.java
new file mode 100644
index 0000000..e3436c0
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitivePrefixDictionaryTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SensitivePrefixDictionaryTest {
+
+  private SensitivePrefixDictionary dictionary;
+
+  @Before
+  public void setUp() {
+    dictionary = new SensitivePrefixDictionary(RedactionDefaults.SENSITIVE_PREFIXES);
+  }
+
+  @Test
+  public void startsWithSyspropHyphenIsTrue() {
+    assertThat(dictionary.isSensitive("sysprop-something")).isTrue();
+  }
+
+  @Test
+  public void startsWithJavaxNetSslIsTrue() {
+    assertThat(dictionary.isSensitive("javax.net.ssl.something")).isTrue();
+  }
+
+  @Test
+  public void startsWithSecurityHyphenIsTrue() {
+    assertThat(dictionary.isSensitive("security-something")).isTrue();
+  }
+
+  @Test
+  public void nullStringIsFalse() {
+    assertThat(dictionary.isSensitive(null)).isFalse();
+  }
+
+  @Test
+  public void emptyStringIsFalse() {
+    assertThat(dictionary.isSensitive("")).isFalse();
+  }
+
+  @Test
+  public void passwordLowerCaseIsFalse() {
+    assertThat(dictionary.isSensitive("password")).isFalse();
+  }
+
+  @Test
+  public void passwordUpperCaseIsFalse() {
+    assertThat(dictionary.isSensitive("PASSWORD")).isFalse();
+  }
+
+  @Test
+  public void startsWithPasswordIsFalse() {
+    assertThat(dictionary.isSensitive("passwordforsomething")).isFalse();
+  }
+
+  @Test
+  public void endsWithPasswordIsFalse() {
+    assertThat(dictionary.isSensitive("mypassword")).isFalse();
+  }
+
+  @Test
+  public void containsPasswordIsFalse() {
+    assertThat(dictionary.isSensitive("mypasswordforsomething")).isFalse();
+  }
+
+  @Test
+  public void passwordWithLeadingHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("-password")).isFalse();
+  }
+
+  @Test
+  public void passwordWithTrailingHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("password-")).isFalse();
+  }
+
+  @Test
+  public void passwordWithMiddleHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("pass-word")).isFalse();
+  }
+
+  @Test
+  public void startsWithSyspropWithoutHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("syspropsomething")).isFalse();
+  }
+
+  @Test
+  public void containsSyspropWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-sysprop-something")).isFalse();
+  }
+
+  @Test
+  public void endsWithSyspropWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-sysprop-")).isFalse();
+  }
+
+  @Test
+  public void syspropIsFalse() {
+    assertThat(dictionary.isSensitive("sysprop")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxSslIsFalse() {
+    assertThat(dictionary.isSensitive("javax.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxNetIsFalse() {
+    assertThat(dictionary.isSensitive("javax.net.something")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxWithoutNetWithSslIsFalse() {
+    assertThat(dictionary.isSensitive("javax.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void containsJavaxNetSslIsFalse() {
+    assertThat(dictionary.isSensitive("my.javax.net.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void endsWithJavaxNetSslIsFalse() {
+    assertThat(dictionary.isSensitive("my.javax.net.ssl")).isFalse();
+  }
+
+  @Test
+  public void startsWithSecurityWithoutHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("securitysomething")).isFalse();
+  }
+
+  @Test
+  public void containsSecurityWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-security-something")).isFalse();
+  }
+
+  @Test
+  public void endsWithSecurityWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-security-")).isFalse();
+  }
+
+  @Test
+  public void securityIsFalse() {
+    assertThat(dictionary.isSensitive("security")).isFalse();
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionaryTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionaryTest.java
new file mode 100644
index 0000000..b958676
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/SensitiveSubstringDictionaryTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SensitiveSubstringDictionaryTest {
+
+  private SensitiveSubstringDictionary dictionary;
+
+  @Before
+  public void setUp() {
+    dictionary = new SensitiveSubstringDictionary(RedactionDefaults.SENSITIVE_SUBSTRINGS);
+  }
+
+  @Test
+  public void passwordLowerCaseIsTrue() {
+    assertThat(dictionary.isSensitive("password")).isTrue();
+  }
+
+  @Test
+  public void passwordUpperCaseIsTrue() {
+    assertThat(dictionary.isSensitive("PASSWORD")).isTrue();
+  }
+
+  @Test
+  public void startsWithPasswordIsTrue() {
+    assertThat(dictionary.isSensitive("passwordforsomething")).isTrue();
+  }
+
+  @Test
+  public void endsWithPasswordIsTrue() {
+    assertThat(dictionary.isSensitive("mypassword")).isTrue();
+  }
+
+  @Test
+  public void containsPasswordIsTrue() {
+    assertThat(dictionary.isSensitive("mypasswordforsomething")).isTrue();
+  }
+
+  @Test
+  public void passwordWithLeadingHyphenIsTrue() {
+    assertThat(dictionary.isSensitive("-password")).isTrue();
+  }
+
+  @Test
+  public void passwordWithTrailingHyphenIsTrue() {
+    assertThat(dictionary.isSensitive("password-")).isTrue();
+  }
+
+  @Test
+  public void nullStringIsFalse() {
+    assertThat(dictionary.isSensitive(null)).isFalse();
+  }
+
+  @Test
+  public void emptyStringIsFalse() {
+    assertThat(dictionary.isSensitive("")).isFalse();
+  }
+
+  @Test
+  public void passwordWithMiddleHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("pass-word")).isFalse();
+  }
+
+  @Test
+  public void startsWithSyspropHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("sysprop-something")).isFalse();
+  }
+
+  @Test
+  public void startsWithSyspropWithoutHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("syspropsomething")).isFalse();
+  }
+
+  @Test
+  public void containsSyspropWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-sysprop-something")).isFalse();
+  }
+
+  @Test
+  public void endsWithSyspropWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-sysprop-")).isFalse();
+  }
+
+  @Test
+  public void syspropIsFalse() {
+    assertThat(dictionary.isSensitive("sysprop")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxNetSslIsFalse() {
+    assertThat(dictionary.isSensitive("javax.net.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxSslIsFalse() {
+    assertThat(dictionary.isSensitive("javax.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxNetIsFalse() {
+    assertThat(dictionary.isSensitive("javax.net.something")).isFalse();
+  }
+
+  @Test
+  public void startsWithJavaxWithoutNetWithSslIsFalse() {
+    assertThat(dictionary.isSensitive("javax.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void containsJavaxNetSslIsFalse() {
+    assertThat(dictionary.isSensitive("my.javax.net.ssl.something")).isFalse();
+  }
+
+  @Test
+  public void endsWithJavaxNetSslIsFalse() {
+    assertThat(dictionary.isSensitive("my.javax.net.ssl")).isFalse();
+  }
+
+  @Test
+  public void startsWithSecurityHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("security-something")).isFalse();
+  }
+
+  @Test
+  public void startsWithSecurityWithoutHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("securitysomething")).isFalse();
+  }
+
+  @Test
+  public void containsSecurityWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-security-something")).isFalse();
+  }
+
+  @Test
+  public void endsWithSecurityWithHyphenIsFalse() {
+    assertThat(dictionary.isSensitive("my-security-")).isFalse();
+  }
+
+  @Test
+  public void securityIsFalse() {
+    assertThat(dictionary.isSensitive("security")).isFalse();
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/internal/util/redaction/StringRedactionTest.java b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/StringRedactionTest.java
new file mode 100644
index 0000000..15896f1
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/internal/util/redaction/StringRedactionTest.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.
+ */
+package org.apache.geode.internal.util.redaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.AdditionalAnswers.returnsFirstArg;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class StringRedactionTest {
+
+  private static final String REDACTED = "redacted";
+
+  private SensitiveDataDictionary sensitiveDataDictionary;
+  private RedactionStrategy redactionStrategy;
+
+  private StringRedaction stringRedaction;
+
+  @Before
+  public void setUp() {
+    sensitiveDataDictionary = mock(SensitiveDataDictionary.class);
+    redactionStrategy = mock(RedactionStrategy.class);
+
+    stringRedaction =
+        new StringRedaction(REDACTED, sensitiveDataDictionary, redactionStrategy);
+  }
+
+  @Test
+  public void redactDelegatesString() {
+    String input = "line";
+    String expected = "expected";
+
+    when(redactionStrategy.redact(input)).thenReturn(expected);
+
+    String result = stringRedaction.redact(input);
+
+    verify(redactionStrategy).redact(input);
+    assertThat(result).isEqualTo(expected);
+  }
+
+  @Test
+  public void redactDelegatesNullString() {
+    String input = null;
+
+    stringRedaction.redact(input);
+
+    verify(redactionStrategy).redact(input);
+  }
+
+  @Test
+  public void redactDelegatesEmptyString() {
+    String input = "";
+
+    stringRedaction.redact(input);
+
+    verify(redactionStrategy).redact(input);
+  }
+
+  @Test
+  public void redactDelegatesIterable() {
+    String line1 = "line1";
+    String line2 = "line2";
+    String line3 = "line3";
+    Collection<String> input = new ArrayList<>();
+    input.add(line1);
+    input.add(line2);
+    input.add(line3);
+    String joinedLine = String.join(" ", input);
+    String expected = "expected";
+
+    when(redactionStrategy.redact(joinedLine)).thenReturn(expected);
+
+    String result = stringRedaction.redact(input);
+
+    verify(redactionStrategy).redact(joinedLine);
+    assertThat(result).isEqualTo(expected);
+  }
+
+  @Test
+  public void redactNullIterableThrowsNullPointerException() {
+    Collection<String> input = null;
+
+    Throwable thrown = catchThrowable(() -> {
+      stringRedaction.redact(input);
+    });
+
+    assertThat(thrown).isInstanceOf(NullPointerException.class);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryDelegatesSensitiveKey() {
+    String key = "key";
+    String value = "value";
+
+    when(sensitiveDataDictionary.isSensitive(key)).thenReturn(true);
+
+    String result = stringRedaction.redactArgumentIfNecessary(key, value);
+
+    verify(sensitiveDataDictionary).isSensitive(key);
+    assertThat(result).isEqualTo(REDACTED);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryDelegatesNonSensitiveKey() {
+    String key = "key";
+    String value = "value";
+
+    when(sensitiveDataDictionary.isSensitive(key)).thenReturn(false);
+
+    String result = stringRedaction.redactArgumentIfNecessary(key, value);
+
+    verify(sensitiveDataDictionary).isSensitive(key);
+    assertThat(result).isEqualTo(value);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryDelegatesNullKey() {
+    String key = null;
+
+    stringRedaction.redactArgumentIfNecessary(key, "value");
+
+    verify(sensitiveDataDictionary).isSensitive(key);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryDelegatesEmptyKey() {
+    String key = "";
+
+    stringRedaction.redactArgumentIfNecessary(key, "value");
+
+    verify(sensitiveDataDictionary).isSensitive(key);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryReturnsNullValue() {
+    String value = null;
+
+    String result = stringRedaction.redactArgumentIfNecessary("key", value);
+
+    assertThat(result).isEqualTo(value);
+  }
+
+  @Test
+  public void redactArgumentIfNecessaryReturnsEmptyValue() {
+    String value = "";
+
+    String result = stringRedaction.redactArgumentIfNecessary("key", value);
+
+    assertThat(result).isEqualTo(value);
+  }
+
+  @Test
+  public void redactEachInListDelegatesEachStringInIterable() {
+    String string1 = "string1";
+    String string2 = "string2";
+    String string3 = "string3";
+    List<String> input = new ArrayList<>();
+    input.add(string1);
+    input.add(string2);
+    input.add(string3);
+
+    when(redactionStrategy.redact(anyString())).then(returnsFirstArg());
+
+    List<String> result = stringRedaction.redactEachInList(input);
+
+    verify(redactionStrategy).redact(string1);
+    verify(redactionStrategy).redact(string2);
+    verify(redactionStrategy).redact(string3);
+    assertThat(result).isEqualTo(input);
+  }
+
+  @Test
+  public void redactEachInListDoesNotDelegateEmptyIterable() {
+    List<String> input = Collections.emptyList();
+
+    when(redactionStrategy.redact(anyString())).then(returnsFirstArg());
+
+    List<String> result = stringRedaction.redactEachInList(input);
+
+    verifyNoInteractions(redactionStrategy);
+    assertThat(result).isEqualTo(input);
+  }
+
+  @Test
+  public void redactEachInListNullIterableThrowsNullPointerException() {
+    List<String> input = null;
+
+    when(redactionStrategy.redact(anyString())).then(returnsFirstArg());
+
+    Throwable thrown = catchThrowable(() -> {
+      stringRedaction.redactEachInList(input);
+    });
+
+    assertThat(thrown).isInstanceOf(NullPointerException.class);
+  }
+
+  @Test
+  public void isSensitiveDelegatesString() {
+    String input = "input";
+
+    when(sensitiveDataDictionary.isSensitive(anyString())).thenReturn(true);
+
+    boolean result = stringRedaction.isSensitive(input);
+
+    assertThat(result).isTrue();
+  }
+
+  @Test
+  public void isSensitiveDelegatesNullString() {
+    String input = null;
+
+    when(sensitiveDataDictionary.isSensitive(isNull())).thenReturn(true);
+
+    boolean result = stringRedaction.isSensitive(input);
+
+    assertThat(result).isTrue();
+  }
+
+  @Test
+  public void isSensitiveDelegatesEmptyString() {
+    String input = "";
+
+    when(sensitiveDataDictionary.isSensitive(anyString())).thenReturn(true);
+
+    boolean result = stringRedaction.isSensitive(input);
+
+    assertThat(result).isTrue();
+  }
+}
diff --git a/geode-junit/src/main/java/org/apache/geode/test/junit/rules/RequiresGeodeHome.java b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/RequiresGeodeHome.java
index 9cc4f06..1741e3b 100644
--- a/geode-junit/src/main/java/org/apache/geode/test/junit/rules/RequiresGeodeHome.java
+++ b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/RequiresGeodeHome.java
@@ -16,7 +16,6 @@ package org.apache.geode.test.junit.rules;
 
 import static java.lang.System.lineSeparator;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertNotNull;
 
 import java.io.File;
 
@@ -42,10 +41,14 @@ public class RequiresGeodeHome extends SerializableExternalResource {
 
   public File getGeodeHome() {
     String geodeHomePath = System.getenv("GEODE_HOME");
-    assertNotNull(GEODE_HOME_NOT_SET_MESSAGE, geodeHomePath);
+    assertThat(geodeHomePath)
+        .withFailMessage(GEODE_HOME_NOT_SET_MESSAGE)
+        .isNotNull();
 
     File geodeHome = new File(geodeHomePath);
-    assertThat(geodeHome).exists();
+    assertThat(geodeHome)
+        .exists()
+        .isDirectoryContaining(file -> file.getName().startsWith("bin"));
 
     return geodeHome;
   }