Skip to content

Commit 6ad0ac8

Browse files
committed
Add module with example of masking sensitive information in logs
1 parent 5fcf9a2 commit 6ad0ac8

14 files changed

Lines changed: 347 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Example of OpenApi generation:
6767

6868
[Example](messaging-stomp-websocket) of STOMP messaging over WebSockets for building interactive web application
6969

70+
### mask-logs-spring-boot-sample
71+
72+
[Example](mask-logs-spring-boot-sample/README.md) of masking secure information in logs in Spring Boot using custom Logback converter/appender
73+
7074
### sound-recorder-n-spectrum-analyzer
7175

7276
- [Application](sound-recorder-n-spectrum-analyzer/src/main/java/by/andd3dfx/capturesound/AudioCaptureApp.java) to
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Mask logs Spring Boot sample
2+
3+
Example of masking sensitive values in Spring Boot logs using custom Logback extensions:
4+
5+
- converter: `MaskingMessageConverter`
6+
- appender: `MaskingConsoleAppender`
7+
8+
Masking is implemented in `LogMaskingSupport` (`password`, `token`, `authorization`, `Bearer ...`, long digit sequences).
9+
10+
## Run
11+
12+
```bash
13+
../mvnw spring-boot:run -pl mask-logs-spring-boot-sample
14+
```
15+
16+
## Try
17+
18+
```bash
19+
curl -X POST http://localhost:8080/api/auth/login \
20+
-H "Content-Type: application/json" \
21+
-d "{\"username\":\"john\",\"password\":\"secret123\",\"cardNumber\":\"4111111111111111\"}"
22+
```
23+
24+
Expected log fragments:
25+
26+
```text
27+
password=***
28+
token=***
29+
Authorization=***
30+
cardNumber=************
31+
```
32+
33+
## Switch converter/appender
34+
35+
`src/main/resources/logback-spring.xml` uses `MASKED_CONVERTER` by default.
36+
To use appender-based masking, switch root appender reference to `MASKED_APPENDER`.
37+
38+
## Tests
39+
40+
```bash
41+
../mvnw -pl mask-logs-spring-boot-sample test
42+
```
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>java-sandbox</artifactId>
7+
<groupId>by.andd3dfx</groupId>
8+
<version>1.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>mask-logs-spring-boot-sample</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>${java.version}</maven.compiler.source>
16+
<maven.compiler.target>${java.version}</maven.compiler.target>
17+
</properties>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-test</artifactId>
27+
<scope>test</scope>
28+
<exclusions>
29+
<exclusion>
30+
<groupId>org.junit.vintage</groupId>
31+
<artifactId>junit-vintage-engine</artifactId>
32+
</exclusion>
33+
</exclusions>
34+
</dependency>
35+
</dependencies>
36+
37+
<dependencyManagement>
38+
<dependencies>
39+
<dependency>
40+
<groupId>org.junit</groupId>
41+
<artifactId>junit-bom</artifactId>
42+
<version>5.9.2</version>
43+
<scope>import</scope>
44+
<type>pom</type>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.springframework.boot</groupId>
48+
<artifactId>spring-boot-dependencies</artifactId>
49+
<version>${spring-boot.version}</version>
50+
<type>pom</type>
51+
<scope>import</scope>
52+
</dependency>
53+
</dependencies>
54+
</dependencyManagement>
55+
56+
</project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package by.andd3dfx.masklogs;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class MaskLogsSpringBootSampleApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(MaskLogsSpringBootSampleApplication.class, args);
11+
}
12+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package by.andd3dfx.masklogs.api;
2+
3+
import java.util.Map;
4+
import java.util.UUID;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequestMapping(path = "/api/auth", produces = MediaType.APPLICATION_JSON_VALUE)
16+
public class AuthController {
17+
18+
private static final Logger LOG = LoggerFactory.getLogger(AuthController.class);
19+
20+
@PostMapping(path = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)
21+
public Map<String, String> login(@RequestBody AuthRequest request) {
22+
LOG.info("Received login request: {}", request);
23+
24+
String token = UUID.randomUUID().toString().replace("-", "");
25+
LOG.info("Generated token={} for user={}", token, request.username());
26+
LOG.info("Authorization=Bearer {}", token);
27+
28+
return Map.of(
29+
"status", "OK",
30+
"token", token
31+
);
32+
}
33+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package by.andd3dfx.masklogs.api;
2+
3+
public record AuthRequest(
4+
String username,
5+
String password,
6+
String cardNumber
7+
) {
8+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package by.andd3dfx.masklogs.logging;
2+
3+
import java.util.List;
4+
import java.util.regex.Pattern;
5+
6+
public final class LogMaskingSupport {
7+
8+
private static final String MASK = "***";
9+
10+
private static final List<MaskRule> RULES = List.of(
11+
new MaskRule(
12+
Pattern.compile("(?i)(\"?(?:password|pass|pwd|secret|token|accessToken|refreshToken|authorization)\"?\\s*[:=]\\s*\")([^\"]+)(\")"),
13+
"$1" + MASK + "$3"
14+
),
15+
new MaskRule(
16+
Pattern.compile("(?i)(\\b(?:password|pass|pwd|secret|token|accessToken|refreshToken|authorization)\\b\\s*[:=]\\s*)([^,}\\]]+)"),
17+
"$1" + MASK
18+
),
19+
new MaskRule(
20+
Pattern.compile("(?i)(\\bBearer\\s+)([A-Za-z0-9._\\-]+)"),
21+
"$1" + MASK
22+
),
23+
new MaskRule(
24+
Pattern.compile("\\b\\d{12,19}\\b"),
25+
"************"
26+
)
27+
);
28+
29+
private LogMaskingSupport() {
30+
}
31+
32+
public static String mask(String source) {
33+
if (source == null || source.isEmpty()) {
34+
return source;
35+
}
36+
37+
String masked = source;
38+
for (MaskRule rule : RULES) {
39+
masked = rule.pattern().matcher(masked).replaceAll(rule.replacement());
40+
}
41+
return masked;
42+
}
43+
44+
private record MaskRule(Pattern pattern, String replacement) {
45+
}
46+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package by.andd3dfx.masklogs.logging;
2+
3+
import java.time.Instant;
4+
import java.time.ZoneId;
5+
import java.time.format.DateTimeFormatter;
6+
7+
import ch.qos.logback.classic.spi.ILoggingEvent;
8+
import ch.qos.logback.core.AppenderBase;
9+
10+
public class MaskingConsoleAppender extends AppenderBase<ILoggingEvent> {
11+
12+
private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
13+
.withZone(ZoneId.systemDefault());
14+
15+
@Override
16+
protected void append(ILoggingEvent eventObject) {
17+
String timestamp = TS_FORMATTER.format(Instant.ofEpochMilli(eventObject.getTimeStamp()));
18+
String message = LogMaskingSupport.mask(eventObject.getFormattedMessage());
19+
String line = "%s %-5s [%s] %s - %s%n".formatted(
20+
timestamp,
21+
eventObject.getLevel(),
22+
eventObject.getThreadName(),
23+
eventObject.getLoggerName(),
24+
message
25+
);
26+
System.out.print(line);
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package by.andd3dfx.masklogs.logging;
2+
3+
import ch.qos.logback.classic.pattern.ClassicConverter;
4+
import ch.qos.logback.classic.spi.ILoggingEvent;
5+
6+
public class MaskingMessageConverter extends ClassicConverter {
7+
8+
@Override
9+
public String convert(ILoggingEvent event) {
10+
return LogMaskingSupport.mask(event.getFormattedMessage());
11+
}
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
spring:
2+
application:
3+
name: mask-logs-spring-boot-sample

0 commit comments

Comments
 (0)