Repository: aquasecurity/tfsec (go)

Issue:
Running tfsec multiple times gives different results on AWS099
**Describe the bug**

My pipeline sometimes fails on `aws-iam-no-policy-wildcards (AWS099)`.
I run tfsec with the following arguments: `tfsec --config-file .tfsec.yml`

But the strangest is: If I run my pipeline again, it succeeds without errors.... 

Running tfsec outside the ci pipeline on my local machine, (in both ci and local I run version `v0.56.0`), I get the same behaviour: If I run tfsec 10 times, it fails about 2 times and succeeds 8 times on the same code block. 

In some cases the error is printed several times (as different problems) for the same codeblock, rather than just only ones.

Output of tfsec failing on KMS resource policy:
```
  Result 1
  [aws-iam-no-policy-wildcards][HIGH] Resource 'data.aws_iam_policy_document' defines a policy with wildcarded actions.
  /builds/<org>/devops/terraform/modules/tf-eks-cluster/main.tf:31
      28 |         data.aws_caller_identity.this.account_id
      29 |       )
      30 |     ]
      31 |     actions = ["kms:*"]    list: ["kms:*"]
      32 | 
      33 |     principals {
      34 |       type = "AWS"
  Legacy ID:  AWS099
  Impact:     Overly permissive policies may grant access to sensitive resources
  Resolution: Specify the exact permissions required, and to which resources they should apply instead of using wildcards.
  More Info:
  - https://tfsec.dev/docs/aws/iam/no-policy-wildcards#aws/iam 
  - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document 
  - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html 
  1 potential problems detected.
```

My .tfsec config file is "empty": 
```
exclude: []
```

**Expected behavior**
The error is either shown every time when I run tfsec, or the error is not shown because it is OK or in the exclude list.

**Desktop (please complete the following information):**
 - OS: both on linux and mac osx

**Additional context**
I suspect this only appears when using multiple files and/or with use of external terraform modules, because when I try to reproduce this with a very small test file that exactly replicates the code it fails on, no errors appear, even after running it multiple times.


The codeblock it seems to fail on: 

```
data "aws_region" "this" {}

data "aws_caller_identity" "this" {}

data "aws_iam_policy_document" "this" {
  statement {
    sid    = "Allow Amazon EKS use of the key"
    effect = "Allow"
    resources = [
      format("arn:aws:kms:%s:%s:key/*",
        data.aws_region.this.name,
        data.aws_caller_identity.this.account_id
      )
    ]
    actions = [
      "kms:DescribeKey",
      "kms:CreateGrant",
    ]

    principals {
      type        = "Service"
      identifiers = ["eks.amazonaws.com"]
    }
  }
  statement {
    sid    = "Enable IAM User Permissions"
    effect = "Allow"
    resources = [
      format("arn:aws:kms:%s:%s:key/*",
        data.aws_region.this.name,
        data.aws_caller_identity.this.account_id
      )
    ]
    actions = ["kms:*"]

    principals {
      type = "AWS"
      identifiers = [
        format("arn:aws:iam::%s:root", data.aws_caller_identity.this.account_id)
      ]
    }
  }
}

resource "aws_kms_key" "this" {
  deletion_window_in_days = 30
  enable_key_rotation     = true
  policy                  = data.aws_iam_policy_document.this.json
}
```

Pull request:
Added multiple module tests and fixed unique identifiers for non-resource blocks
Resolves #1003

Patch:
diff --git a/Makefile b/Makefile
index f24e3785b..84f99caa1 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ image:
 .PHONY: test
 test:
 	which gotestsum || (pushd /tmp && go install gotest.tools/gotestsum@latest && popd)
-	GOMOD=vendor gotestsum
+	gotestsum -- --mod=vendor -race ./...
 
 .PHONY: build
 build:
@@ -80,3 +80,7 @@ pr-lint:
 
 .PHONY: pr-ready
 pr-ready: quality sanity pr-lint typos
+
+.PHONY: bench
+bench:
+	go test -run ^$$ -bench . ./...
diff --git a/internal/app/tfsec/block/reference.go b/internal/app/tfsec/block/reference.go
index 279ad04de..fd7a4df96 100644
--- a/internal/app/tfsec/block/reference.go
+++ b/internal/app/tfsec/block/reference.go
@@ -30,7 +30,7 @@ func newReference(parts []string) (*Reference, error) {
 
 	ref.blockType = *blockType
 
-	if ref.blockType.removeTypeInReference {
+	if ref.blockType.removeTypeInReference && parts[0] != blockType.name {
 		ref.typeLabel = parts[0]
 		if len(parts) > 1 {
 			ref.nameLabel = parts[1]
@@ -40,6 +40,9 @@ func newReference(parts []string) (*Reference, error) {
 			ref.typeLabel = parts[1]
 			if len(parts) > 2 {
 				ref.nameLabel = parts[2]
+			} else {
+				ref.nameLabel = ref.typeLabel
+				ref.typeLabel = ""
 			}
 		}
 	}
@@ -74,7 +77,13 @@ func (r *Reference) String() string {
 	base := fmt.Sprintf("%s.%s", r.typeLabel, r.nameLabel)
 
 	if !r.blockType.removeTypeInReference {
-		base = fmt.Sprintf("%s.%s.%s", r.blockType.Name(), r.typeLabel, r.nameLabel)
+		base = r.blockType.Name()
+		if r.typeLabel != "" {
+			base += "." + r.typeLabel
+		}
+		if r.nameLabel != "" {
+			base += "." + r.nameLabel
+		}
 	}
 
 	if r.key != "" {
diff --git a/internal/app/tfsec/block/type.go b/internal/app/tfsec/block/type.go
index 9dc6496d3..05ee86f26 100644
--- a/internal/app/tfsec/block/type.go
+++ b/internal/app/tfsec/block/type.go
@@ -85,7 +85,7 @@ func IsValidBlockReference(name string) bool {
 
 func TypeFromRefName(name string) (*Type, error) {
 	for _, valid := range ValidTypes {
-		if valid.refName == name {
+		if valid.refName == name || (valid.refName == "" && valid.name == name) {
 			return &valid, nil
 		}
 	}
diff --git a/internal/app/tfsec/scanner/scanner.go b/internal/app/tfsec/scanner/scanner.go
index 7c7a274d4..9db6ef88c 100644
--- a/internal/app/tfsec/scanner/scanner.go
+++ b/internal/app/tfsec/scanner/scanner.go
@@ -1,6 +1,8 @@
 package scanner
 
 import (
+	"sort"
+
 	"github.com/aquasecurity/tfsec/pkg/result"
 	"github.com/aquasecurity/tfsec/pkg/severity"
 
@@ -90,5 +92,15 @@ func (scanner *Scanner) Scan(blocks []block.Block) []result.Result {
 			}(&r)
 		}
 	}
+	sort.Slice(results, func(i, j int) bool {
+		switch {
+		case results[i].RuleID < results[j].RuleID:
+			return true
+		case results[i].RuleID > results[j].RuleID:
+			return false
+		default:
+			return results[i].HashCode() > results[j].HashCode()
+		}
+	})
 	return results
 }


Test patch:
diff --git a/internal/app/tfsec/block/reference_test.go b/internal/app/tfsec/block/reference_test.go
new file mode 100644
index 000000000..cc0cebdf1
--- /dev/null
+++ b/internal/app/tfsec/block/reference_test.go
@@ -0,0 +1,52 @@
+package block
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_ReferenceParsing(t *testing.T) {
+	cases := []struct {
+		input    []string
+		expected string
+	}{
+		{
+			input:    []string{"module", "my-mod"},
+			expected: "module.my-mod",
+		},
+		{
+			input:    []string{"aws_s3_bucket", "test"},
+			expected: "aws_s3_bucket.test",
+		},
+		{
+			input:    []string{"resource", "aws_s3_bucket", "test"},
+			expected: "aws_s3_bucket.test",
+		},
+		{
+			input:    []string{"module", "my-mod"},
+			expected: "module.my-mod",
+		},
+		{
+			input:    []string{"data", "aws_iam_policy_document", "s3_policy"},
+			expected: "data.aws_iam_policy_document.s3_policy",
+		},
+		{
+			input:    []string{"provider", "aws"},
+			expected: "provider.aws",
+		},
+		{
+			input:    []string{"output", "something"},
+			expected: "output.something",
+		},
+	}
+
+	for _, test := range cases {
+		t.Run(test.expected, func(t *testing.T) {
+			ref, err := newReference(test.input)
+			require.NoError(t, err)
+			assert.Equal(t, test.expected, ref.String())
+		})
+	}
+}
diff --git a/internal/app/tfsec/test/module_scan_test.go b/internal/app/tfsec/test/module_scan_test.go
index 4ee388ec5..f57a5360a 100644
--- a/internal/app/tfsec/test/module_scan_test.go
+++ b/internal/app/tfsec/test/module_scan_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
+	"github.com/stretchr/testify/require"
 
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/hclcontext"
@@ -15,76 +16,457 @@ import (
 	"github.com/aquasecurity/tfsec/pkg/severity"
 )
 
-func Test_ProblemInModule(t *testing.T) {
-
-	rule := rule.Rule{
-		LegacyID:  "EXA001",
-		Provider:  provider.AWSProvider,
-		Service:   "service",
-		ShortCode: "abc",
-		Documentation: rule.RuleDocumentation{
-			Summary:     "A stupid example check for a test.",
-			Impact:      "You will look stupid",
-			Resolution:  "Don't do stupid stuff",
-			Explanation: "Bad should not be set.",
-			BadExample: []string{`
+var badRule = rule.Rule{
+	LegacyID:  "EXA001",
+	Provider:  provider.AWSProvider,
+	Service:   "service",
+	ShortCode: "abc",
+	Documentation: rule.RuleDocumentation{
+		Summary:     "A stupid example check for a test.",
+		Impact:      "You will look stupid",
+		Resolution:  "Don't do stupid stuff",
+		Explanation: "Bad should not be set.",
+		BadExample: []string{`
 resource "problem" "x" {
-	bad = "1"
+bad = "1"
 }
 `},
-			GoodExample: []string{`
+		GoodExample: []string{`
 resource "problem" "x" {
-	
+
 }
 `},
-			Links: nil,
-		},
-		RequiredTypes:   []string{"resource"},
-		RequiredLabels:  []string{"problem"},
-		DefaultSeverity: severity.High,
-		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
-			if resourceBlock.GetAttribute("bad") != nil {
-				set.AddResult().
-					WithDescription("example problem")
-			}
-		},
-	}
-	scanner.RegisterCheckRule(rule)
-	defer scanner.DeregisterCheckRule(rule)
-
-	var tests = []struct {
-		name                  string
-		source                string
-		moduleSource          string
-		mustIncludeResultCode string
-		mustExcludeResultCode string
-	}{
-		{
-			name: "check problem in module",
-			source: `
+		Links: nil,
+	},
+	RequiredTypes:   []string{"resource"},
+	RequiredLabels:  []string{"problem"},
+	DefaultSeverity: severity.High,
+	CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+		if resourceBlock.GetAttribute("bad").IsTrue() {
+			set.AddResult().
+				WithDescription("example problem")
+		}
+	},
+}
+
+func Test_ProblemInModuleInSiblingDir(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("/project/main.tf", `
 module "something" {
-	source = "../module"
+	source = "../modules/problem"
 }
-`,
-			moduleSource: `
+`))
+	require.NoError(t, fs.WriteTextFile("modules/problem/main.tf", `
 resource "problem" "uhoh" {
-	bad = "1"
+	bad = true
 }
-`,
-			mustIncludeResultCode: "aws-service-abc",
-		},
-	}
+`))
+
+	blocks, err := parser.New(fs.RealPath("/project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInModuleInSubdirectory(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+	source = "./modules/problem"
+}
+`))
+	require.NoError(t, fs.WriteTextFile("project/modules/problem/main.tf", `
+resource "problem" "uhoh" {
+	bad = true
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInModuleInParentDir(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+	source = "../problem"
+}
+`))
+	require.NoError(t, fs.WriteTextFile("problem/main.tf", `
+resource "problem" "uhoh" {
+	bad = true
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInModuleReuse(t *testing.T) {
 
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			path := testutil.CreateTestFileWithModule(test.source, test.moduleSource)
-			blocks, err := parser.New(path, parser.OptionStopOnHCLError()).ParseDirectory()
-			if err != nil {
-				t.Fatal(err)
-			}
-			results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
-			testutil.AssertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something_good" {
+	source = "../modules/problem"
+	bad = false
+}
+
+module "something_bad" {
+	source = "../modules/problem"
+	bad = true
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/problem/main.tf", `
+variable "bad" {
+	default = false
+}
+resource "problem" "uhoh" {
+	bad = var.bad
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInNestedModule(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+	source = "../modules/a"
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/a/main.tf", `
+	module "something" {
+	source = "../../modules/b"
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/b/main.tf", `
+module "something" {
+	source = "../c"
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/c/main.tf", `
+resource "problem" "uhoh" {
+	bad = true
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInReusedNestedModule(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+  source = "../modules/a"
+  bad = false
+}
+
+module "something-bad" {
+	source = "../modules/a"
+	bad = true
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/a/main.tf", `
+variable "bad" {
+	default = false
+}
+module "something" {
+	source = "../../modules/b"
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/b/main.tf", `
+variable "bad" {
+	default = false
+}
+module "something" {
+	source = "../c"
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/c/main.tf", `
+variable "bad" {
+	default = false
+}
+resource "problem" "uhoh" {
+	bad = var.bad
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInInitialisedModule(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+  	source = "/nowhere"
+	bad = true
+}
+`))
+	require.NoError(t, fs.WriteTextFile("project/.terraform/modules/a/main.tf", `
+variable "bad" {
+	default = false
+}
+resource "problem" "uhoh" {
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("project/.terraform/modules/modules.json", `
+	{"Modules":[{"Key":"something","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"}]}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+func Test_ProblemInReusedInitialisedModule(t *testing.T) {
+
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+  	source = "/nowhere"
+	bad = false
+} 
+module "something2" {
+	source = "/nowhere"
+  	bad = true
+}
+`))
+	require.NoError(t, fs.WriteTextFile("project/.terraform/modules/a/main.tf", `
+variable "bad" {
+	default = false
+}
+resource "problem" "uhoh" {
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("project/.terraform/modules/modules.json", `
+	{"Modules":[{"Key":"something","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"}]}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_ProblemInDuplicateModuleNameAndPath(t *testing.T) {
+	scanner.RegisterCheckRule(badRule)
+	defer scanner.DeregisterCheckRule(badRule)
+
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+  source = "../modules/a"
+  bad = 0
+}
+
+module "something-bad" {
+	source = "../modules/a"
+	bad = 1
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/a/main.tf", `
+variable "bad" {
+	default = 0
+}
+module "something" {
+	source = "../b"
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/b/main.tf", `
+variable "bad" {
+	default = 0
+}
+module "something" {
+	source = "../c"
+	bad = var.bad
+}
+`))
+	require.NoError(t, fs.WriteTextFile("modules/c/main.tf", `
+variable "bad" {
+	default = 0
+}
+resource "problem" "uhoh" {
+	count = var.bad
+	bad = true
+}
+`))
+
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, badRule.ID(), "", results)
+
+}
+
+func Test_UniqueDataBlocksWhenRaceInLoad(t *testing.T) {
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", `
+module "something" {
+  	source = "../modules/iam"
+} 
+`))
+	require.NoError(t, fs.WriteTextFile("/modules/iam/main2.tf", `
+	resource "aws_iam_role_policy" "test_policy" {
+		name = "test_policy"
+		role = aws_iam_role.test_role.id
+	
+		policy = data.aws_iam_policy_document.s3_policy.json
+	}
+	
+	resource "aws_iam_role" "test_role" {
+		name = "test_role"
+		assume_role_policy = jsonencode({
+			Version = "2012-10-17"
+			Statement = [
+			{
+				Action = "sts:AssumeRole"
+				Effect = "Allow"
+				Sid    = ""
+				Principal = {
+				Service = "s3.amazonaws.com"
+				}
+			},
+			]
 		})
 	}
+	
+	data "aws_iam_policy_document" "s3_policy" {
+	  statement {
+		principals {
+		  type        = "AWS"
+		  identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
+		}
+		actions   = ["s3:*"]
+		resources = ["*"]
+	  }
+	}
+`))
+	require.NoError(t, fs.WriteTextFile("/modules/iam/main1.tf", `
+	resource "aws_iam_role_policy" "test_policy2" {
+		name = "test_policy"
+		role = aws_iam_role.test_role.id
+	
+		policy = data.aws_iam_policy_document.s3_policy.json
+	}
+	
+	resource "aws_iam_role" "test_role2" {
+		name = "test_role"
+		assume_role_policy = jsonencode({
+			Version = "2012-10-17"
+			Statement = [
+			{
+				Action = "sts:AssumeRole"
+				Effect = "Allow"
+				Sid    = ""
+				Principal = {
+				Service = "s3.amazonaws.com"
+				}
+			},
+			]
+		})
+	}
+	
+	data "aws_iam_policy_document" "s3_policy2" {
+	  statement {
+		principals {
+		  type        = "AWS"
+		  identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
+		}
+		actions   = ["s3:nope"]
+		resources = ["nopeSSS"]
+	  }
+	}
+`))
 
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	testutil.AssertCheckCode(t, "aws-iam-no-policy-wildcards", "", results)
 }
diff --git a/internal/app/tfsec/test/performance_test.go b/internal/app/tfsec/test/performance_test.go
new file mode 100644
index 000000000..f6d47e798
--- /dev/null
+++ b/internal/app/tfsec/test/performance_test.go
@@ -0,0 +1,37 @@
+package test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/parser"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
+)
+
+func BenchmarkCalculate(b *testing.B) {
+
+	fs, err := testutil.NewFilesystem()
+	if err != nil {
+		panic(err)
+	}
+	defer fs.Close()
+
+	_ = fs.WriteTextFile("/project/main.tf", `
+		module "something" {
+			source = "../modules/problem"
+		}
+		`)
+
+	for _, rule := range scanner.GetRegisteredRules() {
+		_ = fs.WriteTextFile(fmt.Sprintf("/modules/problem/%s.tf", rule.ID()), rule.Documentation.BadExample[0])
+	}
+
+	for i := 0; i < b.N; i++ {
+		blocks, err := parser.New(fs.RealPath("/project"), parser.OptionStopOnHCLError()).ParseDirectory()
+		if err != nil {
+			panic(err)
+		}
+		_ = scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	}
+}
diff --git a/internal/app/tfsec/testutil/fs.go b/internal/app/tfsec/testutil/fs.go
new file mode 100644
index 000000000..f9ae2e03e
--- /dev/null
+++ b/internal/app/tfsec/testutil/fs.go
@@ -0,0 +1,44 @@
+package testutil
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+type FileSystem struct {
+	root string
+}
+
+func NewFilesystem() (*FileSystem, error) {
+	dir, err := ioutil.TempDir(os.TempDir(), "tfsec")
+	if err != nil {
+		return nil, err
+	}
+	return &FileSystem{
+		root: dir,
+	}, nil
+}
+
+func (fs *FileSystem) RealPath(path string) string {
+	return filepath.Join(fs.root, path)
+}
+
+func (fs *FileSystem) Close() error {
+	return os.RemoveAll(fs.root)
+}
+
+func (fs *FileSystem) AddDir(path string) error {
+	return os.MkdirAll(filepath.Join(fs.root, path), 0700)
+}
+
+func (fs *FileSystem) WriteFile(path string, data []byte) error {
+	if err := fs.AddDir(filepath.Dir(path)); err != nil {
+		return err
+	}
+	return ioutil.WriteFile(filepath.Join(fs.root, path), data, 0600)
+}
+
+func (fs *FileSystem) WriteTextFile(path string, text string) error {
+	return fs.WriteFile(path, []byte(text))
+}