fix(terraform): redact the value paired with a secret-named name in name/value lists

The ECS environment idiom { name = "DB_PASSWORD", value = "hunter2" } names the
secret in the name literal, not in a key, so key-name redaction let the value
reach graph.json and the MCP surface verbatim. When a map's name literal
matches the sensitive pattern, redact its value/valueFrom.

Fixes #3787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e11cf947d0)
This commit is contained in:
breken-ai authored and safishamsi committed 2026-09-27 18:39:51 +01:00
1 parent 4377ee93aa
commit cb1016c405
2 files changed
+43 -1

No files matched your search

+13 -1
View File
@@ -32,6 +32,7 @@ _SENSITIVE_KEY_RE = re.compile(
re.IGNORECASE,
)
_REDACTED = "[redacted]"
_PAIRED_VALUE_KEYS = frozenset({"value", "valuefrom", "value_from"})
def _redact_value(key: str, value: object) -> object:
@@ -52,7 +53,18 @@ def _redact_value(key: str, value: object) -> object:
if _SENSITIVE_KEY_RE.search(key):
return _REDACTED
if isinstance(value, dict):
return {k: _redact_value(str(k), v) for k, v in value.items()}
redacted = {k: _redact_value(str(k), v) for k, v in value.items()}
# Name/value-pair idiom (ECS `environment`/`secrets`, `[{name, value}]`):
# the secret signal is the `name` literal, not a key, so key matching
# alone let `{ name = "DB_PASSWORD", value = "hunter2" }` leak (#3787).
if any(
str(k).lower() == "name" and isinstance(v, str) and _SENSITIVE_KEY_RE.search(v)
for k, v in value.items()
):
for k in redacted:
if str(k).lower() in _PAIRED_VALUE_KEYS:
redacted[k] = _REDACTED
return redacted
if isinstance(value, (list, tuple)):
return [_redact_value(key, item) for item in value]
return value
+30
View File
@@ -449,3 +449,33 @@ output "bucket_name" {
dumped = _json.dumps(r["nodes"])
for secret in ("hunter2-default", "tok-in-output", "marked-sensitive"):
assert secret not in dumped
def test_terraform_name_value_pair_secret_is_redacted(tmp_path):
"""The name/value-pair idiom (ECS `environment` / `secrets`, any
`[{ name, value }]` list) names the secret in the `name` LITERAL, while the
credential sits under the generic key `value`. Key-name matching never sees
that signal, so `{ name = "DB_PASSWORD", value = "hunter2" }` reached
graph.json verbatim (#3787). Ordinary name/value config stays intact."""
body = """\
resource "aws_ecs_task_definition" "app" {
family = "app"
environment = [
{ name = "DB_PASSWORD", value = "hunter2" },
{ name = "LOG_LEVEL", value = "debug" },
]
env_refs = [{ name = "API_KEY", valueFrom = "arn:aws:ssm:us-east-1:1:parameter/api" }]
single = { name = "client_secret", value = "in-a-map" }
}
"""
r = extract_terraform(_write(tmp_path, "ecs.tf", body))
node = next(n for n in r["nodes"] if n["label"] == "aws_ecs_task_definition.app")
attrs = node["attributes"]
assert attrs["environment"][0] == {"name": "DB_PASSWORD", "value": "[redacted]"}
assert attrs["environment"][1] == {"name": "LOG_LEVEL", "value": "debug"}
assert attrs["env_refs"][0] == {"name": "API_KEY", "valueFrom": "[redacted]"}
assert attrs["single"] == {"name": "client_secret", "value": "[redacted]"}
import json as _json
dumped = _json.dumps(node)
for secret in ("hunter2", "in-a-map", "parameter/api"):
assert secret not in dumped