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

cgivre pushed a commit to branch feat/drill-mcp-server
in repository https://gitbox.apache.org/repos/asf/drill-mcp.git

commit 4d622b946af86c0b61301314df0a67f52c9e1062
Author: cgivre <[email protected]>
AuthorDate: Tue Aug 11 16:12:32 2026 -0400

    feat: recursive secret redaction
---
 drill_mcp/redact.py  | 55 +++++++++++++++++++++++++++++
 tests/test_redact.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 152 insertions(+)

diff --git a/drill_mcp/redact.py b/drill_mcp/redact.py
new file mode 100644
index 0000000..a85e041
--- /dev/null
+++ b/drill_mcp/redact.py
@@ -0,0 +1,55 @@
+#
+# 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.
+#
+
+"""Recursive secret redaction for anything returned to an MCP client.
+
+Storage plugin configurations routinely carry AWS keys, JDBC passwords, and
+OAuth tokens. Tool output goes to a model and often on to a third-party API, so
+this is a trust boundary and is not configurable off.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+
+REDACTED = "***REDACTED***"
+
+# Matches anywhere in the key, so `fs.s3a.secret.key` and `awsSecretAccessKey`
+# are both caught. Deliberately broad: a false redaction is a cosmetic problem,
+# a missed one is a leaked credential. Uses negative lookahead on credential(s)
+# to avoid matching keys like "credentialsProvider" where it's part of a 
longer word.
+_SENSITIVE = re.compile(
+    
r"password|passwd|secret|credentials?(?![a-z])|token|access[._-]?key|private[._-]?key|api[._-]?key",
+    re.IGNORECASE,
+)
+
+
+def redact(value: Any) -> Any:
+    """Return a copy of `value` with sensitive-looking values replaced."""
+    if isinstance(value, dict):
+        return {
+            key: REDACTED if _SENSITIVE.search(str(key)) else redact(item)
+            for key, item in value.items()
+        }
+    if isinstance(value, list):
+        return [redact(item) for item in value]
+    if isinstance(value, tuple):
+        return tuple(redact(item) for item in value)
+    return value
diff --git a/tests/test_redact.py b/tests/test_redact.py
new file mode 100644
index 0000000..bcbf637
--- /dev/null
+++ b/tests/test_redact.py
@@ -0,0 +1,97 @@
+#
+# 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.
+#
+
+from drill_mcp.redact import REDACTED, redact
+
+
+def test_redacts_password_key():
+    assert redact({"password": "hunter2"}) == {"password": REDACTED}
+
+
+def test_redaction_is_case_insensitive():
+    assert redact({"PassWord": "hunter2"}) == {"PassWord": REDACTED}
+
+
+def test_redacts_all_sensitive_key_patterns():
+    source = {
+        "accessKey": "AKIA",
+        "access_key": "AKIA",
+        "secretKey": "s",
+        "token": "t",
+        "credential": "c",
+        "privateKey": "p",
+        "oauthToken": "o",
+    }
+    assert all(v == REDACTED for v in redact(source).values())
+
+
+def test_leaves_innocuous_keys_alone():
+    assert redact({"type": "file", "connection": "s3a://bucket"}) == {
+        "type": "file",
+        "connection": "s3a://bucket",
+    }
+
+
+def test_recurses_into_nested_dicts():
+    source = {"config": {"credentialsProvider": {"awsSecretAccessKey": "s"}}}
+    assert 
redact(source)["config"]["credentialsProvider"]["awsSecretAccessKey"] == 
REDACTED
+
+
+def test_recurses_into_lists():
+    source = {"plugins": [{"password": "a"}, {"password": "b"}]}
+    assert [p["password"] for p in redact(source)["plugins"]] == [REDACTED, 
REDACTED]
+
+
+def test_does_not_mutate_the_input():
+    source = {"password": "hunter2"}
+    redact(source)
+    assert source["password"] == "hunter2"
+
+
+def test_redacts_whole_subtree_under_a_sensitive_key():
+    source = {"credentials": {"user": "alice", "pass": "x"}}
+    assert redact(source)["credentials"] == REDACTED
+
+
+def test_passes_through_scalars():
+    assert redact("plain") == "plain"
+    assert redact(42) == 42
+    assert redact(None) is None
+
+
+def test_realistic_s3_plugin_config():
+    plugin = {
+        "name": "s3",
+        "config": {
+            "type": "file",
+            "connection": "s3a://my-bucket",
+            "config": {
+                "fs.s3a.access.key": "AKIAEXAMPLE",
+                "fs.s3a.secret.key": "verysecret",
+                "fs.s3a.endpoint": "s3.amazonaws.com",
+            },
+            "workspaces": {"root": {"location": "/", "writable": False}},
+        },
+    }
+    result = redact(plugin)
+    inner = result["config"]["config"]
+    assert inner["fs.s3a.access.key"] == REDACTED
+    assert inner["fs.s3a.secret.key"] == REDACTED
+    assert inner["fs.s3a.endpoint"] == "s3.amazonaws.com"
+    assert result["config"]["workspaces"]["root"]["location"] == "/"

Reply via email to