This is an automated email from the ASF dual-hosted git repository.

ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new cb0fc82057 [CXF-9164]An example to demonstrate how to extend 
MaskSensitiveHelper and mask sensitive xml attributes
cb0fc82057 is described below

commit cb0fc820574af0c0a707fd84a49dad1e083f283e
Author: Freeman Fang <[email protected]>
AuthorDate: Wed Aug 27 16:20:57 2025 -0400

    [CXF-9164]An example to demonstrate how to extend MaskSensitiveHelper and 
mask sensitive xml attributes
---
 .../cxf/ext/logging/AttributeMaskingHelper.java    | 116 ++++++++++
 .../logging/AttributeMaskSensitiveHelperTest.java  | 257 +++++++++++++++++++++
 2 files changed, 373 insertions(+)

diff --git 
a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
 
b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
new file mode 100644
index 0000000000..742659f9b8
--- /dev/null
+++ 
b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
@@ -0,0 +1,116 @@
+/**
+ * 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;
+    }
+}
diff --git 
a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
 
b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
new file mode 100644
index 0000000000..1ff7db1242
--- /dev/null
+++ 
b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
@@ -0,0 +1,257 @@
+/**
+ * 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.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cxf.ext.logging.event.LogEvent;
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.phase.PhaseInterceptor;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(Parameterized.class)
+public class AttributeMaskSensitiveHelperTest {
+
+    private static final String SENSITIVE_LOGGING_CONTENT_XML =
+            "<user>testUser</user><password>my secret password</password>";
+    private static final String MASKED_LOGGING_CONTENT_XML =
+            "<user>testUser</user><password>XXX</password>";
+
+    private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_ATTRIBUTE =
+            "<user>testUser</user><password myAttribute=\"test\">my secret 
password</password>";
+    private static final String MASKED_LOGGING_CONTENT_XML_WITH_ATTRIBUTE =
+            "<user>testUser</user><password 
myAttribute=\"XXX\">XXX</password>";
+
+    private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_MULTILINE =
+        "<user>testUser</user><password>my \nsecret \npassword</password>";
+    private static final String MASKED_LOGGING_CONTENT_XML_WITH_MULTILINE =
+        "<user>testUser</user><password>XXX</password>";
+
+    private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_WRAPPER =
+        "<passwords><password>my secret password</password></passwords>";
+    private static final String MASKED_LOGGING_CONTENT_XML_WITH_WITH_WRAPPER =
+        "<passwords><password>XXX</password></passwords>";
+
+    private static final String SENSITIVE_LOGGING_XML_EMPTY_TAG_REPEATED =
+       "<user1><password/></user1><user2><password>VALUE</password></user2>";
+    private static final String MASKED_LOGGING_XML_EMPTY_TAG_REPEATED =
+        "<user1><password/></user1><user2><password>XXX</password></user2>";
+
+    private static final String SENSITIVE_LOGGING_CONTENT_JSON =
+            "\"user\":\"testUser\", \"password\": \"my secret password\"";
+    private static final String MASKED_LOGGING_CONTENT_JSON =
+            "\"user\":\"testUser\", \"password\": \"XXX\"";
+
+    private static final String SENSITIVE_LOGGING_MULTIPLE_ELEMENT_XML =
+        "<item><user>testUser1</user><password myAttribute=\"test\">my secret 
password 1</password></item>"
+            + "<item><user>testUser2</user><password>my secret password 
2</password></item>";
+    private static final String MASKED_LOGGING_MULTIPLE_ELEMENT_XML =
+        "<item><user>testUser1</user><password 
myAttribute=\"XXX\">XXX</password></item>"
+            + "<item><user>testUser2</user><password>XXX</password></item>";
+
+    private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_NAMESPACE =
+            "<ns:user>testUser</ns:user><ns:password>my secret 
password</ns:password>";
+
+    private static final String MASKED_LOGGING_CONTENT_XML_WITH_NAMESPACE =
+            "<ns:user>testUser</ns:user><ns:password>XXX</ns:password>";
+
+    private static final Set<String> SENSITIVE_ELEMENTS = new 
HashSet<>(Arrays.asList("password"));
+    private static final Set<String> SENSITIVE_ATTRIBUTES = new 
HashSet<>(Arrays.asList("myAttribute"));
+    private static final String APPLICATION_XML = "application/xml";
+    private static final String APPLICATION_JSON = "application/json";
+
+    private final String loggingContent;
+    private final String maskedContent;
+    private final String contentType;
+    private LogEventSenderMock logEventSender = new LogEventSenderMock();
+    public AttributeMaskSensitiveHelperTest(String loggingContent, String 
maskedContent, String contentType) {
+        this.loggingContent = loggingContent;
+        this.maskedContent = maskedContent;
+        this.contentType = contentType;
+    }
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> primeNumbers() {
+        return Arrays.asList(new Object[][] {
+            {SENSITIVE_LOGGING_CONTENT_XML, MASKED_LOGGING_CONTENT_XML, 
APPLICATION_XML},
+            {SENSITIVE_LOGGING_CONTENT_XML_WITH_ATTRIBUTE, 
MASKED_LOGGING_CONTENT_XML_WITH_ATTRIBUTE, APPLICATION_XML},
+            {SENSITIVE_LOGGING_CONTENT_XML_WITH_MULTILINE, 
MASKED_LOGGING_CONTENT_XML_WITH_MULTILINE, APPLICATION_XML},
+            {SENSITIVE_LOGGING_CONTENT_XML_WITH_WRAPPER, 
MASKED_LOGGING_CONTENT_XML_WITH_WITH_WRAPPER, APPLICATION_XML},
+            {SENSITIVE_LOGGING_XML_EMPTY_TAG_REPEATED, 
MASKED_LOGGING_XML_EMPTY_TAG_REPEATED, APPLICATION_XML},
+            {SENSITIVE_LOGGING_MULTIPLE_ELEMENT_XML, 
MASKED_LOGGING_MULTIPLE_ELEMENT_XML, APPLICATION_XML},
+            {SENSITIVE_LOGGING_CONTENT_XML_WITH_NAMESPACE, 
MASKED_LOGGING_CONTENT_XML_WITH_NAMESPACE, APPLICATION_XML},
+            {SENSITIVE_LOGGING_CONTENT_JSON, MASKED_LOGGING_CONTENT_JSON, 
APPLICATION_JSON}
+        });
+    }
+
+    @Test
+    public void shouldReplaceSensitiveDataInWithAdd() {
+        // Arrange
+        final LoggingInInterceptor inInterceptor = new 
LoggingInInterceptor(logEventSender);
+        AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+        attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+        attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+        inInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+        final Message message = prepareInMessage();
+
+        // Act
+        Collection<PhaseInterceptor<? extends Message>> interceptors = 
inInterceptor.getAdditionalInterceptors();
+        for (PhaseInterceptor intercept : interceptors) {
+            intercept.handleMessage(message);
+        }
+        inInterceptor.handleMessage(message);
+
+        // Verify
+        LogEvent event = logEventSender.getLogEvent();
+        assertNotNull(event);
+        assertEquals(maskedContent, event.getPayload());
+    }
+
+    @Test
+    public void shouldReplaceSensitiveDataInWithSet() {
+        // Arrange
+        final LoggingInInterceptor inInterceptor = new 
LoggingInInterceptor(logEventSender);
+        
+        AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+        attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+        attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+        inInterceptor.setSensitiveDataHelper(attrMaskHelper);
+        final Message message = prepareInMessage();
+
+        // Act
+        Collection<PhaseInterceptor<? extends Message>> interceptors = 
inInterceptor.getAdditionalInterceptors();
+        for (PhaseInterceptor intercept : interceptors) {
+            intercept.handleMessage(message);
+        }
+        inInterceptor.handleMessage(message);
+
+        // Verify
+        LogEvent event = logEventSender.getLogEvent();
+        assertNotNull(event);
+        assertEquals(maskedContent, event.getPayload());
+    }
+
+    @Test
+    public void shouldReplaceSensitiveDataOutWithAdd() throws IOException {
+        // Arrange
+        final LoggingOutInterceptor outInterceptor = new 
LoggingOutInterceptor(logEventSender);
+        AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+        attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+        attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+        outInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+        final Message message = prepareOutMessage();
+
+        // Act
+        outInterceptor.handleMessage(message);
+        byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+        OutputStream out = message.getContent(OutputStream.class);
+        out.write(payload);
+        out.close();
+
+        // Verify
+        LogEvent event = logEventSender.getLogEvent();
+        assertNotNull(event);
+        assertEquals(maskedContent, event.getPayload());
+    }
+
+    @Test
+    public void shouldReplaceSensitiveDataOutWithSet() throws IOException {
+        // Arrange
+        final LoggingOutInterceptor outInterceptor = new 
LoggingOutInterceptor(logEventSender);
+        AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+        attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+        attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+        outInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+        final Message message = prepareOutMessage();
+
+        // Act
+        outInterceptor.handleMessage(message);
+        byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+        OutputStream out = message.getContent(OutputStream.class);
+        out.write(payload);
+        out.close();
+
+        // Verify
+        LogEvent event = logEventSender.getLogEvent();
+        assertNotNull(event);
+        assertEquals(maskedContent, event.getPayload());
+    }
+
+    @Test
+    public void shouldNotReplaceSensitiveDataEmptyExpression() throws 
IOException {
+        // Arrange
+        final LoggingOutInterceptor outInterceptor = new 
LoggingOutInterceptor(logEventSender);
+        final Message message = prepareOutMessage();
+
+        // Act
+        outInterceptor.handleMessage(message);
+        byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+        OutputStream out = message.getContent(OutputStream.class);
+        out.write(payload);
+        out.close();
+
+        // Verify
+        LogEvent event = logEventSender.getLogEvent();
+        assertNotNull(event);
+        assertEquals(loggingContent, event.getPayload());
+    }
+
+    private Message prepareInMessage() {
+        Message message = new MessageImpl();
+        ByteArrayInputStream inputStream =
+                new 
ByteArrayInputStream(loggingContent.getBytes(StandardCharsets.UTF_8));
+        message.put(Message.CONTENT_TYPE, contentType);
+        message.setContent(InputStream.class, inputStream);
+        Exchange exchange = new ExchangeImpl();
+        message.setExchange(exchange);
+        return message;
+    }
+
+    private Message prepareOutMessage() {
+        Message message = new MessageImpl();
+        message.put(Message.CONTENT_TYPE, contentType);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        message.setContent(OutputStream.class, outputStream);
+        Exchange exchange = new ExchangeImpl();
+        message.setExchange(exchange);
+        return message;
+    }
+
+}

Reply via email to