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

时间:2014-08-13 04:13:49

标签: logback data-masking

我需要能够在事件中搜索多种模式中的任何一种,并用模糊值替换模式中的文本。这是我们的应用程序中的一项功能,旨在防止敏感信息落入日志。由于信息可能来自各种各样的来源,因此在所有输入上应用过滤器是不切实际的。除了记录之外还有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层次结构上进行扩展,但是每次拦截ILoggingEvent的尝试都会导致整个系统崩溃,通常是通过实例化错误或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>

我已经尝试了很多其他插入,所以我想知道是否有人实际上已经实现了我正在尝试的东西以及他们是否可以提供任何线索或解决方案。

6 个答案:

答案 0 :(得分:12)

您需要使用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,这种实现接近现实用例。

公共类MaskingPatternLayout扩展了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=*******

答案 1 :(得分:5)

来自文档:

replace(p){r, t}    

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

面对同样的问题,必须在邮件中替换2个模式,我只是尝试chain所以p只是一个替换的调用,在我的情况下:

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

工作得很好,不过我想知道我是否会推动它,而p确实可能是任意复杂的。

答案 2 :(得分:3)

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

尝试这个。 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);
}

}

  1. 第二步是我们应该创建添加器,以便将登录回登录到logback.xml

    <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>
    

  2. 现在我们可以在代码中使用记录器了

    log.info(“为creditCard = {}设置的卡上下文”,creditCard);

  3. 因此,我们将看到

    日志中的一行

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

如果没有这些选项,我们的日志将类似于此行

card context set for creditCard=1111111111111

答案 3 :(得分:1)

我在https://github.com/tersesystems/terse-logback中有一个检查器,可让您在一个地方定义一个检查器,然后在多个附加程序中引用它。

答案 4 :(得分:0)

一种非常相似但略有不同的方法是围绕自定义CompositeConverter并在引用自定义转换器的logback中定义<conversionRule ...>

在我的一个技术演示项目中,我定义了一个MaskingConverter类,该类定义了一系列模式,用于对日志记录事件进行分析,并在logback configuration内部使用更新的匹配项。

由于纯链接的答案在这里并不受欢迎,因此,我将在此处发布代码的重要部分,并解释其作用以及为什么要这样设置。从基于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代码包含Markers,可在日志语句中使用该命令将日志语句本身的某些元信息发送到Logback / SLF4J。基于此类标记,可能会实现不同的行为。在提出的方案中,可以使用标记界面来告诉Logback相应的日志行包含机密信息,因此如果匹配则需要屏蔽。该转换器将忽略任何未标记为机密的日志行,这有助于更快地抽出这些行,因为无需在这些行上执行任何模式匹配。

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

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

对于上面提到的自定义转换器,这可能会产生类似于Received basic auth header: Basic QlRXXXlQ=的日志行,使前两个字符保持不变,但用XXX混淆中间的位。

答案 5 :(得分:0)

我已经使用了来自库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>