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
14 changes: 13 additions & 1 deletion graphify/extractors/terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
re.IGNORECASE,
)
_REDACTED = "[redacted]"
_PAIRED_VALUE_KEYS = frozenset({"value", "valuefrom", "value_from"})


def _redact_value(key: str, value: object) -> object:
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions tests/test_terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,33 @@ def test_terraform_secret_named_variable_default_and_output_value_are_redacted(t
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
Loading