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

Мне нужно иметь возможность искать событие для любого из нескольких шаблонов и заменять текст в шаблоне замаскированным значением. Это функция нашего приложения, предназначенная для предотвращения попадания конфиденциальной информации в журналы. Поскольку информация может быть из большого количества источников, нецелесообразно применять фильтры на всех входах. Кроме того, есть использование forString() за пределами ведения журнала, и я не хочу 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>

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

Ответ 1

Вам нужно обернуть макет, используя 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>


UPDATE

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

Открытый класс 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=*******

Ответ 2

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

replace(p){r, t}    

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

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

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

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

Ответ 3

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

Ответ 4

Очень похожий, но немного другой подход развивается вокруг настройки CompositeConverter и определения <conversionRule...> convertRule <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("//(.*?):.*[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.

Ответ 5

Я использовал цензор на основе 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>

где я положил список регулярных выражений замен.

@[email protected]    
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>