From 01e417166b608b504103fa2918be9a26fc6cc037 Mon Sep 17 00:00:00 2001 From: breken-ai Date: Thu, 24 Sep 2026 19:42:10 -0700 Subject: [PATCH] fix(terraform): redact secret-named variable defaults and output values A variable or output names its secret in the block label, so the literal sits under the generic key default/value and the key-name check never flags it: variable "db_password" { default = "..." } reached graph.json verbatim. Redact default/value when the label matches the sensitive-key pattern or the block sets Terraform's own sensitive = true. Co-Authored-By: Claude Opus 5.5 (1M context) --- graphify/extractors/terraform.py | 11 +++++++ tests/test_terraform.py | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/graphify/extractors/terraform.py b/graphify/extractors/terraform.py index 624cd6854..596c108ba 100644 --- a/graphify/extractors/terraform.py +++ b/graphify/extractors/terraform.py @@ -432,6 +432,17 @@ def extract_terraform(path: Path) -> dict: continue if blk_body is not None: attrs = _collect_direct_attributes(blk_body) + # A variable/output names its secret in the block LABEL, so the + # literal sits under a generic key (`default` / `value`) that the + # key-name check never flags. Redact it when the label names a + # secret or the block carries Terraform's own `sensitive = true`. + if btype in ("variable", "output") and ( + _SENSITIVE_KEY_RE.search(labels[0]) + or attrs.get("sensitive") in (True, "true") + ): + for secret_key in ("default", "value"): + if secret_key in attrs: + attrs[secret_key] = _REDACTED if attrs: nodes_by_id[owner]["attributes"] = attrs _collect_refs(blk_body, owner, "references") diff --git a/tests/test_terraform.py b/tests/test_terraform.py index d4aa2ea44..8d6f1e19f 100644 --- a/tests/test_terraform.py +++ b/tests/test_terraform.py @@ -400,3 +400,52 @@ resource "aws_x" "y" { import json as _json assert "leaky-in-list" not in _json.dumps(node) assert "tok-in-list" not in _json.dumps(node) + + +def test_terraform_secret_named_variable_default_and_output_value_are_redacted(tmp_path): + """A `variable`/`output` block names its secret in the block LABEL, not in an + attribute key: `variable "db_password" { default = "..." }` stores the + credential under the generic key `default`, and `output "admin_token"` under + `value`. Key-name matching alone never sees the secret signal, so the + hardcoded default reached graph.json verbatim. Terraform's own + `sensitive = true` marker must be honoured the same way.""" + body = """\ +variable "db_password" { + type = string + default = "hunter2-default" +} + +variable "region" { + type = string + default = "us-east-1" +} + +variable "signing_material" { + type = string + sensitive = true + default = "marked-sensitive" +} + +output "admin_token" { + value = "tok-in-output" +} + +output "bucket_name" { + value = "my-bucket" +} +""" + r = extract_terraform(_write(tmp_path, "vars.tf", body)) + by_label = {n["label"]: n for n in r["nodes"]} + # secret-named blocks: the literal is redacted, the type stays visible + assert by_label["var.db_password"]["attributes"]["default"] == "[redacted]" + assert by_label["var.db_password"]["attributes"]["type"] == "string" + assert by_label["output.admin_token"]["attributes"]["value"] == "[redacted]" + # Terraform's explicit `sensitive = true` marker is honoured + assert by_label["var.signing_material"]["attributes"]["default"] == "[redacted]" + # ordinary variables/outputs are untouched + assert by_label["var.region"]["attributes"]["default"] == "us-east-1" + assert by_label["output.bucket_name"]["attributes"]["value"] == "my-bucket" + import json as _json + dumped = _json.dumps(r["nodes"]) + for secret in ("hunter2-default", "tok-in-output", "marked-sensitive"): + assert secret not in dumped