fix(terraform): redact secrets nested inside list values, not just maps (#3644 follow-up)

`_redact_value` (the #3644 guard that keeps hardcoded credentials in .tf files
out of graph.json and the MCP query/get_node surface) recursed into map values
but not list values. HCL routinely nests objects inside tuples —
`list(object(...))` variables, `dynamic` blocks, tuple defaults — and
`_parse_attr_value` turns those into Python lists of dicts, so:

    config  = { password = "x" }      -> redacted   (map recursion)
    configs = [{ password = "x" }]    -> LEAKED      (list not recursed)

The list form left the secret in the serialized node verbatim, defeating the
guard for a very common Terraform shape (a `connections`/`containers`/`secrets`
tuple of objects).

Recurse into lists and tuples as well, under the same key — the list branch is
only reached when the key is not itself sensitive (a sensitive key redacts the
whole value above), so a scalar element carries no key signal and is returned
as-is while a dict element is checked against its own inner keys. A non-secret
list of scalars is untouched.

Tests: a unit check that a secret inside a list-of-objects and a nested list is
redacted (while a sensitive key still redacts the whole list and a plain scalar
list is left intact), and an end-to-end check that a `password`/`token` inside a
tuple-of-objects attribute never reaches the serialized node. Both fail without
the fix. The terraform suites otherwise stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
This commit is contained in:
abhay-codes07andClaude Opus 4.8 committed 2026-09-23 00:11:14 +05:30
1 parent 20a20d30d8
commit 2998cb2121
2 files changed
+57 -2

No files matched your search

+16 -2
View File
@@ -35,12 +35,26 @@ _REDACTED = "[redacted]"
def _redact_value(key: str, value: object) -> object:
"""Redact a sensitive attribute value; recurse into map values so a nested
`password` inside a `tags`/`connection` map is redacted too."""
"""Redact a sensitive attribute value; recurse into map AND list values so a
nested `password` inside a `tags`/`connection` map — or inside a list of
objects — is redacted too.
HCL routinely nests objects inside tuples (`list(object(...))` variables,
`dynamic` blocks, tuple defaults), and `_parse_attr_value` turns those into
Python lists of dicts. Recursing into dicts but not lists left
`configs = [{ password = "x" }]` leaking verbatim while the map form
`config = { password = "x" }` was redacted — the value still reaches
graph.json and the MCP query/get_node surface unsanitized (#3644 follow-up).
List elements are recursed under the same key: the list branch is only
reached when `key` is NOT itself sensitive (a sensitive key redacts the whole
value above), so a scalar element carries no key signal and is returned
as-is, while a dict element is checked against its own inner keys."""
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()}
if isinstance(value, (list, tuple)):
return [_redact_value(key, item) for item in value]
return value
+41
View File
@@ -359,3 +359,44 @@ resource "aws_db_instance" "main" {
assert "hunter2-super-secret" not in _json.dumps(node)
assert "AKIAWHATEVER" not in _json.dumps(node)
assert "leaky" not in _json.dumps(node)
def test_terraform_redact_value_recurses_into_lists():
"""A secret nested inside a list — a list of objects, or a nested list — must
be redacted, matching the map case. HCL routinely nests objects in tuples
(`list(object(...))`, dynamic blocks), and recursing into dicts but not lists
left those values leaking (#3644 follow-up)."""
from graphify.extractors.terraform import _redact_value
# list of objects: the secret-named inner key is redacted, ordinary key kept
assert _redact_value("connections", [{"host": "db", "password": "x"}]) == [
{"host": "db", "password": "[redacted]"}
]
# nested list -> list -> object
assert _redact_value("stages", [[{"api_key": "sk-1"}]]) == [[{"api_key": "[redacted]"}]]
# a sensitive KEY still redacts the whole list value
assert _redact_value("passwords", ["a", "b"]) == "[redacted]"
# a non-secret list of scalars is left intact
assert _redact_value("ports", [80, 443]) == [80, 443]
def test_terraform_secret_in_list_of_objects_is_redacted(tmp_path):
"""End-to-end: a `password` inside a tuple-of-objects attribute must not reach
the graph verbatim, exactly as it wouldn't inside a map (#3644 follow-up)."""
body = """\
resource "aws_x" "y" {
name = "app"
connections = [
{ host = "db1", password = "leaky-in-list" },
{ host = "db2", token = "tok-in-list" },
]
}
"""
r = extract_terraform(_write(tmp_path, "conns.tf", body))
node = next(n for n in r["nodes"] if n["label"] == "aws_x.y")
conns = node["attributes"]["connections"]
assert conns[0]["host"] == "db1" and conns[0]["password"] == "[redacted]"
assert conns[1]["host"] == "db2" and conns[1]["token"] == "[redacted]"
import json as _json
assert "leaky-in-list" not in _json.dumps(node)
assert "tok-in-list" not in _json.dumps(node)