Repository: cloud-custodian/cloud-custodian

Issue:
c7n-left - cli output for module should show mod resource paths 
else it can be confusing to know what to fix as a developer, given a module block, without knowing which underlying resource that module created that triggered the policy match.


Discussion:

Resolving patch:
diff --git a/tools/c7n_left/c7n_left/output.py b/tools/c7n_left/c7n_left/output.py
--- a/tools/c7n_left/c7n_left/output.py
+++ b/tools/c7n_left/c7n_left/output.py
@@ -87,6 +87,11 @@ def __rich_console__(self, console, options):
             line_numbers=True,
             lexer=resource.format,
         )
+        refs = self.policy_resource.resource.get_references()
+        if refs:
+            yield "  [yellow]References:"
+            for r in refs:
+                yield f"   - {r}"
         yield ""
 
 
diff --git a/tools/c7n_left/c7n_left/providers/terraform/provider.py b/tools/c7n_left/c7n_left/providers/terraform/provider.py
--- a/tools/c7n_left/c7n_left/providers/terraform/provider.py
+++ b/tools/c7n_left/c7n_left/providers/terraform/provider.py
@@ -173,7 +173,7 @@ def resolve_module_ref(self, mod_resource, graph):
 
         call_stack = extract_mod_stack(mod_resource['__tfmeta']['path'])
         ancestor = mod_map[call_stack[0]]
-        ancestor['__tfmeta'].setdefault('call_stack', {})[id(mod_resource)] = call_stack
+        ancestor['__tfmeta'].setdefault('refs', []).append(mod_resource['__tfmeta']['path'])
         return ancestor
 
 
diff --git a/tools/c7n_left/c7n_left/providers/terraform/resource.py b/tools/c7n_left/c7n_left/providers/terraform/resource.py
--- a/tools/c7n_left/c7n_left/providers/terraform/resource.py
+++ b/tools/c7n_left/c7n_left/providers/terraform/resource.py
@@ -33,6 +33,9 @@ def line_end(self):
     def src_dir(self):
         return self.location["src_dir"]
 
+    def get_references(self):
+        return self.location.get('refs', ())
+
     def get_source_lines(self):
         lines = (self.src_dir / self.filename).read_text().split("\n")
         return lines[self.line_start - 1 : self.line_end]  # noqa


Test patch:
diff --git a/tools/c7n_left/tests/test_left.py b/tools/c7n_left/tests/test_left.py
--- a/tools/c7n_left/tests/test_left.py
+++ b/tools/c7n_left/tests/test_left.py
@@ -143,13 +143,7 @@ def test_taggable_module_resource():
     )
 
 
-@pytest.mark.skipif(
-    os.environ.get('GITHUB_ACTIONS') is None,
-    reason="runs in github actions as it requires network access for tf init",
-)
-def test_mod_reference(tmp_path):
-    (tmp_path / "main.tf").write_text(
-        """
+DB_MODULE_TF = """
 module "db" {
   source  = "terraform-aws-modules/rds/aws"
   version = "~> 3.0"
@@ -167,8 +161,15 @@ def test_mod_reference(tmp_path):
   username = "user"
   port     = "3306"
 }
-        """
-    )
+"""
+
+
+@pytest.mark.skipif(
+    os.environ.get('GITHUB_ACTIONS') is None,
+    reason="runs in github actions as it requires network access for tf init",
+)
+def test_mod_reference(tmp_path):
+    (tmp_path / "main.tf").write_text(DB_MODULE_TF)
     subprocess.check_call(args="terraform init", shell=True, cwd=tmp_path)
     results = run_policy(
         {
@@ -182,6 +183,9 @@ def test_mod_reference(tmp_path):
     assert len(results) == 1
     assert results[0].resource['__tfmeta']['filename'] == 'main.tf'
     assert results[0].resource['__tfmeta']['type'] == 'module'
+    assert results[0].resource['__tfmeta']['refs'] == [
+        'module.db.module.db_instance.aws_db_instance.this[0]'
+    ]
 
 
 def test_graph_resolver():
@@ -877,6 +881,34 @@ def test_cli_no_policies(tmp_path, caplog):
     assert caplog.record_tuples == [("c7n.iac", 30, "no policies found")]
 
 
+@pytest.mark.skipif(
+    os.environ.get('GITHUB_ACTIONS') is None,
+    reason="runs in github actions as it requires network access for tf get",
+)
+def test_cli_output_rich_mod_resource_ref(tmp_path, debug_cli_runner):
+    (tmp_path / "main.tf").write_text(DB_MODULE_TF)
+    (tmp_path / "policy.json").write_text(
+        json.dumps(
+            {
+                "policies": [
+                    {
+                        "name": "check-backup",
+                        "resource": "terraform.aws_db_instance",
+                        "filters": [{"backup_retention_period": 0}],
+                    }
+                ]
+            }
+        )
+    )
+    subprocess.check_call(args="terraform get", shell=True, cwd=tmp_path)
+
+    runner = CliRunner()
+    result = runner.invoke(cli.cli, ["run", "-p", str(tmp_path), "-d", str(tmp_path), "-o", "cli"])
+    assert result.exit_code == 1
+    assert "References:" in result.output
+    assert 'module.db.module.db_instance.aws_db_instance.this[0]' in result.output
+
+
 def test_cli_output_rich(tmp_path):
     write_output_test_policy(tmp_path)
     runner = CliRunner()