web-dev-qa-db-ja.com

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>

同時に、できません。 1つの%replaceで2つのパターンをマスクする方法はありません。

バグ間で大まかに議論されたもう1つの方法は、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>

私は他の多くの挿入を試みたので、誰かが私が試みていることを実際に達成したかどうか、そしてそれらが何らかの手掛かりまたは解決策を提供できるかどうか疑問に思っていました。

14
Michael Coxon

LayoutWrappingEncoderを使用してレイアウトをラップする必要があります。また、logbackはSpringによって管理されていないため、ここでは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>

更新

ここで、そのより良いアプローチは、初期化中にパターンを設定することです。パターンの再作成を何度も回避できるため、この実装は現実的なユースケースに近くなります。

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=*******
13
Karthik Prasad

ドキュメントから:

replace(p){r, t}    

パターンpは任意に複雑にすることができ、特に複数の変換キーワードを含めることができます。

メッセージの2つのパターンを置き換える必要がある同じ問題に直面して、私はchainを試してみただけなので、私の場合、pは単にreplaceの呼び出しです。

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

うまくいきましたが、少しプッシュしているのではないかと思いますが、pは実際に任意に複雑になる可能性があります。

5
Dmitri

https://github.com/tersesystems/terse-logback には検閲者がいて、1つの場所で検閲者を定義して、複数のアペンダーで参照することができます。

1
Will Sargent

ライブラリ 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>
0
MarekM

非常によく似ていますが、少し異なるアプローチは、 CompositeConverter のカスタマイズと、カスタムコンバーターを参照するログバック内の_<conversionRule ...>_の定義を中心に進化します。

私のtech-demoプロジェクトの1つで、ロギングイベントが分析される一連のパターンを定義する MaskingConverter クラスを定義し、マッチが更新されると logback configuration 内で使用されます=。

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("//(.*?):.*?@");
    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パターンを定義し、一致すると、パスワードをマスクすることによってイベントの更新につながります。

このコードサンプルでは、​​ログ行に含まれるパスワードは1種類のみであると想定しています。もちろん、複数のパターンマッチについて各ラインをプローブする場合は、必要に応じて動作を自由に調整できます。

このコンバーターを適用するには、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)は、元のログ行を入力として受け取り、その関数に渡された各行に対してパスワードマスキングを実行します。

1つまたは複数のパターンマッチについて各行をプローブするのはコストがかかる可能性があるため、Java上記のコードには Markers が含まれ、ログステートメントで使用して特定のメタ情報を送信できます。ログステートメント自体を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