You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@drill.apache.org by cg...@apache.org on 2026/08/12 20:24:47 UTC
(drill-mcp) 10/44: feat: recursive secret redaction
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 <cg...@gmail.com>
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"] == "/"