or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

audit.mdbuiltin-endpoints.mdendpoint-framework.mdhttp-exchanges.mdindex.mdinfo-contributors.mdjmx-integration.mdmanagement-operations.mdoperation-invocation.mdsanitization.mdsecurity.mdweb-integration.md
tile.json

sanitization.mddocs/

Data Sanitization

QUICK REFERENCE

Key Classes and Packages

// 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, ALWAYS

Sanitization Strategy Selector

What 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 -> ...)

Common Patterns Quick Guide

// 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")

Show Enum Values

ValueBehaviorUse When
NEVERAlways sanitizePublic endpoints
WHEN_AUTHORIZEDSanitize unless authorizedRole-based access
ALWAYSNever sanitizeInternal/admin only

Fluent Method Chaining

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 error

Chaining Rules:

  1. Start with SanitizingFunction.sanitizeValue() or SanitizingFunction.of(lambda)
  2. Chain condition methods: ifKeyContains(), ifKeyEndsWith(), ifKeyMatches(), etc.
  3. Each method returns a new SanitizingFunction for further chaining
  4. Multiple conditions are combined with OR logic (any match triggers sanitization)

AGENT GUIDANCE

When to Sanitize

Always sanitize:

  • Passwords and passphrases
  • API keys and tokens
  • Secret keys and salts
  • Private keys and certificates
  • OAuth tokens and refresh tokens
  • Database connection strings with credentials
  • AWS/GCP/Azure credentials

Consider sanitizing:

  • Email addresses (PII)
  • Phone numbers (PII)
  • Credit card numbers (PCI)
  • IP addresses (security)
  • Session IDs (security)
  • URIs with query parameters

Don't sanitize:

  • Public configuration (port numbers, timeouts)
  • Feature flags
  • Build information
  • Non-sensitive metadata

Pattern vs Anti-Pattern

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: ADMIN

ANTI-PATTERN: All or nothing

// ❌ Either all users see secrets or none
management:
  endpoint:
    env:
      show-values: always  // or never

PATTERN: 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.

Capabilities

Core Sanitization Types

/**
 * 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);
}

COMPLETE WORKING EXAMPLES

Example 1: Basic Password Sanitization

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
        );
    }
}

Example 2: Custom Credit Card Sanitization

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");
    }
}

Example 3: Role-Based Sanitization

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);
    }
}

TESTING EXAMPLES

Test 1: Basic Sanitization

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);
    }
}

Test 2: Custom Sanitization Logic

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");
    }
}

TROUBLESHOOTING

Common Error: Values Not Sanitized

Problem: Sensitive values visible to unauthorized users

Causes:

  1. Sanitization function not matching key
  2. Wrong show-values configuration
  3. Missing sanitizing functions

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);
}

Common Error: Too Much Sanitization

Problem: Even admins see ******

Causes:

  1. show-values set to NEVER
  2. Authorization check failing
  3. Missing roles configuration

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=ADMIN

Common Error: Custom Sanitization Not Applied

Problem: Custom sanitizing function doesn't work

Causes:

  1. Function returns original data (no modification)
  2. Condition doesn't match
  3. Function not registered

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

PERFORMANCE NOTES

Sanitization Overhead

// 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 Pattern Efficiency

// 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 does

COMPLETE API REFERENCE

Sanitizer (Full Signature)

Complete 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);
}

SanitizableData (Full Signature)

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);
}

SanitizingFunction (Full Signature)

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);
}

Show Enum (Full Signature)

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);
}

Built-in Patterns

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)
 */

Usage Patterns Reference

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()
    ));
}

Cross-References

  • For environment endpoint: Built-in Endpoints
  • For security context: Security Integration
  • For configuration properties: Built-in Endpoints
  • For endpoint basics: Endpoint Framework