AttributeMaskingHelper.java

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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.apache.cxf.ext.logging;

import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.cxf.message.Message;


/**
 * Adds XML/HTML attribute value masking on top of the parent MaskSensitiveHelper.
 * 
 */
public class AttributeMaskingHelper extends MaskSensitiveHelper {

    private static final String ATTR_NAME_TEMPLATE = "-ATTR_NAME-";

    // Re-declare namespace prefix class per Namespaces in XML (private in parent; reproduce here)
    private static final String PATTERN_XML_NAMESPACE_PREFIX = "[\\w.\\-\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6"
            + "\\u00F8-\\u02FF\\u0300-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F"
            + "\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD]+";

    // Case-sensitive attribute pattern; supports optional namespace prefix; preserves original quotes
    // Groups: 1=full attr name (w/ optional prefix), 2=open quote, 3=value, 4=close quote (backref to 2)
    private static final String MATCH_PATTERN_XML_ATTR_TEMPLATE =
            "(\\b(?:" + PATTERN_XML_NAMESPACE_PREFIX + ":)?" + ATTR_NAME_TEMPLATE + ")\\s*=\\s*(\"|')(.*?)(\\2)";
    private static final String REPLACEMENT_XML_ATTR_TEMPLATE = "$1=$2XXX$4";

    private static final String XML_CONTENT = "xml";
    private static final String HTML_CONTENT = "html";

    private static class ReplacementPair {
        private final Pattern matchPattern;
        private final String replacement;
        ReplacementPair(String matchPattern, String replacement) {
            
            this.matchPattern = Pattern.compile(matchPattern, Pattern.DOTALL);
            this.replacement = replacement;
        }
    }

    private final Set<ReplacementPair> replacementsXMLAttributes = new HashSet<>();

    /** Adds attribute names to be masked in XML/HTML logs (values replaced with "XXX"). */
    public void addSensitiveAttributeNames(final Set<String> inSensitiveAttirbuteNames) {
        if (inSensitiveAttirbuteNames == null || inSensitiveAttirbuteNames.isEmpty()) {
            return;
        }
        for (final String attr : inSensitiveAttirbuteNames) {
            final String match = MATCH_PATTERN_XML_ATTR_TEMPLATE.replace(ATTR_NAME_TEMPLATE, Pattern.quote(attr));
            final String repl  = REPLACEMENT_XML_ATTR_TEMPLATE.replace(ATTR_NAME_TEMPLATE, escapeForReplacement(attr));
            replacementsXMLAttributes.add(new ReplacementPair(match, repl));
        }
    }

    /** Optional convenience resetter if you want it. */
    public void setSensitiveAttributeNames(final Set<String> inSensitiveAttributeNames) {
        replacementsXMLAttributes.clear();
        addSensitiveAttributeNames(inSensitiveAttributeNames);
    }

    @Override
    public String maskSensitiveElements(final Message message, final String originalLogString) {
        // First, do all base-class masking (elements/JSON/headers)
        String masked = super.maskSensitiveElements(message, originalLogString);
        if (masked == null || message == null) {
            return masked;
        }
        final String contentType = (String) message.get(Message.CONTENT_TYPE);
        if (contentType == null) {
            return masked;
        }
        final String lower = contentType.toLowerCase();
        if (lower.contains(XML_CONTENT) || lower.contains(HTML_CONTENT)) {
            // Then apply attribute-value masking
            return applyMasks(masked, replacementsXMLAttributes);
        }
        return masked;
    }

    // --- helpers (local copy; parent versions are private) ---

    private static String escapeForReplacement(String s) {
        if (s == null || s.isEmpty()) {
            return s;
        }
        return s.replace("\\", "\\\\").replace("$", "\\$");
    }

    private String applyMasks(String input, Set<ReplacementPair> pairs) {
        String out = input;
        for (final ReplacementPair rp : pairs) {
            out = rp.matchPattern.matcher(out).replaceAll(rp.replacement);
        }
        return out;
    }
}