Repository: cloud-custodian/tfparse (language: python)

Issue:
readme



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 65a08b7..4dbc58a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ tfparse.cpython*
 
 .terraform
 .tf_providers
-.terraform.lock.hcl
\ No newline at end of file
+.terraform.lock.hcl
+output.json
diff --git a/gotfparse/README.md b/gotfparse/README.md
index 3f6153f..711fc8a 100644
--- a/gotfparse/README.md
+++ b/gotfparse/README.md
@@ -10,9 +10,13 @@ You can use the `tftest` helper command to easily iterate on terraform and previ
 
     go run cmd/tftest/main.go <path-to-terraform> > output.json
 
+You can use the `tfdump` helper command to reveal the structure that `aquasecurity/defsec` creates to assist with identifying anomalies. It exports the original structure, along with interesting attributes, in json format.  
+
+    go run cmd/tfdump/main.go <path-to-terraform> > output.json
+
 ## Tips
 
 When using a modern IDE like Visual Studio Code or Goland, open the `gotfparse` folder as the root of the workspace to ensure all of the Go tooling works as expected.
 
 
-[defsec_repo]: https://github.com/aquasecurity/defsec
\ No newline at end of file
+[defsec_repo]: https://github.com/aquasecurity/defsec
diff --git a/gotfparse/cmd/tfdump/main.go b/gotfparse/cmd/tfdump/main.go
new file mode 100644
index 0000000..e90f0e1
--- /dev/null
+++ b/gotfparse/cmd/tfdump/main.go
@@ -0,0 +1,172 @@
+// Copyright The Cloud Custodian Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+
+	"github.com/aquasecurity/defsec/pkg/scanners/terraform/parser"
+	"github.com/aquasecurity/defsec/pkg/terraform"
+	"github.com/zclconf/go-cty/cty"
+)
+
+func main() {
+	if len(os.Args) < 2 || len(os.Args) > 2 {
+		executable := filepath.Base(os.Args[0])
+		log.Fatalf("usage: %s PATH", executable)
+	}
+
+	var (
+		err error
+		ctx = context.TODO()
+	)
+
+	path := os.Args[1]
+	fs := os.DirFS(path)
+	p := parser.New(fs, "")
+
+	err = p.ParseFS(ctx, ".")
+	check(err)
+
+	// second parameter always seems to be an empty map
+	modules, _, err := p.EvaluateAll(ctx)
+	check(err)
+
+	objects, err := dumpJson(modules)
+	check(err)
+
+	data, err := json.MarshalIndent(objects, "", "  ")
+	check(err)
+
+	fmt.Println(string(data))
+}
+
+func check(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+func dumpJson(modules terraform.Modules) ([]map[string]interface{}, error) {
+	output := make([]map[string]interface{}, 0)
+
+	for _, m := range modules {
+		output = append(output, dumpModule(m)...)
+	}
+
+	return output, nil
+}
+
+func dumpModule(m *terraform.Module) []map[string]interface{} {
+	output := make([]map[string]interface{}, 0)
+
+	for _, b := range m.GetBlocks() {
+		output = append(output, dumpBlock(m, b))
+	}
+	return output
+}
+
+func dumpBlock(m *terraform.Module, b *terraform.Block) map[string]interface{} {
+	object := make(map[string]interface{})
+	object["__full_name__"] = b.FullName()
+	object["__id__"] = b.ID()
+	object["__is_count_expanded"] = b.IsCountExpanded()
+	object["__is_empty__"] = b.IsEmpty()
+	object["__in_module__"] = b.InModule()
+	object["__is_nil__"] = b.IsNil()
+	object["__is_not_nil__"] = b.IsNotNil()
+	object["__label__"] = b.Label()
+	object["__labels__"] = b.Labels()
+	object["__local_name__"] = b.LocalName()
+	object["__module_name__"] = b.ModuleName()
+	object["__name_label__"] = b.NameLabel()
+	object["__type__"] = b.Type()
+	object["__type_label__"] = b.TypeLabel()
+	object["__unique_name__"] = b.UniqueName()
+
+	r := b.GetMetadata().Range()
+
+	object["__ref__"] = map[string]interface{}{
+		"start":  r.GetStartLine(),
+		"end":    r.GetEndLine(),
+		"fname":  r.GetFilename(),
+		"local":  r.GetLocalFilename(),
+		"prefix": r.GetSourcePrefix(),
+	}
+
+	for _, attr := range b.GetAttributes() {
+		object[attr.Name()] = dumpAttribute(m, b, attr)
+	}
+
+	children := make([]interface{}, 0)
+	for _, sub := range b.AllBlocks() {
+		children = append(children, dumpBlock(m, sub))
+	}
+	if len(children) > 0 {
+		object["__children__"] = children
+	}
+
+	return object
+}
+
+func dumpAttribute(m *terraform.Module, parent *terraform.Block, attr *terraform.Attribute) interface{} {
+	t := attr.Type()
+
+	if attr.IsDataBlockReference() {
+		ref, err := m.GetReferencedBlock(attr, parent)
+		if err != nil {
+			return map[string]interface{}{
+				"__missing_ref__": attr.GetMetadata().Reference(),
+			}
+		}
+
+		return map[string]interface{}{"__ref_id__": ref.ID()}
+	}
+
+	if t.IsTupleType() || t.IsListType() {
+		var result []interface{}
+		for _, v := range attr.Value().AsValueSlice() {
+			result = append(result, getRawValue(v))
+		}
+		return result
+	}
+	return attr.GetRawValue()
+}
+
+func getRawValue(a cty.Value) interface{} {
+	if !a.IsKnown() {
+		return "<<unknown>>"
+	}
+
+	if a.IsNull() {
+		return nil
+	}
+
+	switch typ := a.Type(); typ {
+	case cty.String:
+		return a.AsString()
+	case cty.Bool:
+		return a.True()
+	case cty.Number:
+		float, _ := a.AsBigFloat().Float64()
+		return float
+	default:
+		switch {
+		case typ.IsTupleType(), typ.IsListType():
+			values := a.AsValueSlice()
+
+			var result []interface{}
+			for _, v := range values {
+				result = append(result, getRawValue(v))
+			}
+			return result
+		}
+	}
+	return nil
+}
diff --git a/gotfparse/go.mod b/gotfparse/go.mod
index d331921..a26ca90 100644
--- a/gotfparse/go.mod
+++ b/gotfparse/go.mod
@@ -4,7 +4,7 @@ go 1.18
 
 require (
 	github.com/Jeffail/gabs/v2 v2.6.1
-	github.com/aquasecurity/defsec v0.69.2
+	github.com/aquasecurity/defsec v0.81.0
 	github.com/zclconf/go-cty v1.10.0
 )
 
@@ -15,7 +15,7 @@ require (
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/apparentlymart/go-cidr v1.1.0 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
-	github.com/aws/aws-sdk-go v1.44.48 // indirect
+	github.com/aws/aws-sdk-go v1.44.114 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
 	github.com/bmatcuk/doublestar v1.3.4 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -27,7 +27,7 @@ require (
 	github.com/hashicorp/go-safetemp v1.0.0 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
 	github.com/hashicorp/go-version v1.2.0 // indirect
-	github.com/hashicorp/hcl/v2 v2.12.0 // indirect
+	github.com/hashicorp/hcl/v2 v2.14.1 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/klauspost/compress v1.15.1 // indirect
 	github.com/kr/text v0.2.0 // indirect
@@ -38,16 +38,15 @@ require (
 	github.com/ulikunitz/xz v0.5.8 // indirect
 	github.com/zclconf/go-cty-yaml v1.0.2 // indirect
 	go.opencensus.io v0.23.0 // indirect
-	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
-	golang.org/x/exp v0.0.0-20221012134508-3640c57a48ea
-	golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
-	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
-	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
+	golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
+	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
+	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
+	golang.org/x/text v0.3.8 // indirect
 	golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
 	google.golang.org/api v0.62.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
-	google.golang.org/grpc v1.47.0 // indirect
-	google.golang.org/protobuf v1.28.0 // indirect
+	google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
+	google.golang.org/grpc v1.49.0 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
 )
diff --git a/gotfparse/go.sum b/gotfparse/go.sum
index 05b4ea2..d079d5c 100644
--- a/gotfparse/go.sum
+++ b/gotfparse/go.sum
@@ -57,22 +57,20 @@ github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VM
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
 github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
-github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
 github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
-github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
 github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
-github.com/aquasecurity/defsec v0.69.2 h1:hd/GDbHdGL9EBLkh4SMON7FgVi2LhfN0WS4acq3eDqM=
-github.com/aquasecurity/defsec v0.69.2/go.mod h1:ePeucB2upo/aOqvsks1ML6xflQRxokf4Qol16u0+IQs=
+github.com/aquasecurity/defsec v0.81.0 h1:sdaWWPe0X1Ij4uInXvfScwPhyWMrwaXG+tjYIZo3Q3Y=
+github.com/aquasecurity/defsec v0.81.0/go.mod h1:4SMepRtX/F8bzTd8CucIpMiqpNOB6/BVPnRktDF5iN0=
 github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
-github.com/aws/aws-sdk-go v1.44.48 h1:jLDC9RsNoYMLFlKpB8LdqUnoDdC2yvkS4QbuyPQJ8+M=
-github.com/aws/aws-sdk-go v1.44.48/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.44.114 h1:plIkWc/RsHr3DXBj4MEw9sEW4CcL/e2ryokc+CKyq1I=
+github.com/aws/aws-sdk-go v1.44.114/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
@@ -117,7 +115,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
-github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -211,8 +208,8 @@ github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+d
 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4=
-github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
+github.com/hashicorp/hcl/v2 v2.14.1 h1:x0BpjfZ+CYdbiz+8yZTQ+gdLO7IXvOut7Da+XJayx34=
+github.com/hashicorp/hcl/v2 v2.14.1/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
@@ -239,7 +236,7 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/liamg/iamgo v0.0.9 h1:tADGm3xVotyRJmuKKaH4+zsBn7LOcvgdpuF3WsSKW3c=
 github.com/liamg/jfather v0.0.7 h1:Xf78zS263yfT+xr2VSo6+kyAy4ROlCacRqJG7s5jt4k=
-github.com/liamg/memoryfs v1.4.2 h1:6T9Oy1DdWxGCzIY89p0Ykeya5H0uAlzG2xHEGcvo6MU=
+github.com/liamg/memoryfs v1.4.3 h1:+ChjcuPRYpjJSulD13PXDNR3JeJ5HUYKjLHyWVK0bqU=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@@ -248,7 +245,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/owenrumney/squealer v1.0.1-0.20220510063705-c0be93f0edea h1:RwQ26NYF4vvP7GckFRB4ABL18Byo7vnYBzMpmZKkGwQ=
@@ -261,11 +257,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
 github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -285,11 +279,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
-github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
-github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
 github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
-github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
 github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -302,13 +293,12 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
-golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -319,8 +309,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20221012134508-3640c57a48ea h1:c+JVtpqmcDxL2rnX9ramD4VqXvu01vReExyKr38B/l4=
-golang.org/x/exp v0.0.0-20221012134508-3640c57a48ea/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -383,8 +371,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4=
-golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -400,8 +388,9 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -418,7 +407,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -464,8 +452,8 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -477,8 +465,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -639,8 +628,8 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 h1:Et6SkiuvnBn+SgrSYXs/BrUpGB4mbdwt4R3vaPIlicA=
-google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I=
+google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -667,8 +656,9 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
 google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
-google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
+google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -683,8 +673,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
diff --git a/gotfparse/pkg/converter/converter.go b/gotfparse/pkg/converter/converter.go
index ac3116d..f143074 100644
--- a/gotfparse/pkg/converter/converter.go
+++ b/gotfparse/pkg/converter/converter.go
@@ -4,30 +4,27 @@ package converter
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
+	"log"
 	"os"
 
 	"github.com/Jeffail/gabs/v2"
 	"github.com/aquasecurity/defsec/pkg/scanners/options"
 	"github.com/aquasecurity/defsec/pkg/scanners/terraform/parser"
 	"github.com/aquasecurity/defsec/pkg/terraform"
-	ctyjson "github.com/zclconf/go-cty/cty/json"
+	"github.com/zclconf/go-cty/cty"
 )
 
+var logger = log.New(os.Stderr, "converter", 1)
+
 type terraformConverter struct {
-	data          *gabs.Container
 	filePath      string
 	modules       terraform.Modules
 	debug         bool
 	stopOnError   bool
 	parserOptions []options.ParserOption
-}
 
-type terraformMeta struct {
-	Filename  string `json:"filename"`
-	LineStart int    `json:"line_start"`
-	LineEnd   int    `json:"line_end"`
+	countsByParentPathBlockName map[string]map[string]int
 }
 
 // VisitJSON visits each of the Terraform JSON blocks that the Terraform converter
@@ -36,106 +33,265 @@ type terraformMeta struct {
 func (t *terraformConverter) VisitJSON() *gabs.Container {
 	jsonOut := gabs.New()
 
-	var visitor func(b *terraform.Block, parentKey string)
+	for _, m := range t.modules {
+		t.visitModule(m, jsonOut)
+	}
+
+	return jsonOut
+}
+
+// visitModule takes a module and walks each of the blocks underneath it.
+func (t *terraformConverter) visitModule(m *terraform.Module, out *gabs.Container) {
+	path := t.getModulePath(m)
+
+	for _, b := range m.GetBlocks() {
+		t.visitBlock(b, path, out)
+	}
+}
+
+// visitBlock takes a block, and either builds a json model of the resource or ignores it.
+func (t *terraformConverter) visitBlock(b *terraform.Block, parentPath string, jsonOut *gabs.Container) {
+	switch b.Type() {
+	// These blocks don't have to conform to policies, and they don't have
+	//children that should have policies applied to them, so we ignore them.
+	case "data", "locals", "output", "provider", "terraform", "variable", "module", "moved", "resource":
+		json := t.buildBlock(b)
+		meta := json["__tfmeta"].(map[string]interface{})
+
+		arrayKey := t.getPath(b, parentPath)
+
+		meta["path"] = arrayKey
+
+		var key string
+		switch b.Type() {
+		case "data", "resource":
+			key = b.TypeLabel()
+			meta["type"] = b.Type()
+
+		default:
+			key = b.Type()
+		}
+
+		jsonOut.ArrayAppendP(json, key)
+	default:
+		logger.Printf("unknown block type: %s", b.Type())
+	}
+}
+
+// getList gets a slice from
+func getList(obj map[string][]interface{}, key string) []interface{} {
+	value, ok := obj[key]
+	if !ok {
+		value = make([]interface{}, 0)
+		obj[key] = value
+	}
+	return value
+}
+
+type add func(string, interface{})
+type dump func() map[string]interface{}
 
-	visitor = func(b *terraform.Block, parentKey string) {
-		blockName := b.GetMetadata().String()
+// newBlockCollector creates a few closures to help flatten
+//lists that are actually singletons.
+//Note: This doesn't guarantee that they're _supposed_ to be singeltons, only
+//that there is only a single item in the list as rendered.
+func newBlockCollector() (add, dump) {
+	collection := make(map[string][]interface{})
 
-		blockJSON := gabs.New()
-		arrayKey := blockName
+	add := func(key string, value interface{}) {
+		list := getList(collection, key)
+		collection[key] = append(list, value)
+	}
 
-		if parentKey != "" {
-			if parentKey != blockName {
-				arrayKey = fmt.Sprintf("%s.%s", parentKey, blockName)
+	dump := func() map[string]interface{} {
+		results := make(map[string]interface{})
+		for key, items := range collection {
+			if len(items) == 1 {
+				results[key] = items[0]
 			} else {
-				arrayKey = parentKey
+				results[key] = items
 			}
 		}
+		return results
+	}
 
-		if parentKey == "" {
-			meta := generateTFMeta(b)
-			if meta != nil {
-				blockJSON.SetP(meta, fmt.Sprintf("%s.__tfmeta", arrayKey))
-			}
+	return add, dump
+}
 
-			for _, a := range b.GetAttributes() {
-				attrCtyJSON := ctyjson.SimpleJSONValue{Value: a.Value()}
-				jb, _ := attrCtyJSON.MarshalJSON()
-				gv, _ := gabs.ParseJSON(jb)
-				if gv != nil {
-					childKey := fmt.Sprintf("%s.%s", arrayKey, a.Name())
-					blockJSON.SetP(gv, childKey)
-				}
-
-				rb, _ := t.modules.GetReferencedBlock(a, b)
-				if rb != nil {
-					refKey := fmt.Sprintf("%s.%s", arrayKey, a.Name())
-					blockJSON.SetP(rb.ID(), refKey)
-				}
-
-				if b.ID() != "" {
-					blockJSON.SetP(b.ID(), fmt.Sprintf("%s.id", arrayKey))
-				}
-			}
-		} else {
-			obj := map[string]*gabs.Container{}
-
-			for _, a := range b.GetAttributes() {
-				attrCtyJSON := ctyjson.SimpleJSONValue{Value: a.Value()}
-				jb, _ := attrCtyJSON.MarshalJSON()
-				gv, _ := gabs.ParseJSON(jb)
-				obj[a.Name()] = gv
-
-				rb, _ := t.modules.GetReferencedBlock(a, b)
-				if rb != nil {
-					refIdJ, _ := json.Marshal(rb.ID())
-					refIdP, _ := gabs.ParseJSON(refIdJ)
-					obj[a.Name()] = refIdP
-				}
-			}
+// buildBlock converts a terraform.Block's attributes and children to a json map.
+func (t *terraformConverter) buildBlock(b *terraform.Block) map[string]interface{} {
+	obj := make(map[string]interface{})
 
-			idJ, _ := json.Marshal(b.ID())
-			id, _ := gabs.ParseJSON(idJ)
+	add, dump := newBlockCollector()
+	for _, child := range getChildBlocks(b) {
+		key := child.Type()
+		add(key, t.buildBlock(child))
+	}
+	grouped := dump()
+	for key, result := range grouped {
+		obj[key] = result
+	}
 
-			meta := generateTFMeta(b)
-			if meta != nil {
-				obj["__tfmeta"] = meta
-			}
+	for _, a := range b.GetAttributes() {
+		obj[a.Name()] = t.getAttributeValue(a, b)
+	}
+
+	id := b.ID()
+	if id != "" {
+		obj["id"] = id
+	}
+
+	r := b.GetMetadata().Range()
+	meta := map[string]interface{}{
+		"filename":   r.GetLocalFilename(),
+		"line_start": r.GetStartLine(),
+		"line_end":   r.GetEndLine(),
+	}
+	if tl := b.TypeLabel(); tl != "" {
+		meta["label"] = tl
+	}
+	obj["__tfmeta"] = meta
+
+	return obj
+}
+
+// getAttributeValue converts the attribute into a value that can be
+//encoded into json.
+func (t *terraformConverter) getAttributeValue(
+		a *terraform.Attribute,
+		b *terraform.Block,
+) interface{} {
+	rb, _ := t.modules.GetReferencedBlock(a, b)
+	if rb != nil {
+		return rb.ID()
+	}
+
+	val := a.Value()
+	if raw, ok := convertCtyToNativeValue(val); ok {
+		return raw
+	}
+
+	return a.GetRawValue()
+}
 
-			if id != nil {
-				obj["id"] = id
+// convertCtyToNativeValue converts a `cty.Value`, used by the
+//aquasecurity/defsec library, to a value that can be converted into json by
+//the Jeffail/gabs library.
+func convertCtyToNativeValue(val cty.Value) (interface{}, bool) {
+	var (
+		ok bool
+
+		vType = val.Type()
+	)
+
+	if val.IsNull() {
+		return nil, true
+	}
+
+	if !val.IsKnown() {
+		return nil, true
+	}
+
+	if vType.IsObjectType() || vType.IsMapType() {
+		valueMap := val.AsValueMap()
+		interfaceMap := make(map[string]interface{})
+		for key, val := range valueMap {
+			if interfaceMap[key], ok = convertCtyToNativeValue(val); !ok {
+				return nil, false
 			}
+		}
+		return interfaceMap, true
+	}
 
-			if len(obj) > 0 {
-				blockJSON.SetP(obj, arrayKey)
+	if vType.IsListType() || vType.IsTupleType() {
+		valueSlice := val.AsValueSlice()
+		interfaceSlice := make([]interface{}, len(valueSlice))
+		for idx, item := range valueSlice {
+			if interfaceSlice[idx], ok = convertCtyToNativeValue(item); !ok {
+				return nil, false
 			}
 		}
+		return interfaceSlice, true
+	}
 
-		if jsonOut.ExistsP(arrayKey) {
-			data := blockJSON.Path(arrayKey)
-			jsonOut.ArrayAppendP(data, arrayKey)
-		} else {
-			jsonOut.MergeFn(blockJSON, collisionFn(arrayKey))
+	if vType == cty.String {
+		return val.AsString(), true
+	}
+
+	if vType == cty.Number {
+		num := val.AsBigFloat()
+		if num.IsInt() {
+			i, _ := num.Int64()
+			return i, true
 		}
 
-		for _, b := range b.AllBlocks() {
-			parent := b.GetMetadata().Parent()
-			if parent != nil {
-				if parentKey != "" && parentKey != parent.String() {
-					parentKey = fmt.Sprintf("%s.%s", parentKey, parent.String())
-				} else {
-					parentKey = parent.String()
-				}
-			}
-			visitor(b, parentKey)
+		f, _ := num.Float64()
+		return f, true
+	}
+
+	if vType == cty.Bool {
+		return val.True(), true
+	}
+
+	return nil, false
+}
+
+// getChildBlocks iterates over all children of a given `terraform.Block` and
+// returns a filtered list of the unique children. This is mostly here to avoid
+// issues with dynamic/content blocks.
+// For unknown reasons, dynamic blocks cause two issues:
+// - the block with type 'dynamic' is a template, not a real resource, and
+//   should be skipped
+// - blocks created by the template seem to be duplicated
+func getChildBlocks(b *terraform.Block) []*terraform.Block {
+	var (
+		expectedContentBlocks int
+
+		prevMaxEnd = 0
+		children   = make([]*terraform.Block, 0)
+	)
+
+	getForEachCount := func(b *terraform.Block) int {
+		attr := b.GetAttribute("for_each")
+
+		value := attr.Value()
+		if value.IsNull() {
+			return 0
 		}
+
+		slice := value.AsValueSlice()
+		return len(slice)
 	}
 
-	for _, b := range t.modules.GetBlocks() {
-		visitor(b, "")
+	for _, block := range b.AllBlocks() {
+		// track dynamic blocks
+		if block.Type() == "dynamic" {
+			// no reason to track these, they're just templates
+			// track the expected values though
+			forEachCount := getForEachCount(block)
+			expectedContentBlocks += forEachCount
+			continue
+		}
+
+		// deal with normal blocks
+		blockRange := block.GetMetadata().Range()
+		start := blockRange.GetStartLine()
+		if start >= prevMaxEnd {
+			prevMaxEnd = blockRange.GetEndLine()
+			children = append(children, block)
+			continue
+		}
+
+		// once we start reprocessing previous blocks, assume
+		// they're instances of the dynamic templates
+		expectedContentBlocks--
+		if expectedContentBlocks > 0 {
+			children = append(children, block)
+			continue
+		}
 	}
 
-	return jsonOut
+	return children
 }
 
 // NewTerraformConverter creates a new TerraformConverter.
@@ -143,11 +299,12 @@ func (t *terraformConverter) VisitJSON() *gabs.Container {
 // These blocks get extrated as JSON structured data for use by other tools.
 func NewTerraformConverter(filePath string, opts ...TerraformConverterOption) (*terraformConverter, error) {
 	tfc := &terraformConverter{
-		data:          gabs.New(),
 		filePath:      filePath,
 		debug:         false,
 		stopOnError:   false,
 		parserOptions: []options.ParserOption{},
+
+		countsByParentPathBlockName: make(map[string]map[string]int),
 	}
 
 	for _, opt := range opts {
@@ -181,3 +338,57 @@ func (t *terraformConverter) SetDebug() {
 func (t *terraformConverter) SetStopOnHCLError() {
 	t.parserOptions = append(t.parserOptions, parser.OptionStopOnHCLError(true))
 }
+
+func getModuleName(b *terraform.Block) string {
+	// This field is unexported, but necessary to generate the path of the
+	// module. Hopefully aquasecurity/defsec exports this in a future release.
+	moduleBlockV := getPrivateValue(b, "moduleBlock")
+	moduleBlock := moduleBlockV.Interface().(*terraform.Block)
+	if moduleBlock == nil {
+		return ""
+	}
+
+	moduleName := moduleBlock.LocalName()
+	parentName := getModuleName(moduleBlock)
+	if parentName != "" {
+		moduleName = fmt.Sprintf("%s.%s", parentName, moduleName)
+	}
+
+	return moduleName
+}
+
+// getModulePath gets a string describing the module's path, such as
+//"module.notify_slack_qa.module.lambda", which would refer to a module called
+//"lambda", which was included in a module called "notify_slack_qa"
+func (t *terraformConverter) getModulePath(m *terraform.Module) string {
+	prefixes := make(map[string]struct{})
+	for _, b := range m.GetBlocks() {
+		moduleName := getModuleName(b)
+		if moduleName != "" {
+			prefixes[moduleName] = struct{}{}
+		}
+	}
+
+	if len(prefixes) > 1 {
+		panic("found too many prefixes!")
+	}
+
+	for key := range prefixes {
+		return key
+	}
+
+	return ""
+}
+
+// getPath returns a string describing the location of the block.
+// For example, "module.notify_slack_qa.aws_cloudwatch_log_group.lambda[0]"
+//would describe the first item in the "aws_cloudwatch_log_group" resource
+//array called "lambda", which was created inside a module called "notify_slack_qa".
+func (t *terraformConverter) getPath(b *terraform.Block, parentPath string) string {
+	blockName := b.GetMetadata().String()
+	if parentPath == "" {
+		return blockName
+	}
+
+	return fmt.Sprintf("%s.%s", parentPath, blockName)
+}
diff --git a/gotfparse/pkg/converter/hacks.go b/gotfparse/pkg/converter/hacks.go
new file mode 100644
index 0000000..c3ecc05
--- /dev/null
+++ b/gotfparse/pkg/converter/hacks.go
@@ -0,0 +1,19 @@
+// Copyright The Cloud Custodian Authors.
+// SPDX-License-Identifier: Apache-2.0
+package converter
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+// getPrivateValue returns an unexported field in a struct. Call this only
+//when absolutely necessary, as it is a good indication that we're doing
+//something sketchy, and we're making it harder to upgrade to later versions of
+//this library, as we're relying on private implementation details.
+func getPrivateValue(obj interface{}, key string) reflect.Value {
+	objValue := reflect.ValueOf(obj).Elem()
+	field := objValue.FieldByName(key)
+	field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem()
+	return field
+}
diff --git a/gotfparse/pkg/converter/processing.go b/gotfparse/pkg/converter/processing.go
deleted file mode 100644
index 34823b0..0000000
--- a/gotfparse/pkg/converter/processing.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright The Cloud Custodian Authors.
-// SPDX-License-Identifier: Apache-2.0
-package converter
-
-import (
-	"encoding/json"
-	"log"
-	"reflect"
-	"strings"
-
-	"github.com/Jeffail/gabs/v2"
-	"github.com/aquasecurity/defsec/pkg/terraform"
-)
-
-// generateTFMeta generates a structure that contains the values
-// to store as `__tfmeta` for the block in the JSON output. Returns an error
-// if the function is unable to generate metadata for the given block.
-func generateTFMeta(b *terraform.Block) *gabs.Container {
-	r := b.GetMetadata().Range()
-
-	meta := terraformMeta{
-		Filename:  r.GetFilename(),
-		LineStart: r.GetStartLine(),
-		LineEnd:   r.GetEndLine(),
-	}
-	metaJ, _ := json.Marshal(meta)
-	metaP, _ := gabs.ParseJSON(metaJ)
-	return metaP
-}
-
-// collisionFn merges the dest and source objects in a way
-// that allows the underlying HCL to be represented as useful JSON output
-// for the upstream c7n_left interface that wraps tfpase.
-//
-// *This function has side-effects and directly modifies the source.
-func collisionFn(key string) func(d, s interface{}) interface{} {
-	return func(destination, source interface{}) interface{} {
-		if destination == nil {
-			return source
-		}
-
-		switch t := destination.(type) {
-		case map[string]*gabs.Container:
-			s, ok := source.(map[string]interface{})
-			if !ok {
-				log.Fatal("failed to convert source to map[string]intreface{} during map[string]*gabs.Container processing")
-			}
-			for k, v := range s {
-				tc, ok := t[k]
-				if !ok {
-					c := gabs.New()
-					c.Set(v)
-					t[k] = c
-				} else {
-					switch innerT := tc.Data().(type) {
-					case map[string]*gabs.Container:
-						switch s[k].(type) {
-						case map[string]interface{}:
-							keys := strings.Split(key, ".")
-							lastKey := keys[len(keys)-1]
-							srcMap := s[k].(map[string]interface{})
-							v := srcMap[lastKey]
-							if v != nil {
-								c := gabs.New()
-								c.Set(v)
-								innerT[lastKey] = c
-							}
-						default:
-							log.Fatalf("s[k] unhandled type %s", reflect.TypeOf(s[k]))
-						}
-					}
-				}
-			}
-			return t
-		case []interface{}:
-			c, ok := t[len(t)-1].(*gabs.Container)
-			if !ok {
-				log.Fatal("failed to convert array element to *gabs.Container during []interface{} processing")
-			}
-			s, ok := source.(map[string]interface{})
-			if !ok {
-				log.Fatal("failed to convert source to map[string]intreface{} during []interface{} processing")
-			}
-			for k, v := range s {
-				c.SetP(v, k)
-			}
-			return destination
-		default:
-			log.Fatalf("no handler for destination type of %s", reflect.TypeOf(destination))
-		}
-
-		return source
-	}
-}


Test patch:
diff --git a/gotfparse/cmd/tftest/main.go b/gotfparse/cmd/tftest/main.go
index cf8e120..1ffc30d 100644
--- a/gotfparse/cmd/tftest/main.go
+++ b/gotfparse/cmd/tftest/main.go
@@ -7,26 +7,33 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"path/filepath"
 
 	"github.com/cloud-custodian/tfparse/gotfparse/pkg/converter"
 )
 
 func main() {
 	if len(os.Args) < 2 || len(os.Args) > 2 {
-		log.Fatal("expected 1 argument, path")
+		executable := filepath.Base(os.Args[0])
+		log.Fatalf("usage: %s PATH", executable)
 	}
 
 	path := os.Args[1]
 	tfd, err := converter.NewTerraformConverter(path)
-	if err != nil {
-		fmt.Print(err)
-		return
-	}
+	checkError(err)
+
 	data := tfd.VisitJSON().Data()
+
 	j, err := json.MarshalIndent(data, "", "\t")
-	if err != nil {
-		fmt.Print(err)
+	checkError(err)
+
+	fmt.Print(string(j))
+}
+
+func checkError(err error) {
+	if err == nil {
 		return
 	}
-	fmt.Print(string(j))
+
+	panic(err)
 }
diff --git a/tests/terraform/dynamic-stuff/main.tf b/tests/terraform/dynamic-stuff/main.tf
new file mode 100644
index 0000000..b3e04ad
--- /dev/null
+++ b/tests/terraform/dynamic-stuff/main.tf
@@ -0,0 +1,41 @@
+resource "some_resource" "this" {
+  count = 2
+
+  prop1 = "one"
+
+  dynamic "loop_one" {
+    for_each = [true, false, null]
+
+    content {
+      other = each.value
+    }
+  }
+
+  static {
+    name = "first"
+  }
+
+  prop2 = "two"
+
+  dynamic "loop_two" {
+    for_each = [1, 2, 3]
+
+    content {
+      other = each.value
+    }
+  }
+
+  static {
+    name = "second"
+  }
+
+  dynamic "loop_one" {
+    for_each = ["aaa", "bbb", "ccc"]
+
+    content {
+      other = each.value
+    }
+  }
+
+  prop3 = "end"
+}
diff --git a/tests/terraform/moved/main.tf b/tests/terraform/moved/main.tf
new file mode 100644
index 0000000..ab0ffd7
--- /dev/null
+++ b/tests/terraform/moved/main.tf
@@ -0,0 +1,8 @@
+resource "aws_instance" "b" {
+  count = 2
+}
+
+moved {
+  from = aws_instance.a
+  to   = aws_instance.b
+}
diff --git a/tests/terraform/variables-and-locals/main.tf b/tests/terraform/variables-and-locals/main.tf
new file mode 100644
index 0000000..64f5d01
--- /dev/null
+++ b/tests/terraform/variables-and-locals/main.tf
@@ -0,0 +1,29 @@
+locals {
+  hello = "world"
+  bool = true
+  number = 3
+  list = [1, 2, 3]
+  list_count = length(local.list)
+  object = {
+    hello = "world"
+  }
+  complex = {
+    list = [
+      {index = 1},
+      {index = 2},
+      {index = 3},
+    ]
+  }
+}
+
+variable "has_default" {
+  default = "the default"
+}
+
+variable "no_default" {
+  type = string
+}
+
+variable "local_ref" {
+  default = local.bool
+}
diff --git a/tests/test_tfparse.py b/tests/test_tfparse.py
index 11416bc..c6d5758 100644
--- a/tests/test_tfparse.py
+++ b/tests/test_tfparse.py
@@ -1,9 +1,11 @@
+import os.path
 import shutil
 from pathlib import Path
 from unittest.mock import ANY
 
 import pytest
 from pytest_terraform.tf import TerraformRunner
+
 from tfparse import ParseError, load_from_path
 
 
@@ -38,46 +40,77 @@ def test_parse_no_dir(tmp_path):
 def test_parse_vpc_module(tmp_path):
     mod_path = init_module("vpc_module", tmp_path)
     parsed = load_from_path(mod_path)
-    assert "aws_vpc" in parsed
-    assert "aws_subnet" in parsed
+    summary = {resource_type: len(items) for resource_type, items in parsed.items()}
+
+    assert summary == {
+        "aws_eip": 3,
+        "aws_iam_policy_document": 2,
+        "aws_internet_gateway": 1,
+        "aws_nat_gateway": 3,
+        "aws_route": 4,
+        "aws_route_table": 4,
+        "aws_route_table_association": 6,
+        "aws_subnet": 6,
+        "aws_vpc": 1,
+        "aws_vpn_gateway": 1,
+        "locals": 3,
+        "module": 1,
+        "output": 109,
+        "terraform": 1,
+        "variable": 183,
+    }
 
 
 def test_parse_eks(tmp_path):
     mod_path = init_module("eks", tmp_path)
     parsed = load_from_path(mod_path)
+    print(parsed["aws_availability_zones"])
     assert set(parsed) == {
+        "aws_availability_zones",
         "aws_default_route_table",
         "aws_eks_node_group",
         "aws_subnet",
-        "provider",
         "aws_vpc",
         "aws_iam_role_policy_attachment",
         "aws_iam_role",
-        "data",
         "aws_eks_cluster",
         "aws_internet_gateway",
+        "provider",
     }
-    assert set(parsed["aws_subnet"]) == {
-        "cluster_example[1]",
-        "node_group_example[0]",
-        "node_group_example[1]",
-        "cluster_example[0]",
+    assert {item["__tfmeta"]["path"] for item in parsed["aws_subnet"]} == {
+        "aws_subnet.cluster_example[0]",
+        "aws_subnet.cluster_example[1]",
+        "aws_subnet.node_group_example[0]",
+        "aws_subnet.node_group_example[1]",
     }
 
-    assert parsed["aws_eks_cluster"]["example"]["__tfmeta"] == {
+    assert parsed["aws_eks_cluster"][0]["__tfmeta"] == {
         "filename": "main.tf",
+        "label": "aws_eks_cluster",
         "line_start": 1,
         "line_end": 15,
+        "path": "aws_eks_cluster.example",
+        "type": "resource",
     }
 
 
 def test_parse_apprunner(tmp_path):
     mod_path = init_module("apprunner", tmp_path)
     parsed = load_from_path(mod_path)
+
+    image_id = "public.ecr.aws/aws-containers/hello-app-runner:latest"
+
     assert parsed == {
-        "aws_apprunner_service": {
-            "example": {
-                "__tfmeta": {"filename": "main.tf", "line_end": 18, "line_start": 1},
+        "aws_apprunner_service": [
+            {
+                "__tfmeta": {
+                    "filename": "main.tf",
+                    "label": "aws_apprunner_service",
+                    "line_end": 18,
+                    "line_start": 1,
+                    "path": "aws_apprunner_service.example",
+                    "type": "resource",
+                },
                 "id": ANY,
                 "service_name": "example",
                 "source_configuration": {
@@ -104,21 +137,218 @@ def test_parse_apprunner(tmp_path):
                             "id": ANY,
                             "port": "8000",
                         },
-                        "image_identifier": "public.ecr.aws/aws-containers/hello-app-runner:latest",
+                        "image_identifier": image_id,
                         "image_repository_type": "ECR_PUBLIC",
                     },
                 },
                 "tags": {"Name": "example-apprunner-service"},
             }
-        }
+        ]
     }
 
 
 def test_parse_notify_slack(tmp_path):
     mod_path = init_module("notify_slack", tmp_path)
     parsed = load_from_path(mod_path)
-    module = parsed["module"]
-    assert "notify_slack_qa" in module
-    assert "notify_slack_saas" in module
-    assert "lambda" in module
-    assert len(module["lambda"]) == 2
+
+    assert {resource_type: len(items) for resource_type, items in parsed.items()} == {
+        "aws_arn": 2,
+        "aws_caller_identity": 2,
+        "aws_cloudwatch_log_group": 4,
+        "aws_iam_policy": 6,
+        "aws_iam_policy_document": 12,
+        "aws_iam_role": 2,
+        "aws_iam_role_policy_attachment": 2,
+        "aws_lambda_function": 2,
+        "aws_lambda_permission": 4,
+        "aws_partition": 4,
+        "aws_region": 2,
+        "aws_sns_topic": 2,
+        "aws_sns_topic_subscription": 2,
+        "external": 2,
+        "local_file": 2,
+        "locals": 10,
+        "module": 4,
+        "null_resource": 2,
+        "output": 74,
+        "terraform": 4,
+        "variable": 275,
+    }
+
+    assert [m["__tfmeta"]["label"] for m in parsed["module"]] == [
+        "notify_slack_qa",
+        "notify_slack_saas",
+        "lambda",
+        "lambda",
+    ]
+
+
+def test_moved_blocks(tmp_path):
+    mod_path = init_module("moved", tmp_path)
+    parsed = load_from_path(mod_path)
+
+    (item,) = parsed["moved"]
+    assert item["from"] is None
+    assert len(item["to"]) == 2
+
+
+def test_parse_dynamic_content(tmp_path):
+    here = os.path.dirname(__file__)
+    mod_path = os.path.join(here, "terraform", "dynamic-stuff")
+
+    # mod_path = init_module("dynamic-stuff", tmp_path)
+    parsed = load_from_path(mod_path)
+
+    resource = {
+        "__tfmeta": {
+            "filename": "main.tf",
+            "label": "some_resource",
+            "line_end": 41,
+            "line_start": 1,
+            "path": ANY,
+            "type": "resource",
+        },
+        "count": 2,
+        "id": ANY,
+        "prop1": "one",
+        "prop2": "two",
+        "prop3": "end",
+        "loop_one": [
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 11, "line_start": 9},
+                "id": ANY,
+                "other": True,
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 11, "line_start": 9},
+                "id": ANY,
+                "other": False,
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 11, "line_start": 9},
+                "id": ANY,
+                "other": None,
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 37, "line_start": 35},
+                "id": ANY,
+                "other": "aaa",
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 37, "line_start": 35},
+                "id": ANY,
+                "other": "bbb",
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 37, "line_start": 35},
+                "id": ANY,
+                "other": "ccc",
+            },
+        ],
+        "loop_two": [
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 25, "line_start": 23},
+                "id": ANY,
+                "other": 1,
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 25, "line_start": 23},
+                "id": ANY,
+                "other": 2,
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 25, "line_start": 23},
+                "id": ANY,
+                "other": 3,
+            },
+        ],
+        "static": [
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 16, "line_start": 14},
+                "id": ANY,
+                "name": "first",
+            },
+            {
+                "__tfmeta": {"filename": "main.tf", "line_end": 30, "line_start": 28},
+                "id": ANY,
+                "name": "second",
+            },
+        ],
+    }
+
+    assert parsed == {
+        "some_resource": [
+            resource,
+            resource,
+        ],
+    }
+
+
+def test_parse_variables(tmp_path):
+    here = os.path.dirname(__file__)
+    mod_path = os.path.join(here, "terraform", "variables-and-locals")
+
+    # mod_path = init_module("dynamic-stuff", tmp_path)
+    parsed = load_from_path(mod_path)
+    print(parsed)
+    assert parsed == {
+        "locals": [
+            {
+                "__tfmeta": {
+                    "filename": "main.tf",
+                    "line_end": 17,
+                    "line_start": 1,
+                    "path": "locals",
+                },
+                "bool": True,
+                "complex": {
+                    "list": [
+                        {"index": 1},
+                        {"index": 2},
+                        {"index": 3},
+                    ],
+                },
+                "hello": "world",
+                "id": ANY,
+                "list": [1, 2, 3],
+                "list_count": 3,
+                "number": 3,
+                "object": {"hello": "world"},
+            },
+        ],
+        "variable": [
+            {
+                "__tfmeta": {
+                    "filename": "main.tf",
+                    "label": "has_default",
+                    "line_end": 21,
+                    "line_start": 19,
+                    "path": "variable.has_default",
+                },
+                "default": "the default",
+                "id": ANY,
+            },
+            {
+                "__tfmeta": {
+                    "filename": "main.tf",
+                    "label": "local_ref",
+                    "line_end": 29,
+                    "line_start": 27,
+                    "path": "variable.local_ref",
+                },
+                "default": True,
+                "id": ANY,
+            },
+            {
+                "__tfmeta": {
+                    "filename": "main.tf",
+                    "label": "no_default",
+                    "line_end": 25,
+                    "line_start": 23,
+                    "path": "variable.no_default",
+                },
+                "id": ANY,
+                "type": None,
+            },
+        ],
+    }