it-swarm.com.ru

Маска конфиденциальных данных в журналах с 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>

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

12
Michael Coxon

Вам нужно обернуть макет с помощью 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=*******
11
Karthik Prasad

Из документации:

replace(p){r, t}    

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

Столкнувшись с той же проблемой, связанной с заменой двух шаблонов в сообщении, я просто попытался chain, так что p в моем случае это просто вызов замены:

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

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

4
Dmitri

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

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

Поскольку ответы только на ссылки здесь не так популярны, SO я опубликую здесь важные части кода и объясню, что он делает и почему он настроен таким образом. Начиная с пользовательского класса конвертера на основе 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("//(.*?):.*[email protected]");
    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:[email protected]: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:[email protected]");
      }
    }
    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.

0
Roman Vottner