Маска конфиденциальных данных в журналах с logback

Мне нужно иметь возможность искать событие по любому из нескольких шаблонов и заменять текст в шаблоне маскированным значением. Это функция в нашем приложении, предназначенная для предотвращения попадания конфиденциальной информации в журналы. Поскольку информация может быть из самых разных источников, нецелесообразно применять фильтры ко всем входам. Кроме того, есть использование toString() помимо ведения журнала, и я не хочу, чтобы toString() равномерно маскировал все вызовы (только ведение журнала).

Я пытался использовать метод%replace в logback.xml:

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

Я пробовал много других вставок, поэтому мне было интересно, действительно ли кто-то достиг того, что я пытаюсь сделать, и мог ли бы он дать какие-либо подсказки или решение.

7 ответов

Решение

Вам нужно обернуть макет используя LayoutWrappingEncoder, И также я считаю, что вы не можете использовать здесь пружину, поскольку логбэк не управляется весной.

Вот обновленный класс.

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>


ОБНОВИТЬ

Здесь лучше подходить, устанавливать Pattern во время самого init. так что мы можем избежать повторного создания Pattern снова и снова, и эта реализация близка к реалистичному сценарию использования.

открытый класс 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(%msg){'regex1', 'replacement1'}  ){'regex2', 'replacement2'}

Работал отлично, хотя мне интересно, если я немного подталкиваю и p действительно может быть сколь угодно сложным.

Вот мой подход, может, это кому-нибудь поможет

Попробуй это. 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

Очень похожий, но немного другой подход развивается вокруг настройки CompositeConverter и определения <conversionRule ...> в logback, который ссылается на пользовательский конвертер.

В одном из моих технических демонстрационных проектов я определил класс MaskingConverter, который определяет серию шаблонов, с которыми анализируется событие регистрации и обновляется совпадение, которое используется в моей конфигурации обратного входа.

Поскольку ответы только на ссылки здесь не так популярны, я опубликую здесь важные части кода и объясню, что он делает и почему он так настроен. Начиная с пользовательского класса конвертера на основе 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, с которыми должна сравниваться соответствующая строка журнала, и при совпадении приводить к обновлению события путем маскировки паролей.

Обратите внимание, что в этом примере кода предполагается, что строка журнала содержит только один тип пароля. Вы, конечно, можете свободно адаптировать bahvior к вашим потребностям на тот случай, если вы захотите проверить каждую строку на предмет совпадения нескольких паттернов.

Чтобы применить этот конвертер, нужно просто добавить следующую строку в конфигурацию 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,

У меня есть цензоры в https://github.com/tersesystems/terse-logback которые позволяют вам определять цензор в одном месте, а затем ссылаться на него в нескольких приложениях.

Я пытался скрыть некоторые конфиденциальные данные в журналах моего демонстрационного проекта. Я пробовал, но у меня это не сработало из-за Java Reflections, поскольку я взял имя переменной в качестве шаблона. Я добавляю решение, которое сработало для меня, если оно поможет кому-то еще.

Я добавил ниже код в файл 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();
    }
}

Надеюсь это поможет!!!

Я использовал цензор на основе RegexCensor из библиотеки https://github.com/tersesystems/terse-logback. В 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>

Другие вопросы по тегам