Repository: tenable/terrascan (language: python)

Issue:
Can't parse Terraform variable field: `nullable`
* terrascan version: v1.13.2
* Operating System: macOS 12.2.1

### Description

After adding `nullable = false` to a variable definition, `terrascan` can no longer parse the file.

See https://www.terraform.io/language/values/variables#disallowing-null-input-values for documentation of the field.

Here is a minimal example:

```
terraform {
}

variable "in" {
  type     = string
  nullable = false
}

resource "null_resource" "test_resource" {
  triggers = {
    input_value = var.in
  }
}
```

### What I Did

Terraform validates OK:

```
$ terraform validate
Success! The configuration is valid.
```

The terrascan scan also _appears_ to be successful:

```
$ terrascan scan --iac-type terraform
Scan Summary -

	File/Folder         :	/path/to/terrascan-test
	IaC Type            :	terraform
	Scanned At          :	2022-03-02 09:19:21.960337 +0000 UTC
	Policies Validated  :	786
	Violated Policies   :	0
	Low                 :	0
	Medium              :	0
	High                :	0
```

However the process exited with code 4 (“scan summary has errors but no violations” – https://runterrascan.io/docs/usage/command_line_mode/):

```
$ echo $?
4
```

Enabling debug logging shows the error:

```
$ terrascan scan --iac-type terraform --log-level debug

2022-03-02T09:29:52.023Z	debug	commons/load-dir.go:146	failed to load terraform config dir '/path/to/terrascan-test'. error from terraform:
/path/to/terrascan-test/main.tf:6,3-11: Unsupported argument; An argument named "nullable" is not expected here.
```



Discussion / hints:

Fix patch:
diff --git a/pkg/iac-providers/terraform/commons/load-dir.go b/pkg/iac-providers/terraform/commons/load-dir.go
index 77d208c57..4d73dc10b 100644
--- a/pkg/iac-providers/terraform/commons/load-dir.go
+++ b/pkg/iac-providers/terraform/commons/load-dir.go
@@ -140,7 +140,7 @@ func (t TerraformDirectoryLoader) loadDirRecursive(dirList []string) (output.All
 
 		// load current config directory
 		rootMod, diags := t.parser.LoadConfigDir(dir)
-		if diags.HasErrors() {
+		if rootMod == nil && diags.HasErrors() {
 			// log a debug message and continue with other directories
 			errMessage := fmt.Sprintf("failed to load terraform config dir '%s'. error from terraform:\n%+v\n", dir, getErrorMessagesFromDiagnostics(diags))
 			zap.S().Debug(errMessage)
@@ -148,6 +148,14 @@ func (t TerraformDirectoryLoader) loadDirRecursive(dirList []string) (output.All
 			continue
 		}
 
+		// rootMod can be considered for static analysis
+		if rootMod != nil && diags.HasErrors() {
+			// log a debug message and continue with analysis of the root mod.
+			errMessage := fmt.Sprintf("diagnostic errors while loading terraform config dir '%s'. error from terraform:\n%+v\n", dir, getErrorMessagesFromDiagnostics(diags))
+			zap.S().Debug(errMessage)
+			t.addError(errMessage, dir)
+		}
+
 		// get unified config for the current directory
 		unified, diags := t.buildUnifiedConfig(rootMod, dir)
 		// Get the downloader chache
@@ -257,13 +265,21 @@ func (t TerraformDirectoryLoader) loadDirNonRecursive() (output.AllResourceConfi
 
 	// load current config directory
 	rootMod, diags := t.parser.LoadConfigDir(t.absRootDir)
-	if diags.HasErrors() {
+	if rootMod == nil && diags.HasErrors() {
 		// log a debug message and continue with other directories
 		errMessage := fmt.Sprintf("failed to load terraform config dir '%s'. error from terraform:\n%+v\n", t.absRootDir, getErrorMessagesFromDiagnostics(diags))
 		zap.S().Debug(errMessage)
 		return nil, multierror.Append(t.errIacLoadDirs, results.DirScanErr{IacType: "terraform", Directory: t.absRootDir, ErrMessage: errMessage})
 	}
 
+	// rootMod can be considered for static analysis
+	if rootMod != nil && diags.HasErrors() {
+		// log a debug message and continue with analysis of the root mod.
+		errMessage := fmt.Sprintf("diagnostic errors while loading terraform config dir '%s'. error from terraform:\n%+v\n", t.absRootDir, getErrorMessagesFromDiagnostics(diags))
+		zap.S().Debug(errMessage)
+		t.addError(errMessage, t.absRootDir)
+	}
+
 	// get unified config for the current directory
 	unified, diags := t.buildUnifiedConfig(rootMod, t.absRootDir)
 


Test patch:
diff --git a/pkg/iac-providers/terraform/v12/load-dir_test.go b/pkg/iac-providers/terraform/v12/load-dir_test.go
index 86de9ea30..c16dffb25 100644
--- a/pkg/iac-providers/terraform/v12/load-dir_test.go
+++ b/pkg/iac-providers/terraform/v12/load-dir_test.go
@@ -37,14 +37,14 @@ func TestLoadIacDir(t *testing.T) {
 	destroyProvisionersDir := filepath.Join(testDataDir, "destroy-provisioners")
 	destroyProvisionersMainFile := filepath.Join(destroyProvisionersDir, "main.tf")
 
-	testErrorString1 := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
+	testErrorString1 := fmt.Sprintf(`diagnostic errors while loading terraform config dir '%s'. error from terraform:
 %s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header.
 %s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here.
 `, testDataDir, emptyTfFilePath, emptyTfFilePath)
 
 	multipleProvidersDir := filepath.Join(testDataDir, "multiple-required-providers")
 
-	testErrorString2 := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
+	testErrorString2 := fmt.Sprintf(`diagnostic errors while loading terraform config dir '%s'. error from terraform:
 %s:2,3-21: Duplicate required providers configuration; A module may have only one required providers configuration. The required providers were previously configured at %s:2,3-21.
 `, multipleProvidersDir, filepath.Join(multipleProvidersDir, "b.tf"), filepath.Join(multipleProvidersDir, "a.tf"))
 
@@ -52,7 +52,7 @@ func TestLoadIacDir(t *testing.T) {
 <nil>: Failed to read module directory; Module directory %s does not exist or cannot be read.
 `, filepath.Join(testDataDir, "invalid-moduleconfigs", "cloudfront", "sub-cloudfront"))
 
-	errStringDestroyProvisioners := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
+	errStringDestroyProvisioners := fmt.Sprintf(`diagnostic errors while loading terraform config dir '%s'. error from terraform:
 %s:8,12-22: Invalid reference from destroy provisioner; Destroy-time provisioners and their connection configurations may only reference attributes of the related resource, via 'self', 'count.index', or 'each.key'.
 
 References to other resources during the destroy phase can cause dependency cycles and interact poorly with create_before_destroy.
diff --git a/pkg/iac-providers/terraform/v14/load-dir_test.go b/pkg/iac-providers/terraform/v14/load-dir_test.go
index 3384385af..118a7d287 100644
--- a/pkg/iac-providers/terraform/v14/load-dir_test.go
+++ b/pkg/iac-providers/terraform/v14/load-dir_test.go
@@ -34,7 +34,7 @@ import (
 func TestLoadIacDir(t *testing.T) {
 	var nilMultiErr *multierror.Error = nil
 
-	testErrorMessage := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
+	testErrorMessage := fmt.Sprintf(`diagnostic errors while loading terraform config dir '%s'. error from terraform:
 %s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header.
 %s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here.
 `, testDataDir, emptyTfFilePath, emptyTfFilePath)
diff --git a/pkg/iac-providers/terraform/v15/load-dir_test.go b/pkg/iac-providers/terraform/v15/load-dir_test.go
index e8c4bad2b..8771cd0a1 100644
--- a/pkg/iac-providers/terraform/v15/load-dir_test.go
+++ b/pkg/iac-providers/terraform/v15/load-dir_test.go
@@ -34,7 +34,7 @@ import (
 func TestLoadIacDir(t *testing.T) {
 	var nilMultiErr *multierror.Error = nil
 
-	testErrorMessage := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
+	testErrorMessage := fmt.Sprintf(`diagnostic errors while loading terraform config dir '%s'. error from terraform:
 %s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header.
 %s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here.
 `, testDataDir, emptyTfFilePath, emptyTfFilePath)