diff --git a/README.md b/README.md index 339ce86..821fe40 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ treblle.apiKey= treblle.projectId= treblle.filter-order= # Default Ordered.LOWEST_PRECEDENCE - 10, similar to Springs HttpTraceFilter treblle.debug=false # Default is false +treblle.masking-keywords= # Additional masking keywords separated by comma, to mask whole objects use .* ``` In case you are using the `application.yml` file: @@ -133,6 +134,7 @@ treblle: project-id: filter-order: # Default Ordered.LOWEST_PRECEDENCE - 10, similar to Springs HttpTraceFilter debug: false # Default is false + masking-keywords: # Additional masking keywords separated by comma, to mask whole objects use .* ``` That's it. Your API requests and responses are now being sent to your Treblle project. diff --git a/pom.xml b/pom.xml index ee4d2ab..bcda411 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.treblle treblle-spring-boot-starter - 2.0.5 + 2.0.6 treblle-spring-boot-starter Official Treblle Starter for Spring Boot https://github.com/Treblle/treblle-spring diff --git a/src/main/java/com/treblle/spring/utils/DataMaskerImpl.java b/src/main/java/com/treblle/spring/utils/DataMaskerImpl.java index ea51c8f..7ccf531 100644 --- a/src/main/java/com/treblle/spring/utils/DataMaskerImpl.java +++ b/src/main/java/com/treblle/spring/utils/DataMaskerImpl.java @@ -26,28 +26,63 @@ public class DataMaskerImpl implements DataMasker { "pwd", "secret", "password_confirmation", + "passwordConfirmation", "cc", "card_number", + "cardNumber", "ccv", "ssn", - "credit_score"); + "credit_score", + "creditScore", + "api_key" + ); private Pattern pattern; + private Pattern catchAllPattern; public DataMaskerImpl(TreblleProperties properties) { Set keywords = new HashSet<>(9); keywords.addAll(DEFAULT_KEYWORDS); keywords.addAll(properties.getMaskingKeywords()); - String regex = keywords.stream().map(it -> "\\b" + it + "\\b").collect(Collectors.joining("|")); + String mergedPattern = keywords.stream() + .filter(it -> !it.endsWith(".*")) + .collect(Collectors.joining("|")); try { - pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + pattern = Pattern.compile("^(" + mergedPattern + ")$", Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException exception) { - log.error("Error while compiling regex with custom keywords. Continuing with default."); + log.error("Error while compiling regex with custom keywords. Continuing with default pattern.", exception); String defaultRegex = DEFAULT_KEYWORDS.stream().map(it -> "\\b" + it + "\\b").collect(Collectors.joining("|")); pattern = Pattern.compile(defaultRegex, Pattern.CASE_INSENSITIVE); } + + String mergedCatchAllPattern = keywords.stream() + .filter(it -> it.endsWith(".*")) + .map(this::removeCatchAllSuffix) + .collect(Collectors.joining("|")); + + try { + catchAllPattern = Pattern.compile("^(" + mergedCatchAllPattern + ")$", Pattern.CASE_INSENSITIVE); + } catch (PatternSyntaxException exception) { + log.error("Error while compiling catch all regex with custom keywords. Continuing with empty pattern.", exception); + catchAllPattern = null; + } + } + + private String removeCatchAllSuffix(String input) { + return input.substring(0, input.length() - ".*".length()); + } + + private boolean matchesMaskingKeywords(String key) { + return pattern.matcher(key).matches(); + } + + private boolean matchesCatchAllMaskingKeywords(String key) { + if (catchAllPattern == null) { + return false; + } + return catchAllPattern.matcher(key).matches(); } @Override @@ -60,7 +95,7 @@ public Map mask(Map headers) { return headers.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, entry -> { - if (pattern.matcher(entry.getKey()).matches() && Objects.nonNull(entry.getValue())) { + if (matchesMaskingKeywords(entry.getKey()) && Objects.nonNull(entry.getValue())) { return MASKED_VALUE; } else { return entry.getValue(); @@ -70,8 +105,10 @@ public Map mask(Map headers) { } private JsonNode maskInternal(String key, JsonNode target) { - if (target.isTextual() && key != null && pattern.matcher(key).matches()) { + if (target.isValueNode() && key != null && matchesMaskingKeywords(key)) { return new TextNode(MASKED_VALUE); + } else if (key != null && matchesCatchAllMaskingKeywords(key)) { + return maskAllInternal(target); } if (target.isObject()) { Iterator> fields = target.fields(); @@ -88,4 +125,21 @@ private JsonNode maskInternal(String key, JsonNode target) { return target; } + private JsonNode maskAllInternal(JsonNode target) { + if (target.isValueNode()) { + return new TextNode(MASKED_VALUE); + } else if (target.isArray()) { + for (int index = 0; index < target.size(); index++) { + ((ArrayNode) target).set(index, maskAllInternal(target.get(index))); + } + } else if (target.isObject()) { + Iterator> fields = target.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + ((ObjectNode) target).replace(field.getKey(), maskAllInternal(field.getValue())); + } + } + return target; + } + } diff --git a/src/test/java/com/treblle/spring/DataMaskerTest.java b/src/test/java/com/treblle/spring/DataMaskerTest.java index 4a9c77c..a8ee8a6 100644 --- a/src/test/java/com/treblle/spring/DataMaskerTest.java +++ b/src/test/java/com/treblle/spring/DataMaskerTest.java @@ -7,11 +7,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import java.util.HashMap; import java.util.Map; @SpringBootTest(classes = TestConfig.class) +@TestPropertySource(properties = { + "treblle.masking-keywords=firstLevel.*" +}) public class DataMaskerTest { @Autowired @@ -37,6 +41,25 @@ public void testJsonMasking() { assert "treblle".equals(result.get("hello").asText()); } + @Test + public void testCatchAllJsonMasking() { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode firstLevel = objectMapper.createObjectNode(); + firstLevel.put("some_field", "some_secret"); + firstLevel.put("some_field2", "some_secret2"); + + root.set("firstLevel", firstLevel); + root.put("CCV", "some_secret"); + root.put("hello", "treblle"); + + JsonNode result = dataMasker.mask(root); + + assert "******".equals(result.get("CCV").asText()); + assert "******".equals(result.get("firstLevel").get("some_field").asText()); + assert "******".equals(result.get("firstLevel").get("some_field2").asText()); + assert "treblle".equals(result.get("hello").asText()); + } + @Test public void testHeaderMasking() { Map headers = new HashMap<>();