diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java
index eaaa15c54d..696e3b96e3 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java
@@ -16,6 +16,8 @@
package org.springframework.boot.actuate.endpoint;
+import java.util.Locale;
+
import org.springframework.core.env.PropertySource;
/**
@@ -36,6 +38,8 @@ public final class SanitizableData {
private final String key;
+ private String lowerCaseKey;
+
private final Object value;
/**
@@ -67,6 +71,20 @@ public final class SanitizableData {
return this.key;
}
+ /**
+ * Return the key as a lowercase value.
+ * @return the key as a lowercase value
+ * @since 3.5.0
+ */
+ public String getLowerCaseKey() {
+ String result = this.lowerCaseKey;
+ if (result == null && this.key != null) {
+ result = this.key.toLowerCase(Locale.getDefault());
+ this.lowerCaseKey = result;
+ }
+ return result;
+ }
+
/**
* Return the value of the data.
* @return the data value
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java
index 3ad0497f79..76ddf4a10a 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java
@@ -72,7 +72,7 @@ public class Sanitizer {
return SanitizableData.SANITIZED_VALUE;
}
for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) {
- data = sanitizingFunction.apply(data);
+ data = sanitizingFunction.applyUnlessFiltered(data);
Object sanitizedValue = data.getValue();
if (!value.equals(sanitizedValue)) {
return sanitizedValue;
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java
index 3d9ffecefe..bc47f686c8 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java
@@ -16,12 +16,27 @@
package org.springframework.boot.actuate.endpoint;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+import org.springframework.util.Assert;
+
/**
* Function that takes a {@link SanitizableData} and applies sanitization to the value, if
* necessary. Can be used by a {@link Sanitizer} to determine the sanitized value.
+ *
+ * This interface also provides convenience methods that can help build a
+ * {@link SanitizingFunction} instances, for example to return from a {@code @Bean}
+ * method. See {@link #sanitizeValue()} for an example.
*
* @author Madhura Bhave
+ * @author Phillip Webb
* @since 2.6.0
+ * @see Sanitizer
*/
@FunctionalInterface
public interface SanitizingFunction {
@@ -33,4 +48,387 @@ public interface SanitizingFunction {
*/
SanitizableData apply(SanitizableData data);
+ /**
+ * Return an optional filter that determines if the sanitizing function applies.
+ * @return a predicate used to filter functions or {@code null} if no filter is
+ * declared
+ * @since 3.5.0
+ * @see #applyUnlessFiltered(SanitizableData)
+ */
+ default Predicate filter() {
+ return null;
+ }
+
+ /**
+ * Apply the sanitizing function as long as the filter passes or there is no filter.
+ * @param data the data to sanitize
+ * @return the sanitized data or the original instance is no sanitization is applied
+ * @since 3.5.0
+ */
+ default SanitizableData applyUnlessFiltered(SanitizableData data) {
+ return (filter() == null || filter().test(data)) ? apply(data) : data;
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data is
+ * likely to contain a sensitive value. This method can help construct a useful
+ * sanitizing function, but may not catch all sensitive data so care should be taken
+ * to test the results for your specific environment.
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifLikelySenstive() {
+ return ifLikelyCredential().ifLikelyUri().ifLikelySenstiveEnvironmentVariable().ifVcapServices();
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data is
+ * likely to contain a credential. This method can help construct a useful sanitizing
+ * function, but may not catch all sensitive data so care should be taken to test the
+ * results for your specific environment.
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifLikelyCredential() {
+ return ifKeyEndsWith("password", "secret", "key", "token").ifKeyContains("credentials");
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data is
+ * likely to contain a URI. This method can help construct a useful sanitizing
+ * function, but may not catch all sensitive data so care should be taken to test the
+ * results for your specific environment.
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifLikelyUri() {
+ return ifKeyEndsWith("uri", "uris", "url", "urls", "address", "addresses");
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data is
+ * likely to sensitive environment variable value. This method can help construct a
+ * useful sanitizing function, but may not catch all sensitive data so care should be
+ * taken to test the results for your specific environment.
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifLikelySenstiveEnvironmentVariable() {
+ return ifKeyMatches("sun.java.command", "^spring[._]application[._]json$");
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data is for
+ * VCAP services.
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+
+ default SanitizingFunction ifVcapServices() {
+ return ifKeyEquals("vcap_services").ifKeyMatches("^vcap\\.services.*$");
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key is
+ * equal to any of the given values (ignoring case).
+ * @param values the case insensitive values that the key can equal
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyEquals(String... values) {
+ Assert.notNull(values, "'values' must not be null");
+ return ifKeyMatchesIgnoringCase(String::equals, values);
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key ends
+ * with any of the given values (ignoring case).
+ * @param suffixes the case insensitive suffixes that they key can end with
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyEndsWith(String... suffixes) {
+ Assert.notNull(suffixes, "'suffixes' must not be null");
+ return ifKeyMatchesIgnoringCase(String::endsWith, suffixes);
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key
+ * contains any of the given values (ignoring case).
+ * @param values the case insensitive values that the key can contain
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyContains(String... values) {
+ Assert.notNull(values, "'values' must not be null");
+ return ifKeyMatchesIgnoringCase(String::contains, values);
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key and
+ * any of the values match the given predicate. The predicate is only called with
+ * lower case values.
+ * @param predicate the predicate used to check the key against a value. The key is
+ * the first argument and the value is the second. Both are converted to lower case
+ * @param values the case insensitive values that the key can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyMatchesIgnoringCase(BiPredicate predicate, String... values) {
+ Assert.notNull(predicate, "'predicate' must not be null");
+ Assert.notNull(values, "'values' must not be null");
+ return ifMatches(Arrays.stream(values).map((value) -> onKeyIgnoringCase(predicate, value)).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key
+ * matches any of the given regex patterns (ignoring case).
+ * @param regexes the case insensitive regexes that the key can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyMatches(String... regexes) {
+ Assert.notNull(regexes, "'regexes' must not be null");
+ return ifKeyMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new));
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key
+ * matches any of the given patterns.
+ * @param patterns the patterns that the key can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyMatches(Pattern... patterns) {
+ Assert.notNull(patterns, "'patterns' must not be null");
+ return ifKeyMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key
+ * matches any of the given predicates.
+ * @param predicates the predicates that the key can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyMatches(List> predicates) {
+ Assert.notNull(predicates, "'predicates' must not be null");
+ return ifMatches(predicates.stream().map(this::onKey).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data key
+ * matches any of the given predicate.
+ * @param predicate the predicate that the key can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifKeyMatches(Predicate predicate) {
+ Assert.notNull(predicate, "'predicate' must not be null");
+ return ifMatches(onKey(predicate));
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data string
+ * value matches any of the given regex patterns (ignoring case).
+ * @param regexes the case insensitive regexes that the values string can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifValueStringMatches(String... regexes) {
+ Assert.notNull(regexes, "'regexes' must not be null");
+ return ifValueStringMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new));
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data string
+ * value matches any of the given patterns.
+ * @param patterns the patterns that the value string can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifValueStringMatches(Pattern... patterns) {
+ Assert.notNull(patterns, "'patterns' must not be null");
+ return ifValueStringMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data string
+ * value matches any of the given predicates.
+ * @param predicates the predicates that the value string can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+
+ default SanitizingFunction ifValueStringMatches(List> predicates) {
+ Assert.notNull(predicates, "'predicates' must not be null");
+ return ifMatches(predicates.stream().map(this::onValueString).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data value
+ * matches any of the given predicates.
+ * @param predicates the predicates that the value can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifValueMatches(List> predicates) {
+ Assert.notNull(predicates, "'predicates' must not be null");
+ return ifMatches(predicates.stream().map(this::onValue).toList());
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data string
+ * value matches the given predicate.
+ * @param predicate the predicate that the value string can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+
+ default SanitizingFunction ifValueStringMatches(Predicate predicate) {
+ Assert.notNull(predicate, "'predicate' must not be null");
+ return ifMatches(onValueString(predicate));
+ }
+
+ /**
+ * Return a new function with a filter that also applies if the data value
+ * matches the given predicate.
+ * @param predicate the predicate that the value can match
+ * @return a new sanitizing function with an updated {@link #filter()}
+ * @since 3.5.0
+ * @see #filter()
+ * @see #sanitizeValue()
+ */
+ default SanitizingFunction ifValueMatches(Predicate