使用logback屏蔽日志中的敏感数据



我需要能够搜索事件中的任何一个模式,并将模式中的文本替换为遮罩值。这是我们应用程序中的一个功能,用于防止敏感信息落入日志。由于信息可能来自各种各样的来源,因此对所有输入应用过滤器是不实际的。此外,toString()除了日志记录之外还有其他用途,我不希望toString()为所有调用统一屏蔽(仅日志记录)。

我已经尝试在logback.xml中使用%replace方法:

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'f k="pin">(.*?)&lt;/f','f k="pin">**********&lt;/f'}%n</pattern>

这是成功的(在用字符实体替换尖括号之后),但它只能替换单个模式。我还想执行

的等价物
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'pin=(.*?),','pin=**********,'}%n</pattern>

,但不能。在一个%的替换中没有办法掩盖两个模式。

在interblags上被广泛讨论的另一种方式是扩展appender/encoder/layout层次结构,但是每次尝试拦截ilogingevent都会导致整个系统崩溃,通常是通过实例化错误或UnsupportedOperationException。

例如,我尝试扩展PatternLayout:

@Component("maskingPatternLayout")
public class MaskingPatternLayout extends PatternLayout {
    @Autowired
    private Environment env;
    @Override
    public String doLayout(ILoggingEvent event) {
        String message=super.doLayout(event);
        String patternsProperty = env.getProperty("bowdleriser.patterns");
        if( patternsProperty != null ) {
            String[] patterns = patternsProperty.split("|");
            for (int i = 0; i < patterns.length; i++ ) {
                Pattern pattern = Pattern.compile(patterns[i]);
                Matcher matcher = pattern.matcher(event.getMessage());
                matcher.replaceAll("*");
            }
        } else {
            System.out.println("Bowdleriser not cleaning! Naughty strings are getting through!");
        }
        return message;
    }
}

然后调整logback.xml

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <layout class="com.touchcorp.touchpoint.utils.MaskingPatternLayout">
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </layout>
    </encoder>
  </appender>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>logs/touchpoint.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>logs/touchpoint.%i.log.zip</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>3</maxIndex>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>10MB</maxFileSize>
        </triggeringPolicy>
      <encoder>
          <layout class="com.touchcorp.touchpoint.utils.MaskingPatternLayout">
            <pattern>%date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
          </layout>
      </encoder>
    </appender>

  <logger name="com.touchcorp.touchpoint" level="DEBUG" />
  <logger name="org.springframework.web.servlet.mvc" level="TRACE" />
  <root level="INFO">
    <appender-ref ref="FILE" />
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

我已经尝试了许多其他插入,所以我想知道是否有人已经真正实现了我正在尝试,如果他们可以提供任何线索或解决方案。

您需要使用LayoutWrappingEncoder来包装布局。而且我相信你不能在这里使用spring,因为logback不是由spring管理的。

这是更新后的类。

public class MaskingPatternLayout extends PatternLayout {
    private String patternsProperty;
    public String getPatternsProperty() {
        return patternsProperty;
    }
    public void setPatternsProperty(String patternsProperty) {
        this.patternsProperty = patternsProperty;
    }
    @Override
    public String doLayout(ILoggingEvent event) {
        String message = super.doLayout(event);
        
        if (patternsProperty != null) {
            String[] patterns = patternsProperty.split("\|");
            for (int i = 0; i < patterns.length; i++) {
                Pattern pattern = Pattern.compile(patterns[i]);
                Matcher matcher = pattern.matcher(event.getMessage());
                if (matcher.find()) {
                    message = matcher.replaceAll("*");
                }
            }
        } else {
        }
        return message;
    }
}

和样例logback.xml

<appender name="fileAppender1" class="ch.qos.logback.core.FileAppender">
    <file>c:/logs/kp-ws.log</file>
    <append>true</append>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="com.kp.MaskingPatternLayout">
            <patternsProperty>.*password.*|.*karthik.*</patternsProperty>
            <pattern>%d [%thread] %-5level %logger{35} - %msg%n</pattern>
        </layout>
    </encoder>
</appender>
<root level="DEBUG">
    <appender-ref ref="fileAppender1" />
</root>

更新这里是更好的方法,在init本身中设置Pattern。这样我们就可以避免一次又一次地重新创建Pattern,并且这个实现更接近实际的用例。
public class MaskingPatternLayout extends PatternLayout {
    private String patternsProperty;
    private Optional<Pattern> pattern;
    public String getPatternsProperty() {
        return patternsProperty;
    }
    public void setPatternsProperty(String patternsProperty) {
        this.patternsProperty = patternsProperty;
        if (this.patternsProperty != null) {
            this.pattern = Optional.of(Pattern.compile(patternsProperty, Pattern.MULTILINE));
        } else {
            this.pattern = Optional.empty();
        }
    }
        @Override
        public String doLayout(ILoggingEvent event) {
            final StringBuilder message = new StringBuilder(super.doLayout(event));
    
            if (pattern.isPresent()) {
                Matcher matcher = pattern.get().matcher(message);
                while (matcher.find()) {
    
                    int group = 1;
                    while (group <= matcher.groupCount()) {
                        if (matcher.group(group) != null) {
                            for (int i = matcher.start(group); i < matcher.end(group); i++) {
                                message.setCharAt(i, '*');
                            }
                        }
                        group++;
                    }
                }
            }
            return message.toString();
        }
    
    }

和更新后的配置文件。

<appender name="fileAppender1" class="ch.qos.logback.core.FileAppender">
    <file>c:/logs/kp-ws.log</file>
    <append>true</append>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="com.kp.MaskingPatternLayout">
            <patternsProperty>(password)|(karthik)</patternsProperty>
            <pattern>%d [%thread] %-5level %logger{35} - %msg%n</pattern>
        </layout>
    </encoder>
</appender>
<root level="DEBUG">
    <appender-ref ref="fileAppender1" />
</root>

输出
My username=test and password=*******

来自文档:

replace(p){r, t}    

模式p可以任意复杂,特别是可以包含多个转换关键字。

面对同样的问题,必须在消息中替换2种模式,我只是试图chain,所以p只是replace的调用,在我的情况下:

%replace(  %replace(%msg){'regex1', 'replacement1'}  ){'regex2', 'replacement2'}

工作得很好,虽然我想知道我是否有点推动它,p确实可以任意复杂。

围绕自定义CompositeConverter和在日志back中定义引用自定义转换器的<conversionRule ...>,有一种非常相似但略有不同的方法。

在我的一个技术演示项目中,我定义了一个MaskingConverter类,它定义了一系列模式,日志事件是在匹配更新的情况下进行分析的,这些模式在我的logback配置中使用。

因为只有链接的答案在这里不那么受欢迎,所以我将在这里发布代码的重要部分,并解释它的作用以及为什么它是这样设置的。从基于java的自定义转换器类开始:
public class MaskingConverter<E extends ILoggingEvent> extends CompositeConverter<E> {
  public static final String CONFIDENTIAL = "CONFIDENTIAL";
  public static final Marker CONFIDENTIAL_MARKER = MarkerFactory.getMarker(CONFIDENTIAL);
  private Pattern keyValPattern;
  private Pattern basicAuthPattern;
  private Pattern urlAuthorizationPattern;
  @Override
  public void start() {
    keyValPattern = Pattern.compile("(pw|pwd|password)=.*?(&|$)");
    basicAuthPattern = Pattern.compile("(B|b)asic ([a-zA-Z0-9+/=]{3})[a-zA-Z0-9+/=]*([a-zA-Z0-9+/=]{3})");
    urlAuthorizationPattern = Pattern.compile("//(.*?):.*?@");
    super.start();
  }
  @Override
  protected String transform(E event, String in) {
    if (!started) {
      return in;
    }
    Marker marker = event.getMarker();
    if (null != marker && CONFIDENTIAL.equals(marker.getName())) {
      // key=value[&...] matching
      Matcher keyValMatcher = keyValPattern.matcher(in);
      // Authorization: Basic dXNlcjpwYXNzd29yZA==
      Matcher basicAuthMatcher = basicAuthPattern.matcher(in);
      // sftp://user:password@host:port/path/to/resource
      Matcher urlAuthMatcher = urlAuthorizationPattern.matcher(in);
      if (keyValMatcher.find()) {
        String replacement = "$1=XXX$2";
        return keyValMatcher.replaceAll(replacement);
      } else if (basicAuthMatcher.find()) {
        return basicAuthMatcher.replaceAll("$1asic $2XXX$3");
      } else if (urlAuthMatcher.find()) {
        return urlAuthMatcher.replaceAll("//$1:XXX@");
      }
    }
    return in;
  }
}

这个类定义了许多RegEx模式,应该与相应的日志行进行比较,并在匹配时通过屏蔽密码导致事件更新。

注意,此代码示例假设日志行只包含一种密码。当然,如果您想要探测每一行中的多个模式匹配,您可以根据需要自由调整行为。

要应用此转换器,只需在logback配置中添加以下行:

<conversionRule conversionWord="mask" converterClass="at.rovo.awsxray.utils.MaskingConverter"/>

定义了一个新函数mask,该函数可以在模式中使用,以掩盖与自定义转换器中定义的任何模式匹配的任何日志事件。这个函数现在可以在模式中使用,告诉Logback对每个日志事件执行逻辑。各自的模式可能是如下所示:

<property name="patternValue"
          value="%date{yyyy-MM-dd HH:mm:ss} [%-5level] - %X{FILE_ID} - %mask(%msg) [%thread] [%logger{5}] %n"/>
<!-- Appender definitions-->
<appender class="ch.qos.logback.core.ConsoleAppender" name="console">
    <encoder>
        <pattern>${patternValue}</pattern>
    </encoder>
</appender>

,其中%mask(%msg)将把原始日志行作为输入,并对传递给该函数的每一行执行密码屏蔽。

由于探测每行中的一个或多个模式匹配可能代价高昂,因此上面的Java代码包括可以在日志语句中使用的标记,以便将日志语句本身的某些元信息发送给Logback/SLF4J。基于这些标记,可以实现不同的行为。在所提供的场景中,可以使用标记接口告诉Logback,相应的日志行包含机密信息,因此如果匹配则需要屏蔽。任何未标记为机密的日志行将被此转换器忽略,这有助于更快地抽取行,因为不需要在这些行上执行模式匹配。

在Java中这样的标记可以像这样添加到日志语句中:

LOG.debug(MaskingConverter.CONFIDENTIAL_MARKER, "Received basic auth header: {}",
      connection.getBasicAuthentication());

,对于上面提到的自定义转换器,它可能产生类似于Received basic auth header: Basic QlRXXXlQ=的日志行,它保留了第一个和最后两个字符,但用XXX混淆了中间的位。

这是我的方法,也许它可以帮助别人

试试这个。1. 首先,我们应该创建一个类来处理日志(每一行)

public class PatternMaskingLayout extends PatternLayout {
private Pattern multilinePattern;
private List<String> maskPatterns = new ArrayList<>();
public void addMaskPattern(String maskPattern) { // invoked for every single entry in the xml
    maskPatterns.add(maskPattern);
    multilinePattern = Pattern.compile(
            String.join("|", maskPatterns), // build pattern using logical OR
            Pattern.MULTILINE
    );
}
@Override
public String doLayout(ILoggingEvent event) {
    return maskMessage(super.doLayout(event)); // calling superclass method is required
}
private String maskMessage(String message) {
    if (multilinePattern == null) {
        return message;
    }
    StringBuilder sb = new StringBuilder(message);
    Matcher matcher = multilinePattern.matcher(sb);
    while (matcher.find()) {
        if (matcher.group().contains("creditCard")) {
            maskCreditCard(sb, matcher);
        } else if (matcher.group().contains("email")) {
            // your logic for this case
        }
    }
    return sb.toString();
}
private void maskCreditCard(StringBuilder sb, Matcher matcher) {
    //here is our main logic for masking sensitive data
    String targetExpression = matcher.group();
    String[] split = targetExpression.split("=");
    String pan = split[1];
    String maskedPan = Utils.getMaskedPan(pan);
    int start = matcher.start() + split[0].length() + 1;
    int end = matcher.end();
    sb.replace(start, end, maskedPan);
}

}

  • 第二步是我们应该在logback.xml

    中为logback创建appender
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="com.bpcbt.micro.utils.PatternMaskingLayout">
            <maskPattern>creditCard=d+</maskPattern> <!-- SourcePan pattern -->
            <pattern>%d{dd/MM/yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%ex</pattern>-->
        </layout>
    </encoder>
    

  • 现在我们可以在代码中使用logger

    log.info("card context set for creditCard={}", creditCard);

  • 因此,我们将看到

    从日志中取出一行

    信用卡上下文设置为creditCard=11111******111

  • 如果没有这些选项,我们的日志将是这样的

    card context set for creditCard=1111111111111
    

    我使用了基于图书馆https://github.com/tersesystems/terse-logback的RegexCensor的审查器。在logback.xml

    <!--censoring information-->
    <newRule pattern="*/censor" actionClass="com.tersesystems.logback.censor.CensorAction"/>
    <conversionRule conversionWord="censor" converterClass="com.tersesystems.logback.censor.CensorConverter" />
    <!--impl inspired by com.tersesystems.logback.censor.RegexCensor -->
    <censor name="censor-sensitive" class="com.mycompaqny.config.logging.SensitiveDataCensor"></censor>
    

    ,我把列表正则表达式替换。

    @Getter@Setter    
    public class SensitiveDataCensor extends ContextAwareBase implements Censor, LifeCycle {
        protected volatile boolean started = false;
        protected String name;
        private List<Pair<Pattern, String>> replacementPhrases = new ArrayList<>();
        public void start() {
            String ssnJsonPattern = ""(ssn|socialSecurityNumber)("\W*:\W*".*?)-(.*?)"";
            replacementPhrases.add(Pair.of(Pattern.compile(ssnJsonPattern), ""$1$2-****""));
            String ssnXmlPattern = "<(ssn|socialSecurityNumber)>(\W*.*?)-(.*?)</";
            replacementPhrases.add(Pair.of(Pattern.compile(ssnXmlPattern), "<$1>$2-****</"));
            started = true;
        }
        public void stop() {
            replacementPhrases.clear();
            started = false;
        }
        public CharSequence censorText(CharSequence original) {
            CharSequence outcome = original;
            for (Pair<Pattern, String> replacementPhrase : replacementPhrases) {
                outcome = replacementPhrase.getLeft().matcher(outcome).replaceAll(replacementPhrase.getRight());
            } 
            return outcome;
        }
    }
    

    并在logback.xml中使用,如下所示

    <message>[ignore]</message> <---- IMPORTANT to disable original message field so you get only censored message
    ...
    <pattern>
        {"message": "%censor(%msg){censor-sensitive}"}
    </pattern>
    

    我试图在我的演示项目日志中隐藏一些敏感数据。我尝试过,但它没有为我工作,因为Java反射,因为我把变量名作为模式。我正在添加对我有效的解决方案,以防它也有助于其他人。

    我在logback.xml(内部编码器标签)文件中添加了以下代码,用于屏蔽日志中的field1和field2信息。

    <encoder class="com.demo.config.CustomJsonMaskLogEncoder">
    <patterns>
        <pattern>"field1"s*:s*"(.*?)"</pattern>
        <pattern>"field2"s*:s*"(.*?)"</pattern>
        <pattern>%-5p [%d{ISO8601,UTC}] [%thread] %c: %m%n%rootException</pattern>
    </patterns>
    </encoder>
    

    我写了一个CustomJsonMaskLogEncoder,它按照正则表达式屏蔽字段数据。

    package com.demo.config;
    import ch.qos.logback.classic.Logger;
    import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter;
    import ch.qos.logback.classic.spi.ILoggingEvent;
    import ch.qos.logback.classic.spi.LoggingEvent;
    import java.util.ArrayList;
    import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder;
    import org.slf4j.LoggerFactory;
    public class CustomJsonMaskLogEncoder extends LoggingEventCompositeJsonEncoder {
        private final CustomPatternMaskingLayout customPatternMaskingLayout;
        private boolean maskEnabled;
        public JsonMaskLogEncoder() {
            super();
            customPatternMaskingLayout = new CustomPatternMaskingLayout();
            maskEnabled = true;
        }
        @Override
        public byte[] encode(ILoggingEvent event) {
            return maskEnabled ? getMaskedJson(event) : super.encode(event);
        }
        private byte[] getMaskedJson(ILoggingEvent event) {
            final Logger logger =
                    (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(event.getLoggerName());
            final String message = customPatternMaskingLayout.maskMessage(event.getFormattedMessage());
            final LoggingEvent loggingEvent =
                    new LoggingEvent(
                            "", logger, event.getLevel(), message, getThrowable(event), event.getArgumentArray());
            return super.encode(loggingEvent);
        }
        private Throwable getThrowable(ILoggingEvent event) {
            return event.getThrowableProxy() == null ? null : new Throwable(getStackTrace(event));
        }
        private String getStackTrace(ILoggingEvent event) {
            final ExtendedThrowableProxyConverter throwableConverter =
                    new ExtendedThrowableProxyConverter();
            throwableConverter.start();
            final String errorMessageWithStackTrace = throwableConverter.convert(event);
            throwableConverter.stop();
            return errorMessageWithStackTrace;
        }
        @SuppressWarnings("unused")
        public void setEnableMasking(boolean enabled) {
            this.maskEnabled = enabled;
        }
        @SuppressWarnings("unused")
        public void setPatterns(Patterns patterns) {
            customPatternMaskingLayout.addMaskPatterns(patterns);
        }
        public static class Patterns extends ArrayList<String> {
            @SuppressWarnings("unused")
            public void addPattern(String pattern) {
                add(pattern);
            }
        }
    }
    
    下面是实际CustomPatternMaskingLayout的代码:
    package com.demo.config;
    import static java.lang.String.format;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    
    public class CustomPatternMaskingLayout {
        private Pattern multilinePattern;
        private final List<String> maskPatterns = new ArrayList<>();
        public CustomPatternMaskingLayout() {
            compilePattern();
        }
        void addMaskPatterns(CustomJsonMaskLogEncoder.Patterns patterns) {
            maskPatterns.addAll(patterns);
            compilePattern();
        }
        private void compilePattern() {
            multilinePattern = Pattern.compile(String.join("|", maskPatterns),Pattern.MULTILINE);
        }
        String maskMessage(String message) {
            if (multilinePattern == null) {
                return message;
            }
            StringBuilder sb = new StringBuilder(message);
            Matcher matcher = multilinePattern.matcher(sb);
            while (matcher.find()) {
                IntStream.rangeClosed(1, matcher.groupCount()).forEach(group -> {
                    if (matcher.group(group) != null) {
                        IntStream.range(matcher.start(group), matcher.end(group)).forEach(i -> sb.setCharAt(i, '*'));
                    }
                });
            }
            return sb.toString();
        }
    }
    

    希望这有帮助!!

    最新更新