package com.xfestudio.xfeservermanager.core.audit;
import com.xfestudio.xfeservermanager.api.audit.AuditEvent;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Central redaction pass that must run before an audit event reaches persistent storage. */
public final class AuditRedactor {
public static final String REDACTED = "[REDACTED]";
private static final Set<String> SENSITIVE_PARTS = Set.of(
"password", "passwd", "secret", "token", "authorization", "cookie",
"recovery", "totp", "nbt", "components", "raw_item", "ip", "address"
);
private static final Pattern IPV4 = Pattern.compile(
"(?<![0-9])(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?![0-9])");
private static final Pattern IPV6 = Pattern.compile(
"(?i)(?<![0-9a-f:.])(?:[0-9a-f]{0,4}:){2,7}"
+ "(?:[0-9a-f]{0,4}|(?:[0-9]{1,3}\\.){3}[0-9]{1,3})"
+ "(?:%[0-9a-z._-]+)?(?![0-9a-f:.])");
private static final Pattern BEARER = Pattern.compile(
"(?i)\\bbearer\\s+[a-z0-9._~+/=-]+");
private static final Pattern INLINE_SECRET = Pattern.compile(
"(?i)\\b(password|passwd|token|secret|authorization)\\s*[:=]\\s*([^\\s,;]+)");
private static final Pattern OPAQUE_TOKEN = Pattern.compile(
"(?<![A-Za-z0-9_+/-])[A-Za-z0-9_+/-]{32,}={0,2}(?![A-Za-z0-9_+/=-])");
private static final Pattern UUID_TEXT = Pattern.compile(
"(?i)[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}");
public AuditEvent redact(AuditEvent event) {
return new AuditEvent(
event.eventId(),
event.timestamp(),
event.actorId(),
event.actorDisplayName(),
event.source(),
event.action(),
redactInline(event.target()),
redactInline(event.reason()),
redactMap(event.beforeValues()),
redactMap(event.afterValues()),
event.policyVersion(),
event.requestId(),
event.outcome(),
event.durationMillis(),
redactMap(event.metadata())
);
}
public Map<String, String> redactMap(Map<String, String> values) {
Map<String, String> result = new LinkedHashMap<>();
values.forEach((key, value) -> result.put(key, isSensitive(key) ? REDACTED : redactInline(value)));
return Map.copyOf(result);
}
private static boolean isSensitive(String key) {
String normalized = key.toLowerCase(Locale.ROOT);
return SENSITIVE_PARTS.stream().anyMatch(normalized::contains);
}
private static String redactInline(String value) {
String withoutBearer = BEARER.matcher(value).replaceAll("Bearer " + REDACTED);
String withoutSecrets = INLINE_SECRET.matcher(withoutBearer).replaceAll("$1=" + REDACTED);
String withoutOpaqueTokens = redactOpaqueTokens(withoutSecrets);
String withoutIpv6 = IPV6.matcher(withoutOpaqueTokens).replaceAll("[IP_REDACTED]");
return IPV4.matcher(withoutIpv6).replaceAll("[IP_REDACTED]");
}
private static String redactOpaqueTokens(String value) {
Matcher matcher = OPAQUE_TOKEN.matcher(value);
StringBuffer output = new StringBuffer(value.length());
while (matcher.find()) {
String candidate = matcher.group();
String replacement = UUID_TEXT.matcher(candidate).matches() ? candidate : REDACTED;
matcher.appendReplacement(output, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(output);
return output.toString();
}
}
package com.xfestudio.xfeservermanager.core.audit;
import com.xfestudio.xfeservermanager.api.audit.AuditEvent;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Central redaction pass that must run before an audit event reaches persistent storage. */
public final class AuditRedactor {
public static final String REDACTED = "[REDACTED]";
private static final Set<String> SENSITIVE_PARTS = Set.of(
"password", "passwd", "secret", "token", "authorization", "cookie",
"recovery", "totp", "nbt", "components", "raw_item", "ip", "address"
);
private static final Pattern IPV4 = Pattern.compile(
"(?<![0-9])(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?![0-9])");
private static final Pattern IPV6 = Pattern.compile(
"(?i)(?<![0-9a-f:.])(?:[0-9a-f]{0,4}:){2,7}"
+ "(?:[0-9a-f]{0,4}|(?:[0-9]{1,3}\\.){3}[0-9]{1,3})"
+ "(?:%[0-9a-z._-]+)?(?![0-9a-f:.])");
private static final Pattern BEARER = Pattern.compile(
"(?i)\\bbearer\\s+[a-z0-9._~+/=-]+");
private static final Pattern INLINE_SECRET = Pattern.compile(
"(?i)\\b(password|passwd|token|secret|authorization)\\s*[:=]\\s*([^\\s,;]+)");
private static final Pattern OPAQUE_TOKEN = Pattern.compile(
"(?<![A-Za-z0-9_+/-])[A-Za-z0-9_+/-]{32,}={0,2}(?![A-Za-z0-9_+/=-])");
private static final Pattern UUID_TEXT = Pattern.compile(
"(?i)[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}");
public AuditEvent redact(AuditEvent event) {
return new AuditEvent(
event.eventId(),
event.timestamp(),
event.actorId(),
event.actorDisplayName(),
event.source(),
event.action(),
redactInline(event.target()),
redactInline(event.reason()),
redactMap(event.beforeValues()),
redactMap(event.afterValues()),
event.policyVersion(),
event.requestId(),
event.outcome(),
event.durationMillis(),
redactMap(event.metadata())
);
}
public Map<String, String> redactMap(Map<String, String> values) {
Map<String, String> result = new LinkedHashMap<>();
values.forEach((key, value) -> result.put(key, isSensitive(key) ? REDACTED : redactInline(value)));
return Map.copyOf(result);
}
private static boolean isSensitive(String key) {
String normalized = key.toLowerCase(Locale.ROOT);
return SENSITIVE_PARTS.stream().anyMatch(normalized::contains);
}
private static String redactInline(String value) {
String withoutBearer = BEARER.matcher(value).replaceAll("Bearer " + REDACTED);
String withoutSecrets = INLINE_SECRET.matcher(withoutBearer).replaceAll("$1=" + REDACTED);
String withoutOpaqueTokens = redactOpaqueTokens(withoutSecrets);
String withoutIpv6 = IPV6.matcher(withoutOpaqueTokens).replaceAll("[IP_REDACTED]");
return IPV4.matcher(withoutIpv6).replaceAll("[IP_REDACTED]");
}
private static String redactOpaqueTokens(String value) {
Matcher matcher = OPAQUE_TOKEN.matcher(value);
StringBuffer output = new StringBuffer(value.length());
while (matcher.find()) {
String candidate = matcher.group();
String replacement = UUID_TEXT.matcher(candidate).matches() ? candidate : REDACTED;
matcher.appendReplacement(output, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(output);
return output.toString();
}
}