Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions graphify/extractors/terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,26 @@


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


Expand Down
41 changes: 41 additions & 0 deletions tests/test_terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,44 @@ def test_terraform_sensitive_attribute_values_are_redacted(tmp_path):
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)
Loading