Skip to content

Commit

Permalink
Object masking support
Browse files Browse the repository at this point in the history
  • Loading branch information
IvoMajic committed Apr 7, 2024
1 parent 88d6335 commit dc0f814
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 7 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ treblle.apiKey=<API_KEY>
treblle.projectId=<PROJECT_ID>
treblle.filter-order=<ORDER_OF_TREBLLE_FILTER> # Default Ordered.LOWEST_PRECEDENCE - 10, similar to Springs HttpTraceFilter
treblle.debug=false # Default is false
treblle.masking-keywords=<ADDITIONAL_KEYWORDS_TO_MASK> # Additional masking keywords separated by comma, to mask whole objects use <keyword>.*
```

In case you are using the `application.yml` file:
Expand All @@ -133,6 +134,7 @@ treblle:
project-id: <PROJECT_ID>
filter-order: <ORDER_OF_TREBLLE_FILTER> # Default Ordered.LOWEST_PRECEDENCE - 10, similar to Springs HttpTraceFilter
debug: false # Default is false
masking-keywords: <ADDITIONAL_KEYWORDS_TO_MASK> # Additional masking keywords separated by comma, to mask whole objects use <keyword>.*
```
That's it. Your API requests and responses are now being sent to your Treblle project.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.treblle</groupId>
<artifactId>treblle-spring-boot-starter</artifactId>
<version>2.0.5</version>
<version>2.0.6</version>
<name>treblle-spring-boot-starter</name>
<description>Official Treblle Starter for Spring Boot</description>
<url>https://github.com/Treblle/treblle-spring</url>
Expand Down
66 changes: 60 additions & 6 deletions src/main/java/com/treblle/spring/utils/DataMaskerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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
Expand All @@ -60,7 +95,7 @@ public Map<String, String> mask(Map<String, String> 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();
Expand All @@ -70,8 +105,10 @@ public Map<String, String> mask(Map<String, String> 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<Entry<String, JsonNode>> fields = target.fields();
Expand All @@ -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<Entry<String, JsonNode>> fields = target.fields();
while (fields.hasNext()) {
Entry<String, JsonNode> field = fields.next();
((ObjectNode) target).replace(field.getKey(), maskAllInternal(field.getValue()));
}
}
return target;
}

}
23 changes: 23 additions & 0 deletions src/test/java/com/treblle/spring/DataMaskerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, String> headers = new HashMap<>();
Expand Down

0 comments on commit dc0f814

Please sign in to comment.