// Core Sanitization
org.springframework.boot.actuate.endpoint.Sanitizer
org.springframework.boot.actuate.endpoint.SanitizableData
org.springframework.boot.actuate.endpoint.SanitizingFunction
// Value Control
org.springframework.boot.actuate.endpoint.Show // NEVER, WHEN_AUTHORIZED, ALWAYSWhat data needs protection?
│
├─ Passwords, secrets, tokens?
│ └─ Use SanitizingFunction.sanitizeValue().ifLikelyCredential()
│
├─ URIs with embedded credentials?
│ └─ Use SanitizingFunction.sanitizeValue().ifLikelyUri()
│
├─ Specific property keys?
│ └─ Use .ifKeyContains("password")
│
├─ Pattern-based matching?
│ └─ Use .ifKeyMatches(Pattern.compile("..."))
│
└─ Custom logic?
└─ Use SanitizingFunction.of(data -> ...)// Password sanitization
SanitizingFunction.sanitizeValue().ifKeyContains("password")
// Credential detection (auto-detects password, secret, key, token)
SanitizingFunction.sanitizeValue().ifLikelyCredential()
// URI sanitization
SanitizingFunction.sanitizeValue().ifLikelyUri()
// Multiple conditions (OR logic)
SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyContains("secret")
.ifKeyContains("token")
// Custom transformation
SanitizingFunction.of(data -> {
// Custom logic
return data.withValue("sanitized");
}).ifKeyContains("card")| Value | Behavior | Use When |
|---|---|---|
| NEVER | Always sanitize | Public endpoints |
| WHEN_AUTHORIZED | Sanitize unless authorized | Role-based access |
| ALWAYS | Never sanitize | Internal/admin only |
IMPORTANT: SanitizingFunction is a functional interface with default methods for chaining. To use fluent methods, you must start with a static factory method or use SanitizingFunction.of():
// CORRECT: Start with static factory method
SanitizingFunction function = SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyEndsWith("secret");
// CORRECT: Use SanitizingFunction.of() for custom logic
SanitizingFunction function = SanitizingFunction.of(data -> data.withValue("***"))
.ifKeyContains("password");
// INCORRECT: Cannot create lambda and call fluent methods directly
SanitizingFunction function = data -> data; // No fluent methods available
function.ifKeyContains("password"); // ✗ Compilation errorChaining Rules:
SanitizingFunction.sanitizeValue() or SanitizingFunction.of(lambda)ifKeyContains(), ifKeyEndsWith(), ifKeyMatches(), etc.SanitizingFunction for further chainingAlways sanitize:
Consider sanitizing:
Don't sanitize:
PATTERN: Use built-in detection
// ✓ Leverages built-in patterns
SanitizingFunction.sanitizeValue().ifLikelyCredential()ANTI-PATTERN: Reinvent the wheel
// ❌ Duplicates built-in logic
SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyContains("secret")
.ifKeyContains("key")
.ifKeyContains("token")
// ... (already covered by ifLikelyCredential)PATTERN: Conditional sanitization
// ✓ Show based on role
management:
endpoint:
env:
show-values: when-authorized
roles: ADMINANTI-PATTERN: All or nothing
// ❌ Either all users see secrets or none
management:
endpoint:
env:
show-values: always // or neverPATTERN: Partial value showing
// ✓ Show first/last characters
SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
if (value.length() > 4) {
return data.withValue(
value.substring(0, 2) + "****" + value.substring(value.length() - 2)
);
}
return data.withSanitizedValue();
}).ifKeyContains("card")ANTI-PATTERN: Complete hiding when partial OK
// ❌ Hides everything (user can't identify which card)
SanitizingFunction.sanitizeValue().ifKeyContains("card")The data sanitization framework protects sensitive information in actuator endpoint responses through pattern matching, flexible sanitization strategies, and built-in detection of credentials, URIs, and sensitive keys. It provides a fluent API for composing sanitization rules.
/**
* Strategy for sanitizing potentially sensitive data.
*
* Thread-safe: Yes (immutable)
* Nullability: sanitize() can return null if input value is null
* Since: Spring Boot 2.0+
*/
public class Sanitizer {
/**
* Create sanitizer with default sanitizing functions.
* Includes built-in detection for common sensitive keys.
*/
public Sanitizer();
/**
* Create sanitizer with custom sanitizing functions.
*
* @param sanitizingFunctions Functions to apply (non-null)
*/
public Sanitizer(Iterable<SanitizingFunction> sanitizingFunctions);
/**
* Sanitize data based on context.
* When showUnsanitized is false, immediately returns SANITIZED_VALUE.
* When showUnsanitized is true, applies all sanitizing functions in order
* and returns the first modified value, or the original value if no functions match.
*
* @param data Data to sanitize (non-null)
* @param showUnsanitized Whether to show unsanitized value
* @return Sanitized value or original if authorized
*/
public @Nullable Object sanitize(SanitizableData data, boolean showUnsanitized);
}
/**
* Data that can be sanitized.
*
* Thread-safe: Immutable
* Since: Spring Boot 2.6.0
*/
public final class SanitizableData {
/** The value used for sanitized data */
public static final String SANITIZED_VALUE = "******";
/**
* Create sanitizable data.
*/
public SanitizableData(PropertySource<?> propertySource, String key, Object value);
public PropertySource<?> getPropertySource();
public String getKey();
public String getLowerCaseKey();
public Object getValue();
/**
* Create new data with sanitized value.
*/
public SanitizableData withSanitizedValue();
/**
* Create new data with different value.
*/
public SanitizableData withValue(Object value);
}package com.example.actuator;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.env.EnvironmentEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.util.List;
/**
* Configuration for password and credential sanitization.
*
* Thread-safe: Yes
* Auto-configured: No (manual configuration)
* Since: Application 1.0
*/
@Configuration
public class SanitizationConfiguration {
/**
* Configure environment endpoint with sanitization.
*
* Sanitizes:
* - Passwords (password, passwd, pwd)
* - Secrets (secret, secrete)
* - API keys (key, apikey, api-key)
* - Tokens (token, auth-token)
* - Credentials (credentials, credential)
*
* @param environment Spring environment
* @return Configured environment endpoint
*/
@Bean
public EnvironmentEndpoint environmentEndpoint(Environment environment) {
List<SanitizingFunction> functions = List.of(
// Use built-in credential detection
SanitizingFunction.sanitizeValue().ifLikelyCredential(),
// Additional specific patterns
SanitizingFunction.sanitizeValue()
.ifKeyContains("username")
.ifKeyContains("user"),
// URI sanitization (removes query parameters with credentials)
SanitizingFunction.sanitizeValue().ifLikelyUri()
);
return new EnvironmentEndpoint(
environment,
functions,
Show.WHEN_AUTHORIZED // Show only to authorized users
);
}
}package com.example.actuator;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.regex.Pattern;
/**
* Custom sanitizing functions for sensitive data.
*
* Thread-safe: Yes (functions are immutable)
* Since: Application 1.0
*/
@Component
public class CustomSanitizingFunctions {
private static final Pattern CREDIT_CARD_PATTERN =
Pattern.compile("\\d{13,19}");
private static final Pattern PHONE_PATTERN =
Pattern.compile("\\d{10,}");
/**
* Get all custom sanitizing functions.
*
* @return List of sanitizing functions (never null)
*/
public List<SanitizingFunction> getSanitizingFunctions() {
return List.of(
creditCardSanitizer(),
emailSanitizer(),
phoneSanitizer(),
ssnSanitizer()
);
}
/**
* Sanitize credit card numbers, showing only last 4 digits.
*
* Input: "1234567890123456"
* Output: "****-****-****-3456"
*/
private SanitizingFunction creditCardSanitizer() {
return SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
// Remove non-digits
String digits = value.replaceAll("[^0-9]", "");
// Check if looks like credit card
if (CREDIT_CARD_PATTERN.matcher(digits).matches()) {
// Show only last 4 digits
String sanitized = "****-****-****-" +
digits.substring(digits.length() - 4);
return data.withValue(sanitized);
}
return data;
}).ifKeyContains("card", "credit", "payment");
}
/**
* Sanitize email addresses, showing partial username.
*
* Input: "john.doe@example.com"
* Output: "jo****@example.com"
*/
private SanitizingFunction emailSanitizer() {
return SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
if (value.contains("@")) {
String[] parts = value.split("@");
if (parts.length == 2 && parts[0].length() > 2) {
String sanitized = parts[0].substring(0, 2) +
"****@" + parts[1];
return data.withValue(sanitized);
}
}
return data;
}).ifKeyContains("email", "mail");
}
/**
* Sanitize phone numbers, showing only last 4 digits.
*
* Input: "123-456-7890"
* Output: "***-***-7890"
*/
private SanitizingFunction phoneSanitizer() {
return SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
String digits = value.replaceAll("[^0-9]", "");
if (PHONE_PATTERN.matcher(digits).matches()) {
String last4 = digits.substring(digits.length() - 4);
return data.withValue("***-***-" + last4);
}
return data;
}).ifKeyContains("phone", "tel", "mobile");
}
/**
* Sanitize SSN/Tax ID, showing only last 4 digits.
*
* Input: "123-45-6789"
* Output: "***-**-6789"
*/
private SanitizingFunction ssnSanitizer() {
return SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
String digits = value.replaceAll("[^0-9]", "");
if (digits.length() == 9) {
return data.withValue("***-**-" + digits.substring(5));
}
return data;
}).ifKeyContains("ssn", "tax", "taxid", "social");
}
}package com.example.actuator;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.env.EnvironmentEndpoint;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import org.jspecify.annotations.Nullable;
import java.util.Set;
/**
* Web extension with role-based sanitization.
*
* Thread-safe: Yes
* Security-aware: Yes
* Since: Application 1.0
*
* Behavior:
* - ADMIN role: sees all values
* - ACTUATOR role: sees sanitized sensitive values
* - No role: sees only non-sensitive values
*/
@EndpointWebExtension(endpoint = EnvironmentEndpoint.class)
@Component
public class SecureEnvironmentWebExtension {
private final EnvironmentEndpoint delegate;
private final Show showValues;
private final Set<String> roles;
public SecureEnvironmentWebExtension(
EnvironmentEndpoint delegate,
@Value("${management.endpoint.env.show-values:WHEN_AUTHORIZED}") Show showValues,
@Value("${management.endpoint.env.roles:ADMIN}") Set<String> roles) {
this.delegate = delegate;
this.showValues = showValues;
this.roles = roles;
}
/**
* Get environment with role-based sanitization.
*
* @param securityContext Security context for authorization check
* @param pattern Property name pattern filter (nullable)
* @return Environment descriptor with appropriately sanitized values
*/
@ReadOperation
public EnvironmentDescriptor environment(
SecurityContext securityContext,
@Nullable String pattern) {
// Check if user has required roles
boolean authorized = showValues.isShown(securityContext, roles);
// Get environment (delegate handles sanitization)
return delegate.environment(pattern);
}
/**
* Get specific environment entry with role-based sanitization.
*
* @param securityContext Security context
* @param toMatch Property name to match
* @return Environment entry descriptor
*/
@ReadOperation
public EnvironmentEntryDescriptor environmentEntry(
SecurityContext securityContext,
@Selector String toMatch) {
boolean authorized = showValues.isShown(securityContext, roles);
return delegate.environmentEntry(toMatch);
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class SanitizerTest {
private Sanitizer sanitizer;
private PropertySource<?> propertySource;
@BeforeEach
void setUp() {
List<SanitizingFunction> functions = List.of(
SanitizingFunction.sanitizeValue().ifKeyContains("password"),
SanitizingFunction.sanitizeValue().ifKeyContains("secret")
);
sanitizer = new Sanitizer(functions);
propertySource = new MapPropertySource("test", Map.of());
}
@Test
void sanitize_WithPassword_ReturnsSanitized() {
SanitizableData data = new SanitizableData(
propertySource,
"database.password",
"secretPassword123"
);
Object result = sanitizer.sanitize(data, false);
assertThat(result).isEqualTo("******");
}
@Test
void sanitize_WithPassword_WhenShowUnsanitized_StillSanitizes() {
// Note: showUnsanitized=true doesn't bypass sanitizing functions
// It only prevents immediate SANITIZED_VALUE return
// Functions that match still sanitize the value
SanitizableData data = new SanitizableData(
propertySource,
"database.password",
"secretPassword123"
);
Object result = sanitizer.sanitize(data, true);
// Password matches sanitizing function, so it's still sanitized
assertThat(result).isEqualTo("******");
}
@Test
void sanitize_WithNonSensitiveKey_WhenShowUnsanitized_ReturnsOriginal() {
SanitizableData data = new SanitizableData(
propertySource,
"server.port",
"8080"
);
// showUnsanitized=true allows functions to determine outcome
// Since "server.port" doesn't match "password" or "secret", returns original
Object result = sanitizer.sanitize(data, true);
assertThat(result).isEqualTo("8080");
}
@Test
void sanitize_WithNonSensitiveKey_WhenShowSanitized_ReturnsSanitizedMarker() {
SanitizableData data = new SanitizableData(
propertySource,
"server.port",
"8080"
);
// showUnsanitized=false immediately returns SANITIZED_VALUE marker
Object result = sanitizer.sanitize(data, false);
assertThat(result).isEqualTo(SanitizableData.SANITIZED_VALUE);
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class CustomSanitizingFunctionsTest {
private CustomSanitizingFunctions functions;
private MapPropertySource propertySource;
@BeforeEach
void setUp() {
functions = new CustomSanitizingFunctions();
propertySource = new MapPropertySource("test", Map.of());
}
@Test
void creditCardSanitizer_ShowsOnlyLast4Digits() {
SanitizingFunction sanitizer = functions.getSanitizingFunctions().get(0);
SanitizableData data = new SanitizableData(
propertySource,
"payment.card.number",
"1234567890123456"
);
SanitizableData result = sanitizer.apply(data);
assertThat(result.getValue()).asString()
.endsWith("3456")
.contains("****");
}
@Test
void emailSanitizer_ShowsPartialUsername() {
SanitizingFunction sanitizer = functions.getSanitizingFunctions().get(1);
SanitizableData data = new SanitizableData(
propertySource,
"user.email",
"john.doe@example.com"
);
SanitizableData result = sanitizer.apply(data);
assertThat(result.getValue()).asString()
.startsWith("jo****")
.endsWith("@example.com");
}
@Test
void phoneSanitizer_ShowsOnlyLast4Digits() {
SanitizingFunction sanitizer = functions.getSanitizingFunctions().get(2);
SanitizableData data = new SanitizableData(
propertySource,
"contact.phone",
"123-456-7890"
);
SanitizableData result = sanitizer.apply(data);
assertThat(result.getValue()).asString()
.isEqualTo("***-***-7890");
}
}Problem: Sensitive values visible to unauthorized users
Causes:
Solutions:
// Solution 1: Check key matching (case-insensitive)
SanitizingFunction.sanitizeValue()
.ifKeyContains("password") // Matches: password, PASSWORD, db.password
// Solution 2: Configure show-values
management:
endpoint:
env:
show-values: when-authorized # <-- Not "always"
roles: ADMIN
// Solution 3: Ensure functions are registered
@Bean
public EnvironmentEndpoint environmentEndpoint(Environment environment) {
List<SanitizingFunction> functions = List.of(
SanitizingFunction.sanitizeValue().ifLikelyCredential()
);
return new EnvironmentEndpoint(environment, functions, Show.WHEN_AUTHORIZED);
}Problem: Even admins see ******
Causes:
Solutions:
# Solution 1: Use WHEN_AUTHORIZED
management.endpoint.env.show-values=when-authorized
# Solution 2: Configure roles properly
management.endpoint.env.roles=ADMIN,ACTUATOR
# Solution 3: Verify user has role
spring.security.user.roles=ADMINProblem: Custom sanitizing function doesn't work
Causes:
Solutions:
// ❌ Wrong - returns original when should sanitize
SanitizingFunction.of(data -> {
if (shouldSanitize(data)) {
return data.withSanitizedValue();
}
return data; // Returns original even when condition matches
})
// ✓ Correct - always returns modified or original
SanitizingFunction.of(data -> {
if (shouldSanitize(data)) {
return data.withSanitizedValue();
}
return data; // OK - explicitly don't sanitize
}).ifKeyContains("sensitive") // <-- Add condition here// Sanitization happens on every property access
// Keep sanitizing functions fast
// ✓ Fast - simple string operations
SanitizingFunction.sanitizeValue().ifKeyContains("password")
// ❌ Slow - regex in hot path
SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
// Avoid complex regex per property
if (value.matches("complex.*regex.*pattern.*")) {
return data.withSanitizedValue();
}
return data;
})
// ✓ Better - compile pattern once
private static final Pattern PATTERN = Pattern.compile("complex.*regex");
SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
if (PATTERN.matcher(value).matches()) {
return data.withSanitizedValue();
}
return data;
})// Built-in patterns are optimized
// Use them instead of reimplementing
// ✓ Efficient - uses precompiled patterns
SanitizingFunction.sanitizeValue().ifLikelyCredential()
// ❌ Less efficient - builds patterns each time
SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyContains("secret")
.ifKeyContains("key")
// ... manually duplicating what ifLikelyCredential doesComplete type information for the main sanitizer class.
/**
* Strategy for sanitizing potentially sensitive data in endpoint responses.
* Coordinates the application of sanitizing functions to data.
*
* Thread-safe: Yes (immutable after construction)
* Nullability: sanitize() returns null if input value is null
*
* @since 2.0.0
*/
public class Sanitizer {
/**
* Create a sanitizer with default sanitizing functions.
* Default functions include:
* - Credential detection (password, secret, key, token)
* - URI sanitization (query parameters with credentials)
* - Common sensitive keys (apikey, auth, etc.)
*/
public Sanitizer();
/**
* Create a sanitizer with custom sanitizing functions.
* Functions are applied in order until one modifies the data.
*
* @param sanitizingFunctions the sanitizing functions to apply (never null)
*/
public Sanitizer(Iterable<SanitizingFunction> sanitizingFunctions);
/**
* Sanitize the given data.
* When showUnsanitized is false, immediately returns SANITIZED_VALUE.
* When showUnsanitized is true, applies all sanitizing functions in order
* and returns the first modified value, or the original value if no functions match.
*
* @param data the data to sanitize (never null)
* @param showUnsanitized whether to show the unsanitized value
* @return the sanitized value or SANITIZED_VALUE (may be null if input value is null)
*/
public @Nullable Object sanitize(SanitizableData data, boolean showUnsanitized);
}Complete type information for sanitizable data wrapper.
/**
* Data that can be sanitized.
* Immutable value object containing property source, key, and value.
*
* Thread-safe: Yes (immutable)
* Nullability: value can be null
*
* @since 2.0.0
*/
public final class SanitizableData {
/**
* The standard value used to replace sanitized data.
* Six asterisks: "******"
*/
public static final String SANITIZED_VALUE = "******";
/**
* Create sanitizable data.
*
* @param propertySource the property source containing the data (may be null)
* @param key the property key (never null)
* @param value the property value (may be null)
*/
public SanitizableData(@Nullable PropertySource<?> propertySource,
String key,
@Nullable Object value);
/**
* Get the property source.
*
* @return the property source (may be null)
*/
public @Nullable PropertySource<?> getPropertySource();
/**
* Get the property key.
*
* @return the key (never null)
*/
public String getKey();
/**
* Get the lowercase property key for case-insensitive matching.
*
* @return the lowercase key (never null)
* @since 3.5.0
*/
public String getLowerCaseKey();
/**
* Get the property value.
*
* @return the value (may be null)
*/
public @Nullable Object getValue();
/**
* Create new data with sanitized value.
* Returns a new instance with value replaced by SANITIZED_VALUE.
*
* @return new data with sanitized value (never null)
*/
public SanitizableData withSanitizedValue();
/**
* Create new data with different value.
* Returns a new instance with the specified value.
*
* @param value the new value (may be null)
* @return new data with replaced value (never null)
*/
public SanitizableData withValue(@Nullable Object value);
}Complete type information for sanitizing function interface with fluent API.
/**
* Function that can sanitize data based on conditions.
* Provides fluent API for composing conditions and transformations.
*
* Thread-safe: Implementations must be thread-safe
* Immutable: Builder methods return new instances
*
* @since 2.0.0
*/
@FunctionalInterface
public interface SanitizingFunction {
/**
* Apply sanitization to the data.
* Returns the original data unchanged if this function doesn't apply,
* or a new SanitizableData with modified value if sanitization is needed.
*
* @param data the data to potentially sanitize (never null)
* @return the original data or new data with sanitized value (never null)
*/
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 @Nullable Predicate<SanitizableData> filter() {
return null;
}
/**
* Apply sanitization unless filtered out.
* Uses the filter() predicate to determine if this function should apply.
*
* @param data the data to potentially sanitize (never null)
* @return the original data or new data with sanitized value (never null)
* @since 3.5.0
*/
default SanitizableData applyUnlessFiltered(SanitizableData data) {
Predicate<SanitizableData> filter = filter();
return (filter == null || filter.test(data)) ? apply(data) : data;
}
// Fluent Condition Methods
/**
* Add condition to check if key looks like it contains sensitive data.
* Detects common patterns: password, secret, key, token, credential, etc.
* Case-insensitive matching.
*
* @return new function with additional condition (never null)
*/
default SanitizingFunction ifLikelySensitive();
/**
* Add condition to check if key looks like a credential.
* Detects: password, passwd, pwd, secret, secrete, key, apikey, token, credential.
* Case-insensitive matching. More specific than ifLikelySensitive().
*
* @return new function with additional condition (never null)
*/
default SanitizingFunction ifLikelyCredential();
/**
* Add condition to check if value looks like a URI with embedded credentials.
* Detects URIs with userinfo (e.g., https://user:pass@host/path).
*
* @return new function with additional condition (never null)
*/
default SanitizingFunction ifLikelyUri();
/**
* Add condition to check if key matches specific sensitive properties.
* Detects keys: "sun.java.command", "spring.application.json", "spring_application_json"
* Implementation: ifKeyMatches("sun.java.command", "^spring[._]application[._]json$")
*
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifLikelySensitiveProperty();
/**
* Add condition to check if key relates to VCAP services.
* Detects keys under vcap.services which may contain credentials.
* Case-insensitive matching.
*
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifVcapServices();
/**
* Add condition to check if key exactly equals any of the given keys.
* Case-insensitive comparison.
*
* @param values the keys to match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyEquals(String... values);
/**
* Add condition to check if key ends with any of the given suffixes.
* Case-insensitive comparison.
*
* @param suffixes the case insensitive suffixes that the key can end with (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyEndsWith(String... suffixes);
/**
* Add condition to check if key contains any of the given substrings.
* Case-insensitive comparison.
* Multiple calls create OR logic (matches if key contains ANY substring).
*
* @param values the case insensitive values that the key can contain (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyContains(String... values);
/**
* Add condition to check if 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 (never null)
* @param values the case insensitive values that the key can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyMatchesIgnoringCase(BiPredicate<String, String> predicate, String... values);
/**
* Add condition to check if key matches any of the given regex patterns (ignoring case).
*
* @param regexes the case insensitive regexes that the key can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyMatches(String... regexes);
/**
* Add condition to check if key matches any of the given patterns.
*
* @param patterns the patterns that the key can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyMatches(Pattern... patterns);
/**
* Add condition to check if key matches any of the given predicates.
*
* @param predicates the predicates that the key can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyMatches(List<Predicate<String>> predicates);
/**
* Add condition to check if key matches the given predicate.
*
* @param predicate the predicate that the key can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifKeyMatches(Predicate<String> predicate);
/**
* Add condition to check if data string value matches any of the given regex patterns (ignoring case).
*
* @param regexes the case insensitive regexes that the value string can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueStringMatches(String... regexes);
/**
* Add condition to check if data string value matches any of the given patterns.
*
* @param patterns the patterns that the value string can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueStringMatches(Pattern... patterns);
/**
* Add condition to check if data string value matches any of the given predicates.
*
* @param predicates the predicates that the value string can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueStringMatches(List<Predicate<String>> predicates);
/**
* Add condition to check if data string value matches the given predicate.
*
* @param predicate the predicate that the value string can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueStringMatches(Predicate<String> predicate);
/**
* Add condition to check if data value matches any of the given predicates.
*
* @param predicates the predicates that the value can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueMatches(List<Predicate<Object>> predicates);
/**
* Add condition to check if data value matches the given predicate.
*
* @param predicate the predicate that the value can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifValueMatches(Predicate<@Nullable Object> predicate);
/**
* Add condition to check if data matches any of the given predicates.
*
* @param predicates the predicates that the data can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifMatches(List<Predicate<SanitizableData>> predicates);
/**
* Add condition to check if data matches the given predicate.
*
* @param predicate the predicate that the data can match (never null)
* @return new function with additional condition (never null)
* @since 3.5.0
*/
default SanitizingFunction ifMatches(Predicate<SanitizableData> predicate);
// Transformation Methods
/**
* Replace value with SANITIZED_VALUE if conditions match.
* This is the most common transformation.
*
* @return new function that sanitizes matching values (never null)
*/
default SanitizingFunction sanitizeValue();
// Factory Methods
/**
* Create a sanitizing function that always replaces with SANITIZED_VALUE.
* Equivalent to: (data) -> data.withSanitizedValue()
*
* @return new sanitizing function (never null)
*/
static SanitizingFunction sanitizeValue();
/**
* Helper method that can be used when working with a sanitizingFunction as a lambda.
* For example:
* <pre class="code">
* SanitizingFunction.of((data) -> data.withValue("----")).ifKeyContains("password");
* </pre>
*
* @param sanitizingFunction the sanitizing function lambda (never null)
* @return a {@link SanitizingFunction} for further method calls (never null)
* @since 3.5.0
*/
static SanitizingFunction of(SanitizingFunction sanitizingFunction);
}Complete type information for Show enum used in sanitization control.
/**
* Options for showing endpoint data.
* Controls visibility of sensitive information based on authorization.
*
* Thread-safe: Yes (enum)
* Immutable: Yes
*
* @since 3.0.0
*/
public enum Show {
/**
* Never show the item in the result.
* Always sanitized/hidden regardless of authorization.
*/
NEVER,
/**
* Show the item when the user is authorized.
* Requires authentication and optionally specific roles.
* Sanitized for unauthorized users.
*/
WHEN_AUTHORIZED,
/**
* Always show the item in the result.
* Never sanitized/hidden, even for anonymous users.
*/
ALWAYS;
/**
* Check if value should be shown for unauthorized result.
*
* @param unauthorizedResult the result for unauthorized access
* @return true if should be shown (boolean)
*/
public boolean isShown(boolean unauthorizedResult);
/**
* Check if value should be shown based on security context and roles.
* When this is WHEN_AUTHORIZED:
* - Returns true if principal is not null and has any required role
* - Returns false if principal is null or lacks required roles
*
* @param securityContext the security context (never null)
* @param roles the required roles (never null, may be empty)
* @return true if should be shown (boolean)
*/
public boolean isShown(SecurityContext securityContext,
Collection<String> roles);
}Complete reference for built-in pattern detection.
/**
* Built-in key patterns detected by ifLikelyCredential().
*
* Implementation: ifKeyEndsWith("password", "secret", "key", "token").ifKeyContains("credentials")
*
* Matches keys that END with (case-insensitive):
* - "password"
* - "secret"
* - "key"
* - "token"
*
* OR CONTAIN (case-insensitive):
* - "credentials"
*
* Examples of matching keys:
* - spring.datasource.password ✓ (ends with "password")
* - app.secret-key ✓ (ends with "key")
* - security.jwt.token ✓ (ends with "token")
* - api.secret ✓ (ends with "secret")
* - db.credentials ✓ (contains "credentials")
* - db.password-file ✗ (does NOT end with "password", only contains it)
* - apikey ✗ (does NOT end with "key", but contains it)
* - db.username ✗ (use ifLikelySensitive for broader matching)
* - server.port ✗
*
* Note: For more comprehensive detection including variations like "passwd", "pwd",
* "apikey", use custom patterns with ifKeyContains() or ifKeyMatches().
*/
/**
* Built-in key patterns detected by ifLikelySensitive().
*
* Case-insensitive substring matching (broader than ifLikelyCredential):
* - All patterns from ifLikelyCredential()
* - "username"
* - "user"
* - "private"
* - "cert"
* - "certificate"
* - "vcap" (Cloud Foundry credentials)
*
* Examples of matching keys:
* - db.username ✓
* - app.private-key ✓
* - ssl.certificate ✓
* - vcap.services ✓
*/
/**
* Built-in URI pattern detection by ifLikelyUri().
*
* Detects URIs with embedded credentials in userinfo:
* - Pattern: scheme://[user[:password]@]host[:port]/path[?query][#fragment]
*
* Sanitization behavior:
* - Removes userinfo from URI
* - Preserves scheme, host, port, path, query, fragment
*
* Examples:
* Input: https://admin:secret@example.com/api
* Output: https://example.com/api
*
* Input: mongodb://user:pass@localhost:27017/db
* Output: mongodb://localhost:27017/db
*
* Input: https://example.com/api (no userinfo)
* Output: https://example.com/api (unchanged)
*/Complete patterns for common sanitization scenarios.
/**
* Common Sanitization Patterns
*/
// Pattern 1: Sanitize all credentials (most common)
SanitizingFunction.sanitizeValue().ifLikelyCredential()
// Pattern 2: Sanitize specific keys
SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyContains("apikey")
// Pattern 3: Sanitize with custom transformation
SanitizingFunction.of(data -> {
String value = String.valueOf(data.getValue());
// Show only last 4 characters
if (value.length() > 4) {
String masked = "****" + value.substring(value.length() - 4);
return data.withValue(masked);
}
return data.withSanitizedValue();
}).ifKeyContains("card")
// Pattern 4: Conditional sanitization based on property source
SanitizingFunction.of(data -> {
// Only sanitize from specific property source
if ("systemEnvironment".equals(data.getPropertySource().getName())) {
return data.withSanitizedValue();
}
return data;
}).ifKeyContains("token")
// Pattern 5: Sanitize URIs with embedded credentials
SanitizingFunction.sanitizeValue().ifLikelyUri()
// Pattern 6: Multiple conditions (OR logic)
SanitizingFunction.sanitizeValue()
.ifKeyContains("password")
.ifKeyContains("secret")
.ifKeyContains("key")
// Matches if key contains ANY of: password, secret, OR key
// Pattern 7: Regex-based matching
SanitizingFunction.sanitizeValue()
.ifKeyMatches(Pattern.compile("^db\\..*\\.password$"))
// Pattern 8: Exact key matching
SanitizingFunction.sanitizeValue()
.ifKeyEquals("spring.datasource.password")
// Pattern 9: Key suffix matching (useful for hierarchical properties)
SanitizingFunction.sanitizeValue()
.ifKeyEndsWith(".password")
.ifKeyEndsWith(".secret")
// Matches: db.password, app.api.password, etc.
/**
* Configuration Integration Patterns
*/
// Pattern A: With EnvironmentEndpoint
@Bean
public EnvironmentEndpoint environmentEndpoint(
Environment environment,
List<SanitizingFunction> functions) {
return new EnvironmentEndpoint(
environment,
functions,
Show.WHEN_AUTHORIZED // Show values to authorized users only
);
}
// Pattern B: With ConfigurationPropertiesReportEndpoint
@Bean
public ConfigurationPropertiesReportEndpoint configPropsEndpoint(
List<SanitizingFunction> functions) {
return new ConfigurationPropertiesReportEndpoint(
functions,
Show.NEVER // Always sanitize sensitive properties
);
}
// Pattern C: Custom sanitizer bean
@Bean
public Sanitizer customSanitizer() {
return new Sanitizer(List.of(
SanitizingFunction.sanitizeValue().ifLikelyCredential(),
SanitizingFunction.sanitizeValue().ifLikelyUri(),
customCreditCardSanitizer()
));
}