Repository: tenable/terrascan (language: python)

Issue:
terrascan doesn't resolve terraform complex variables
* terrascan version: 1.4.0
* Operating System: all

### Description

when terraform config has reference to complex variables like list(object), map(object), list(tuple), tuple with object etc, its value is not resolved.

### What I Did

created a terraform config which used complex variables of type specified above.


Discussion / hints:

Fix patch:
diff --git a/pkg/iac-providers/terraform/commons/cty-converters.go b/pkg/iac-providers/terraform/commons/cty-converters.go
index d6d2ed845..09fb094ef 100644
--- a/pkg/iac-providers/terraform/commons/cty-converters.go
+++ b/pkg/iac-providers/terraform/commons/cty-converters.go
@@ -24,9 +24,8 @@ import (
 	"github.com/zclconf/go-cty/cty/gocty"
 )
 
-// list of available cty to golang type converters
+// list of available primitive cty to golang type converters
 var (
-	ctyConverterFuncs       = []func(cty.Value) (interface{}, error){ctyToStr, ctyToInt, ctyToFloat, ctyToBool, ctyToSlice, ctyToMap}
 	ctyNativeConverterFuncs = []func(cty.Value) (interface{}, error){ctyToStr, ctyToInt, ctyToFloat, ctyToBool}
 )
 
@@ -66,14 +65,12 @@ func ctyToSlice(ctyVal cty.Value) (interface{}, error) {
 
 	if ctyVal.Type().IsListType() || ctyVal.Type().IsTupleType() || ctyVal.Type().IsSetType() {
 		for _, v := range ctyVal.AsValueSlice() {
-			for _, converter := range ctyNativeConverterFuncs {
-				resolved, err := converter(v)
-				if err == nil {
-					val = append(val, resolved)
-					break
-				}
+			nativeVal, err := convertCtyToGoNative(v)
+			if err != nil {
 				allErrs = errors.Wrap(allErrs, err.Error())
+				continue
 			}
+			val = append(val, nativeVal)
 		}
 		if allErrs != nil {
 			return nil, allErrs
@@ -107,14 +104,12 @@ func ctyToMap(ctyVal cty.Value) (interface{}, error) {
 	// golang value
 	for k, v := range ctyValMap {
 		// convert cty.Value to native golang type based on cty.Type
-		for _, converter := range ctyNativeConverterFuncs {
-			resolved, err := converter(v)
-			if err == nil {
-				val[k] = resolved
-				break
-			}
+		nativeVal, err := convertCtyToGoNative(v)
+		if err != nil {
 			allErrs = errors.Wrap(allErrs, err.Error())
+			continue
 		}
+		val[k] = nativeVal
 	}
 	if allErrs != nil {
 		return nil, allErrs
@@ -123,3 +118,29 @@ func ctyToMap(ctyVal cty.Value) (interface{}, error) {
 	// hopefully successful!
 	return val, nil
 }
+
+// convertCtyToGoNative converts a cty.Value to its go native type
+func convertCtyToGoNative(ctyVal cty.Value) (interface{}, error) {
+	if ctyVal.Type().IsPrimitiveType() {
+		return convertPrimitiveType(ctyVal)
+	}
+	return convertComplexType(ctyVal)
+}
+
+// convertPrimitiveType converts a primitive cty.Value to its go native type
+func convertPrimitiveType(ctyVal cty.Value) (interface{}, error) {
+	for _, converter := range ctyNativeConverterFuncs {
+		if val, err := converter(ctyVal); err == nil {
+			return val, err
+		}
+	}
+	return nil, fmt.Errorf("ctyVal could not be resolved to native go type")
+}
+
+// convertPrimitiveType converts a complex cty.Value (list, tuple, set, map, object) to its go native type
+func convertComplexType(ctyVal cty.Value) (interface{}, error) {
+	if ctyVal.Type().IsListType() || ctyVal.Type().IsTupleType() || ctyVal.Type().IsSetType() {
+		return ctyToSlice(ctyVal)
+	}
+	return ctyToMap(ctyVal)
+}
diff --git a/pkg/iac-providers/terraform/commons/module-references.go b/pkg/iac-providers/terraform/commons/module-references.go
index ea4027360..3c519140d 100644
--- a/pkg/iac-providers/terraform/commons/module-references.go
+++ b/pkg/iac-providers/terraform/commons/module-references.go
@@ -93,22 +93,23 @@ func (r *RefResolver) ResolveModuleRef(moduleRef string,
 
 	// default value is of cty.Value type, convert it to native golang type
 	// based on cty.Type, determine golang type
-	for _, converter := range ctyConverterFuncs {
-		if val, err := converter(hclVar.Default); err == nil {
-			zap.S().Debugf("resolved module variable ref: '%v', value: '%v'", moduleRef, val)
-
-			// replace the variable reference string with actual value
-			if reflect.TypeOf(val).Kind() == reflect.String {
-				valStr := val.(string)
-				resolvedVal := strings.Replace(moduleRef, moduleExpr, valStr, 1)
-				zap.S().Debugf("resolved str module value ref: '%v', value: '%v'", moduleRef, resolvedVal)
-				return r.ResolveStrRef(resolvedVal)
-			}
-			zap.S().Debugf("resolved module value ref: '%v', value: '%v'", moduleRef, val)
-			return val
+	val, err := convertCtyToGoNative(hclVar.Default)
+	if err != nil {
+		zap.S().Debugf(err.Error())
+		zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
+		return moduleRef
+	}
+	zap.S().Debugf("resolved variable ref '%v', value: '%v'", moduleRef, val)
+
+	if reflect.TypeOf(val).Kind() == reflect.String {
+		valStr := val.(string)
+		resolvedVal := strings.Replace(moduleRef, moduleExpr, valStr, 1)
+		if moduleRef == resolvedVal {
+			zap.S().Debugf("resolved str variable ref refers to self: '%v'", moduleRef)
+			return moduleRef
 		}
+		zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", moduleRef, resolvedVal)
+		return r.ResolveStrRef(resolvedVal)
 	}
-	zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
-
-	return moduleRef
+	return val
 }
diff --git a/pkg/iac-providers/terraform/commons/variable-references.go b/pkg/iac-providers/terraform/commons/variable-references.go
index c68e8f780..56443bc9d 100644
--- a/pkg/iac-providers/terraform/commons/variable-references.go
+++ b/pkg/iac-providers/terraform/commons/variable-references.go
@@ -79,29 +79,25 @@ func (r *RefResolver) ResolveVarRef(varRef string) interface{} {
 
 	// default value is of cty.Value type, convert it to native golang type
 	// based on cty.Type, determine golang type
-	for _, converter := range ctyConverterFuncs {
-		if val, err := converter(hclVar.Default); err == nil {
-			zap.S().Debugf("resolved variable ref '%v', value: '%v'", varRef, val)
-
-			// replace the variable reference string with actual value
-			if reflect.TypeOf(val).Kind() == reflect.String {
-				valStr := val.(string)
-				resolvedVal := strings.Replace(varRef, varExpr, valStr, 1)
-				if varRef == resolvedVal {
-					zap.S().Debugf("resolved str variable ref refers to self: '%v'", varRef)
-					return varRef
-				}
-				zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", varRef, resolvedVal)
-				return r.ResolveStrRef(resolvedVal)
-			}
-			zap.S().Debugf("resolved variable ref: '%v', value: '%v'", varRef, val)
-			return val
-		}
+	val, err := convertCtyToGoNative(hclVar.Default)
+	if err != nil {
+		zap.S().Debugf(err.Error())
+		zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
+		return varRef
 	}
-	zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
+	zap.S().Debugf("resolved variable ref '%v', value: '%v'", varRef, val)
 
-	// return original reference if cty conversion attempts fail
-	return varRef
+	if reflect.TypeOf(val).Kind() == reflect.String {
+		valStr := val.(string)
+		resolvedVal := strings.Replace(varRef, varExpr, valStr, 1)
+		if varRef == resolvedVal {
+			zap.S().Debugf("resolved str variable ref refers to self: '%v'", varRef)
+			return varRef
+		}
+		zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", varRef, resolvedVal)
+		return r.ResolveStrRef(resolvedVal)
+	}
+	return val
 }
 
 // ResolveVarRefFromParentModuleCall returns the variable value as configured in


Test patch:
diff --git a/pkg/iac-providers/terraform/commons/cty-converters_test.go b/pkg/iac-providers/terraform/commons/cty-converters_test.go
index 7ef4f8376..540ed932e 100644
--- a/pkg/iac-providers/terraform/commons/cty-converters_test.go
+++ b/pkg/iac-providers/terraform/commons/cty-converters_test.go
@@ -503,3 +503,206 @@ func TestCtyToMap(t *testing.T) {
 		})
 	}
 }
+
+func TestConvertPrimitiveType(t *testing.T) {
+	intCtyVal := cty.NumberIntVal(-10)
+	floatCtyVal := cty.NumberFloatVal(3.141)
+	unsignedIntCtyVal := cty.NumberUIntVal(44)
+	stringCtyVal := cty.StringVal("Peter Parker")
+	boolCtyVal := cty.BoolVal(true)
+	nonPrimitiveCtyVal := cty.ListVal([]cty.Value{cty.StringVal("test")})
+
+	type args struct {
+		ctyVal cty.Value
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			name: "signed int input",
+			args: args{
+				ctyVal: intCtyVal,
+			},
+			want: -10,
+		},
+		{
+			name: "float input",
+			args: args{
+				ctyVal: floatCtyVal,
+			},
+			want: 3.141,
+		},
+		{
+			name: "unsigned int input",
+			args: args{
+				ctyVal: unsignedIntCtyVal,
+			},
+			want: 44,
+		},
+		{
+			name: "string input",
+			args: args{
+				ctyVal: stringCtyVal,
+			},
+			want: "Peter Parker",
+		},
+		{
+			name: "boolean input",
+			args: args{
+				ctyVal: boolCtyVal,
+			},
+			want: true,
+		},
+		{
+			name: "non primitive input",
+			args: args{
+				ctyVal: nonPrimitiveCtyVal,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := convertPrimitiveType(tt.args.ctyVal)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("convertPrimitiveType() got error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("convertPrimitiveType() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestConvertComplexType(t *testing.T) {
+
+	nonComplexCtyVal := cty.NumberIntVal(5)
+	complexListCtyVal := cty.ListVal([]cty.Value{
+		cty.ObjectVal(map[string]cty.Value{
+			"application": cty.StringVal("app1"),
+			"port":        cty.NumberIntVal(9010),
+		}),
+		cty.ObjectVal(map[string]cty.Value{
+			"application": cty.StringVal("app2"),
+			"port":        cty.NumberIntVal(8000),
+		}),
+	})
+	complexMapCtyVal := cty.MapVal(map[string]cty.Value{
+		"first": cty.ObjectVal(map[string]cty.Value{
+			"name": cty.StringVal("Thor"),
+			"ID":   cty.NumberIntVal(1),
+		}),
+		"second": cty.ObjectVal(map[string]cty.Value{
+			"name": cty.StringVal("Spiderman"),
+			"ID":   cty.NumberIntVal(2),
+		}),
+	})
+
+	type args struct {
+		ctyVal cty.Value
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			name: "non complex input",
+			args: args{
+				ctyVal: nonComplexCtyVal,
+			},
+			wantErr: true,
+		},
+		{
+			name: "list of objects input",
+			args: args{
+				ctyVal: complexListCtyVal,
+			},
+			want: []interface{}{
+				map[string]interface{}{
+					"application": "app1",
+					"port":        9010,
+				},
+				map[string]interface{}{
+					"application": "app2",
+					"port":        8000,
+				},
+			},
+		},
+		{
+			name: "map of objects input",
+			args: args{
+				ctyVal: complexMapCtyVal,
+			},
+			want: map[string]interface{}{
+				"first": map[string]interface{}{
+					"name": "Thor",
+					"ID":   1,
+				},
+				"second": map[string]interface{}{
+					"name": "Spiderman",
+					"ID":   2,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := convertComplexType(tt.args.ctyVal)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("convertComplexType() got error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("convertComplexType() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestConvertCtyToGoNative(t *testing.T) {
+	primitiveCtyVal := cty.StringVal("primitive")
+	complexCtyVal := cty.ListVal([]cty.Value{cty.BoolVal(false)})
+
+	type args struct {
+		ctyVal cty.Value
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			name: "primitive cty input",
+			args: args{
+				ctyVal: primitiveCtyVal,
+			},
+			want: "primitive",
+		},
+		{
+			name: "complex cty input",
+			args: args{
+				ctyVal: complexCtyVal,
+			},
+			want: []interface{}{false},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := convertCtyToGoNative(tt.args.ctyVal)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("convertCtyToGoNative() got error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("convertCtyToGoNative() want = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/iac-providers/terraform/v14/load-dir_test.go b/pkg/iac-providers/terraform/v14/load-dir_test.go
index 7d4972836..2121d1b38 100644
--- a/pkg/iac-providers/terraform/v14/load-dir_test.go
+++ b/pkg/iac-providers/terraform/v14/load-dir_test.go
@@ -112,6 +112,13 @@ func TestLoadIacDir(t *testing.T) {
 			tfv14:       TfV14{},
 			wantErr:     nil,
 		},
+		{
+			name:        "complex variables",
+			tfConfigDir: filepath.Join(testDataDir, "complex-variables"),
+			tfJSONFile:  filepath.Join(tfJSONDir, "complex-variables.json"),
+			tfv14:       TfV14{},
+			wantErr:     nil,
+		},
 	}
 
 	for _, tt := range table2 {
diff --git a/pkg/iac-providers/terraform/v14/testdata/complex-variables/main.tf b/pkg/iac-providers/terraform/v14/testdata/complex-variables/main.tf
new file mode 100644
index 000000000..20f4f6307
--- /dev/null
+++ b/pkg/iac-providers/terraform/v14/testdata/complex-variables/main.tf
@@ -0,0 +1,16 @@
+resource "complex_var_resource" "complex" {
+  stringList = var.stringList
+  intList = var.intList
+  floatList = var.floatList
+  boolList = var.boolList
+  list_no_type = var.list_no_type
+  setVar = var.set_var
+  tupleVar = var.tuple_var
+  listTuple = var.list_of_tuple
+  tupleVarComplex = var.tuple_var_complex
+  objecVar = var.object_var
+  mapVar = var.map_var
+  mapVarComplex = var.map_var_complex
+  objectList = var.object_list
+  objectListComplex = var.object_list_complex
+}
\ No newline at end of file
diff --git a/pkg/iac-providers/terraform/v14/testdata/complex-variables/variables.tf b/pkg/iac-providers/terraform/v14/testdata/complex-variables/variables.tf
new file mode 100644
index 000000000..75cbe2157
--- /dev/null
+++ b/pkg/iac-providers/terraform/v14/testdata/complex-variables/variables.tf
@@ -0,0 +1,136 @@
+variable "stringList" {
+  type = list(string)
+  default = ["one", "two", "three"]
+}
+
+variable "intList" {
+  type = list(number)
+  default = [1, 2, 3]
+}
+
+variable "floatList" {
+  type = list(number)
+  default = [1.01, 2.02, 3.03]
+}
+
+variable "boolList" {
+  type = list(bool)
+  default = [true, true, false, true, false]
+}
+
+variable "list_no_type" {
+  default = [1,2]
+}
+
+variable "set_var" {
+  type = set(string)
+  default = ["first","second"]
+}
+
+variable "tuple_var" {
+  type = tuple([string, number, bool])
+  default = ["one", 1, true]
+}
+
+variable "list_of_tuple" {
+  type = list(tuple([string, number, bool]))
+  default = [
+    ["one", 1, true],
+    ["two", 2, false]
+    ]
+}
+
+variable "tuple_var_complex" {
+  type = tuple([number, object({
+    field1 = number
+    field2 = number
+  })])
+
+  default = [10, {
+    field1 = 11
+    field2 = 12
+  }]
+}
+
+variable "object_var" {
+  type = object({
+    name    = string
+    address = string
+  })
+  default = {
+      name = "pankaj"
+      address = "pune"
+  }
+}
+
+variable "map_var" {
+  type = map
+  default = {
+    "5USD"  = "1xCPU-1GB"
+    "10USD" = "1xCPU-2GB"
+    "20USD" = "2xCPU-4GB"
+  }
+}
+
+variable "map_var_complex" {
+  type = map(object({
+    name = string
+    ID = number
+  }))
+  default = {
+    "first"  = {
+      name = "Thor"
+      ID = 1
+    }
+    "second" = {
+      name = "Antman"
+      ID = 2
+    }
+  }
+}
+
+variable "object_list" {
+  type = list(object({
+    internal = number
+    external = number
+    protocol = string
+  }))
+  default = [
+    {
+      internal = 8300
+      external = 8300
+      protocol = "tcp"
+    },
+    {
+      internal = 4000
+      external = 3000
+      protocol = "udp"
+    }
+  ]
+}
+
+variable "object_list_complex" {
+  type = list(object({
+    key1 = list(number)
+    key2 = object({
+      port = number
+    })
+    key3 = map(string)
+    key4 = map(number)
+  }))
+  default = [
+    {
+      key1 = [1, 2, 3]
+      key2 = {
+        port = 9010
+      }
+      key3 = {
+        "name": "hero"
+      }
+      key4 = {
+        "first": 11.23
+        "second": 50
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json b/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json
new file mode 100644
index 000000000..a46c5c4d5
--- /dev/null
+++ b/pkg/iac-providers/terraform/v14/testdata/tfjson/complex-variables.json
@@ -0,0 +1,118 @@
+{
+    "complex_var_resource": [
+      {
+        "id": "complex_var_resource.complex",
+        "name": "complex",
+        "source": "main.tf",
+        "line": 1,
+        "type": "complex_var_resource",
+        "config": {
+          "boolList": [
+            true,
+            true,
+            false,
+            true,
+            false
+          ],
+          "floatList": [
+            1.01,
+            2.02,
+            3.03
+          ],
+          "intList": [
+            1,
+            2,
+            3
+          ],
+          "listTuple": [
+            [
+              "one",
+              1,
+              true
+            ],
+            [
+              "two",
+              2,
+              false
+            ]
+          ],
+          "list_no_type": [
+            1,
+            2
+          ],
+          "mapVar": {
+            "10USD": "1xCPU-2GB",
+            "20USD": "2xCPU-4GB",
+            "5USD": "1xCPU-1GB"
+          },
+          "mapVarComplex": {
+            "first": {
+              "ID": 1,
+              "name": "Thor"
+            },
+            "second": {
+              "ID": 2,
+              "name": "Antman"
+            }
+          },
+          "objecVar": {
+            "address": "pune",
+            "name": "pankaj"
+          },
+          "objectList": [
+            {
+              "external": 8300,
+              "internal": 8300,
+              "protocol": "tcp"
+            },
+            {
+              "external": 3000,
+              "internal": 4000,
+              "protocol": "udp"
+            }
+          ],
+          "objectListComplex": [
+            {
+              "key1": [
+                1,
+                2,
+                3
+              ],
+              "key2": {
+                "port": 9010
+              },
+              "key3": {
+                "name": "hero"
+              },
+              "key4": {
+                "first": 11.23,
+                "second": 50
+              }
+            }
+          ],
+          "setVar": [
+            "first",
+            "second"
+          ],
+          "stringList": [
+            "one",
+            "two",
+            "three"
+          ],
+          "tupleVar": [
+            "one",
+            1,
+            true
+          ],
+          "tupleVarComplex": [
+            10,
+            {
+              "field1": 11,
+              "field2": 12
+            }
+          ]
+        },
+        "skip_rules": null
+      }
+    ]
+  }
\ No newline at end of file