XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
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();
    }
}