killbill-memoizeit

server: best effort to avoid logging sensitive data Import

4/6/2015 12:57:48 PM

Details

diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ConfigMagicObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ConfigMagicObfuscator.java
new file mode 100644
index 0000000..6ecd7ce
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ConfigMagicObfuscator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.regex.Pattern;
+
+import com.google.common.collect.ImmutableList;
+
+// See ConfigurationObjectFactory
+public class ConfigMagicObfuscator extends Obfuscator {
+
+    private static final String[] DEFAULT_SENSITIVE_KEYS = {
+            "key",
+            "pass",
+            "password"
+    };
+
+    private final Collection<Pattern> patterns = new LinkedList<Pattern>();
+
+    public ConfigMagicObfuscator() {
+        this(ImmutableList.<Pattern>of());
+    }
+
+    public ConfigMagicObfuscator(final Collection<Pattern> extraPatterns) {
+        super();
+
+        for (final String sensitiveKey : DEFAULT_SENSITIVE_KEYS) {
+            this.patterns.add(buildPattern(sensitiveKey));
+        }
+        this.patterns.addAll(extraPatterns);
+    }
+
+    @Override
+    public String obfuscate(final String originalString) {
+        return obfuscate(originalString, patterns);
+    }
+
+    private Pattern buildPattern(final String key) {
+        return Pattern.compile("^Assigning value \\[([^\\]]*)\\] for \\[[^\\]]*" + key + "\\] on \\[[^\\]]*\\]$", DEFAULT_PATTERN_FLAGS);
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LuhnMaskingObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LuhnMaskingObfuscator.java
new file mode 100644
index 0000000..88bda32
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LuhnMaskingObfuscator.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * LuhnMaskingObfuscator replaces sequences of digits that pass the Luhn check
+ * with a masking string, leaving only the suffix containing the last four
+ * digits.
+ * <p/>
+ * Inspired from https://github.com/esamson/logback-luhn-mask (licensed under the Apache License, Version 2.0)
+ */
+public class LuhnMaskingObfuscator extends Obfuscator {
+
+    /**
+     * The minimum number of digits a credit card can have.
+     */
+    private static final int MIN_CC_DIGITS = 13;
+
+    public LuhnMaskingObfuscator() {
+        super();
+    }
+
+    @Override
+    public String obfuscate(final String originalString) {
+        return mask(originalString);
+    }
+
+    private String mask(final String formattedMessage) {
+        if (!hasEnoughDigits(formattedMessage)) {
+            return formattedMessage;
+        }
+
+        final int length = formattedMessage.length();
+        int unwrittenStart = 0;
+        int numberStart = -1;
+        int numberEnd;
+        int digitsSeen = 0;
+        final int[] last4pos = {-1, -1, -1, -1};
+        int pos;
+        char current;
+
+        final StringBuilder masked = new StringBuilder(formattedMessage.length());
+
+        for (pos = 0; pos < length; pos++) {
+            current = formattedMessage.charAt(pos);
+            if (isDigit(current)) {
+                digitsSeen++;
+
+                if (numberStart == -1) {
+                    numberStart = pos;
+                }
+
+                last4pos[0] = last4pos[1];
+                last4pos[1] = last4pos[2];
+                last4pos[2] = last4pos[3];
+                last4pos[3] = pos;
+            } else if (digitsSeen > 0 && current != ' ' && current != '-') {
+                numberEnd = last4pos[3] + 1;
+                if ((digitsSeen >= MIN_CC_DIGITS)
+                    && luhnCheck(stripSeparators(formattedMessage.substring(numberStart, numberEnd)))) {
+                    masked.append(formattedMessage, unwrittenStart, numberStart);
+                    masked.append(obfuscateConfidentialData(formattedMessage.substring(numberStart, numberEnd),
+                                                            formattedMessage.substring(last4pos[0], numberEnd)));
+                    masked.append(formattedMessage, last4pos[0], numberEnd);
+                    unwrittenStart = numberEnd;
+                }
+                numberStart = -1;
+                digitsSeen = 0;
+            }
+        }
+
+        if (numberStart != -1 && (digitsSeen >= MIN_CC_DIGITS)
+            && luhnCheck(stripSeparators(formattedMessage.substring(numberStart, pos)))) {
+            masked.append(formattedMessage, unwrittenStart, numberStart);
+            masked.append(obfuscateConfidentialData(formattedMessage.substring(numberStart, pos),
+                                                    formattedMessage.substring(last4pos[0], pos)));
+            masked.append(formattedMessage, last4pos[0], pos);
+        } else {
+            masked.append(formattedMessage, unwrittenStart, pos);
+        }
+
+        return masked.toString();
+    }
+
+    private boolean hasEnoughDigits(final CharSequence formattedMessage) {
+        int digits = 0;
+        final int length = formattedMessage.length();
+        char current;
+
+        for (int i = 0; i < length; i++) {
+            current = formattedMessage.charAt(i);
+            if (isDigit(current)) {
+                if (++digits == MIN_CC_DIGITS) {
+                    return true;
+                }
+            } else if (digits > 0 && current != ' ' && current != '-') {
+                digits = 0;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Implementation of the [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm)
+     * to check if the given string is possibly a credit card number.
+     *
+     * @param cardNumber the number to check. It must only contain numeric characters
+     * @return `true` if the given string is a possible credit card number
+     */
+    @VisibleForTesting
+    boolean luhnCheck(final String cardNumber) {
+        int sum = 0;
+        int digit, addend;
+        boolean doubled = false;
+        for (int i = cardNumber.length() - 1; i >= 0; i--) {
+            digit = Integer.parseInt(cardNumber.substring(i, i + 1));
+            if (doubled) {
+                addend = digit * 2;
+                if (addend > 9) {
+                    addend -= 9;
+                }
+            } else {
+                addend = digit;
+            }
+            sum += addend;
+            doubled = !doubled;
+        }
+        return (sum % 10) == 0;
+    }
+
+    /**
+     * Remove any ` ` and `-` characters from the given string.
+     *
+     * @param cardNumber the number to clean up
+     * @return if the given string contains no ` ` or `-` characters, the string
+     * itself is returned, otherwise a new string containing no ` ` or `-`
+     * characters is returned
+     */
+    @VisibleForTesting
+    String stripSeparators(final String cardNumber) {
+        final int length = cardNumber.length();
+        final char[] result = new char[length];
+        int count = 0;
+        char cur;
+        for (int i = 0; i < length; i++) {
+            cur = cardNumber.charAt(i);
+            if (!(cur == ' ' || cur == '-')) {
+                result[count++] = cur;
+            }
+        }
+        if (count == length) {
+            return cardNumber;
+        }
+        return new String(result, 0, count);
+    }
+
+    private static boolean isDigit(final char c) {
+        switch (c) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/Obfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/Obfuscator.java
new file mode 100644
index 0000000..68675e7
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/Obfuscator.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public abstract class Obfuscator {
+
+    protected static final int DEFAULT_PATTERN_FLAGS = Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL;
+
+    protected static final String MASK_LABEL = "MASKED";
+    protected static final int MASK_LABEL_LENGTH = MASK_LABEL.length();
+    protected static final char PAD_CHAR = '*';
+    protected static final int MASK_LOOKUPS_SIZE = 20;
+    protected final String[] MASK_LOOKUPS = new String[MASK_LOOKUPS_SIZE];
+
+    public Obfuscator() {
+        for (int i = 0; i < MASK_LOOKUPS.length; i++) {
+            MASK_LOOKUPS[i] = buildMask(i);
+        }
+    }
+
+    public abstract String obfuscate(final String originalString);
+
+    protected String obfuscate(final String originalString, final Iterable<Pattern> patterns) {
+        final StringBuilder obfuscatedStringBuilder = new StringBuilder(originalString);
+
+        for (final Pattern pattern : patterns) {
+            int currentOffset = 0;
+            // Create a matcher with a copy of the current obfuscated String
+            Matcher matcher = pattern.matcher(obfuscatedStringBuilder.toString());
+            while (matcher.find()) {
+                for (int groupNb = 1; groupNb <= matcher.groupCount(); groupNb++) {
+                    final String confidentialData = matcher.group(groupNb);
+                    final String obfuscatedConfidentialData = obfuscateConfidentialData(confidentialData);
+                    obfuscatedStringBuilder.replace(currentOffset + matcher.start(groupNb), currentOffset + matcher.end(groupNb), obfuscatedConfidentialData);
+
+                    // The original String is modified in place, which will confuse the Matcher if it becomes bigger
+                    if (obfuscatedConfidentialData.length() > confidentialData.length()) {
+                        currentOffset += obfuscatedConfidentialData.length() - confidentialData.length();
+                    }
+                }
+            }
+        }
+
+        return obfuscatedStringBuilder.toString();
+    }
+
+    private String obfuscateConfidentialData(final CharSequence confidentialSequence) {
+        return obfuscateConfidentialData(confidentialSequence, null);
+    }
+
+    /**
+     * Get a mask string for masking the given `confidentialSequence`.
+     *
+     * @param confidentialSequence the string to be obfuscated
+     * @param unmasked             the section of `confidentialSequence` to be left unmasked
+     * @return a mask string
+     */
+    @VisibleForTesting
+    String obfuscateConfidentialData(final CharSequence confidentialSequence, @Nullable final CharSequence unmasked) {
+        final int maskedLength = unmasked == null ? confidentialSequence.length() : confidentialSequence.length() - unmasked.length();
+        if (maskedLength < MASK_LOOKUPS_SIZE) {
+            return MASK_LOOKUPS[maskedLength];
+        } else {
+            return buildMask(maskedLength);
+        }
+    }
+
+    /**
+     * Create a masking string with the given length.
+     *
+     * @param maskedLength obfuscated String length
+     * @return a mask string
+     */
+    private String buildMask(final int maskedLength) {
+        final int pads = maskedLength - MASK_LABEL_LENGTH;
+        final StringBuilder mask = new StringBuilder(maskedLength);
+        if (pads <= 0) {
+            mask.append(MASK_LABEL);
+        } else {
+            for (int i = 0; i < pads / 2; i++) {
+                mask.append(PAD_CHAR);
+            }
+            mask.append(MASK_LABEL);
+            while (mask.length() < maskedLength) {
+                mask.append(PAD_CHAR);
+            }
+        }
+        return mask.toString();
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
new file mode 100644
index 0000000..2b5f9e5
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.Collection;
+
+import ch.qos.logback.classic.pattern.ClassicConverter;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * ObfuscatorConverter attempts to mask sensitive data in the log files.
+ * <p/>
+ * To use, define a new conversion word in your Logback configuration, e.g.:
+ * <pre>
+ *     <configuration>
+ *         <conversionRule conversionWord="maskedMsg"
+ *             converterClass="org.killbill.billing.server.log.obfuscators.ObfuscatorConverter" />
+ *         <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ *             <encoder>
+ *                 <pattern>%date [%thread] - %maskedMsg%n</pattern>
+ *             </encoder>
+ *         </appender>
+ *         <root level="DEBUG">
+ *             <appender-ref ref="STDOUT" />
+ *         </root>
+ *     </configuration>
+ * </pre>
+ */
+public class ObfuscatorConverter extends ClassicConverter {
+
+    private final Collection<Obfuscator> obfuscators = ImmutableList.<Obfuscator>of(new ConfigMagicObfuscator(),
+                                                                                    new PatternObfuscator(),
+                                                                                    new LuhnMaskingObfuscator());
+
+    @Override
+    public String convert(final ILoggingEvent event) {
+        String convertedMessage = event.getFormattedMessage();
+        for (final Obfuscator obfuscator : obfuscators) {
+            try {
+                convertedMessage = obfuscator.obfuscate(convertedMessage);
+            } catch (final RuntimeException e) {
+                // Ignore? Not sure the impact of importing a logger here
+            }
+        }
+        return convertedMessage;
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
new file mode 100644
index 0000000..f9a38a5
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.regex.Pattern;
+
+import com.google.common.collect.ImmutableList;
+
+public class PatternObfuscator extends Obfuscator {
+
+    // Hide by default sensitive bank, PCI and PII data. For PANs, see LuhnMaskingObfuscator
+    private static final String[] DEFAULT_SENSITIVE_KEYS = {
+            "accountnumber",
+            "authenticationdata",
+            "bankaccountnumber",
+            "banknumber",
+            "bic",
+            "cardvalidationnum",
+            "cavv",
+            "ccvv",
+            "cvNumber",
+            "cvc",
+            "email",
+            "iban",
+            "name",
+            "number",
+            "password",
+            "xid"
+    };
+
+    private final Collection<Pattern> patterns = new LinkedList<Pattern>();
+
+    public PatternObfuscator() {
+        this(ImmutableList.<Pattern>of());
+    }
+
+    public PatternObfuscator(final Collection<Pattern> extraPatterns) {
+        super();
+
+        for (final String sensitiveKey : DEFAULT_SENSITIVE_KEYS) {
+            this.patterns.add(buildJSONPattern(sensitiveKey));
+            this.patterns.add(buildXMLPattern(sensitiveKey));
+            this.patterns.add(buildMultiValuesXMLPattern(sensitiveKey));
+        }
+        this.patterns.addAll(extraPatterns);
+    }
+
+    @Override
+    public String obfuscate(final String originalString) {
+        return obfuscate(originalString, patterns);
+    }
+
+    private Pattern buildJSONPattern(final String key) {
+        return Pattern.compile(key + "\":\\s*([^,{]+)", DEFAULT_PATTERN_FLAGS);
+    }
+
+    private Pattern buildXMLPattern(final String key) {
+        return Pattern.compile(key + ">([^<\\n]+)", DEFAULT_PATTERN_FLAGS);
+    }
+
+    private Pattern buildMultiValuesXMLPattern(final String key) {
+        return Pattern.compile(key + "</key>\\s*<value[^>]*>([^<\\n]+)</value>", DEFAULT_PATTERN_FLAGS);
+    }
+}
diff --git a/profiles/killbill/src/main/resources/logback.xml b/profiles/killbill/src/main/resources/logback.xml
index fc17204..3e86876 100644
--- a/profiles/killbill/src/main/resources/logback.xml
+++ b/profiles/killbill/src/main/resources/logback.xml
@@ -17,9 +17,11 @@
   -->
 
 <configuration>
+    <conversionRule conversionWord="maskedMsg" converterClass="org.killbill.billing.server.log.obfuscators.ObfuscatorConverter" />
+
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%date [%thread] %-5level %logger{36} - %msg%n%ex</pattern>
+            <pattern>%date [%thread] %-5level %logger{36} - %maskedMsg%n%ex</pattern>
         </encoder>
     </appender>
 
@@ -40,7 +42,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -61,7 +63,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -82,7 +84,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -103,7 +105,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -124,7 +126,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
@@ -145,7 +147,7 @@
                     </timeBasedFileNamingAndTriggeringPolicy>
                 </rollingPolicy>
                 <encoder>
-                    <pattern>%date [%thread] %msg%n%ex</pattern>
+                    <pattern>%date [%thread] %maskedMsg%n%ex</pattern>
                 </encoder>
             </appender>
         </sift>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestConfigMagicObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestConfigMagicObfuscator.java
new file mode 100644
index 0000000..382783a
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestConfigMagicObfuscator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestConfigMagicObfuscator extends ServerTestSuiteNoDB {
+
+    private final ConfigMagicObfuscator obfuscator = new ConfigMagicObfuscator();
+
+    @Test(groups = "fast")
+    public void testKey() throws Exception {
+        verify("Assigning value [pass2b78b7cef] for [org.killbill.billing.plugin.avatax.licenseKey] on [org.killbill.billing.plugins.avatax#getLicenseKey()]",
+               "Assigning value [***MASKED****] for [org.killbill.billing.plugin.avatax.licenseKey] on [org.killbill.billing.plugins.avatax#getLicenseKey()]");
+
+        verify("Assigning value [pass2b78b7cef] for [org.killbill.billing.plugin.avatax.apiKey] on [org.killbill.billing.plugins.avatax#getApiKey()]",
+               "Assigning value [***MASKED****] for [org.killbill.billing.plugin.avatax.apiKey] on [org.killbill.billing.plugins.avatax#getApiKey()]");
+    }
+
+    @Test(groups = "fast")
+    public void testPassword() throws Exception {
+        verify("Assigning value [pass2b78b7ce] for [org.killbill.dao.pass] on [org.killbill.commons.jdbi.guice.DaoConfig#getPass()]",
+               "Assigning value [***MASKED***] for [org.killbill.dao.pass] on [org.killbill.commons.jdbi.guice.DaoConfig#getPass()]");
+
+        verify("Assigning value [pass2b78b7ce] for [org.killbill.dao.password] on [org.killbill.commons.jdbi.guice.DaoConfig#getPassword()]",
+               "Assigning value [***MASKED***] for [org.killbill.dao.password] on [org.killbill.commons.jdbi.guice.DaoConfig#getPassword()]");
+    }
+
+    private void verify(final String input, final String output) {
+        final String obfuscated = obfuscator.obfuscate(input);
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLuhnMaskingObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLuhnMaskingObfuscator.java
new file mode 100644
index 0000000..5795287
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLuhnMaskingObfuscator.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestLuhnMaskingObfuscator extends ServerTestSuiteNoDB {
+
+    private final LuhnMaskingObfuscator obfuscator = new LuhnMaskingObfuscator();
+
+    @Test(groups = "fast")
+    public void testStripSeparatorsSpaces() {
+        Assert.assertEquals(obfuscator.stripSeparators("5137 0049 8639 6403"), "5137004986396403");
+    }
+
+    @Test(groups = "fast")
+    public void testStripSeparatorsDashes() {
+        Assert.assertEquals(obfuscator.stripSeparators("5137-0049-8639-6403"), "5137004986396403");
+    }
+
+    @Test(groups = "fast")
+    public void testLuhnGood() {
+        Assert.assertTrue(obfuscator.luhnCheck("5137004986396403"));
+    }
+
+    @Test(groups = "fast")
+    public void testLuhnBad() {
+        Assert.assertFalse(obfuscator.luhnCheck("5137004986396404"));
+    }
+
+    @Test(groups = "fast")
+    public void testConvert() {
+        verify("try 5137 0049 8639 6404 and 5137 0049 8639 6403", "try 5137 0049 8639 6404 and ****MASKED*****6403");
+    }
+
+    @Test(groups = "fast")
+    public void testConvertCcNumberAtStartNonCcNumberAtEnd() {
+        verify("5137 0049 8639 6403 and 5137 0049 8639 6404", "****MASKED*****6403 and 5137 0049 8639 6404");
+    }
+
+    @Test(groups = "fast")
+    public void testConvertMultiple() {
+        verify("try 5137 0049 8639 6403 multiple 5137 0049 8639 6404 possible 4111-1111-1111 1111 card 4111111111111112 numbers", "try ****MASKED*****6403 multiple 5137 0049 8639 6404 possible ****MASKED*****1111 card 4111111111111112 numbers");
+    }
+
+    @Test(groups = "fast")
+    public void testLotsOfCcNumbers() {
+        verify("American Express"
+               + "378282246310005"
+               + "American Express"
+               + "371449635398431"
+               + "American Express Corporate"
+               + "378734493671000"
+               + "Australian BankCard"
+               + "5610591081018250"
+               + "Diners Club"
+               + "30569309025904"
+               + "Diners Club"
+               + "38520000023237"
+               + "Discover"
+               + "6011111111111117"
+               + "Discover"
+               + "6011000990139424"
+               + "JCB"
+               + "3530111333300000"
+               + "JCB"
+               + "3566002020360505"
+               + "MasterCard"
+               + "5555555555554444"
+               + "MasterCard"
+               + "5105105105105100"
+               + "Visa"
+               + "4111111111111111"
+               + "Visa"
+               + "4012888888881881"
+               + "Visa"
+               + "4222222222222"
+               + "Note : Even though this number has a different character count than the other test numbers, it is the correct and functional number."
+               + "Processor-specific Cards"
+               + "Dankort (PBS)"
+               + "5019717010103742"
+               + "Switch/Solo (Paymentech)"
+               + "6331101999990016",
+               "American Express"
+               + "**MASKED***0005"
+               + "American Express"
+               + "**MASKED***8431"
+               + "American Express Corporate"
+               + "**MASKED***1000"
+               + "Australian BankCard"
+               + "***MASKED***8250"
+               + "Diners Club"
+               + "**MASKED**5904"
+               + "Diners Club"
+               + "**MASKED**3237"
+               + "Discover"
+               + "***MASKED***1117"
+               + "Discover"
+               + "***MASKED***9424"
+               + "JCB"
+               + "***MASKED***0000"
+               + "JCB"
+               + "***MASKED***0505"
+               + "MasterCard"
+               + "***MASKED***4444"
+               + "MasterCard"
+               + "***MASKED***5100"
+               + "Visa"
+               + "***MASKED***1111"
+               + "Visa"
+               + "***MASKED***1881"
+               + "Visa"
+               + "*MASKED**2222"
+               + "Note : Even though this number has a different character count than the other test numbers, it is the correct and functional number."
+               + "Processor-specific Cards"
+               + "Dankort (PBS)"
+               + "***MASKED***3742"
+               + "Switch/Solo (Paymentech)"
+               + "***MASKED***0016");
+    }
+
+    @Test(groups = "fast")
+    public void testLotsOfNonCcNumbers() {
+        verify("American Express"
+               + "378282246310006"
+               + "American Express"
+               + "371449635398432"
+               + "American Express Corporate"
+               + "378734493671001"
+               + "Australian BankCard"
+               + "5610591081018251"
+               + "Diners Club"
+               + "30569309025905"
+               + "Diners Club"
+               + "38520000023238"
+               + "Discover"
+               + "6011111111111118"
+               + "Discover"
+               + "6011000990139425"
+               + "JCB"
+               + "3530111333300001"
+               + "JCB"
+               + "3566002020360506"
+               + "MasterCard"
+               + "5555555555554445"
+               + "MasterCard"
+               + "5105105105105102"
+               + "Visa"
+               + "4111111111111112"
+               + "Visa"
+               + "4012888888881882"
+               + "Visa"
+               + "4222222222223"
+               + "Note : Even though this number has a different character count than the other test numbers, it is the correct and functional number."
+               + "Processor-specific Cards"
+               + "Dankort (PBS)"
+               + "5019717010103743"
+               + "Switch/Solo (Paymentech)"
+               + "6331101999990017",
+               "American Express"
+               + "378282246310006"
+               + "American Express"
+               + "371449635398432"
+               + "American Express Corporate"
+               + "378734493671001"
+               + "Australian BankCard"
+               + "5610591081018251"
+               + "Diners Club"
+               + "30569309025905"
+               + "Diners Club"
+               + "38520000023238"
+               + "Discover"
+               + "6011111111111118"
+               + "Discover"
+               + "6011000990139425"
+               + "JCB"
+               + "3530111333300001"
+               + "JCB"
+               + "3566002020360506"
+               + "MasterCard"
+               + "5555555555554445"
+               + "MasterCard"
+               + "5105105105105102"
+               + "Visa"
+               + "4111111111111112"
+               + "Visa"
+               + "4012888888881882"
+               + "Visa"
+               + "4222222222223"
+               + "Note : Even though this number has a different character count than the other test numbers, it is the correct and functional number."
+               + "Processor-specific Cards"
+               + "Dankort (PBS)"
+               + "5019717010103743"
+               + "Switch/Solo (Paymentech)"
+               + "6331101999990017");
+    }
+
+    private void verify(final String input, final String output) {
+        final String obfuscated = obfuscator.obfuscate(input);
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscator.java
new file mode 100644
index 0000000..59bd626
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.regex.Pattern;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestObfuscator extends ServerTestSuiteNoDB {
+
+    private final Obfuscator obfuscator = new Obfuscator() {
+        @Override
+        public String obfuscate(final String originalString) {
+            return null;
+        }
+    };
+
+    @Test(groups = "fast")
+    public void testObfuscateWithOnePattern() throws Exception {
+        final Pattern pattern = Pattern.compile("number=([^;]+)");
+        final ImmutableList<Pattern> patterns = ImmutableList.<Pattern>of(pattern);
+        Assert.assertEquals(obfuscator.obfuscate("number=1234;number=12345;number=123456;number=1234567;number=12345678;number=123456789", patterns),
+                            "number=MASKED;number=MASKED;number=MASKED;number=MASKED*;number=*MASKED*;number=*MASKED**");
+
+    }
+
+    @Test(groups = "fast")
+    public void testObfuscateWithMultiplePatterns() throws Exception {
+        final Pattern pattern1 = Pattern.compile("number=([^;]+)");
+        final Pattern pattern2 = Pattern.compile("numberB=([^;]+)");
+        final ImmutableList<Pattern> patterns = ImmutableList.<Pattern>of(pattern1, pattern2);
+        Assert.assertEquals(obfuscator.obfuscate("number=1234;numberB=12345;number=123456;numberB=1234567;number=12345678;numberB=123456789", patterns),
+                            "number=MASKED;numberB=MASKED;number=MASKED;numberB=MASKED*;number=*MASKED*;numberB=*MASKED**");
+
+    }
+
+    @Test(groups = "fast")
+    public void testObfuscateConfidentialData() {
+        Assert.assertEquals(obfuscator.obfuscateConfidentialData("5137004986396403", "6403"), "***MASKED***");
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java
new file mode 100644
index 0000000..943a620
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestObfuscatorConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+
+public class TestObfuscatorConverter extends ServerTestSuiteNoDB {
+
+    private final ObfuscatorConverter converter = new ObfuscatorConverter();
+
+    @Test(groups = "fast")
+    public void testLogNonSensitiveData() throws Exception {
+        verify("Starting purchase call: \n" +
+               "<gateway>\n" +
+               "<card>tokenized</card>\n" +
+               "<bankAccountNumber></bankAccountNumber>\n" +
+               "<password></password>\n" +
+               "</gateway>",
+               "Starting purchase call: \n" +
+               "<gateway>\n" +
+               "<card>tokenized</card>\n" +
+               "<bankAccountNumber></bankAccountNumber>\n" +
+               "<password></password>\n" +
+               "</gateway>");
+    }
+
+    @Test(groups = "fast")
+    public void testLogSensitiveData() throws Exception {
+        verify("Starting purchase call: \n" +
+               "<gateway>\n" +
+               "<card>4111111111111111</card>\n" +
+               "<bankAccountNumber>482391823</bankAccountNumber>\n" +
+               "<password>supersecret</password>\n" +
+               "</gateway>",
+               "Starting purchase call: \n" +
+               "<gateway>\n" +
+               "<card>***MASKED***1111</card>\n" +
+               "<bankAccountNumber>*MASKED**</bankAccountNumber>\n" +
+               "<password>**MASKED***</password>\n" +
+               "</gateway>");
+    }
+
+    private void verify(final String input, final String output) {
+        final ILoggingEvent event = Mockito.mock(ILoggingEvent.class);
+        Mockito.when(event.getFormattedMessage()).thenReturn(input);
+
+        final String obfuscated = converter.convert(event);
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
new file mode 100644
index 0000000..6147466
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestPatternObfuscator extends ServerTestSuiteNoDB {
+
+    private final PatternObfuscator obfuscator = new PatternObfuscator();
+
+    @Test(groups = "fast")
+    public void testAdyen() throws Exception {
+        verify("<ns:expiryMonth>04</expiryMonth>\n" +
+               "<ns:expiryYear>2015</expiryYear>\n" +
+               "<ns:holderName>  test  </holderName>\n" +
+               "<ns:number>5123456789012346</number>\n" +
+               "<ns2:shopperEmail>Bob@example.org</ns2:shopperEmail>\n" +
+               "<ns2:shopperIP>127.0.0.1</ns2:shopperIP>\n" +
+               "<ns2:shopperInteraction xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "<ns2:shopperName>\n" +
+               "    <firstName>Bob</firstName>\n" +
+               "    <gender xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "    <infix xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "    <lastName>Smith</lastName>\n" +
+               "</ns2:shopperName>\n",
+               "<ns:expiryMonth>04</expiryMonth>\n" +
+               "<ns:expiryYear>2015</expiryYear>\n" +
+               "<ns:holderName>*MASKED*</holderName>\n" +
+               "<ns:number>*****MASKED*****</number>\n" +
+               "<ns2:shopperEmail>****MASKED*****</ns2:shopperEmail>\n" +
+               "<ns2:shopperIP>127.0.0.1</ns2:shopperIP>\n" +
+               "<ns2:shopperInteraction xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "<ns2:shopperName>\n" +
+               "    <firstName>MASKED</firstName>\n" +
+               "    <gender xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "    <infix xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>\n" +
+               "    <lastName>MASKED</lastName>\n" +
+               "</ns2:shopperName>\n");
+    }
+
+    @Test(groups = "fast")
+    public void testCyberSource() throws Exception {
+        verify("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+               "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
+               "  <s:Header>\n" +
+               "    <wsse:Security s:mustUnderstand=\"1\" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n" +
+               "      <wsse:UsernameToken>\n" +
+               "        <wsse:Username/>\n" +
+               "        <wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText\"/>\n" +
+               "      </wsse:UsernameToken>\n" +
+               "    </wsse:Security>\n" +
+               "  </s:Header>\n" +
+               "  <s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n" +
+               "    <requestMessage xmlns=\"urn:schemas-cybersource-com:transaction-data-1.109\">\n" +
+               "      <merchantID/>\n" +
+               "      <merchantReferenceCode>e92a3bfd-0713-4396-a1e2-ff46cb051f8c</merchantReferenceCode>\n" +
+               "      <clientLibrary>Ruby Active Merchant</clientLibrary>\n" +
+               "      <clientLibraryVersion>1.47.0</clientLibraryVersion>\n" +
+               "      <clientEnvironment>java</clientEnvironment>\n" +
+               "<billTo>\n" +
+               "  <firstName>John</firstName>\n" +
+               "  <lastName>Doe</lastName>\n" +
+               "  <street1>5, oakriu road</street1>\n" +
+               "  <street2>apt. 298</street2>\n" +
+               "  <city>Gdio Foia</city>\n" +
+               "  <state>FL</state>\n" +
+               "  <postalCode>49302</postalCode>\n" +
+               "  <country>US</country>\n" +
+               "  <email>1428324461-test@tester.com</email>\n" +
+               "</billTo>\n" +
+               "<purchaseTotals>\n" +
+               "  <currency>USD</currency>\n" +
+               "  <grandTotalAmount>0.00</grandTotalAmount>\n" +
+               "</purchaseTotals>\n" +
+               "<card>\n" +
+               "  <accountNumber>4242424242424242</accountNumber>\n" +
+               "  <expirationMonth>12</expirationMonth>\n" +
+               "  <expirationYear>2017</expirationYear>\n" +
+               "  <cvNumber>1234</cvNumber>\n" +
+               "  <cardType>001</cardType>\n" +
+               "</card>\n" +
+               "<subscription>\n" +
+               "  <paymentMethod>credit card</paymentMethod>\n" +
+               "</subscription>\n" +
+               "<recurringSubscriptionInfo>\n" +
+               "  <amount>0.00</amount>\n" +
+               "  <frequency>on-demand</frequency>\n" +
+               "  <approvalRequired>false</approvalRequired>\n" +
+               "</recurringSubscriptionInfo>\n" +
+               "<paySubscriptionCreateService run=\"true\"/>\n" +
+               "<businessRules>\n" +
+               "</businessRules>\n" +
+               "    </requestMessage>\n" +
+               "  </s:Body>\n" +
+               "</s:Envelope>",
+               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+               "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
+               "  <s:Header>\n" +
+               "    <wsse:Security s:mustUnderstand=\"1\" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n" +
+               "      <wsse:UsernameToken>\n" +
+               "        <wsse:Username/>\n" +
+               "        <wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText\"/>\n" +
+               "      </wsse:UsernameToken>\n" +
+               "    </wsse:Security>\n" +
+               "  </s:Header>\n" +
+               "  <s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n" +
+               "    <requestMessage xmlns=\"urn:schemas-cybersource-com:transaction-data-1.109\">\n" +
+               "      <merchantID/>\n" +
+               "      <merchantReferenceCode>e92a3bfd-0713-4396-a1e2-ff46cb051f8c</merchantReferenceCode>\n" +
+               "      <clientLibrary>Ruby Active Merchant</clientLibrary>\n" +
+               "      <clientLibraryVersion>1.47.0</clientLibraryVersion>\n" +
+               "      <clientEnvironment>java</clientEnvironment>\n" +
+               "<billTo>\n" +
+               "  <firstName>MASKED</firstName>\n" +
+               "  <lastName>MASKED</lastName>\n" +
+               "  <street1>5, oakriu road</street1>\n" +
+               "  <street2>apt. 298</street2>\n" +
+               "  <city>Gdio Foia</city>\n" +
+               "  <state>FL</state>\n" +
+               "  <postalCode>49302</postalCode>\n" +
+               "  <country>US</country>\n" +
+               "  <email>**********MASKED**********</email>\n" +
+               "</billTo>\n" +
+               "<purchaseTotals>\n" +
+               "  <currency>USD</currency>\n" +
+               "  <grandTotalAmount>0.00</grandTotalAmount>\n" +
+               "</purchaseTotals>\n" +
+               "<card>\n" +
+               "  <accountNumber>*****MASKED*****</accountNumber>\n" +
+               "  <expirationMonth>12</expirationMonth>\n" +
+               "  <expirationYear>2017</expirationYear>\n" +
+               "  <cvNumber>MASKED</cvNumber>\n" +
+               "  <cardType>001</cardType>\n" +
+               "</card>\n" +
+               "<subscription>\n" +
+               "  <paymentMethod>credit card</paymentMethod>\n" +
+               "</subscription>\n" +
+               "<recurringSubscriptionInfo>\n" +
+               "  <amount>0.00</amount>\n" +
+               "  <frequency>on-demand</frequency>\n" +
+               "  <approvalRequired>false</approvalRequired>\n" +
+               "</recurringSubscriptionInfo>\n" +
+               "<paySubscriptionCreateService run=\"true\"/>\n" +
+               "<businessRules>\n" +
+               "</businessRules>\n" +
+               "    </requestMessage>\n" +
+               "  </s:Body>\n" +
+               "</s:Envelope>");
+    }
+
+    @Test(groups = "fast")
+    public void testLitle() throws Exception {
+        verify("<litleOnlineRequest merchantId=\\\"merchant_id\\\" version=\\\"8.18\\\" xmlns=\\\"http://www.litle.com/schema\\\"><authentication><user>login</user><password>password</password></authentication><sale id=\\\"615b9cb3-8580-4f57-bf69-9\\\" reportGroup=\\\"Default Report Group\\\"><orderId>615b9cb3-8580-4f57-bf69-9</orderId><amount>10000</amount><orderSource>ecommerce</orderSource><billToAddress><name>John Doe</name><email>1428325948-test@tester.com</email><addressLine1>5, oakriu road</addressLine1><addressLine2>apt. 298</addressLine2><city>Gdio Foia</city><state>FL</state><zip>49302</zip><country>US</country></billToAddress><shipToAddress/><card><type>VI</type><number>4242424242424242</number><expDate>1217</expDate><cardValidationNum>1234</cardValidationNum></card></sale></litleOnlineRequest>",
+               "<litleOnlineRequest merchantId=\\\"merchant_id\\\" version=\\\"8.18\\\" xmlns=\\\"http://www.litle.com/schema\\\"><authentication><user>login</user><password>*MASKED*</password></authentication><sale id=\\\"615b9cb3-8580-4f57-bf69-9\\\" reportGroup=\\\"Default Report Group\\\"><orderId>615b9cb3-8580-4f57-bf69-9</orderId><amount>10000</amount><orderSource>ecommerce</orderSource><billToAddress><name>*MASKED*</name><email>**********MASKED**********</email><addressLine1>5, oakriu road</addressLine1><addressLine2>apt. 298</addressLine2><city>Gdio Foia</city><state>FL</state><zip>49302</zip><country>US</country></billToAddress><shipToAddress/><card><type>VI</type><number>*****MASKED*****</number><expDate>1217</expDate><cardValidationNum>MASKED</cardValidationNum></card></sale></litleOnlineRequest>");
+    }
+
+    @Test(groups = "fast")
+    public void testJSON() throws Exception {
+        verify("{\n" +
+               "  \"card\": {\n" +
+               "    \"id\": \"card_483etw4er9fg4vF3sQdrt3FG\",\n" +
+               "    \"object\": \"card\",\n" +
+               "    \"banknumber\": 4111111111111111,\n" +
+               "    \"last4\": \"0000\",\n" +
+               "    \"brand\": \"Visa\",\n" +
+               "    \"funding\": \"credit\",\n" +
+               "    \"exp_month\": 6,\n" +
+               "    \"exp_year\": 2019,\n" +
+               "    \"fingerprint\": \"HOh74kZU387WlUvy\",\n" +
+               "    \"country\": \"US\",\n" +
+               "    \"name\": \"Bob Smith\",\n" +
+               "    \"address_line1\": null,\n" +
+               "    \"address_line2\": null,\n" +
+               "    \"address_city\": null,\n" +
+               "    \"address_state\": null,\n" +
+               "    \"address_zip\": null,\n" +
+               "    \"address_country\": null,\n" +
+               "    \"dynamic_last4\": \"4242\",\n" +
+               "    \"customer\": null,\n" +
+               "    \"type\": \"Visa\"}\n" +
+               "}",
+               "{\n" +
+               "  \"card\": {\n" +
+               "    \"id\": \"card_483etw4er9fg4vF3sQdrt3FG\",\n" +
+               "    \"object\": \"card\",\n" +
+               "    \"banknumber\": *****MASKED*****,\n" +
+               "    \"last4\": \"0000\",\n" +
+               "    \"brand\": \"Visa\",\n" +
+               "    \"funding\": \"credit\",\n" +
+               "    \"exp_month\": 6,\n" +
+               "    \"exp_year\": 2019,\n" +
+               "    \"fingerprint\": \"HOh74kZU387WlUvy\",\n" +
+               "    \"country\": \"US\",\n" +
+               "    \"name\": **MASKED***,\n" +
+               "    \"address_line1\": null,\n" +
+               "    \"address_line2\": null,\n" +
+               "    \"address_city\": null,\n" +
+               "    \"address_state\": null,\n" +
+               "    \"address_zip\": null,\n" +
+               "    \"address_country\": null,\n" +
+               "    \"dynamic_last4\": \"4242\",\n" +
+               "    \"customer\": null,\n" +
+               "    \"type\": \"Visa\"}\n" +
+               "}");
+
+    }
+
+    @Test(groups = "fast")
+    public void testPayU() throws Exception {
+        verify("<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccvv</key>\n" +
+               "  <value xsi:type=\"xsd:string\">1234</value>\n" +
+               "</entry>\n" +
+               "<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccnum</key>\n" +
+               "  <value xsi:type=\"xsd:string\">4111111111111111</value>\n" +
+               "</entry>\n" +
+               "<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccexpmon</key>\n" +
+               "  <value xsi:type=\"xsd:string\">12</value>\n" +
+               "</entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccexpyear</key>\n" +
+               "  <value xsi:type=\"xsd:string\">2018</value>\n" +
+               "</entry>\n",
+               "<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccvv</key>\n" +
+               "  <value xsi:type=\"xsd:string\">MASKED</value>\n" +
+               "</entry>\n" +
+               "<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccnum</key>\n" +
+               "  <value xsi:type=\"xsd:string\">4111111111111111</value>\n" +
+               "</entry>\n" +
+               "<entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccexpmon</key>\n" +
+               "  <value xsi:type=\"xsd:string\">12</value>\n" +
+               "</entry>\n" +
+               "  <key xsi:type=\"xsd:string\">PayU.ccexpyear</key>\n" +
+               "  <value xsi:type=\"xsd:string\">2018</value>\n" +
+               "</entry>\n"
+              );
+    }
+
+    private void verify(final String input, final String output) {
+        final String obfuscated = obfuscator.obfuscate(input);
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/ServerTestSuiteNoDB.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/ServerTestSuiteNoDB.java
new file mode 100644
index 0000000..f2328af
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/ServerTestSuiteNoDB.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+
+public abstract class ServerTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+}