Repository: bridgecrewio/checkov
Pull request: Terraform parser & variable evaluation rework
⚠️ This is a big change, please review carefully. Some development and testing is still ongoing, so it is currently a draft PR. All feedback is welcome! ⚠️ 

This PR is an overhaul of the parsing and evaluation logic for Terraform. It brings number of new abilities and improvements:

- `.tfvars` support (`terraform.tfvars` and `.auto.tfvars`)
- JSON-format Terraform support (`.tf.json` and `.tfvars.json`)
- Improved variable handling
- Function support: `tomap`, `tolist`, `toset`, `tostring`, `tobool`, `tonumber` currently. More in development.
- module loading framework with local module loading currently (_mostly_) implemented
- Improved testability of TF parsing scenarios
- Environment variable support

**Implementation notes & details**

I set out to improve the existing parser and evaluation code and kept hitting walls in terms of how and when evaluation needed to be done, so this ended up being a rewrite of those pieces of code into `parser2`. (That name doesn't need to stick, but it was easiest to keep around for comparison and testing for the time being.)

Like my serverless parser implementation, this is looping parser/evaluator which keeps processing until the structure is stabilized (or it hits a limit). This might have performance implications and should be checked against large data sets.

There are many test scenarios under `tests/terraform/evaluation/resources/parser_scenarios` and the corresponding `test_parser_scenarios.py`.

One of the biggest struggles I had was keeping track of what data needed to go where in the various dicts floating around, particularly around evaluations. To aid that, I created a dataclass in `checkov/common/variables/context.py` which `parser2` emits and the `runner` expands for the report. This will feel different for anyone familiar with the previous approach.

I used typing in new pieces I wrote to help with documentation and keeping things straight. I haven't used any tool to formally enforce, but simply for documentation and IDE convenience. I know some python devs don't like typing, so it can be pulled if we don't want it cluttering things.


**Remaining work**

- ~Module loading basically works, but references to outputs from modules is not yet implemented.~
- ~How module data is stored in the structure map is a little weird. Currently everything is designed to organized under the file it's defined in, but this gets pretty inconvenient with loaded modules (for resolving `module.` references). I haven't settled on what we'd want here and opinions are welcome. See the `maze_of_variables` test case.~ <br>Resolved module data is now stored within the calling module block. See comment [here](https://github.com/bridgecrewio/checkov/pull/671/commits/27c62beb3f90c136398c0f160bf36aa09613414e).
- ~Test cases were light previously for cases that I know must be relevant. More work should be done to find special cases from the old parser/evaluators to ensure they're captured in test cases.~ <br>I've dug fairly deep on this and think I've caught everything.
- ~Some test cases aren't thoroughly checking their variable evaluation. Need to add `eval.json` files to their scenarios.~ <br>Much better now.
- ~There are probably a few other functions that would be good.~ <br>Can be handled in future work.
- ~Context info (code & line numbers) is not properly associated with loaded module data by the runner.~ <br>Fixed.

Fixes: #386, #622, #651, #446
Related to: #314, #494 

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Problem statement:
Support for .tfvars
We use .tfvars files to assign values to variables. Currently it looks like there is no support for files with a .tfvars extension. This is a great tool and adding this support would greatly benefit The terraform community.

Hints from issue discussion:
Thank you @ymohamme for reporting this one. We are working on it. We do see it as part of a larger task that will enable deep-assignment of variables. We have estimated in a few weeks of work and @rotemavni is working hard on this one. 
@schosterbarak - I have an implementation (outside checkov) that I can contribute as a starting point if @rotemavni would like something to reference.
Thanks, @robeden  that will definitely help. let me connect I'll open a joined channel on this topic on our slack workspace :) 
I think we could handle "simple" variable parsing similar to what was done with serverless variables in #529. I've handled .tfvars, module and local references with a similar approach. There's some data that trickier (like existing resource lookups), but for basic variables it's pretty straight-forward.
This looks useful - any further progress on this one?
I’m actively working on it right now. Hopefully we’ll have something to show soon.

Commit message: Test case to ensure quote in quote syntax doesn't explode

Fixes: #446

Patch to review:
diff --git a/HomebrewFormula/checkov.rb b/HomebrewFormula/checkov.rb
index 90aae7617a..57d4bebc14 100644
--- a/HomebrewFormula/checkov.rb
+++ b/HomebrewFormula/checkov.rb
@@ -38,6 +38,11 @@ class Checkov < Formula
     sha256 "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
   end
 
+  resource "deep_merge" do
+    url "https://files.pythonhosted.org/packages/a5/25/aa35c20acd8a4f515f9e4c8dee4c7731446234101a6dae0c34cf498bb342/deep_merge-0.0.4.tar.gz"
+    sha256 "b54415f90934c42e334114e2864cb4d4e7335b34ad396e35ad8610c96065a47e"
+  end
+
   resource "docopt" do
     url "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz"
     sha256 "49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
@@ -103,6 +108,11 @@ class Checkov < Formula
     sha256 "921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
   end
 
+  resource "semantic_version" do
+    url "https://files.pythonhosted.org/packages/d4/52/3be868c7ed1f408cb822bc92ce17ffe4e97d11c42caafce0589f05844dd0/semantic_version-2.8.5.tar.gz"
+    sha256 "d2cb2de0558762934679b9a104e82eca7af448c9f4974d1f3eeccff651df8a54"
+  end
+
   resource "six" do
     url "https://files.pythonhosted.org/packages/6b/34/415834bfdafca3c5f451532e8a8d9ba89a21c9743a0c59fbd0205c7f9426/six-1.15.0.tar.gz"
     sha256 "30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"
diff --git a/Pipfile b/Pipfile
index 6f2fb4fb77..14944fc8dd 100644
--- a/Pipfile
+++ b/Pipfile
@@ -15,6 +15,7 @@ bc-python-hcl2 = "*"
 
 [packages]
 bc-python-hcl2 = "*"
+deep_merge = "*"
 tabulate = "*"
 colorama="*"
 termcolor="*"
@@ -27,6 +28,7 @@ six = "==1.15.0"
 jmespath = "*"
 tqdm = "*"
 update_checker = "*"
+semantic_version = "*"      # For TF version resolution
 
 [requires]
 python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index b8f584ea37..73943b09ea 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "35df5c76b3bbf3d0dc92f365d2a7d9bff13c0943d8695c7ac14105cc9418cf2f"
+            "sha256": "571ed2c6484abd03e21cdd371167fac8daf2ef728b0f134375bd3ece5c62eb18"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -26,18 +26,18 @@
         },
         "boto3": {
             "hashes": [
-                "sha256:454a8dfb7b367a058c7967ef6b4e2a192c318f10761769fd1003cf7f2f5a7db9",
-                "sha256:557320fe8b65cfc85953e6a63d2328e8efec95bf4ec383b92fa2d01119209716"
+                "sha256:ada5af5cd813997fa2fe30ee47460bbea253f5a728a5946916d44bb12370d6e9",
+                "sha256:b5537e814c7d3f6c7efbc26049a868f3bd5b60f3a3a86d5f2527f98fca6ed33f"
             ],
             "index": "pypi",
-            "version": "==1.15.16"
+            "version": "==1.16.10"
         },
         "botocore": {
             "hashes": [
-                "sha256:e586e4d6eddbca31e6447a25df9972329ea3de64b1fb0eb17e7ab0c9b91f7720",
-                "sha256:f0616d2c719691b94470307cee8adf89ceb1657b7b6f9aa1bf61f9de5543dbbb"
+                "sha256:0391922fc6fc1f36a51b9eb8c54b1278c37d9df1a0c6e40361213680d180c358",
+                "sha256:d44ecd33311cf163aec2c033e8941ae11f3a34e278e4b59aa7ee29b7455a5e62"
             ],
-            "version": "==1.18.16"
+            "version": "==1.19.10"
         },
         "certifi": {
             "hashes": [
@@ -55,11 +55,20 @@
         },
         "colorama": {
             "hashes": [
+                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
                 "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
             ],
             "index": "pypi",
             "version": "==0.4.4"
         },
+        "deep-merge": {
+            "hashes": [
+                "sha256:8056b4b43c6dfddf5c7b1feb3a09f1ab1cbd74e8382e43736ea8c5619e8e5a4e",
+                "sha256:b54415f90934c42e334114e2864cb4d4e7335b34ad396e35ad8610c96065a47e"
+            ],
+            "index": "pypi",
+            "version": "==0.0.4"
+        },
         "dpath": {
             "hashes": [
                 "sha256:496615b4ea84236d18e0d286122de74869a60e0f87e2c7ec6787ff286c48361b"
@@ -72,23 +81,21 @@
                 "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
                 "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
             ],
-            "markers": "python_version >= '3.4'",
             "version": "==4.0.5"
         },
         "gitpython": {
             "hashes": [
-                "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
-                "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+                "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+                "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
             ],
             "index": "pypi",
-            "version": "==3.1.9"
+            "version": "==3.1.11"
         },
         "idna": {
             "hashes": [
                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.10"
         },
         "jmespath": {
@@ -117,7 +124,6 @@
                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.8.1"
         },
         "pyyaml": {
@@ -142,7 +148,6 @@
                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==2.24.0"
         },
         "s3transfer": {
@@ -152,6 +157,14 @@
             ],
             "version": "==0.3.3"
         },
+        "semantic-version": {
+            "hashes": [
+                "sha256:45e4b32ee9d6d70ba5f440ec8cc5221074c7f4b0e8918bdab748cc37912440a9",
+                "sha256:d2cb2de0558762934679b9a104e82eca7af448c9f4974d1f3eeccff651df8a54"
+            ],
+            "index": "pypi",
+            "version": "==2.8.5"
+        },
         "six": {
             "hashes": [
                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
@@ -165,7 +178,6 @@
                 "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
                 "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==3.0.4"
         },
         "tabulate": {
@@ -185,11 +197,11 @@
         },
         "tqdm": {
             "hashes": [
-                "sha256:43ca183da3367578ebf2f1c2e3111d51ea161ed1dc4e6345b86e27c2a93beff7",
-                "sha256:69dfa6714dee976e2425a9aab84b622675b7b1742873041e3db8a8e86132a4af"
+                "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad",
+                "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432"
             ],
             "index": "pypi",
-            "version": "==4.50.2"
+            "version": "==4.51.0"
         },
         "update-checker": {
             "hashes": [
@@ -201,11 +213,11 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
-                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
+                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
             ],
             "markers": "python_version != '3.4'",
-            "version": "==1.25.10"
+            "version": "==1.25.11"
         }
     },
     "develop": {
@@ -221,7 +233,6 @@
                 "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
                 "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==20.2.0"
         },
         "bandit": {
@@ -280,11 +291,11 @@
                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==7.1.2"
         },
         "colorama": {
             "hashes": [
+                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
                 "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
             ],
             "index": "pypi",
@@ -350,23 +361,21 @@
                 "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
                 "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
             ],
-            "markers": "python_version >= '3.4'",
             "version": "==4.0.5"
         },
         "gitpython": {
             "hashes": [
-                "sha256:138016d519bf4dd55b22c682c904ed2fd0235c3612b2f8f65ce218ff358deed8",
-                "sha256:a03f728b49ce9597a6655793207c6ab0da55519368ff5961e4a74ae475b9fa8e"
+                "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
+                "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
             ],
             "index": "pypi",
-            "version": "==3.1.9"
+            "version": "==3.1.11"
         },
         "idna": {
             "hashes": [
                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.10"
         },
         "importlib-metadata": {
@@ -379,10 +388,10 @@
         },
         "iniconfig": {
             "hashes": [
-                "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
-                "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
+                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
             ],
-            "version": "==1.0.1"
+            "version": "==1.1.1"
         },
         "lark-parser": {
             "hashes": [
@@ -402,7 +411,6 @@
                 "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
                 "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==20.4"
         },
         "pathspec": {
@@ -414,25 +422,23 @@
         },
         "pbr": {
             "hashes": [
-                "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
-                "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
+                "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
+                "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
             ],
-            "markers": "python_version >= '2.6'",
-            "version": "==5.5.0"
+            "version": "==5.5.1"
         },
         "pep517": {
             "hashes": [
-                "sha256:576c480be81f3e1a70a16182c762311eb80d1f8a7b0d11971e5234967d7a342c",
-                "sha256:8e6199cf1288d48a0c44057f112acf18aa5ebabbf73faa242f598fbe145ba29e"
+                "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51",
+                "sha256:aeb78601f2d1aa461960b43add204cc7955667687fbcf9cdb5170f00556f117f"
             ],
-            "version": "==0.8.2"
+            "version": "==0.9.1"
         },
         "pip-shims": {
             "hashes": [
                 "sha256:05b00ade9d1e686a98bb656dd9b0608a933897283dc21913fad6ea5409ff7e91",
                 "sha256:16ca9f87485667b16b978b68a1aae4f9cc082c0fa018aed28567f9f34a590569"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==0.5.3"
         },
         "pipenv-setup": {
@@ -457,7 +463,6 @@
                 "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26",
                 "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==0.2.3"
         },
         "pluggy": {
@@ -465,7 +470,6 @@
                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==0.13.1"
         },
         "py": {
@@ -473,7 +477,6 @@
                 "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
                 "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==1.9.0"
         },
         "pyparsing": {
@@ -481,23 +484,21 @@
                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.4.7"
         },
         "pytest": {
             "hashes": [
-                "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9",
-                "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"
+                "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
+                "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
             ],
             "index": "pypi",
-            "version": "==6.1.1"
+            "version": "==6.1.2"
         },
         "python-dateutil": {
             "hashes": [
                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.8.1"
         },
         "pyyaml": {
@@ -519,51 +520,65 @@
         },
         "regex": {
             "hashes": [
-                "sha256:1a16afbfadaadc1397353f9b32e19a65dc1d1804c80ad73a14f435348ca017ad",
-                "sha256:2308491b3e6c530a3bb38a8a4bb1dc5fd32cbf1e11ca623f2172ba17a81acef1",
-                "sha256:39a5ef30bca911f5a8a3d4476f5713ed4d66e313d9fb6755b32bec8a2e519635",
-                "sha256:3d5a8d007116021cf65355ada47bf405656c4b3b9a988493d26688275fde1f1c",
-                "sha256:4302153abb96859beb2c778cc4662607a34175065fc2f33a21f49eb3fbd1ccd3",
-                "sha256:463e770c48da76a8da82b8d4a48a541f314e0df91cbb6d873a341dbe578efafd",
-                "sha256:46ab6070b0d2cb85700b8863b3f5504c7f75d8af44289e9562195fe02a8dd72d",
-                "sha256:4f5c0fe46fb79a7adf766b365cae56cafbf352c27358fda811e4a1dc8216d0db",
-                "sha256:60c4f64d9a326fe48e8738c3dbc068e1edc41ff7895a9e3723840deec4bc1c28",
-                "sha256:671c51d352cfb146e48baee82b1ee8d6ffe357c292f5e13300cdc5c00867ebfc",
-                "sha256:6cf527ec2f3565248408b61dd36e380d799c2a1047eab04e13a2b0c15dd9c767",
-                "sha256:7c4fc5a8ec91a2254bb459db27dbd9e16bba1dabff638f425d736888d34aaefa",
-                "sha256:850339226aa4fec04916386577674bb9d69abe0048f5d1a99f91b0004bfdcc01",
-                "sha256:8ba3efdd60bfee1aa784dbcea175eb442d059b576934c9d099e381e5a9f48930",
-                "sha256:8c8c42aa5d3ac9a49829c4b28a81bebfa0378996f9e0ca5b5ab8a36870c3e5ee",
-                "sha256:8e7ef296b84d44425760fe813cabd7afbb48c8dd62023018b338bbd9d7d6f2f0",
-                "sha256:a2a31ee8a354fa3036d12804730e1e20d58bc4e250365ead34b9c30bbe9908c3",
-                "sha256:a63907332531a499b8cdfd18953febb5a4c525e9e7ca4ac147423b917244b260",
-                "sha256:a8240df4957a5b0e641998a5d78b3c4ea762c845d8cb8997bf820626826fde9a",
-                "sha256:b8806649983a1c78874ec7e04393ef076805740f6319e87a56f91f1767960212",
-                "sha256:c077c9d04a040dba001cf62b3aff08fd85be86bccf2c51a770c77377662a2d55",
-                "sha256:c529ba90c1775697a65b46c83d47a2d3de70f24d96da5d41d05a761c73b063af",
-                "sha256:d537e270b3e6bfaea4f49eaf267984bfb3628c86670e9ad2a257358d3b8f0955",
-                "sha256:d629d750ebe75a88184db98f759633b0a7772c2e6f4da529f0027b4a402c0e2f",
-                "sha256:d9d53518eeed12190744d366ec4a3f39b99d7daa705abca95f87dd8b442df4ad",
-                "sha256:e490f08897cb44e54bddf5c6e27deca9b58c4076849f32aaa7a0b9f1730f2c20",
-                "sha256:f579caecbbca291b0fcc7d473664c8c08635da2f9b1567c22ea32311c86ef68c"
-            ],
-            "version": "==2020.10.11"
+                "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a",
+                "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f",
+                "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb",
+                "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5",
+                "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de",
+                "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c",
+                "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0",
+                "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c",
+                "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64",
+                "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53",
+                "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12",
+                "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740",
+                "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c",
+                "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd",
+                "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504",
+                "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427",
+                "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b",
+                "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e",
+                "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582",
+                "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0",
+                "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c",
+                "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9",
+                "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1",
+                "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0",
+                "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf",
+                "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898",
+                "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd",
+                "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d",
+                "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab",
+                "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f",
+                "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e",
+                "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786",
+                "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b",
+                "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de",
+                "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e",
+                "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789",
+                "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520",
+                "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa",
+                "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b",
+                "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4",
+                "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625",
+                "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d",
+                "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"
+            ],
+            "version": "==2020.10.28"
         },
         "requests": {
             "hashes": [
                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==2.24.0"
         },
         "requirementslib": {
             "hashes": [
-                "sha256:cdf8aa652ac52216d156cee2b89c3c9ee53373dded0035184d0b9af569a0f10c",
-                "sha256:fd98ea873effaede6b3394725a232bcbd3fe3985987e226109a841c85a69e2e3"
+                "sha256:029dd232c5e384ce9b3d7c14876558f9b6176e564ac3628de149243667ccdaa0",
+                "sha256:fe4486d936489239ae2f10b114fcc5e81e13e41fe742b924c2970be32c6efeb2"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.5.13"
+            "version": "==1.5.15"
         },
         "six": {
             "hashes": [
@@ -578,7 +593,6 @@
                 "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
                 "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==3.0.4"
         },
         "stevedore": {
@@ -586,57 +600,64 @@
                 "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62",
                 "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"
             ],
-            "markers": "python_version >= '3.6'",
             "version": "==3.2.2"
         },
         "toml": {
             "hashes": [
-                "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
-                "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
             ],
-            "version": "==0.10.1"
+            "version": "==0.10.2"
         },
         "tomlkit": {
             "hashes": [
                 "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831",
                 "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==0.7.0"
         },
         "typed-ast": {
             "hashes": [
                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
                 "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
                 "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
             ],
             "version": "==1.4.1"
         },
         "urllib3": {
             "hashes": [
-                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
-                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
+                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
             ],
             "markers": "python_version != '3.4'",
-            "version": "==1.25.10"
+            "version": "==1.25.11"
         },
         "urllib3-mock": {
             "hashes": [
@@ -651,7 +672,6 @@
                 "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb",
                 "sha256:eff1d19ef50c703a329ed294e5ec0b0fbb35b96c1b3ee6dcdb266dddbe1e935a"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==0.5.2"
         },
         "wheel": {
@@ -659,16 +679,15 @@
                 "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2",
                 "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==0.35.1"
         },
         "zipp": {
             "hashes": [
-                "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b",
-                "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066"
+                "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
+                "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
             ],
             "markers": "python_version < '3.8'",
-            "version": "==3.3.0"
+            "version": "==3.4.0"
         }
     }
 }
diff --git a/checkov/common/output/record.py b/checkov/common/output/record.py
index c2bc87cff4..a9a105d687 100644
--- a/checkov/common/output/record.py
+++ b/checkov/common/output/record.py
@@ -21,6 +21,12 @@ class Record:
 
     def __init__(self, check_id, check_name, check_result, code_block, file_path, file_line_range, resource,
                  evaluations, check_class):
+        """
+        :param evaluations: A dict with the key being the variable name, value being a dict containing:
+                             - 'var_file'
+                             - 'value'
+                             - 'definitions', a list of dicts which contain 'definition_expression'
+        """
         self.check_id = check_id
         self.check_name = check_name
         self.check_result = check_result
diff --git a/checkov/terraform/evaluation/evaluation_methods/__init__.py b/checkov/common/variables/__init__.py
similarity index 100%
rename from checkov/terraform/evaluation/evaluation_methods/__init__.py
rename to checkov/common/variables/__init__.py
diff --git a/checkov/common/variables/context.py b/checkov/common/variables/context.py
new file mode 100644
index 0000000000..2213500d7f
--- /dev/null
+++ b/checkov/common/variables/context.py
@@ -0,0 +1,47 @@
+from dataclasses import dataclass, field
+from typing import List, Any
+
+
+# NOTE: These would be better as TypedDict, but that requires python 3.8 :-(
+
+
+@dataclass
+class VarReference:
+    definition_name: str               # Example: 'region'
+    definition_expression: str         # Example: '${var.region}'
+    definition_path: str               # Example: 'resource/0/aws_s3_bucket/foo-bucket/region/0'
+
+    # Make the object subscriptable for backwards compatibility to when a simple dict was used
+    def __getitem__(self, item):
+        if not isinstance(item, str):
+            raise TypeError("Item key must be a str")
+
+        if item == "definition_name":
+            return self.definition_name
+        if item == "definition_expression":
+            return self.definition_expression
+        if item == "definition_path":
+            return self.definition_path
+
+        raise KeyError(f"Unknown key: {item}")
+
+
+@dataclass
+class EvaluationContext:
+    var_file: str                     # File the variable was defined in (e.g., '/tf/example.tf'
+    value: Any = None                 # Example: 'us-east-1'
+    definitions: List[VarReference] = field(default_factory=list)
+
+    # Make the object subscriptable for backwards compatibility to when a simple dict was used
+    def __getitem__(self, item):
+        if not isinstance(item, str):
+            raise TypeError("Item key must be a str")
+
+        if item == "var_file":
+            return self.var_file
+        if item == "value":
+            return self.value
+        if item == "definitions":
+            return self.definitions
+
+        raise KeyError(f"Unknown key: {item}")
diff --git a/checkov/serverless/parsers/parser.py b/checkov/serverless/parsers/parser.py
index 3fd54ce747..e91ea4ab42 100644
--- a/checkov/serverless/parsers/parser.py
+++ b/checkov/serverless/parsers/parser.py
@@ -106,6 +106,7 @@ def process_variables(template, filename):
     # Loop while the data is being changed, stop when no more changes are happening.
     # To ensure there's not some kind of oscillation, a cap of 25 passes is in place.
     # More than a couple loops isn't normally expected.
+    # NOTE: If this approach proves to be a performance liability, a DAG will be needed.
     loop_count = 0
     for i in range(0, 25):
         loop_count += 1
diff --git a/checkov/terraform/context_parsers/base_parser.py b/checkov/terraform/context_parsers/base_parser.py
index a94508a86a..f130ae9752 100644
--- a/checkov/terraform/context_parsers/base_parser.py
+++ b/checkov/terraform/context_parsers/base_parser.py
@@ -110,7 +110,12 @@ def _compute_definition_end_line(self, start_line_num):
         return end_line_num
 
     def run(self, tf_file, definition_blocks, collect_skip_comments=True):
-        self.tf_file = tf_file
+        # TF files for loaded modules have this formation:  <file>[<referrer>#<index>]
+        # Chop off everything after the file name for our purposes here
+        if tf_file.endswith("]") and "[" in tf_file:
+            self.tf_file = tf_file[:tf_file.index("[")]
+        else:
+            self.tf_file = tf_file
         self.context = {}
         self.file_lines = self._read_file_lines()
         self.context = self.enrich_definition_block(definition_blocks)
diff --git a/checkov/terraform/evaluation/base_variable_evaluation.py b/checkov/terraform/evaluation/base_variable_evaluation.py
index 757e54bbaa..94b2a90744 100644
--- a/checkov/terraform/evaluation/base_variable_evaluation.py
+++ b/checkov/terraform/evaluation/base_variable_evaluation.py
@@ -29,8 +29,8 @@ def evaluate_variables(self):
 
     @staticmethod
     def _generate_evaluation_regex(definition_type, var_name):
-        return r'((?:\$\{)?' + re.escape(DEFINITION_TYPES_REGEX_MAPPING[definition_type]) + r'[.]' + re.escape(
-            var_name) + r'(?:\})?)'
+        return r'((?:\$\{)?' + re.escape(DEFINITION_TYPES_REGEX_MAPPING[definition_type]) + \
+               r'[.]' + re.escape(var_name) + r'(?:\})?)'
 
     @staticmethod
     def _is_variable_only_expression(assignment_regex, entry_expression):
diff --git a/checkov/terraform/evaluation/evaluation_methods/const_variable_evaluation.py b/checkov/terraform/evaluation/evaluation_methods/const_variable_evaluation.py
deleted file mode 100644
index 5f176204b0..0000000000
--- a/checkov/terraform/evaluation/evaluation_methods/const_variable_evaluation.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import logging
-import os
-import re
-
-import dpath
-
-from checkov.terraform.evaluation.base_variable_evaluation import BaseVariableEvaluation
-
-
-class ConstVariableEvaluation(BaseVariableEvaluation):
-    def __init__(self, root_folder, tf_definitions, definitions_context):
-        super().__init__(root_folder, tf_definitions, definitions_context)
-
-    def _locate_variables_assignments(self, definition_type, folder, var_name):
-        var_assignments_paths = {}
-        assignment_regex = self._generate_evaluation_regex(definition_type, var_name)
-        for source_file in [file for file in os.listdir(folder) if
-                            file.endswith('.tf') and self.tf_definitions.get(os.path.join(folder, file))]:
-            file_path = os.path.join(folder, source_file)
-            var_entries = dpath.search(self.tf_definitions[file_path], '**',
-                                       afilter=lambda x: re.findall(assignment_regex, str(x)), yielded=True)
-            var_assignments_paths[file_path] = []
-            for definition_path, expression in var_entries:
-                context_path, definition_name = self.extract_context_path(definition_path)
-                var_assignments_paths[file_path].append({
-                    'definition_name': definition_name,
-                    'definition_expression': expression,
-                    'definition_path': definition_path})
-        var_assignments_paths = {k: v for (k, v) in var_assignments_paths.items() if len(v) > 0}
-        return var_assignments_paths
-
-    def _assign_definition_value(self, definition_type, var_name, var_value, var_assignments):
-        """
-        assigns var_value to variable var_name in tf_definitions
-        :param definition_type: the entity's block type
-        :param var_name: variable name
-        :param var_value: variable value
-        :param var_assignments: variable assignments
-        """
-        assignment_regex = self._generate_evaluation_regex(definition_type, var_name)
-        var_file = var_assignments['var_file']
-        var_value_string = str(var_value)
-        for (assignment_file, assignments) in var_assignments['definitions'].items():
-            # Save evaluation information in context
-            for assignment_obj in assignments:
-                definition_path = assignment_obj.get('definition_path')
-                entry_expression = assignment_obj.get('definition_expression')
-                definition_name = assignment_obj.get('definition_name')
-                if not isinstance(entry_expression, str):
-                    # Example of unsupported evaluation:
-                    # cidr_blocks = local.ip_ranges.ipv4Prefixes[*].prefix
-                    logging.info(f'Ran into a complex evaluation which isn\'t supported yet, on {assignment_file}')
-                    continue
-                context_path, _ = self.extract_context_path(definition_path)
-                if assignment_file in self.definitions_context.keys():
-                    dpath.new(self.definitions_context[assignment_file], f'evaluations/{var_name}/var_file',
-                              var_file)
-                    dpath.new(self.definitions_context[assignment_file], f'evaluations/{var_name}/value',
-                              var_value)
-                    dpath.new(self.definitions_context[assignment_file],
-                              f'evaluations/{var_name}/definitions',
-                              assignments)
-                if self._is_variable_only_expression(assignment_regex, entry_expression):
-                    # Preserve the original type of the variable if not part of a composite expression
-                    evaluated_definition = var_value
-                else:
-                    evaluated_definition = re.sub(assignment_regex, re.escape(var_value_string), entry_expression)
-
-                dpath.set(self.tf_definitions[assignment_file], definition_path, evaluated_definition)
-                self.logger.debug(
-                    f'Evaluated definition {definition_name} in file {assignment_file}: default value of variable {var_file}: '
-                    f'{var_name} to "{var_value_string}"')
-
-    def evaluate_variables(self):
-        """
-        Evaluate all default variables found in tf_definitions expressions, per a scanned directory of Terraform files
-        """
-        definitions_folders = set([os.path.split(file_path)[0] for file_path in self.definitions_context.keys()])
-        for folder in definitions_folders:
-            self._evaluate_folder_variables(folder)
-
-    def _evaluate_folder_variables(self, folder):
-        """
-        Locate all assignments extracted by the context parser, and assigns them to corresponding variables found in
-        tf_definitions
-        :param folder: folder to assign variables in
-        :return:
-        """
-        assignment_files = dpath.search(self.definitions_context, f'**.assignments', separator='.')
-        assignment_files = {k: v for k, v in assignment_files.items() if folder in k}
-        if assignment_files:
-            self._assign_definitions(assignment_files, folder)
-
-    def _assign_definitions(self, assignment_files, folder):
-        """
-        assign variables in expressions under a Terraform folder
-        :param assignment_files:
-        :param folder:
-        :return:
-        """
-        for var_file, variable_assignments in assignment_files.items():
-            relative_var_file = f'/{os.path.relpath(var_file, self.root_folder)}'
-            for definition_type in variable_assignments.keys():
-                for var_name, var_value in variable_assignments[definition_type].get('assignments', {}).items():
-                    if var_name == '':
-                        continue
-                    evaluated_definitions = self._locate_variables_assignments(definition_type, folder, var_name)
-                    var_assignments = {'definitions': evaluated_definitions, 'var_file': relative_var_file}
-                    self._assign_definition_value(definition_type, var_name, var_value, var_assignments)
diff --git a/checkov/terraform/module_loading/__init__.py b/checkov/terraform/module_loading/__init__.py
new file mode 100644
index 0000000000..1b42e8c213
--- /dev/null
+++ b/checkov/terraform/module_loading/__init__.py
@@ -0,0 +1 @@
+from checkov.terraform.module_loading.loaders import *
diff --git a/checkov/terraform/module_loading/content.py b/checkov/terraform/module_loading/content.py
new file mode 100644
index 0000000000..265cb005ae
--- /dev/null
+++ b/checkov/terraform/module_loading/content.py
@@ -0,0 +1,38 @@
+import tempfile
+from typing import Optional, Union
+
+
+class ModuleContent(object):
+    def __init__(self, dir: Optional[Union[tempfile.TemporaryDirectory, str]]) -> None:
+        self.dir = dir
+
+    def loaded(self) -> bool:
+        """
+Indicates whether or not the module content could be loaded. If False is returned, `path()` will return None.
+        """
+        return self.dir is not None
+
+    def path(self) -> Optional[str]:
+        """
+Returns the directory path containing module resources.
+        """
+        if isinstance(self.dir, tempfile.TemporaryDirectory):
+            return self.dir.name
+        else:
+            return self.dir
+
+    def cleanup(self):
+        """
+Clean up any temporary resources, if applicable.
+        """
+        if isinstance(self.dir, tempfile.TemporaryDirectory):
+            self.dir.cleanup()
+
+    def __repr__(self) -> str:
+        return self.path()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc, value, tb):
+        self.cleanup()
\ No newline at end of file
diff --git a/checkov/terraform/module_loading/loader.py b/checkov/terraform/module_loading/loader.py
new file mode 100644
index 0000000000..19de7346d4
--- /dev/null
+++ b/checkov/terraform/module_loading/loader.py
@@ -0,0 +1,37 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from checkov.terraform.module_loading.content import ModuleContent
+from checkov.terraform.module_loading.registry import module_loader_registry
+
+
+# ModuleContent allows access to a directory containing module file via the `path()`
+# function. Instances may be used in a `with` context to ensure temporary directories
+# are removed, if applicable.
+
+
+class ModuleLoader(ABC):
+    def __init__(self) -> None:
+        module_loader_registry.register(self)
+
+
+    @abstractmethod
+    def load(self, current_dir: str, source: str, source_version: Optional[str]) -> ModuleContent:
+        """
+This function provides an opportunity for the loader to load a module's content if it chooses to do so.
+There are three resulting states that can occur when calling this function:
+ 1) the loader can't handle the source type, in which case a ModuleContent is returned for which
+    the `loaded()` method will return False.
+ 2) the loader can handle the source type and loading is successful, in which case a ModuleContent
+    object is returned for which `loaded()` returns True and which provides the directory containing
+    the module files
+ 3) the loader tried to load the module content but and error occurred, in which case an exception
+    is raised.
+        :param current_dir: Directory containing the reference to the module.
+        :param source: the raw source string from the module's `source` attribute (e.g.,
+                       "hashicorp/consul/aws" or "git::https://example.com/vpc.git?ref=v1.2.0")
+        :param source_version: contains content from the module's `version` attribute, if provided
+
+        :return: A ModuleContent object which may or may not being loaded.
+        """
+        raise NotImplementedError()
diff --git a/checkov/terraform/module_loading/loaders/__init__.py b/checkov/terraform/module_loading/loaders/__init__.py
new file mode 100644
index 0000000000..1cb91d4ca0
--- /dev/null
+++ b/checkov/terraform/module_loading/loaders/__init__.py
@@ -0,0 +1,4 @@
+from os.path import dirname, basename, isfile, join
+import glob
+modules = glob.glob(join(dirname(__file__), "*.py"))
+__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
\ No newline at end of file
diff --git a/checkov/terraform/module_loading/loaders/local_path_loader.py b/checkov/terraform/module_loading/loaders/local_path_loader.py
new file mode 100644
index 0000000000..e8d0cfa1f4
--- /dev/null
+++ b/checkov/terraform/module_loading/loaders/local_path_loader.py
@@ -0,0 +1,21 @@
+import os
+from typing import Optional
+
+from checkov.terraform.module_loading.loader import ModuleLoader
+from checkov.terraform.module_loading.content import ModuleContent
+
+
+class LocalPathLoader(ModuleLoader):
+    def load(self, current_dir: str, source: str, source_version: Optional[str]) -> ModuleContent:
+        # Local path references start with "./" or "../"
+        if not source.startswith("./") and not source.startswith("../"):
+            return ModuleContent(None)
+
+        module_path = os.path.normpath(os.path.join(current_dir, source))
+        if not os.path.exists(module_path):
+            raise FileNotFoundError(module_path)
+
+        return ModuleContent(module_path)
+
+
+loader = LocalPathLoader()
diff --git a/checkov/terraform/module_loading/loaders/registry_loader.py b/checkov/terraform/module_loading/loaders/registry_loader.py
new file mode 100644
index 0000000000..bae96b70e5
--- /dev/null
+++ b/checkov/terraform/module_loading/loaders/registry_loader.py
@@ -0,0 +1,77 @@
+import os
+import tempfile
+
+import requests
+import semantic_version
+from typing import Optional
+
+from checkov.terraform.module_loading.loader import ModuleLoader
+from checkov.terraform.module_loading.content import ModuleContent
+
+
+class RegistryLoader(ModuleLoader):
+    def load(self, current_dir: str, source: str, source_version: Optional[str]) -> ModuleContent:
+        # Reference: https://www.terraform.io/docs/modules/sources.html#terraform-registry
+
+        # Format: [<HOSTNAME>/]<NAMESPACE>/<NAME>/<PROVIDER>
+        # Example: hashicorp/consul/aws
+        slash_count = source.count("/") != 2
+        if source.startswith("/") or source.endswith("/") or slash_count < 2 or slash_count > 3:
+            return ModuleContent(None)
+
+        tokens = source.split("/")
+
+        if len(tokens) == 3:
+            host = "registry.terraform.io"
+            namespace = tokens[0]
+            name = tokens[1]
+            provider = tokens[2]
+        else:
+            host = tokens[0]
+            namespace = tokens[1]
+            name = tokens[2]
+            provider = tokens[3]
+
+        # Info: https://www.terraform.io/docs/internals/module-registry-protocol.html#sample-request-1
+        base_url = f"https://{host}/{namespace}/{name}/{provider}"
+
+        with requests.session() as session:
+            if not source_version:
+                source_version = self._determine_latest_version(base_url, session)
+
+            temp_dir = tempfile.TemporaryDirectory()
+            try:
+                self._download_source(base_url, source_version, session)
+                return ModuleContent(temp_dir)
+            except:
+                temp_dir.cleanup()
+                raise
+
+    @staticmethod
+    def _determine_latest_version(base_url: str, session: requests.Session) -> str:
+        # Example: https://registry.terraform.io/v1/modules/hashicorp/consul/aws/versions
+
+        response = session.get(f"{base_url}/versions")
+        response.raise_for_status()
+
+        # TODO
+        return "not implemented yet... fix me!"
+
+    @staticmethod
+    def _download_source(base_url: str, source_version: str, session: requests.Session):
+        # Example: https://registry.terraform.io/v1/modules/hashicorp/consul/aws/0.0.1/download
+
+        # Sample response:
+        #
+        # HTTP/1.1 204 No Content
+        # Content-Length: 0
+        # X-Terraform-Get: https://api.github.com/repos/hashicorp/terraform-aws-consul/tarball/v0.0.1//*?archive=tar.gz
+        response = session.get(f"{base_url}/{source_version}/download")
+        response.raise_for_status()
+
+        # TODO
+
+        pass
+
+
+loader = RegistryLoader()
diff --git a/checkov/terraform/module_loading/registry.py b/checkov/terraform/module_loading/registry.py
new file mode 100644
index 0000000000..be6d531558
--- /dev/null
+++ b/checkov/terraform/module_loading/registry.py
@@ -0,0 +1,45 @@
+import logging
+from typing import Optional, List
+
+from checkov.terraform.module_loading.content import ModuleContent
+
+
+class ModuleLoaderRegistry:
+    loaders: List = []      # List[ModuleLoader]
+
+    def __init__(self):
+        self.logger = logging.getLogger(__name__)
+
+    def load(self, current_dir: str, source: str, source_version: Optional[str]) -> ModuleContent:
+        """
+Search all registered loaders for the first one which is able to load the module source type. For more
+information, see `loader.ModuleLoader.load`.
+        """
+        last_exception = None
+        for loader in self.loaders:
+            try:
+                content = loader.load(current_dir, source, source_version)
+            except Exception as e:
+                last_exception = e
+                continue
+
+            if content is None:
+                continue
+            elif not content.loaded():
+                content.cleanup()
+                continue
+            else:
+                return content
+
+        if last_exception is not None:
+            raise last_exception
+        return ModuleContent(None)
+
+    def register(self, loader):
+        self.loaders.append(loader)
+
+    def clear_all_loaders(self):
+        self.loaders.clear()
+
+
+module_loader_registry = ModuleLoaderRegistry()
diff --git a/checkov/terraform/parser.py b/checkov/terraform/parser.py
deleted file mode 100644
index 8e75518d8b..0000000000
--- a/checkov/terraform/parser.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import logging
-import os
-from os import path
-
-import hcl2
-from checkov.common.runners.base_runner import filter_ignored_directories
-
-
-class Parser:
-    logger = logging.getLogger(__name__)
-
-    def __init__(self):
-        self._parsed_directories = set()
-
-    def _mark_parsed(self, directory):
-        self._parsed_directories.add(directory)
-
-    def _is_parsed(self, directory):
-        return directory in self._parsed_directories
-
-    @staticmethod
-    def clean_bad_definitions(tf_definition_list):
-        return {
-            block_type: list(filter(lambda definition_list: len(definition_list.keys()) == 1, tf_definition_list[block_type]))
-            for block_type in tf_definition_list.keys()
-        }
-
-    @staticmethod
-    def clean_bad_definitions(tf_definition_list):
-        return {
-            block_type: list(filter(lambda definition_list: block_type == 'locals' or len(definition_list.keys()) == 1, tf_definition_list[block_type]))
-            for block_type in tf_definition_list.keys()
-        }
-
-    @staticmethod
-    def _parse_tf_definitions(tf_file):
-        with(open(tf_file, 'r')) as file:
-            file.seek(0)
-            raw_tf_definition = hcl2.load(file)
-            tf_definition = Parser.clean_bad_definitions(raw_tf_definition)
-            for resource_type in tf_definition.get('resource', []):
-                for resource in resource_type.values():
-                    for named_resource in resource.values():
-                        for dynamic_block in named_resource.get('dynamic', []):
-                            for dynamic_field_name, dynamic_field_value in dynamic_block.items():
-                                named_resource[dynamic_field_name] = dynamic_field_value['for_each']
-        return tf_definition
-
-    def hcl2(self, directory, tf_definitions={}, parsing_errors={}):
-        modules_scan = set()
-        for root, d_names, f_names in os.walk(directory):
-            filter_ignored_directories(d_names)
-            self._mark_parsed(os.path.abspath(root))
-            for file in f_names:
-                if file.endswith(".tf"):
-                    tf_file = os.path.join(root, file)
-                    if tf_file not in tf_definitions.keys():
-                        try:
-                            tf_definition = self._parse_tf_definitions(tf_file)
-                            if tf_definition:
-                                tf_definitions[tf_file] = tf_definition
-                            for modules in tf_definition.get("module", []):
-                                for module in modules.values():
-                                    relative_path = module['source'][0]
-                                    abs_path = os.path.abspath(os.path.join(root, relative_path))
-                                    if not self._is_parsed(abs_path):
-                                        modules_scan.add(abs_path)
-                        except Exception as e:
-                            self.logger.debug(f'failed while parsing file {tf_file}', exc_info=e)
-                            parsing_errors[tf_file] = e
-        for m in modules_scan:
-            if path.exists(m):
-                self.hcl2(directory=m, tf_definitions=tf_definitions)
-
-    def parse_file(self, file, parsing_errors={}):
-        if file.endswith(".tf"):
-            try:
-                return self._parse_tf_definitions(file)
-            except Exception as e:
-                self.logger.warning(f'failed while parsing file {file}', exc_info=e)
-                parsing_errors[file] = e
diff --git a/checkov/terraform/parser2.py b/checkov/terraform/parser2.py
new file mode 100644
index 0000000000..810833e080
--- /dev/null
+++ b/checkov/terraform/parser2.py
@@ -0,0 +1,695 @@
+import json
+import logging
+import os
+import re
+from pathlib import Path
+from typing import Mapping, Optional, Dict, Any, List, Callable, Tuple
+
+import deep_merge
+import hcl2
+import jmespath
+
+from checkov.common.runners.base_runner import filter_ignored_directories
+from checkov.common.variables.context import EvaluationContext, VarReference
+from checkov.terraform.module_loading.registry import ModuleLoaderRegistry
+from checkov.terraform.module_loading.registry import module_loader_registry as default_ml_registry
+
+
+LOGGER = logging.getLogger(__name__)
+
+_VAR_PATTERN = re.compile(r'\${([^{}]+?)}')
+_SIMPLE_TYPES = frozenset(["string", "number", "bool"])
+
+_RESOURCE_REF_PATTERN = re.compile(r'[\d\w]+(\.[\d\w]+)+')
+
+
+class Parser2:
+    def __init__(self):
+        self._parsed_directories = set()
+
+    def _check_process_dir(self, directory):
+        if directory not in self._parsed_directories:
+            self._parsed_directories.add(directory)
+            return True
+        else:
+            return False
+
+    def parse_directory(self, directory: str, out_definitions: Optional[Dict],
+                        out_evaluations_context: Dict[str, Dict[str, EvaluationContext]] = None,
+                        out_parsing_errors: Dict[str, Exception] = None,
+                        env_vars: Mapping[str, str] = None):
+
+        self._parsed_directories.clear()
+        _parse_directory(directory, True, out_definitions, out_evaluations_context,
+                         out_parsing_errors, env_vars,
+                         dir_filter=lambda d: self._check_process_dir(d))
+
+    @staticmethod
+    def parse_file(file: str, parsing_errors: Dict[str, Exception] = None) -> Optional[Dict]:
+        if not file.endswith(".tf") and not file.endswith(".tf.json"):
+            return None
+        return _load_or_die_quietly(Path(file), parsing_errors)
+
+
+def _parse_directory(directory: str, include_sub_dirs: bool, out_definitions: Dict,
+                     out_evaluations_context: Optional[Dict[str, Dict[str, EvaluationContext]]] = None,
+                     out_parsing_errors: Optional[Dict[str, Exception]] = None,
+                     env_vars: Optional[Mapping[str, str]] = None,
+                     module_loader_registry: ModuleLoaderRegistry = default_ml_registry,
+                     dir_filter: Callable[[str], bool] = lambda _: True):
+    """
+Load and resolve configuration files starting in the given directory, merging the
+resulting data into `tf_definitions`. This loads data according to the Terraform Code Organization
+specification (https://www.terraform.io/docs/configuration/index.html#code-organization), starting
+in the given directory and possibly moving out from there.
+
+The resulting data dictionary generally follows the layout of HCL parsing with a couple distinctions:
+- Data is broken out by file from which the data was loaded. So: <file>: <data>
+  - Loaded modules will also be keyed by referrer info: <file>[<referring_file>#<index>]: <data>
+- Module block will included a "__resolved__" key with a list of the file/referrer names under
+  which data for the file was loaded. For example: "__resolved__": ["main.tf#0"]. The values will
+  correspond to the file names mentioned in the first bullet.
+- All variables that can be resolved will be resolved.
+
+    :param directory:                  Directory in which .tf and .tfvars files will be loaded.
+    :param include_sub_dirs:           If true, subdirectories will be walked.
+    :param out_definitions:            Dict into which the "simple" TF data with variables resolved is put.
+    :param out_evaluations_context:    Dict into which context about resource definitions is placed. Outer
+                                       key is the file, inner key is a variable name.
+    :param out_parsing_errors:         Dict into which parsing errors, keyed on file path, are placed.
+    :param env_vars:                   Optional values to use for resolving environment variables in TF code.
+                                       If nothing is specified, Checkov's local environment will be used.
+    :param module_loader_registry:     Registry used for resolving modules. This allows customization of how
+                                       much resolution is performed (and easier testing) by using a manually
+                                       constructed registry rather than the default.
+    :param dir_filter:                 Determines whether or not a directory should be processed. Returning
+                                       True will allow processing. The argument will be the absolute path of
+                                       the directory.
+    """
+
+    if out_evaluations_context is None:
+        out_evaluations_context = {}
+    if out_parsing_errors is None:
+        out_parsing_errors = {}
+    if env_vars is None:
+        env_vars = dict(os.environ)
+
+    if include_sub_dirs:
+        for sub_dir, d_names, f_names in os.walk(directory):
+            filter_ignored_directories(d_names)
+            if dir_filter(os.path.abspath(sub_dir)):
+                _internal_dir_load(sub_dir, out_definitions,
+                                   out_evaluations_context, out_parsing_errors, env_vars, None,
+                                   module_loader_registry, dir_filter)
+    else:
+        _internal_dir_load(directory, out_definitions, out_evaluations_context,
+                           out_parsing_errors, env_vars, None, module_loader_registry, dir_filter)
+
+
+def _internal_dir_load(directory: str, out_definitions: Dict,
+                       out_evaluations_context: Dict[str, Dict[str, EvaluationContext]],
+                       out_parsing_errors: Dict[str, Exception],
+                       env_vars: Mapping[str, str],
+                       specified_vars: Optional[Mapping[str, str]],
+                       module_loader_registry: ModuleLoaderRegistry,
+                       dir_filter: Callable[[str], bool],
+                       module_load_context: Optional[str] = None):
+    """
+See `parse_directory` docs.
+    :param specified_vars:     Specifically defined variable values, overriding values from any other source.
+    """
+
+    # Stage 1: Look for applicable files in the directory:
+    #          https://www.terraform.io/docs/configuration/index.html#code-organization
+    #          Load the raw data for non-variable files, but perform no processing other than loading
+    #          variable default values.
+    #          Variable files are also flagged for later processing.
+    var_value_and_file_map: Dict[str, Tuple[Any, str]] = {}
+    hcl_tfvars: Optional[os.DirEntry] = None
+    json_tfvars: Optional[os.DirEntry] = None
+    auto_vars_files: Optional[List[os.DirEntry]] = None      # lazy creation
+    for file in os.scandir(directory):
+        # Ignore directories and hidden files
+        try:
+            if not file.is_file() or file.name.startswith("."):
+                continue
+        except OSError:
+            # Skip files that can't be accessed
+            continue
+
+        # Variable files
+        # See: https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files
+        if file.name == "terraform.tfvars.json":
+            json_tfvars = file
+            continue
+        elif file.name == "terraform.tfvars":
+            hcl_tfvars = file
+            continue
+        elif file.name.endswith(".auto.tfvars.json") or file.name.endswith(".auto.tfvars"):
+            if auto_vars_files is None:
+                auto_vars_files = [file]
+            else:
+                auto_vars_files.append(file.path)
+            continue
+
+        # Resource files
+        if file.name.endswith(".tf.json") or file.name.endswith(".tf"):
+            data = _load_or_die_quietly(file, out_parsing_errors)
+        else:
+            continue
+
+        if not data:        # failed loads or empty files
+            continue
+
+        out_definitions[file.path] = data
+
+        # Load variable defaults
+        #  (see https://www.terraform.io/docs/configuration/variables.html#declaring-an-input-variable)
+        var_blocks = data.get("variable")
+        if var_blocks and isinstance(var_blocks, list):
+            for var_block in var_blocks:
+                if not isinstance(var_block, dict):
+                    continue
+                for var_name, var_definition in var_block.items():
+                    if not isinstance(var_definition, dict):
+                        continue
+
+                    default_value = var_definition.get("default")
+                    if default_value is not None and isinstance(default_value, list):
+                        var_value_and_file_map[var_name] = default_value[0], file.path
+
+    # Stage 2: Load vars in proper order:
+    #          https://www.terraform.io/docs/configuration/variables.html#variable-definition-precedence
+    #          Defaults are loaded in stage 1.
+    #          Then loading in this order with later taking precedence:
+    #             - Environment variables
+    #             - The terraform.tfvars file, if present.
+    #             - The terraform.tfvars.json file, if present.
+    #             - Any *.auto.tfvars or *.auto.tfvars.json files, processed in lexical order of
+    #               their filenames.
+    #          Overriding everything else, variables form `specified_vars`, which are considered
+    #          directly set.
+    for key, value in env_vars.items():                                 # env vars
+        if not key.startswith("TF_VAR_"):
+            continue
+        var_value_and_file_map[key[7:]] = value, f"env:{key}"
+    if hcl_tfvars:                                                      # terraform.tfvars
+        data = _load_or_die_quietly(hcl_tfvars, out_parsing_errors)
+        if data:
+            var_value_and_file_map.update({k: (v, hcl_tfvars.path) for k, v in data.items()})
+    if json_tfvars:                                                     # terraform.tfvars.json
+        data = _load_or_die_quietly(json_tfvars, out_parsing_errors)
+        if data:
+            var_value_and_file_map.update({k: (v, json_tfvars.path) for k, v in data.items()})
+    if auto_vars_files:                                                 # *.auto.tfvars / *.auto.tfvars.json
+        for var_file in sorted(auto_vars_files, key=os.DirEntry.name):
+            data = _load_or_die_quietly(var_file, out_parsing_errors)
+            if data:
+                var_value_and_file_map.update({k: (v, var_file.path) for k, v in data.items()})
+    if specified_vars:                                                  # specified
+        var_value_and_file_map.update({k: (v, "manual specification") for k, v in specified_vars.items()})
+
+    # IMPLEMENTATION NOTE: When resolving `module.` references, access to the entire data map is needed. It
+    #                      may be a little overboard, but I don't want to just pass the entire data map down
+    #                      because it break encapsulations and I don't want to cause confusion about what data
+    #                      set it being processed. To avoid this, here's a Callable that will get the data
+    #                      map for a particular module reference. (Might be OCD, but...)
+    module_data_retrieval = lambda module_ref: out_definitions.get(module_ref)
+
+    # Stage 3: Variable resolution round 1 - no modules yet
+    _process_vars_and_locals(out_definitions, out_evaluations_context, directory, var_value_and_file_map,
+                             module_data_retrieval)
+
+    # Stage 4: Load modules
+    _load_modules(out_definitions, out_evaluations_context, out_parsing_errors,
+                  env_vars, directory, module_loader_registry, dir_filter, module_load_context)
+
+    # Stage 5: Variable resolution round 2 - now with modules
+    _process_vars_and_locals(out_definitions, out_evaluations_context, directory, var_value_and_file_map,
+                             module_data_retrieval)
+
+
+def _process_vars_and_locals(out_definitions: Dict,
+                             evaluations_context: Dict[str, Dict[str, EvaluationContext]],
+                             directory: str,
+                             var_value_and_file_map: Dict[str, Tuple[Any, str]],
+                             module_data_retrieval: Callable[[str], Dict[str, Any]]):
+    locals_values = {}
+    for file_data in out_definitions.values():
+        file_locals = file_data.get("locals")
+        if not file_locals:
+            continue
+        for k, v in file_locals[0].items():
+            locals_values[k] = v[0]
+
+    # Processing is done in a loop to deal with chained references and the like.
+    # Loop while the data is being changed, stop when no more changes are happening.
+    # To ensure there's not some kind of oscillation, a cap of 25 passes is in place.
+    # More than a couple loops isn't normally expected.
+    # NOTE: If this approach proves to be a performance liability, a DAG will be needed.
+    loop_count = 0
+    for i in range(0, 25):
+        loop_count += 1
+
+        made_change = False
+        # Put out file layer here so the context works inside the loop
+        for file, file_data in out_definitions.items():
+            eval_context_dict = evaluations_context.get(file)
+            if eval_context_dict is None:
+                eval_context_dict = {}
+                evaluations_context[file] = eval_context_dict
+                # out_evaluations_context[os.path.join(directory, file)] = {
+                #     var_name: EvaluationContext(os.path.relpath(file.path, directory))
+                # }
+
+            if _process_vars_and_locals_loop(file_data,
+                                             eval_context_dict,
+                                             os.path.relpath(file, directory),
+                                             var_value_and_file_map, locals_values,
+                                             file_data.get("resource"),
+                                             file_data.get("module"),
+                                             module_data_retrieval,
+                                             directory):
+                made_change = True
+
+            if len(eval_context_dict) == 0:
+                del evaluations_context[file]
+        if not made_change:
+            break
+
+    LOGGER.debug("Processing variables took %d loop iterations", loop_count)
+
+
+def _process_vars_and_locals_loop(out_definitions: Dict,
+                                  eval_map_by_var_name: Dict[str, EvaluationContext], relative_file_path: str,
+                                  var_value_and_file_map: Dict[str, Tuple[Any, str]],
+                                  locals_values: Dict[str, Any],
+                                  resource_list: Optional[List[Dict[str, Any]]],
+                                  module_list: Optional[List[Dict[str, Any]]],
+                                  module_data_retrieval: Callable[[str], Dict[str, Any]],
+                                  root_directory: str,
+                                  outer_context: str = "") -> bool:
+
+    # Generic loop for handling a source of key/value tuples (e.g., enumerate() or <dict>.items())
+    def process_items_helper(key_value_iterator, data_map, context, allow_str_bool_translation: bool):
+        made_change = False
+        for key, value in list(key_value_iterator()):       # Copy to list to allow deletion
+            new_context = f"{context}/{key}" if len(context) != 0 else key
+
+            if isinstance(value, str):
+                altered_value = value
+
+                had_pattern_match = False
+                for match in _VAR_PATTERN.finditer(value):
+                    var_base = match[1]
+
+                    # Expressions such as (from variable definition):
+                    #    type = string
+                    # are turned into:
+                    #    "type = ${string}"
+                    if var_base in _SIMPLE_TYPES and match[0] == value:
+                        altered_value = var_base
+                        had_pattern_match = True
+                    else:
+                        replaced = _handle_single_var_pattern(var_base, var_value_and_file_map, locals_values,
+                                                              resource_list,
+                                                              module_list, module_data_retrieval,
+                                                              eval_map_by_var_name,
+                                                              new_context, value, root_directory)
+                        if replaced != var_base:
+                            if match[0] == value:
+                                altered_value = replaced
+                            else:
+                                altered_value = altered_value.replace(match[0], str(replaced))
+                            had_pattern_match = True
+
+                if not had_pattern_match:
+                    # tomap is annoying because the curly braces in the string break the regex. Rather than
+                    # coming up with something that's significantly more complex, we'll special case this
+                    # check. Only do this when there wasn't a pattern match to make sure there are no
+                    # variables lingering in the map value.
+                    # https://www.terraform.io/docs/configuration/functions/tomap.html
+                    if value.startswith("${tomap(") and value.endswith(")}"):
+                        trimmed = value[8:-2]
+                        trimmed = trimmed.replace(":", "=")     # converted to colons by parser #shrug
+                        altered_value = _eval_string(trimmed)
+
+                        if not isinstance(altered_value, dict):
+                            continue
+
+                        # If there is a string and anything else, convert to string
+                        had_string = False
+                        had_something_else = False
+                        for k, v in altered_value.items():
+                            if v == "${True}":
+                                altered_value[k] = True
+                                v = True
+                            elif v == "${False}":
+                                altered_value[k] = False
+                                v = False
+
+                            if isinstance(v, str):
+                                had_string = True
+                                if had_something_else:
+                                    break
+                            else:
+                                had_something_else = True
+                                if had_string:
+                                    break
+                        if had_string and had_something_else:
+                            altered_value = {k: _tostring(v) for k, v in altered_value.items()}
+                    # Same as above, regex can blow this up
+                    # (see parser scenario: tostring_function, INNER_CURLY
+                    elif value.startswith("${tostring(\"") and value.endswith("\")}"):
+                        altered_value = value[12:-3]
+
+                    # Support HCL 0.11 optional boolean syntax - evaluate "true" to true and "false" to false
+                    #
+                    # `allow_str_bool_translation` exists because we want to prevent conversion in a dict
+                    # which is a direct value. See the "MIXED_BOOL" variable in the "tomap_function" parser
+                    # scenario for a situation which worked incorrectly without this.
+                    # NOTE: This is probably not a big deal to be removed if this causes problems in other
+                    #       places. The MIXED_BOOL test case is technically correct with the TF spec, but
+                    #       isn't essential operation for Checkov.
+                    elif allow_str_bool_translation and value == "true":
+                        altered_value = True
+                    elif allow_str_bool_translation and value == "false":
+                        altered_value = False
+
+                if value != altered_value:
+                    LOGGER.debug(f"Resolve: %s --> %s", value, altered_value)
+                    data_map[key] = altered_value
+                    made_change = True
+            elif isinstance(value, dict):
+                if _process_vars_and_locals_loop(value, eval_map_by_var_name, relative_file_path,
+                                                 var_value_and_file_map,
+                                                 locals_values, resource_list,
+                                                 module_list, module_data_retrieval, root_directory,
+                                                 new_context):
+                    made_change = True
+
+            elif isinstance(value, list):
+                if process_items_helper(lambda: enumerate(value), value, new_context, True):
+                    made_change = True
+                # Some special cases that should be pruned from datasets
+                if value == [None] or value == [{}] or len(value) == 0:
+                    del data_map[key]
+        return made_change
+
+    return process_items_helper(out_definitions.items, out_definitions, outer_context, False)
+
+
+def _load_modules(out_definitions: Dict,
+                  out_evaluations_context: Dict[str, Dict[str, EvaluationContext]],
+                  out_parsing_errors: Dict[str, Exception],
+                  env_vars: Mapping[str, str],
+                  directory: str, module_loader_registry: ModuleLoaderRegistry,
+                  dir_filter: Callable[[str], bool],
+                  module_load_context: Optional[str]):
+
+    all_module_definitions = {}
+    all_module_evaluations_context = {}
+    for file, file_data in out_definitions.items():
+        module_calls = file_data.get("module")
+        if not module_calls or not isinstance(module_calls, list):
+            continue
+
+        for module_index, module_call in enumerate(module_calls):
+            if not isinstance(module_call, dict):
+                continue
+
+            # There should only be one module reference per outer dict, but... safety first
+            for module_call_name, module_call_data in module_call.items():
+                if not isinstance(module_call_data, dict):
+                    continue
+
+                source = module_call_data.get("source")
+                if not source or not isinstance(source, list):
+                    continue
+                source = source[0]
+
+                # Special handling for local sources to make sure we aren't double-parsing
+                if source.startswith("./") or source.startswith("../"):
+                    module_path = os.path.normpath(os.path.join(directory, source))
+                    if not dir_filter(os.path.abspath(module_path)):
+                        continue
+
+                version = module_call_data.get("version")
+                if version and isinstance(version, list):
+                    version = version[0]
+
+                try:
+                    with module_loader_registry.load(directory, source, version) as content:
+                        if not content.loaded():
+                            continue
+
+                        # Variables being passed to module, "source" and "version" are reserved
+                        specified_vars = {k: v[0] for k, v in module_call_data.items()
+                                          if k != "source" and k != "version"}
+
+                        module_definitions = {}
+                        module_evaluations_context = {}
+                        _internal_dir_load(content.path(), module_definitions,
+                                           module_evaluations_context, out_parsing_errors, env_vars,
+                                           specified_vars, module_loader_registry, dir_filter,
+                                           module_load_context)
+
+                        if not module_definitions:
+                            continue
+
+                        # NOTE: Modules are put into the main TF definitions structure "as normal" with the
+                        #       notable exception of the file name. For loaded modules referrer information is
+                        #       appended to the file name to create this format:
+                        #         <file_name>[<referred_file>#<referrer_index>]
+                        #       For example:
+                        #         /the/path/module/my_module.tf[/the/path/main.tf#0]
+                        #       The referrer and index allow a module allow a module to be loaded multiple
+                        #       times with differing data.
+                        #
+                        #       In addition, the referring block will have a "__resolved__" key added with a
+                        #       list pointing to the location of the module data that was resolved. For example:
+                        #         "__resolved__": ["/the/path/module/my_module.tf[/the/path/main.tf#0]"]
+
+                        resolved_loc_list = module_call_data.get("__resolved__")
+                        if resolved_loc_list is None:
+                            resolved_loc_list = []
+                            module_call_data["__resolved__"] = resolved_loc_list
+
+                        # NOTE: Modules can load other modules, so only append referrer information where it
+                        #       has not already been added.
+                        keys = list(module_definitions.keys())
+                        for key in keys:
+                            if key.endswith("]"):
+                                continue
+                            new_key = f"{key}[{file}#{module_index}]"
+                            module_definitions[new_key] = \
+                                module_definitions[key]
+                            del module_definitions[key]
+
+                            resolved_loc_list.append(new_key)
+
+                        deep_merge.merge(all_module_definitions, module_definitions)
+                        # TODO: Not sure what to do with variable evaluations
+                        deep_merge.merge(all_module_evaluations_context, module_evaluations_context)
+                except Exception as e:
+                    logging.warning("Unable to load module (source=\"%s\" version=\"%s\"): %s",
+                                    source, version, e)
+                    pass
+
+    if all_module_definitions:
+        deep_merge.merge(out_definitions, all_module_definitions)
+        deep_merge.merge(out_evaluations_context, all_module_evaluations_context)
+
+
+def _handle_single_var_pattern(orig_variable: str, var_value_and_file_map: Dict[str, Tuple[Any, str]],
+                               locals_values: Dict[str, Any],
+                               resource_list: Optional[List[Dict[str, Any]]],
+                               module_list: Optional[List[Dict[str, Any]]],
+                               module_data_retrieval: Callable[[str], Dict[str, Any]],
+                               eval_map_by_var_name: Dict[str, EvaluationContext],
+                               context, orig_variable_full, root_directory: str) -> Any:
+    if "${" in orig_variable:
+        return orig_variable
+
+    elif orig_variable.startswith("module."):
+        if not module_list:
+            return orig_variable
+
+        # Reference to module outputs, example: 'module.bucket.bucket_name'
+        ref_tokens = orig_variable.split(".")
+        if len(ref_tokens) != 3:
+            return orig_variable        # fail safe, can the length ever be something other than 3?
+
+        try:
+            ref_list = jmespath.search(f"[].{ref_tokens[1]}.__resolved__[]", module_list)
+            #                                ^^^^^^^^^^^^^ module name
+
+            if not ref_list or not isinstance(ref_list, list):
+                return orig_variable
+
+            for ref in ref_list:
+                module_data = module_data_retrieval(ref)
+                if not module_data:
+                    continue
+
+                result = _handle_indexing(ref_tokens[2],
+                                          lambda r: jmespath.search(f"output[].{ref_tokens[2]}.value[] | [0]",
+                                                                    module_data))
+                if result:
+                    logging.debug("Resolved module ref:  %s --> %s", orig_variable, result)
+                    return result
+        except ValueError:
+            pass
+        return orig_variable
+
+    elif orig_variable == "True":
+        return True
+    elif orig_variable == "False":
+        return False
+
+    elif orig_variable.startswith("var."):
+        var_name = orig_variable[4:]
+        var_value_and_file = _handle_indexing(var_name, lambda r: var_value_and_file_map.get(r))
+        if var_value_and_file is not None:
+            var_value, var_file = var_value_and_file
+            eval_context = eval_map_by_var_name.get(var_name)
+            if eval_context is None:
+                eval_map_by_var_name[var_name] = EvaluationContext(os.path.relpath(var_file, root_directory),
+                                                                   var_value,
+                                                                   [VarReference(var_name,
+                                                                                 orig_variable_full,
+                                                                                 context)])
+            else:
+                eval_context.definitions.append(VarReference(var_name, orig_variable_full, context))
+            return var_value
+    elif orig_variable.startswith("local."):
+        var_value = _handle_indexing(orig_variable[6:], lambda r: locals_values.get(r))
+        if var_value is not None:
+            return var_value
+    elif orig_variable.startswith("to") and orig_variable.endswith(")"):
+        # https://www.terraform.io/docs/configuration/functions/tobool.html
+        if orig_variable.startswith("tobool("):
+            bool_variable = orig_variable[7:-1].lower()
+            if bool_variable == "true" or bool_variable == '"true"':
+                return True
+            elif bool_variable == "false" or bool_variable == '"false"':
+                return False
+            else:
+                return orig_variable
+        # https://www.terraform.io/docs/configuration/functions/tolist.html
+        elif orig_variable.startswith("tolist("):
+            altered_value = _eval_string(orig_variable[7:-1])
+            return altered_value if isinstance(altered_value, list) else list(altered_value)
+        # NOTE: tomap as handled outside this loop (see below)
+        # https://www.terraform.io/docs/configuration/functions/tonumber.html
+        elif orig_variable.startswith("tonumber("):
+            num_variable = orig_variable[9:-1]
+            if num_variable.startswith('"') and num_variable.endswith('"'):
+                num_variable = num_variable[1:-1]
+            try:
+                if "." in num_variable:
+                    return float(num_variable)
+                else:
+                    return int(num_variable)
+            except ValueError:
+                return orig_variable
+        # https://www.terraform.io/docs/configuration/functions/toset.html
+        elif orig_variable.startswith("toset("):
+            return set(_eval_string(orig_variable[6:-1]))
+        # https://www.terraform.io/docs/configuration/functions/tostring.html
+        elif orig_variable.startswith("tostring("):
+            altered_value = orig_variable[9:-1]
+            # Indicates a safe string, all good
+            if altered_value.startswith('"') and altered_value.endswith('"'):
+                return altered_value[1:-1]
+            # Otherwise, need to check for valid types (number or bool)
+            elif altered_value == "true":
+                return True
+            elif altered_value == "false":
+                return False
+            else:
+                try:
+                    if "." in altered_value:
+                        return str(float(altered_value))
+                    else:
+                        return str(int(altered_value))
+                except ValueError:
+                    return orig_variable     # no change
+    # TODO - format() support, still in progress
+    # elif orig_variable.startswith("format(") and orig_variable.endswith(")"):
+    #     format_tokens = orig_variable[7:-1].split(",")
+    #     return format_tokens[0].format([_to_native_value(t) for t in format_tokens[1:]])
+
+    elif _RESOURCE_REF_PATTERN.match(orig_variable):
+        # Reference to resources, example: 'aws_s3_bucket.example.bucket'
+        # TODO: handle index into map/list
+        try:
+            result = jmespath.search(f"[].{orig_variable}[] | [0]", resource_list)
+        except ValueError:
+            pass
+        else:
+            if result is not None:
+                return result
+
+    return orig_variable        # fall back to no change
+
+
+def _handle_indexing(reference: str, data_source: Callable[[str], Optional[Any]]) -> Optional[Any]:
+    if reference.endswith("]") and "[" in reference:
+        base_ref = reference[:reference.rindex("[")]
+        value = data_source(base_ref)
+        if isinstance(value, dict):
+            return value.get(reference[reference.rindex("[")+1: -1])
+        elif isinstance(value, list):
+            return value[int(reference[reference.rindex("[")+1: -1])]
+    else:
+        return data_source(reference)
+
+
+def _load_or_die_quietly(file: os.PathLike, parsing_errors: Dict) -> Optional[Mapping]:
+    """
+Load JSON or HCL, depending on filename.
+    :return: None if the file can't be loaded
+    """
+
+    file_path = os.fspath(file)
+    file_name = os.path.basename(file_path)
+
+    try:
+        with open(file, "r") as f:
+            if file_name.endswith(".json"):
+                return _clean_bad_definitions(json.load(f))
+            else:
+                return _clean_bad_definitions(hcl2.load(f))
+    except Exception as e:
+        LOGGER.debug(f'failed while parsing file {file}', exc_info=e)
+        parsing_errors[file_path] = e
+        return None
+
+
+def _clean_bad_definitions(tf_definition_list):
+    return {
+        block_type: list(filter(lambda definition_list: block_type == 'locals' or len(definition_list.keys()) == 1, tf_definition_list[block_type]))
+        for block_type in tf_definition_list.keys()
+    }
+
+
+def _eval_string(value: str) -> Any:
+    value_string = value.replace("'", '"')
+    parsed = hcl2.loads(f'eval = {value_string}\n')      # NOTE: newline is needed
+    return parsed["eval"][0]
+
+
+def _to_native_value(value: str) -> Any:
+    if value.startswith('"') or value.startswith("'"):
+        return value[1:-1]
+    else:
+        return _eval_string(value)
+
+
+def _tostring(value: Any) -> str:
+    if value is True:
+        return "true"
+    elif value is False:
+        return "false"
+    return str(value)
diff --git a/checkov/terraform/runner.py b/checkov/terraform/runner.py
index c388e2effb..3a12435544 100644
--- a/checkov/terraform/runner.py
+++ b/checkov/terraform/runner.py
@@ -1,5 +1,7 @@
+import dataclasses
 import logging
 import os
+from typing import Dict
 
 import dpath.util
 
@@ -7,6 +9,7 @@
 from checkov.common.output.report import Report
 from checkov.common.util import dict_utils
 from checkov.common.runners.base_runner import BaseRunner
+from checkov.common.variables.context import EvaluationContext
 from checkov.runner_filter import RunnerFilter
 from checkov.terraform.checks.data.registry import data_registry
 from checkov.terraform.checks.module.registry import module_registry
@@ -14,19 +17,17 @@
 from checkov.terraform.checks.resource.registry import resource_registry
 from checkov.terraform.context_parsers.registry import parser_registry
 from checkov.terraform.evaluation.base_variable_evaluation import BaseVariableEvaluation
-from checkov.terraform.evaluation.evaluation_methods.const_variable_evaluation import ConstVariableEvaluation
-from checkov.terraform.parser import Parser
+from checkov.terraform.parser2 import Parser2 as Parser
 
 # Allow the evaluation of empty variables
 dpath.options.ALLOW_EMPTY_STRING_KEYS = True
 
-TRUE_STRING = "true"
-ONE_STRING = "1"
-FALSE_STRING = "false"
-ZERO_STRING = "0"
 LOG_LEVEL = os.getenv('LOG_LEVEL', 'WARNING').upper()
 logging.basicConfig(level=LOG_LEVEL)
 
+CHECK_BLOCK_TYPES = frozenset(['resource', 'data', 'provider', 'module'])
+
+
 class Runner(BaseRunner):
     check_type = "terraform"
 
@@ -34,6 +35,7 @@ def __init__(self, parser=Parser()):
         self.parser = parser
         self.tf_definitions = {}
         self.definitions_context = {}
+        self.evaluations_context: Dict[str, Dict[str, EvaluationContext]] = {}
 
     block_type_registries = {
         'resource': resource_registry,
@@ -51,7 +53,11 @@ def run(self, root_folder, external_checks_dir=None, files=None, runner_filter=R
                 resource_registry.load_external_checks(directory, runner_filter)
         if root_folder:
             root_folder = os.path.abspath(root_folder)
-            self.parser.hcl2(directory=root_folder, tf_definitions=self.tf_definitions, parsing_errors=parsing_errors)
+
+            self.parser.parse_directory(directory=root_folder,
+                                        out_definitions=self.tf_definitions,
+                                        out_evaluations_context=self.evaluations_context,
+                                        out_parsing_errors=parsing_errors)
             self.check_tf_definition(report, root_folder, runner_filter, collect_skip_comments)
 
         if files:
@@ -66,64 +72,80 @@ def run(self, root_folder, external_checks_dir=None, files=None, runner_filter=R
 
         return report
 
-    def evaluate_string_booleans(self):
-        # Support HCL 0.11 optional boolean syntax - evaluate "true" and "1" to true, "false" and "0" to false
-        for tf_file in self.tf_definitions.keys():
-            values_to_replace = dpath.util.search(self.tf_definitions[tf_file], "**",
-                                                  afilter=lambda x: x in (TRUE_STRING, ONE_STRING, FALSE_STRING, ZERO_STRING),
-                                                  yielded=True)
-            for var_path, var_value in values_to_replace:
-                if not var_path.endswith('alias/0'):
-                    dpath.set(self.tf_definitions[tf_file], var_path, True if var_value in (TRUE_STRING, ONE_STRING) else False)
-
     def check_tf_definition(self, report, root_folder, runner_filter, collect_skip_comments=True, external_definitions_context=None):
         parser_registry.reset_definitions_context()
         if external_definitions_context:
             definitions_context = external_definitions_context
         else:
-            logging.debug('Evaluating string booleans')
-            self.evaluate_string_booleans()
-            logging.debug('Evaluated string booleans. Creating definitions context')
             definitions_context = {}
             for definition in self.tf_definitions.items():
                 definitions_context = parser_registry.enrich_definitions_context(definition, collect_skip_comments)
-            variable_evaluator = ConstVariableEvaluation(root_folder, self.tf_definitions, definitions_context)
-            variable_evaluator.evaluate_variables()
-            self.tf_definitions, self.definitions_context = variable_evaluator.tf_definitions, variable_evaluator.definitions_context
+            self.definitions_context = definitions_context
             logging.debug('Created definitions context')
 
         for full_file_path, definition in self.tf_definitions.items():
-            scanned_file = f"/{os.path.relpath(full_file_path, root_folder)}"
+            scanned_file = f"/{os.path.relpath(self._strip_module_referrer(full_file_path), root_folder)}"
             logging.debug(f"Scanning file: {scanned_file}")
-            for block_type in definition.keys():
-                if block_type in ['resource', 'data', 'provider', 'module']:
-                    self.run_block(definition[block_type], definitions_context, full_file_path, report, scanned_file,
-                                   block_type, runner_filter)
+            self.run_all_blocks(definition, definitions_context, full_file_path, root_folder, report,
+                                scanned_file, runner_filter)
+
+    def run_all_blocks(self, definition, definitions_context, full_file_path, root_folder, report,
+                       scanned_file, runner_filter):
+        for block_type in definition.keys():
+            if block_type in CHECK_BLOCK_TYPES:
+                self.run_block(definition[block_type], definitions_context,
+                               full_file_path, root_folder, report,
+                               scanned_file, block_type, runner_filter)
+
+    def run_block(self, entities,
+                  definition_context,
+                  full_file_path, root_folder, report, scanned_file,
+                  block_type, runner_filter=None, entity_context_path_header=None):
 
-    def run_block(self, entities, definition_context, full_file_path, report, scanned_file, block_type,
-                  runner_filter=None):
         registry = self.block_type_registries[block_type]
-        if registry:
-            for entity in entities:
-                entity_evaluations = None
-                context_parser = parser_registry.context_parsers[block_type]
-                definition_path = context_parser.get_entity_context_path(entity)
-                entity_id = ".".join(definition_path)
+        if not registry:
+            return
+
+        for entity in entities:
+            entity_evaluations = None
+            context_parser = parser_registry.context_parsers[block_type]
+            definition_path = context_parser.get_entity_context_path(entity)
+            entity_id = ".".join(definition_path)
+            if entity_context_path_header is None:
                 entity_context_path = [block_type] + definition_path
-                # Entity can exist only once per dir, for file as well
+            else:
+                entity_context_path = entity_context_path_header + block_type + definition_path
+            # Entity can exist only once per dir, for file as well
+            try:
                 entity_context = dict_utils.getInnerDict(definition_context[full_file_path], entity_context_path)
                 entity_lines_range = [entity_context.get('start_line'), entity_context.get('end_line')]
                 entity_code_lines = entity_context.get('code_lines')
                 skipped_checks = entity_context.get('skipped_checks')
-                variables_evaluations = definition_context[full_file_path].get('evaluations')
-                if variables_evaluations:
-                    entity_evaluations = BaseVariableEvaluation.reduce_entity_evaluations(variables_evaluations,
-                                                                                          entity_context_path)
-                results = registry.scan(scanned_file, entity, skipped_checks, runner_filter)
-                for check, check_result in results.items():
-                    record = Record(check_id=check.id, check_name=check.name, check_result=check_result,
-                                    code_block=entity_code_lines, file_path=scanned_file,
-                                    file_line_range=entity_lines_range,
-                                    resource=entity_id, evaluations=entity_evaluations,
-                                    check_class=check.__class__.__module__)
-                    report.add_record(record=record)
+            except KeyError:
+                # TODO: Context info isn't working for modules
+                entity_lines_range = None
+                entity_code_lines = None
+                skipped_checks = None
+
+            if full_file_path in self.evaluations_context:
+                variables_evaluations = {}
+                for var_name, context_info in self.evaluations_context.get(full_file_path, {}).items():
+                    variables_evaluations[var_name] = dataclasses.asdict(context_info)
+                entity_evaluations = BaseVariableEvaluation.reduce_entity_evaluations(variables_evaluations,
+                                                                                      entity_context_path)
+            results = registry.scan(scanned_file, entity, skipped_checks, runner_filter)
+            for check, check_result in results.items():
+                record = Record(check_id=check.id, check_name=check.name, check_result=check_result,
+                                code_block=entity_code_lines, file_path=scanned_file,
+                                file_line_range=entity_lines_range,
+                                resource=entity_id, evaluations=entity_evaluations,
+                                check_class=check.__class__.__module__)
+                report.add_record(record=record)
+
+    @staticmethod
+    def _strip_module_referrer(file_path:str) -> str:
+        if file_path.endswith("]") and "[" in file_path:
+            return file_path[:file_path.index("[")]
+        else:
+            return file_path
+
diff --git a/docs/2.Concepts/Evaluations.md b/docs/2.Concepts/Evaluations.md
index 3c7074e999..219e83cf9f 100644
--- a/docs/2.Concepts/Evaluations.md
+++ b/docs/2.Concepts/Evaluations.md
@@ -117,7 +117,7 @@ Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public access."
 If available, each `PASSED/FAILED` check contains the evaluation information, which contains all the variables
  who were evaluated.
 
-Each variable contains it's variable source file path, the evaluated value and the expressions in
+Each variable contains its variable source file path, the evaluated value, and the expressions in
 which it was referenced:
 
 ```json
diff --git a/setup.py b/setup.py
index f72b7a277c..c31b7cc091 100644
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,7 @@
         "boto3==1.12.43",
         "chardet==3.0.4",
         "colorama==0.4.3",
+        "deep_merge==0.0.4",
         "docopt==0.6.2",
         "idna==2.8",
         "jmespath==0.10.0",
@@ -81,6 +82,7 @@
         "pyyaml==5.3.1",
         "requests==2.22.0",
         "six==1.15.0",
+        "semantic_version==2.8.5",
         "tabulate==0.8.6",
         "termcolor==1.1.0",
         "urllib3==1.25.10",
diff --git a/tests/common/checks/test_base_check.py b/tests/common/checks/test_base_check.py
index 59a3afe25b..965faf020b 100644
--- a/tests/common/checks/test_base_check.py
+++ b/tests/common/checks/test_base_check.py
@@ -60,7 +60,7 @@ def __init__(self):
                 def scan_entity_conf(self, conf, some_unexpected_parameter_123):
                     return CheckResult.PASSED
         self.assertIsInstance(context.exception, NotImplementedError)
-        self.assertEquals(
+        self.assertEqual(
             "The signature ((\'self\', \'conf\', \'some_unexpected_parameter_123\'), None, None) for scan_entity_conf "
             "is not supported.",
             context.exception.args[0]
diff --git a/tests/common/goget/test_goget_base.py b/tests/common/goget/test_goget_base.py
index 8988e38c66..6fceebcf63 100644
--- a/tests/common/goget/test_goget_base.py
+++ b/tests/common/goget/test_goget_base.py
@@ -7,13 +7,16 @@
 class TestBaseGetter(unittest.TestCase):
 
     def test_directory_creation(self):
-        current_dir =  os.getcwd()
+        current_dir = os.getcwd()
         getter = LocalGetter(current_dir)
         result_dir = getter.get()
         print(current_dir)
         print(result_dir)
         self.assertTrue(current_dir in result_dir)
 
+        # Cleanup
+        os.rmdir(getter.temp_dir)
+
 
 
 if __name__ == '__main__':
diff --git a/tests/common/runner_registry/example_multi_iac/tf/example.tf b/tests/common/runner_registry/example_multi_iac/tf/example.tf
index a384c5c38b..845e224bb5 100644
--- a/tests/common/runner_registry/example_multi_iac/tf/example.tf
+++ b/tests/common/runner_registry/example_multi_iac/tf/example.tf
@@ -1,3 +1,8 @@
+variable "region" {
+  default = "us-east-1"
+}
+variable "VERSIONING_ENABLED" {}
+
 resource "aws_s3_bucket" "foo-bucket" {
   region        = var.region
   bucket        = local.bucket_name
@@ -6,7 +11,7 @@ resource "aws_s3_bucket" "foo-bucket" {
     Name = "foo-${data.aws_caller_identity.current.account_id}"
   }
   versioning {
-    enabled = true
+    enabled = var.VERSIONING_ENABLED
     mfa_delete = true
   }
   logging {
diff --git a/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py b/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py
index 9c6b0c4363..8b656b7b81 100644
--- a/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py
+++ b/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py
@@ -1,7 +1,10 @@
+import os
 import unittest
 
 from checkov.common.models.enums import CheckResult
+from checkov.runner_filter import RunnerFilter
 from checkov.terraform.checks.resource.aws.CloudfrontDistributionLogging import check
+from checkov.terraform.runner import Runner
 
 
 class TestCloudfrontDistributionLogging(unittest.TestCase):
@@ -25,6 +28,19 @@ def test_success(self):
         scan_result = check.scan_resource_conf(conf=resource_conf)
         self.assertEqual(CheckResult.PASSED, scan_result)
 
+    def test_null_var_651(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+        valid_dir_path = os.path.join(current_dir,
+                                      "../../../parser/resources/parser_scenarios/null_variables_651")
+        valid_dir_path = os.path.normpath(valid_dir_path)
+        runner = Runner()
+        checks_allowlist = ['CKV_AWS_86']
+        report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
+                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+        self.assertEqual(len(report.failed_checks), 1)
+        for record in report.failed_checks:
+            self.assertIn(record.check_id, checks_allowlist)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/terraform/context_parsers/test_variable_context_parser.py b/tests/terraform/context_parsers/test_variable_context_parser.py
index cba330a605..a38a42035c 100644
--- a/tests/terraform/context_parsers/test_variable_context_parser.py
+++ b/tests/terraform/context_parsers/test_variable_context_parser.py
@@ -1,5 +1,5 @@
 import unittest
-from checkov.terraform.parser import Parser
+from checkov.terraform.parser2 import Parser2
 from checkov.terraform.context_parsers.registry import parser_registry
 import os
 
@@ -10,7 +10,9 @@ def setUp(self):
         test_root_dir = os.path.dirname(os.path.realpath(__file__)) + '/../evaluation/resources/default_evaluation/'
         tf_definitions = {}
         parsing_errors = {}
-        Parser().hcl2(directory=test_root_dir, tf_definitions=tf_definitions, parsing_errors=parsing_errors)
+        Parser2().parse_directory(directory=test_root_dir,
+                                  out_definitions=tf_definitions,
+                                  out_parsing_errors=parsing_errors)
         for definition in tf_definitions.items():
             definitions_context = parser_registry.enrich_definitions_context(definition)
         self.definitions_context = definitions_context
diff --git a/tests/terraform/context_parsers/test_variable_context_parser2.py b/tests/terraform/context_parsers/test_variable_context_parser2.py
new file mode 100644
index 0000000000..91444f3d1f
--- /dev/null
+++ b/tests/terraform/context_parsers/test_variable_context_parser2.py
@@ -0,0 +1,42 @@
+import unittest
+from checkov.terraform.parser2 import Parser2 as Parser
+from checkov.terraform.context_parsers.registry import parser_registry
+import os
+
+
+class TestVariableContextParser(unittest.TestCase):
+
+    def setUp(self):
+        test_root_dir = os.path.dirname(os.path.realpath(__file__)) + '/../evaluation/resources/default_evaluation/'
+        tf_definitions = {}
+        parsing_errors = {}
+        Parser().parse_directory(directory=test_root_dir,
+                                 out_definitions=tf_definitions,
+                                 out_evaluations_context={},
+                                 out_parsing_errors=parsing_errors)
+        for definition in tf_definitions.items():
+            definitions_context = parser_registry.enrich_definitions_context(definition)
+        self.definitions_context = definitions_context
+
+    def test_assignments_exists(self):
+        self.assertIsNotNone(
+            self.definitions_context[os.path.dirname(
+                os.path.realpath(__file__)) + '/../evaluation/resources/default_evaluation/variables.tf'][
+                'variable'].get(
+                'assignments'))
+
+    def test_assignment_value(self):
+        self.assertEqual(
+            self.definitions_context[os.path.dirname(
+                os.path.realpath(__file__)) + '/../evaluation/resources/default_evaluation/variables.tf'][
+                'variable'].get(
+                'assignments').get('user_exists'), False)
+        self.assertEqual(
+            self.definitions_context[os.path.dirname(
+                os.path.realpath(__file__)) + '/../evaluation/resources/default_evaluation/variables.tf'][
+                'variable'].get(
+                'assignments').get('app_client_id'), 'Temp')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/terraform/evaluation/evaluation_methods/test_const_variable_evaluation.py b/tests/terraform/evaluation/evaluation_methods/test_const_variable_evaluation.py
deleted file mode 100644
index d2dc189e32..0000000000
--- a/tests/terraform/evaluation/evaluation_methods/test_const_variable_evaluation.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import unittest
-from checkov.terraform.parser import Parser
-from checkov.terraform.evaluation.evaluation_methods.const_variable_evaluation import ConstVariableEvaluation
-from checkov.terraform.context_parsers.registry import parser_registry
-import dpath.util
-import os
-
-
-class TestConstVariableEvaluation(unittest.TestCase):
-
-    def setUp(self):
-        test_root_dir = os.path.dirname(os.path.realpath(__file__)) + '/../resources/default_evaluation'
-        tf_definitions = {}
-        parsing_errors = {}
-        Parser().hcl2(directory=test_root_dir, tf_definitions=tf_definitions, parsing_errors=parsing_errors)
-        for definition in tf_definitions.items():
-            definitions_context = parser_registry.enrich_definitions_context(definition)
-        variable_evaluator = ConstVariableEvaluation(test_root_dir, tf_definitions, definitions_context)
-        variable_evaluator.evaluate_variables()
-        self.tf_definitions = variable_evaluator.tf_definitions
-        self.definitions_context = variable_evaluator.definitions_context
-
-    def test_evaluate_variables(self):
-        self.assertEqual(
-            dpath.get(self.tf_definitions[
-                          os.path.dirname(os.path.realpath(__file__)) + '/../resources/default_evaluation/main.tf'],
-                      'resource/0/aws_cognito_user_group/user_group/name/0'),
-            'Pavel_Checkov_group')
-
-    def test_all_expressions_evaluated(self):
-        self.assertEqual(
-            len(dpath.get(self.definitions_context[
-                              os.path.dirname(os.path.realpath(__file__)) + '/../resources/default_evaluation/main.tf'],
-                          'evaluations/dummy_1/definitions')),
-            2)
-
-    def tearDown(self):
-        parser_registry.definitions_context = {}
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/terraform/evaluation/test_base_variable_evaluation.py b/tests/terraform/evaluation/test_base_variable_evaluation.py
deleted file mode 100644
index eda7677afe..0000000000
--- a/tests/terraform/evaluation/test_base_variable_evaluation.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import os
-import unittest
-
-import dpath.util
-
-from checkov.terraform.context_parsers.registry import parser_registry
-from checkov.terraform.evaluation.base_variable_evaluation import BaseVariableEvaluation
-from checkov.terraform.evaluation.evaluation_methods.const_variable_evaluation import ConstVariableEvaluation
-from checkov.terraform.parser import Parser
-
-
-class TestBaseVariableEvaluation(unittest.TestCase):
-
-    def setUp(self):
-        test_root_dir = os.path.dirname(os.path.realpath(__file__)) + '/resources/default_evaluation'
-        tf_definitions = {}
-        parsing_errors = {}
-        definitions_context = {}
-        Parser().hcl2(directory=test_root_dir, tf_definitions=tf_definitions, parsing_errors=parsing_errors)
-        for definition in tf_definitions.items():
-            definitions_context = parser_registry.enrich_definitions_context(definition)
-        variable_evaluator = ConstVariableEvaluation(test_root_dir, tf_definitions, definitions_context)
-        variable_evaluator.evaluate_variables()
-        self.tf_definitions = variable_evaluator.tf_definitions
-        self.definitions_context = variable_evaluator.definitions_context
-
-    def test_extract_context_path(self):
-        path = 'resource/0/aws_cognito_user_group/user_group/name/0'
-        self.assertEqual(ConstVariableEvaluation.extract_context_path(path),
-                         ('resource/aws_cognito_user_group/user_group', 'name'))
-
-    def test_all_expressions_evaluated(self):
-        self.assertEqual(
-            len(dpath.get(self.definitions_context[
-                              os.path.dirname(os.path.realpath(__file__)) + '/resources/default_evaluation/main.tf'],
-                          'evaluations/dummy_1/definitions')),
-            2)
-
-    def test__is_variable_only_expression(self):
-        entry_expression1 = "${local.dummy_with_comma}"
-        assignment_regex1 = '((?:\\$\\{)?local\\.dummy_with_comma(?:\\})?)'
-        entry_expression2 = "var.customer_name"
-        assignment_regex2 = '((?:\\$\\{)?var\\.customer_name(?:\\})?)'
-        entry_expression3 = "${var.customer_name}_with_more_stuff"
-        assignment_regex3 = '((?:\\$\\{)?var\\.customer_name(?:\\})?)'
-        self.assertTrue(BaseVariableEvaluation._is_variable_only_expression(assignment_regex1, entry_expression1))
-        self.assertTrue(BaseVariableEvaluation._is_variable_only_expression(assignment_regex2, entry_expression2))
-        self.assertFalse(BaseVariableEvaluation._is_variable_only_expression(assignment_regex3, entry_expression3))
-
-    def test__generate_evaluation_regex(self):
-        definition_type1, var_name1 = 'locals', 'dummy'
-        definition_type2, var_name2 = 'variable', 'customer_name'
-        self.assertEqual(BaseVariableEvaluation._generate_evaluation_regex(definition_type1, var_name1),
-                         r"((?:\$\{)?local[.]dummy(?:\})?)")
-        self.assertEqual(BaseVariableEvaluation._generate_evaluation_regex(definition_type2, var_name2),
-                         r"((?:\$\{)?var[.]customer_name(?:\})?)")
-
-    def tearDown(self):
-        parser_registry.definitions_context = {}
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/terraform/evaluation/test_hcl2_load_assumptions.py b/tests/terraform/evaluation/test_hcl2_load_assumptions.py
new file mode 100644
index 0000000000..7aac8c1184
--- /dev/null
+++ b/tests/terraform/evaluation/test_hcl2_load_assumptions.py
@@ -0,0 +1,181 @@
+import json
+import unittest
+
+import hcl2
+
+
+# This group of tests is used to confirm assumptions about how the hcl2 library parses into json.
+# We want to make sure important assumptions are caught if behavior changes.
+class TestHCL2LoadAssumptions(unittest.TestCase):
+
+    def test_variable_block(self):
+        tf = '''
+        variable "my_var" {
+          type = string
+          default = "my_default_value"
+        }'''
+        expect = {
+            "variable": [
+                {
+                    "my_var": {
+                        "type": ["${string}"],              # NOTE: wrapped in eval markers
+                        "default": ["my_default_value"]
+                    }
+                }
+            ]
+        }
+        self.go(tf, expect)
+
+    def test_module_block(self):
+        tf = '''
+        module "bucket" {
+          source   = "./bucket"
+          name     = "module_bucket"
+          BLAH     = "a value"
+        }'''
+        expect = {
+            "module": [
+                {
+                    "bucket": {
+                        "source": ["./bucket"],
+                        "name": ["module_bucket"],
+                        "BLAH": ["a value"]
+                    }
+                }
+            ]
+        }
+        self.go(tf, expect)
+
+    def test_raw_assignment(self):
+        tf = 'my_var = "my_value"\n'
+        expect = {
+            "my_var": ["my_value"]
+        }
+        self.go(tf, expect)
+
+    def test_raw_assignment_true_string(self):
+        tf = 'my_var = "true"\n'
+        expect = {
+            "my_var": ["true"]
+        }
+        self.go(tf, expect)
+
+    def test_raw_assignment_false_string(self):
+        tf = 'my_var = "false"\n'
+        expect = {
+            "my_var": ["false"]
+        }
+        self.go(tf, expect)
+
+    def test_raw_assignment_1_string(self):
+        tf = 'my_var = "1"\n'
+        expect = {
+            "my_var": ["1"]
+        }
+        self.go(tf, expect)
+
+    def test_raw_assignment_0_string(self):
+        tf = 'my_var = "0"\n'
+        expect = {
+            "my_var": ["0"]
+        }
+        self.go(tf, expect)
+
+    def test_map_separators(self):
+        tf = '''
+        locals {
+          INTS = tomap({"a" = 1, "b" = 2})
+        }'''
+        expect = {
+            "locals": [
+                {
+                    "INTS": ["${tomap({'a': 1, 'b': 2})}"]          # WHA?? Equals to colons? Okay...
+                }
+            ]
+        }
+        self.go(tf, expect)
+
+    # from the "maze_of_variables" scenario
+    def test_maze_of_variables(self):
+        tf = '''
+        variable "gratuitous_var_default" {
+          type = string
+          default = "-yay"
+        }
+
+        variable "input" {
+          default = "module-input"
+        }
+         
+        locals {
+          NAME = {
+            "module-input-bucket" = "mapped-bucket-name"
+          }
+          TAIL = "works"
+        }
+          
+        module "bucket" {
+          source   = "./bucket"
+          name     = var.input
+        }
+        
+        resource "aws_s3_bucket" "example2" {
+          #             resolves to: mapped-bucket-name
+          #             |            resolves to: module-input-bucket
+          #             |            |                              resolves to: works
+          #             |            |                              |           resolves to: -yay
+          #             |            |                              |           |
+          #             v            v                              v           v
+          bucket = "${local.NAME[${module.bucket.bucket_name}]}-${local.TAIL}${var.gratuitous_var_default}"
+          # final result: mapped-bucket-name-works-yay
+        }'''
+        expect = {
+            "variable": [
+                {
+                    "gratuitous_var_default": {
+                        "type": ["${string}"],              # NOTE: wrapped in eval markers
+                        "default": ["-yay"]
+                    }
+                },
+                {
+                    "input": {
+                        "default": ["module-input"]
+                    }
+                }
+            ],
+            "locals": [
+                {
+                    "NAME": [{
+                        "module-input-bucket": "mapped-bucket-name"
+                    }],
+                    "TAIL": ["works"]
+                }
+            ],
+            "module": [
+                {
+                    "bucket": {
+                        "source": ["./bucket"],
+                        "name": ["${var.input}"]            # NOTE: wrapped in eval markers
+                    }
+                }
+            ],
+            "resource": [
+                {
+                    "aws_s3_bucket": {
+                        "example2": {
+                            "bucket": ["${local.NAME[${module.bucket.bucket_name}]}-${local.TAIL}${var.gratuitous_var_default}"]
+                        }
+                    }
+                }
+            ]
+        }
+        self.go(tf, expect)
+
+    @staticmethod
+    def go(tf, expected_result):
+        actual_result = hcl2.loads(tf)
+        assert actual_result == expected_result, "Results mismatch:\n" \
+                                                 "** EXPECTED **\n" \
+                                                 f"{json.dumps(expected_result, indent=2)}\n" \
+                                                 f"** ACTUAL **\n" \
+                                                 f"{json.dumps(actual_result, indent=2)}"
diff --git a/tests/terraform/evaluation/evaluation_methods/__init__.py b/tests/terraform/module_loading/__init__.py
similarity index 100%
rename from tests/terraform/evaluation/evaluation_methods/__init__.py
rename to tests/terraform/module_loading/__init__.py
diff --git a/tests/terraform/module_loading/loaders/__init__.py b/tests/terraform/module_loading/loaders/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/terraform/module_loading/loaders/resources/README.txt b/tests/terraform/module_loading/loaders/resources/README.txt
new file mode 100644
index 0000000000..76261c050c
--- /dev/null
+++ b/tests/terraform/module_loading/loaders/resources/README.txt
@@ -0,0 +1 @@
+This space intentionally left blank.
\ No newline at end of file
diff --git a/tests/terraform/module_loading/loaders/test_local_path_loader.py b/tests/terraform/module_loading/loaders/test_local_path_loader.py
new file mode 100644
index 0000000000..e460580920
--- /dev/null
+++ b/tests/terraform/module_loading/loaders/test_local_path_loader.py
@@ -0,0 +1,21 @@
+import os
+import unittest
+
+from checkov.terraform.module_loading.loaders.local_path_loader import loader
+
+
+class TestLocalPathLoader(unittest.TestCase):
+    def test_child_dir(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+        with loader.load(current_dir, "./resources", None) as content:
+            assert content.loaded()
+            assert content.path() == os.path.join(current_dir, "resources")
+
+    def test_unhandled_source(self):
+        with loader.load("current_dir", "hashicorp/consul/aws", None) as content:
+            assert not content.loaded()
+
+    def test_bad_source(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+        with self.assertRaises(FileNotFoundError):
+            loader.load(current_dir, "./path_that_doesnt_exist", None)
diff --git a/tests/terraform/parser/hcl1_example.tf b/tests/terraform/parser/hcl1_example.tf
deleted file mode 100644
index a93e90fde6..0000000000
--- a/tests/terraform/parser/hcl1_example.tf
+++ /dev/null
@@ -1,11 +0,0 @@
-# Example of HCL1 syntax that won't parse in HCL2
-# (from https://github.com/amplify-education/python-hcl2/issues/13)
-
-output "ebs_sizes" {
-  value = {
-    "x" = 600,
-    "y" = 350,
-    "z" = 350,
-    "xx" = 200
-  }
-}
\ No newline at end of file
diff --git a/tests/terraform/parser/hcl1_example_expected.json b/tests/terraform/parser/hcl1_example_expected.json
deleted file mode 100644
index 3566ea78bf..0000000000
--- a/tests/terraform/parser/hcl1_example_expected.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "output": [{
-    "ebs_sizes": {
-      "value": [{
-        "x": 600,
-        "y": 350,
-        "z": 350,
-        "xx": 200
-      }]
-    }
-  }]
-}
\ No newline at end of file
diff --git a/tests/terraform/parser/hcl2_example.tf b/tests/terraform/parser/hcl2_example.tf
deleted file mode 100644
index d4b28101d8..0000000000
--- a/tests/terraform/parser/hcl2_example.tf
+++ /dev/null
@@ -1,32 +0,0 @@
-# Example HCL2 syntax that won't parse in HCL1
-# (from https://github.com/virtuald/pyhcl/issues/74)
-
-locals {
-  map = {
-    r1 = "21.0.0.0/16"
-    r2 = "22.4.0.0/16"
-  }
-
-  sg = {
-    "test" = [
-      "r1",
-      "r2"
-    ]
-  }
-
-}
-
-resource "aws_security_group" "test" {
-  name   = "test"
-  vpc_id = "vpc-xxxxxxxxx"
-
-  dynamic "ingress" {
-    for_each = local.sg.test
-    content {
-      from_port   = 80
-      to_port     = 80
-      protocol    = "tcp"
-      cidr_blocks = split(",", lookup(local.map, ingress.value, ingress.value))
-    }
-  }
-}
\ No newline at end of file
diff --git a/tests/terraform/parser/hcl2_example_expected.json b/tests/terraform/parser/hcl2_example_expected.json
deleted file mode 100644
index dd2c5273f9..0000000000
--- a/tests/terraform/parser/hcl2_example_expected.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
-  "locals": [
-    {
-      "map": [
-        {
-          "r1": "21.0.0.0/16",
-          "r2": "22.4.0.0/16"
-        }
-      ],
-      "sg": [
-        {
-          "test": [
-            "r1",
-            "r2"
-          ]
-        }
-      ]
-    }
-  ],
-  "resource": [
-    {
-      "aws_security_group": {
-        "test": {
-          "name": [
-            "test"
-          ],
-          "vpc_id": [
-            "vpc-xxxxxxxxx"
-          ],
-          "dynamic": [
-            {
-              "ingress": {
-                "for_each": [
-                  "${local.sg.test}"
-                ],
-                "content": [
-                  {
-                    "from_port": [
-                      80
-                    ],
-                    "to_port": [
-                      80
-                    ],
-                    "protocol": [
-                      "tcp"
-                    ],
-                    "cidr_blocks": [
-                      "${split(\",\",lookup(local.map,ingress.value,ingress.value))}"
-                    ]
-                  }
-                ]
-              }
-            }
-          ],
-          "ingress": [
-            "${local.sg.test}"
-          ]
-        }
-      }
-    }
-  ]
-}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/README.md b/tests/terraform/parser/resources/parser_scenarios/README.md
new file mode 100644
index 0000000000..1f47e851ff
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/README.md
@@ -0,0 +1,7 @@
+Child directories contain parsing scenarios along with an `expected.json` file with the
+total expected result output. During real evaluation all files will use absolute paths. To
+make tests work across various systems, test expectations are written with relative paths
+and tests will convert to absolute paths on the fly.
+
+If evaluations are also being tested, an `eval.json` file may be created with the expected
+evaluation data.
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/expected.json b/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/expected.json
new file mode 100644
index 0000000000..5c9ffed5e3
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/expected.json
@@ -0,0 +1,23 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "BAD_VAR": ["${var.var_not_there}"],
+        "BAD_LOCAL": ["${local.local_not_there}"],
+        "BAD_MODULE": ["${module.module_not_there.nope}"],
+        "BAD_MODULE2": ["${module.module_not_there}"],
+        "BAD_MODULE3": ["${module.module_not_there.nope.still_not}"],
+        "QUOTE_IN_QUOTE_446": ["${filemd5(\"${path.module}/templates/some-file.json\")}"]
+      }
+    ],
+    "resource": [
+      {
+        "aws_lambda_function": {
+          "test_lambda": {
+            "source_code_hash": ["${filemd5(\"${path.module}/templates/some-file.json\")}"]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/main.tf b/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/main.tf
new file mode 100644
index 0000000000..7479fa5dd0
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/bad_ref_fallbacks/main.tf
@@ -0,0 +1,12 @@
+locals {
+  BAD_VAR = var.var_not_there
+  BAD_LOCAL = local.local_not_there
+  BAD_MODULE = module.module_not_there.nope
+  BAD_MODULE2 = module.module_not_there
+  BAD_MODULE3 = module.module_not_there.nope.still_not
+  QUOTE_IN_QUOTE_446 = "${filemd5("${path.module}/templates/some-file.json")}"
+}
+
+resource "aws_lambda_function" "test_lambda" {
+  source_code_hash = "${filemd5("${path.module}/templates/some-file.json")}"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/bad_tf/expected.json b/tests/terraform/parser/resources/parser_scenarios/bad_tf/expected.json
new file mode 100644
index 0000000000..0a2e718a8f
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/bad_tf/expected.json
@@ -0,0 +1,22 @@
+{
+  "skip_bad_tf_example.tf": {
+    "module": [
+      {
+        "bar": {
+          "memory": ["1G"],
+          "source": ["baz"]
+        }
+      },
+      {
+        "okay": {
+          "source": ["./okay", "baz2"]
+        }
+      }
+    ],
+    "variable": [
+      {
+        "okay": {}
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/bad_tf/skip_bad_tf_example.tf b/tests/terraform/parser/resources/parser_scenarios/bad_tf/skip_bad_tf_example.tf
new file mode 100644
index 0000000000..329a594e32
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/bad_tf/skip_bad_tf_example.tf
@@ -0,0 +1,25 @@
+variable "okay" {
+}
+
+// Variable is missing a name, not valid terraform syntex
+variable {
+  name    = "test"
+  default = "test_value"
+  type    = "string"
+}
+
+module "bar" {
+    memory = "1G"
+    source = "baz"
+}
+
+module "okay" {
+  source = "./okay"
+  source = "baz2"
+}
+
+// Module is missing a name, can't be referenced or deployed
+module {
+  source = "./not-okay"
+  memory = "far"
+}
diff --git a/tests/terraform/parser/resources/parser_scenarios/compound_local/checkov.tf b/tests/terraform/parser/resources/parser_scenarios/compound_local/checkov.tf
new file mode 100644
index 0000000000..eb3c89500f
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/compound_local/checkov.tf
@@ -0,0 +1,7 @@
+locals {
+  FIVE = "five"
+  NINE = "nine"
+  TWO = "two"
+  VICTOR = "wictor"
+  AUTHORIZATION_CODE = "${local.NINE}-${local.FIVE}-${local.VICTOR}-${local.VICTOR}-${local.TWO}"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/compound_local/expected.json b/tests/terraform/parser/resources/parser_scenarios/compound_local/expected.json
new file mode 100644
index 0000000000..82df120268
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/compound_local/expected.json
@@ -0,0 +1,13 @@
+{
+  "checkov.tf": {
+    "locals": [
+      {
+        "FIVE": ["five"],
+        "NINE": ["nine"],
+        "TWO": ["two"],
+        "VICTOR": ["wictor"],
+        "AUTHORIZATION_CODE": ["nine-five-wictor-wictor-two"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/default_evaluation/expected.json b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/expected.json
new file mode 100644
index 0000000000..1db050036f
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/expected.json
@@ -0,0 +1,35 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "dummy_with_dash": ["my-bucket-name"],
+        "dummy_with_comma": ["my-bucket-name"],
+        "bucket_name": ["my-bucket-name"]
+      }
+    ],
+    "resource": [
+      {
+        "aws_cognito_user_group": {
+          "user_group": {
+            "name": ["this-is-my-default"],
+            "description": ["this-is-my-default"],
+            "user_pool_id": ["this-is-my-default"]
+          }
+        },
+        "null_resource": {
+          "create_cognito_user": {
+
+          }
+        }
+      }
+    ],
+    "variable": [
+      {
+        "BUCKET_NAME": {
+          "type": ["string"],
+          "default": ["this-is-my-default"]
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/default_evaluation/main.tf b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/main.tf
new file mode 100644
index 0000000000..5f9cccb932
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/main.tf
@@ -0,0 +1,49 @@
+locals {
+  dummy_with_dash      = format("-%s", var.dummy_1)
+  dummy_with_comma     = format(":%s", var.dummy_1)
+  bucket_name          = var.bucket_name
+}
+
+resource "aws_cognito_user_group" "user_group" {
+  name         = "${var.customer_name}_group"
+  description  = "${var.customer_name} user group"
+  user_pool_id = var.user_pool_id
+}
+
+resource "null_resource" "create_cognito_user" {
+  count = var.user_exists ? 0 : 1
+  triggers = {
+    build_number = var.user_email
+  }
+
+  provisioner "local-exec" {
+    command = "aws --profile=${var.aws_profile} --region=${var.region} cognito-idp admin-create-user --user-pool-id ${var.user_pool_id} --username ${var.user_email}"
+  }
+}
+
+data "aws_iam_policy_document" "event_stream_bucket_role_assume_role_policy" {
+  statement {
+    actions = [var.action]
+
+    resources = [
+      "*",
+      "abc"
+    ]
+    principals {
+      type        = "Service"
+      identifiers = ["firehose.amazonaws.com"]
+    }
+
+    principals {
+      type        = "AWS"
+      identifiers = var.trusted_role_arn
+    }
+  }
+}
+
+resource "aws_s3_bucket" "template_bucket" {
+  region        = var.region
+  bucket        = local.bucket_name
+  acl           = var.acl
+  force_destroy = true
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/default_evaluation/variables.tf b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/variables.tf
new file mode 100644
index 0000000000..2dc44c2e17
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/default_evaluation/variables.tf
@@ -0,0 +1,48 @@
+variable "customer_name" {
+  default = "Pavel_Checkov"
+}
+
+variable "user_email" {
+  default = "checkov@bridgecrew.io"
+}
+
+variable "bucket_name" {
+  default = "MyBucket"
+}
+
+variable "acl" {
+  default = "public-acl"
+}
+
+variable "action" {
+  default = "*"
+}
+
+variable "trusted_role_arn" {
+  default = "aws:arn:xxx:xxx"
+}
+
+variable "region" {
+  default = "us-west-2"
+}
+
+variable "app_client_id" {
+  description = "Indicates whether the app client has been created"
+  default = "Temp"
+}
+
+variable "user_pool_id" {
+  default = "123"
+}
+
+variable "aws_profile" {
+  default = "default"
+}
+
+variable "dummy_1" {
+  default = "dummy_1"
+}
+
+variable "user_exists" {
+  default = false
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/README.md b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/README.md
new file mode 100644
index 0000000000..d923e5a201
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/README.md
@@ -0,0 +1 @@
+This is verifying behavior of the "Concepts/Evaluations.md" doc.
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/eval.json b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/eval.json
new file mode 100644
index 0000000000..f412b7bccd
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/eval.json
@@ -0,0 +1,26 @@
+{
+  "main.tf": {
+    "acl": {
+      "var_file": "variables.tf",
+      "value": "public-read",
+      "definitions": [
+        {
+          "definition_name": "acl",
+          "definition_expression": "${var.acl}",
+          "definition_path": "resource/0/aws_s3_bucket/my_bucket/acl/0"
+        }
+      ]
+    },
+    "region": {
+      "var_file": "variables.tf",
+      "value": "us-west-2",
+      "definitions": [
+        {
+          "definition_name": "region",
+          "definition_expression": "${var.region}",
+          "definition_path": "resource/0/aws_s3_bucket/my_bucket/region/0"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/expected.json b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/expected.json
new file mode 100644
index 0000000000..4cf10a2853
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/expected.json
@@ -0,0 +1,39 @@
+{
+  "main.tf": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "my_bucket": {
+            "region": ["us-west-2"],
+            "bucket": ["${local.bucket_name}"],
+            "acl": ["public-read"],
+            "force_destroy": [true]
+          }
+        }
+      }
+    ]
+  },
+  "variables.tf": {
+    "variable": [
+      {
+        "bucket_name": {
+          "default": [
+            "MyBucket"
+          ]
+        }
+      },
+      {
+        "acl": {
+          "default": [
+            "public-read"
+          ]
+        }
+      },
+      {
+        "region": {
+          "default": ["us-west-2"]
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/main.tf b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/main.tf
new file mode 100644
index 0000000000..f87f114694
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/main.tf
@@ -0,0 +1,6 @@
+resource "aws_s3_bucket" "my_bucket" {
+  region        = var.region
+  bucket        = local.bucket_name
+  acl           = var.acl
+  force_destroy = true
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/variables.tf b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/variables.tf
new file mode 100644
index 0000000000..831ddf5b5f
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/doc_evaluations_verify/variables.tf
@@ -0,0 +1,11 @@
+variable "bucket_name" {
+  default = "MyBucket"
+}
+
+variable "acl" {
+  default = "public-read"
+}
+
+variable "region" {
+  default = "us-west-2"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/empty_file/evaluation.json b/tests/terraform/parser/resources/parser_scenarios/empty_file/evaluation.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/empty_file/evaluation.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/empty_file/expected.json b/tests/terraform/parser/resources/parser_scenarios/empty_file/expected.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/empty_file/expected.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/empty_file/main.tf b/tests/terraform/parser/resources/parser_scenarios/empty_file/main.tf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/terraform/parser/resources/parser_scenarios/formatting/expected.json b/tests/terraform/parser/resources/parser_scenarios/formatting/expected.json
new file mode 100644
index 0000000000..473cc5ad09
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/formatting/expected.json
@@ -0,0 +1,10 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "example1": ["Hello, Ander!"],
+        "example2": ["There are 4 lights"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/formatting/main.tf b/tests/terraform/parser/resources/parser_scenarios/formatting/main.tf
new file mode 100644
index 0000000000..a8b3772ccf
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/formatting/main.tf
@@ -0,0 +1,4 @@
+locals {
+  example1 = format("Hello, %s!", "Ander")
+  example2 = format("There are %d lights", 4)
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/list_default_622/expected.json b/tests/terraform/parser/resources/parser_scenarios/list_default_622/expected.json
new file mode 100644
index 0000000000..76e9296d56
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/list_default_622/expected.json
@@ -0,0 +1,36 @@
+{
+  "test.tf": {
+    "resource": [
+      {
+        "aws_eks_cluster": {
+          "example_direct": {
+            "name": ["example"],
+            "enabled_cluster_log_types": [
+              ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+            ]
+          }
+        }
+      },
+      {
+        "aws_eks_cluster": {
+          "example_var": {
+            "name": ["example"],
+            "enabled_cluster_log_types": [
+              ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+            ]
+          }
+        }
+      }
+    ],
+    "variable": [
+      {
+        "log_types_enabled": {
+          "type": ["${list(string)}"],
+          "default": [
+            ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+          ]
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/list_default_622/test.tf b/tests/terraform/parser/resources/parser_scenarios/list_default_622/test.tf
new file mode 100644
index 0000000000..b5382bcff7
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/list_default_622/test.tf
@@ -0,0 +1,14 @@
+resource "aws_eks_cluster" "example_direct" {
+  name     = "example"
+  enabled_cluster_log_types =  ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+}
+
+resource "aws_eks_cluster" "example_var" {
+  name     = "example"
+  enabled_cluster_log_types = var.log_types_enabled
+}
+
+variable "log_types_enabled" {
+ type = list(string)
+ default = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_block/eval.json b/tests/terraform/parser/resources/parser_scenarios/local_block/eval.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_block/eval.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_block/expected.json b/tests/terraform/parser/resources/parser_scenarios/local_block/expected.json
new file mode 100644
index 0000000000..ede03e649d
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_block/expected.json
@@ -0,0 +1,20 @@
+{
+  "name_doesnt_matter.tf": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "test_with_locals": {
+            "bucket": [
+              "my-bucket-name"
+            ]
+          }
+        }
+      }
+    ],
+    "locals": [
+      {
+        "BUCKET_NAME": ["my-bucket-name"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_block/name_doesnt_matter.tf b/tests/terraform/parser/resources/parser_scenarios/local_block/name_doesnt_matter.tf
new file mode 100644
index 0000000000..557f7e5b6c
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_block/name_doesnt_matter.tf
@@ -0,0 +1,7 @@
+locals {
+  BUCKET_NAME = "my-bucket-name"
+}
+
+resource "aws_s3_bucket" "test_with_locals" {
+  bucket = local.BUCKET_NAME
+}
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/eval.json b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/eval.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/eval.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/expected.json b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/expected.json
new file mode 100644
index 0000000000..33aeb39ed8
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/expected.json
@@ -0,0 +1,10 @@
+{
+  "name_doesnt_matter.tf": {
+    "locals": [
+      {
+        "TRUE_STRING": [true],
+        "FALSE_STRING": [false]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/name_doesnt_matter.tf b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/name_doesnt_matter.tf
new file mode 100644
index 0000000000..4f57f5fc98
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/local_bool_string_conversion/name_doesnt_matter.tf
@@ -0,0 +1,4 @@
+locals {
+  TRUE_STRING = "true"
+  FALSE_STRING = "false"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/bucket/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/bucket/bucket.tf
new file mode 100644
index 0000000000..a328677769
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/bucket/bucket.tf
@@ -0,0 +1,13 @@
+variable "name" {}
+
+locals {
+  MODULE_TAIL = "bucket"
+}
+
+output "bucket_name" {
+  value = aws_s3_bucket.example.bucket
+}
+
+resource "aws_s3_bucket" "example" {
+  bucket = "${var.name}-${local.MODULE_TAIL}"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/expected.json b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/expected.json
new file mode 100644
index 0000000000..c08741d2ff
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/expected.json
@@ -0,0 +1,82 @@
+{
+  "maze.tf": {
+    "variable": [
+      {
+        "gratuitous_var_default": {
+          "type": [
+            "string"
+          ],
+          "default": [
+            "-yay"
+          ]
+        }
+      },
+      {
+        "input": {
+          "default": ["module-input"]
+        }
+      }
+    ],
+    "locals": [
+      {
+        "BUCKET": ["bucket"],
+        "NAME": [
+          {
+            "module-input-bucket": "mapped-bucket-name"
+          }
+        ],
+        "TAIL": [
+          "works"
+        ]
+      }
+    ],
+    "module": [
+      {
+        "bucket": {
+          "source": ["./bucket"],
+          "name": ["module-input"],
+          "__resolved__": ["bucket/bucket.tf[maze.tf#0]"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example2": {
+            "bucket": [
+              "mapped-bucket-name-works-yay"
+            ]
+          }
+        }
+      }
+    ]
+  },
+  "bucket/bucket.tf[maze.tf#0]": {
+    "variable": [
+      {
+        "name": {}
+      }
+    ],
+    "locals": [
+      {
+        "MODULE_TAIL": ["bucket"]
+      }
+    ],
+    "output": [
+      {
+        "bucket_name": {
+          "value": ["module-input-bucket"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example": {
+            "bucket": ["module-input-bucket"]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/maze.tf b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/maze.tf
new file mode 100644
index 0000000000..2d4d52b3cf
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/maze_of_variables/maze.tf
@@ -0,0 +1,34 @@
+
+variable "gratuitous_var_default" {
+  type = string
+  default = "-yay"
+}
+
+variable "input" {
+  default = "module-input"
+}
+
+locals {
+  BUCKET = "bucket"
+  NAME = {
+    "module-input-bucket" = "mapped-${local.BUCKET}-name"
+  }
+  TAIL = "works"
+}
+
+
+module "bucket" {
+  source   = "./bucket"
+  name     = var.input
+}
+
+resource "aws_s3_bucket" "example2" {
+  #             resolves to: mapped-bucket-name
+  #             |            resolves to: module-input-bucket
+  #             |            |                              resolves to: works
+  #             |            |                              |           resolves to: -yay
+  #             |            |                              |           |
+  #             v            v                              v           v
+  bucket = "${local.NAME[${module.bucket.bucket_name}]}-${local.TAIL}${var.gratuitous_var_default}"
+  # final result: mapped-bucket-name-works-yay
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket.tf
new file mode 100644
index 0000000000..c9f8d90d69
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket.tf
@@ -0,0 +1,7 @@
+module "bucket2" {
+  source   = "./bucket2"
+}
+
+resource "aws_s3_bucket" "example1" {
+  bucket = "bucket1"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket.tf
new file mode 100644
index 0000000000..a9b04444a3
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket.tf
@@ -0,0 +1,7 @@
+module "bucket3" {
+  source   = "./bucket3"
+}
+
+resource "aws_s3_bucket" "example2" {
+  bucket = "bucket2"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket3/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket3/bucket.tf
new file mode 100644
index 0000000000..6aca14b048
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/bucket1/bucket2/bucket3/bucket.tf
@@ -0,0 +1,4 @@
+resource "aws_s3_bucket" "example3" {
+  bucket = "bucket3"
+  acl    = "public-read"      # used by test_runner.py
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/buckets.tf b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/buckets.tf
new file mode 100644
index 0000000000..7c654144c4
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/buckets.tf
@@ -0,0 +1,7 @@
+module "bucket1" {
+  source   = "./bucket1"
+}
+
+resource "aws_s3_bucket" "example0" {
+  bucket = "bucket0"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/expected.json b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/expected.json
new file mode 100644
index 0000000000..996e17ea70
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_matryoshka/expected.json
@@ -0,0 +1,75 @@
+{
+  "buckets.tf": {
+    "module": [
+      {
+        "bucket1": {
+          "source": ["./bucket1"],
+          "__resolved__": ["bucket1/bucket.tf[buckets.tf#0]"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example0": {
+            "bucket": ["bucket0"]
+          }
+        }
+      }
+    ]
+  },
+  "bucket1/bucket2/bucket3/bucket.tf[bucket1/bucket2/bucket.tf#0]": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example3": {
+            "bucket": ["bucket3"],
+            "acl": ["public-read"]
+          }
+        }
+      }
+    ]
+  },
+  "bucket1/bucket2/bucket.tf[bucket1/bucket.tf#0]": {
+    "module": [
+      {
+        "bucket3": {
+          "source": ["./bucket3"],
+          "__resolved__": ["bucket1/bucket2/bucket3/bucket.tf[bucket1/bucket2/bucket.tf#0]"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example2": {
+            "bucket": [
+              "bucket2"
+            ]
+          }
+        }
+      }
+    ]
+  },
+  "bucket1/bucket.tf[buckets.tf#0]": {
+    "module": [
+      {
+        "bucket2": {
+          "source": ["./bucket2"],
+          "__resolved__": ["bucket1/bucket2/bucket.tf[bucket1/bucket.tf#0]"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example1": {
+            "bucket": [
+              "bucket1"
+            ]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/bucket/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/bucket/bucket.tf
new file mode 100644
index 0000000000..efd0269482
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/bucket/bucket.tf
@@ -0,0 +1,9 @@
+variable "name" {}
+
+locals {
+  BUCKET_NAME = var.name
+}
+
+resource "aws_s3_bucket" "example" {
+  bucket = local.BUCKET_NAME
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/buckets.tf b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/buckets.tf
new file mode 100644
index 0000000000..bf5ead686d
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/buckets.tf
@@ -0,0 +1,9 @@
+module "bucket" {
+  source   = "./bucket"
+  name     = "my_bucket1"
+}
+
+module "bucket" {
+  source   = "./bucket"
+  name     = "my_bucket2"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/expected.json b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/expected.json
new file mode 100644
index 0000000000..e32119a55c
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_multiple_usage/expected.json
@@ -0,0 +1,66 @@
+{
+  "buckets.tf": {
+    "module": [
+      {
+        "bucket": {
+          "source": ["./bucket"],
+          "name": ["my_bucket1"],
+          "__resolved__": ["bucket/bucket.tf[buckets.tf#0]"]
+        }
+      },
+      {
+        "bucket": {
+          "source": ["./bucket"],
+          "name": ["my_bucket2"],
+          "__resolved__": ["bucket/bucket.tf[buckets.tf#1]"]
+        }
+      }
+    ]
+  },
+  "bucket/bucket.tf[main.tf#0]": {
+    "variable": [
+      {
+        "name": {}
+      }
+    ],
+    "locals": [
+      {
+        "BUCKET_NAME": ["my_bucket1"]
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example": {
+            "bucket": [
+              "my_bucket1"
+            ]
+          }
+        }
+      }
+    ]
+  },
+  "bucket/bucket.tf[main.tf#1]": {
+    "variable": [
+      {
+        "name": {}
+      }
+    ],
+    "locals": [
+      {
+        "BUCKET_NAME": ["my_bucket2"]
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example": {
+            "bucket": [
+              "my_bucket2"
+            ]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_reference/bucket/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_reference/bucket/bucket.tf
new file mode 100644
index 0000000000..0566c8b7ed
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_reference/bucket/bucket.tf
@@ -0,0 +1,7 @@
+output "bucket_name" {
+  value = aws_s3_bucket.example.bucket
+}
+
+resource "aws_s3_bucket" "example" {
+  bucket = "my_bucket"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_reference/expected.json b/tests/terraform/parser/resources/parser_scenarios/module_reference/expected.json
new file mode 100644
index 0000000000..c318f34b63
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_reference/expected.json
@@ -0,0 +1,37 @@
+{
+  "main.tf": {
+    "module": [
+      {
+        "bucket": {
+          "source": ["./bucket"],
+          "__resolved__": ["bucket/bucket.tf[main.tf#0]"]
+        }
+      }
+    ],
+    "locals": [
+      {
+        "BUCKET_NAME": ["my_bucket"]
+      }
+    ]
+  },
+  "bucket/bucket.tf[main.tf#0]": {
+    "output": [
+      {
+        "bucket_name": {
+          "value": ["my_bucket"]
+        }
+      }
+    ],
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "example": {
+            "bucket": [
+              "my_bucket"
+            ]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_reference/main.tf b/tests/terraform/parser/resources/parser_scenarios/module_reference/main.tf
new file mode 100644
index 0000000000..5927639036
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_reference/main.tf
@@ -0,0 +1,7 @@
+module "bucket" {
+  source   = "./bucket"
+}
+
+locals {
+  BUCKET_NAME = "${module.bucket.bucket_name}"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_simple/bucket/bucket.tf b/tests/terraform/parser/resources/parser_scenarios/module_simple/bucket/bucket.tf
new file mode 100644
index 0000000000..459fe9754c
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_simple/bucket/bucket.tf
@@ -0,0 +1,3 @@
+resource "aws_s3_bucket" "mybucket" {
+  bucket = "MyBucket"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_simple/expected.json b/tests/terraform/parser/resources/parser_scenarios/module_simple/expected.json
new file mode 100644
index 0000000000..a0c19ef569
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_simple/expected.json
@@ -0,0 +1,23 @@
+{
+  "main.tf": {
+    "module": [
+      {
+        "bucket": {
+          "source": ["./bucket"],
+          "__resolved__": ["bucket/bucket.tf[main.tf#0]"]
+        }
+      }
+    ]
+  },
+  "bucket/bucket.tf[main.tf#0]": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "mybucket": {
+            "bucket": ["MyBucket"]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/module_simple/main.tf b/tests/terraform/parser/resources/parser_scenarios/module_simple/main.tf
new file mode 100644
index 0000000000..f82c7cae23
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/module_simple/main.tf
@@ -0,0 +1,3 @@
+module "bucket" {
+  source   = "./bucket"
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/null_variables_651/expected.json b/tests/terraform/parser/resources/parser_scenarios/null_variables_651/expected.json
new file mode 100644
index 0000000000..4abe3944f8
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/null_variables_651/expected.json
@@ -0,0 +1,41 @@
+{
+  "main.tf": {
+    "resource": [
+      {
+        "aws_cloudfront_distribution": {
+          "cf_dis": {
+            "enabled": [true]
+          }
+        }
+      },
+      {
+        "aws_s3_bucket": {
+          "website_bucket": {}
+        }
+      }
+    ],
+    "variable": [
+      {
+        "logging_include_cookies": {
+          "type": ["bool"],
+          "description": ["Whether to enable cookies in access logging"]
+        }
+      },
+      {
+        "logging_bucket_id": {
+          "type": ["string"],
+          "description": ["The bucket ID where to store access logs"]
+        }
+      },
+      {
+        "logging_bucket_prefix": {
+          "type": ["string"],
+          "description": ["The prefix where to store access logs"]
+        }
+      },
+      {
+        "versioning": {}
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/null_variables_651/main.tf b/tests/terraform/parser/resources/parser_scenarios/null_variables_651/main.tf
new file mode 100644
index 0000000000..e0220458c6
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/null_variables_651/main.tf
@@ -0,0 +1,37 @@
+variable "logging_include_cookies" {
+  type        = bool
+  description = "Whether to enable cookies in access logging"
+  default     = null
+}
+
+variable "logging_bucket_id" {
+  type        = string
+  description = "The bucket ID where to store access logs"
+  default     = null
+}
+
+variable "logging_bucket_prefix" {
+  type        = string
+  description = "The prefix where to store access logs"
+  default     = null
+}
+
+resource "aws_cloudfront_distribution" "cf_dis" {
+  enabled           = true
+  logging_config {
+    include_cookies = var.logging_include_cookies
+    bucket          = var.logging_bucket_id
+    prefix          = var.logging_bucket_prefix
+  }
+}
+
+
+resource "aws_s3_bucket" "website_bucket" {
+  versioning {
+      enabled = var.versioning
+  }
+}
+
+variable "versioning" {
+    default = null
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/expected.json b/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/expected.json
new file mode 100644
index 0000000000..45e9d67dc3
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/expected.json
@@ -0,0 +1,15 @@
+{
+  "main.tf": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "test": {
+            "bucket": [
+              "my-test-bucket"
+            ]
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/main.tf b/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/main.tf
new file mode 100644
index 0000000000..ee70da863a
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/simple_bucket_single_file/main.tf
@@ -0,0 +1,3 @@
+resource "aws_s3_bucket" "test" {
+  bucket = "my-test-bucket"
+}
diff --git a/tests/terraform/parser/resources/parser_scenarios/tobool_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/tobool_function/expected.json
new file mode 100644
index 0000000000..5282cc950d
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tobool_function/expected.json
@@ -0,0 +1,13 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "RAW_T": [true],
+        "STRING_T": [true],
+        "RAW_F": [false],
+        "STRING_F": [false],
+        "JUNK": ["${tobool(\"invalid\")}"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tobool_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/tobool_function/main.tf
new file mode 100644
index 0000000000..7ebf04b2bc
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tobool_function/main.tf
@@ -0,0 +1,7 @@
+locals {
+  RAW_T = tobool(true)
+  STRING_T = tobool("true")
+  RAW_F = tobool(false)
+  STRING_F = tobool("false")
+  JUNK = tobool("invalid")
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tolist_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/tolist_function/expected.json
new file mode 100644
index 0000000000..0723a96c27
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tolist_function/expected.json
@@ -0,0 +1,15 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "VAR": [
+          [
+            "a",
+            "b",
+            "c"
+          ]
+        ]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tolist_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/tolist_function/main.tf
new file mode 100644
index 0000000000..bd165b5eca
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tolist_function/main.tf
@@ -0,0 +1,3 @@
+locals {
+  VAR = tolist(["a", "b", "c"])
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tomap_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/tomap_function/expected.json
new file mode 100644
index 0000000000..6b6448efea
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tomap_function/expected.json
@@ -0,0 +1,44 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "INTS": [
+          {
+            "a": 1,
+            "b": 2
+          }
+        ],
+        "FLOATS": [
+          {
+            "a": 1.1,
+            "b": 2.2
+          }
+        ],
+        "STRINGS": [
+          {
+            "a": "one",
+            "b": "two"
+          }
+        ],
+        "BOOLS": [
+          {
+            "a": true,
+            "b": false
+          }
+        ],
+        "MIXED_BOOL": [
+          {
+            "a": "foo",
+            "b": "true"
+          }
+        ],
+        "MIXED_FLOAT": [
+          {
+            "a": "foo",
+            "b": "1.2"
+          }
+        ]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tomap_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/tomap_function/main.tf
new file mode 100644
index 0000000000..29304f1a3a
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tomap_function/main.tf
@@ -0,0 +1,9 @@
+locals {
+  INTS = tomap({"a" = 1, "b" = 2})
+  FLOATS = tomap({"a" = 1.1, "b" = 2.2})
+  STRINGS = tomap({"a" = "one", "b" = "two"})
+  BOOLS = tomap({"a" = true, "b" = false})
+
+  MIXED_BOOL = tomap({"a" = "foo", "b" = true})
+  MIXED_FLOAT = tomap({"a" = "foo", "b" = 1.2})
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tonumber_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/tonumber_function/expected.json
new file mode 100644
index 0000000000..b46dee7f72
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tonumber_function/expected.json
@@ -0,0 +1,12 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "NUM": [1],
+        "FLOAT": [3.14],
+        "STRING": [1],
+        "INVALID": ["${tonumber(\"no\")}"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tonumber_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/tonumber_function/main.tf
new file mode 100644
index 0000000000..bf69cee58d
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tonumber_function/main.tf
@@ -0,0 +1,6 @@
+locals {
+  NUM = tonumber(1)
+  FLOAT = tonumber(3.14)
+  STRING = tonumber("1")
+  INVALID = tonumber("no")
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/toset_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/toset_function/expected.json
new file mode 100644
index 0000000000..7c39a40e53
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/toset_function/expected.json
@@ -0,0 +1,16 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "VAR": [
+          [
+            "__this_is_a_set__",
+            "a",
+            "b",
+            "c"
+          ]
+        ]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/toset_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/toset_function/main.tf
new file mode 100644
index 0000000000..b169c44210
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/toset_function/main.tf
@@ -0,0 +1,3 @@
+locals {
+  VAR = toset(["a", "b", "c"])
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tostring_function/expected.json b/tests/terraform/parser/resources/parser_scenarios/tostring_function/expected.json
new file mode 100644
index 0000000000..8673a0ce01
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tostring_function/expected.json
@@ -0,0 +1,12 @@
+{
+  "main.tf": {
+    "locals": [
+      {
+        "NUM": ["1"],
+        "STRING": ["a string"],
+        "INVALID_ARRAY": ["${tostring([])}"],
+        "INNER_CURLY": ["annoying {"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/tostring_function/main.tf b/tests/terraform/parser/resources/parser_scenarios/tostring_function/main.tf
new file mode 100644
index 0000000000..e7c2d8a30e
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/tostring_function/main.tf
@@ -0,0 +1,20 @@
+locals {
+  NUM = tostring(1)
+  STRING = tostring("a string")
+
+  # NOTE: These cases should keep the values as string, but they are currently (2020-11-15) converted to
+  #       boolean values. This is caused by the str->bool translation happening on a second loop. (The first
+  #       correctly converts to a str, the second translates to a bool.)
+  #       Desired expected:
+  #          "TRUE": ["true"],
+  #          "FALSE": ["false"],
+  #       Actual is currently:
+  #          "TRUE": [true],
+  #          "FALSE": [false],
+  # TRUE = tostring("true")
+  # FALSE = tostring("false")
+
+  INVALID_ARRAY = tostring([])
+
+  INNER_CURLY = tostring("annoying {")
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/variable_defaults/eval.json b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/eval.json
new file mode 100644
index 0000000000..d11d5f8bdd
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/eval.json
@@ -0,0 +1,15 @@
+{
+  "name_doesnt_matter.tf": {
+    "BUCKET_NAME": {
+      "var_file": "name_doesnt_matter.tf",
+      "value": "this-is-my-default",
+      "definitions": [
+        {
+          "definition_name": "BUCKET_NAME",
+          "definition_expression": "${var.BUCKET_NAME}",
+          "definition_path": "resource/0/aws_s3_bucket/test/bucket/0"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/variable_defaults/expected.json b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/expected.json
new file mode 100644
index 0000000000..a60d450818
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/expected.json
@@ -0,0 +1,23 @@
+{
+  "name_doesnt_matter.tf": {
+    "resource": [
+      {
+        "aws_s3_bucket": {
+          "test": {
+            "bucket": [
+              "this-is-my-default"
+            ]
+          }
+        }
+      }
+    ],
+    "variable": [
+      {
+        "BUCKET_NAME": {
+          "type": ["string"],
+          "default": ["this-is-my-default"]
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/terraform/parser/resources/parser_scenarios/variable_defaults/name_doesnt_matter.tf b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/name_doesnt_matter.tf
new file mode 100644
index 0000000000..25a1d0ceb2
--- /dev/null
+++ b/tests/terraform/parser/resources/parser_scenarios/variable_defaults/name_doesnt_matter.tf
@@ -0,0 +1,8 @@
+variable "BUCKET_NAME" {
+  type = string
+  default = "this-is-my-default"
+}
+
+resource "aws_s3_bucket" "test" {
+  bucket = var.BUCKET_NAME
+}
diff --git a/tests/terraform/parser/test_parser_scenarios.py b/tests/terraform/parser/test_parser_scenarios.py
new file mode 100644
index 0000000000..af7becba5a
--- /dev/null
+++ b/tests/terraform/parser/test_parser_scenarios.py
@@ -0,0 +1,173 @@
+import dataclasses
+import json
+import os
+import re
+import unittest
+
+import jmespath
+
+from checkov.terraform import parser2
+
+
+def json_encoder(val):
+    if dataclasses.is_dataclass(val):
+        return dataclasses.asdict(val)
+    if isinstance(val, set):
+        return list(sorted(["__this_is_a_set__"] + list(val)))
+    return val
+
+
+class TestParserScenarios(unittest.TestCase):
+
+    def test_empty_file(self):
+        self.go("empty_file")
+
+    def test_simple_bucket_single_file(self):
+        self.go("simple_bucket_single_file")
+
+    def test_variable_defaults(self):
+        self.go("variable_defaults")
+
+    def test_local_block(self):
+        self.go("local_block")
+
+    def test_local_bool_string_conversion(self):
+        self.go("local_bool_string_conversion")
+
+    def test_compound_local(self):
+        self.go("compound_local")
+
+    def test_tobool_function(self):
+        self.go("tobool_function")
+
+    def test_tolist_function(self):
+        self.go("tolist_function")
+
+    def test_tomap_function(self):
+        self.go("tomap_function")
+
+    def test_tonumber_function(self):
+        self.go("tonumber_function")
+
+    def test_toset_function(self):
+        self.go("toset_function")
+
+    def test_tostring_function(self):
+        self.go("tostring_function")
+
+    def test_module_simple(self):
+        self.go("module_simple")
+
+    def test_module_matryoshka(self):
+        self.go("module_matryoshka")
+
+    def test_list_default_622(self):            # see https://github.com/bridgecrewio/checkov/issues/622
+        self.go("list_default_622")
+
+    # TODO ROB - Implementation in progress
+    # def test_formatting(self):
+    #     self.go("formatting")
+
+    def test_maze_of_variables(self):
+        self.go("maze_of_variables")
+
+    def test_module_reference(self):
+        self.go("module_reference")
+
+    def test_bad_ref_fallbacks(self):
+        self.go("bad_ref_fallbacks")
+
+    def test_doc_evaluations_verify(self):
+        self.go("doc_evaluations_verify")
+
+    def test_bad_tf(self):
+        self.go("bad_tf")
+
+    def test_null_variables_651(self):
+        self.go("null_variables_651")
+
+    @staticmethod
+    def go(dir_name):
+        dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                f"resources/parser_scenarios/{dir_name}")
+        assert os.path.exists(dir_path)
+
+        expected_data = TestParserScenarios.load_expected_data("expected.json", dir_path)
+        assert expected_data is not None, f"{dir_name}: expected.json file not found"
+
+        evaluation_data = TestParserScenarios.load_expected_data("eval.json", dir_path)
+
+        actual_data = {}
+        actual_eval_data = {}
+        errors = {}
+        parser2._parse_directory(dir_path, False, actual_data, actual_eval_data, errors)
+        assert not errors, f"{dir_name}: Unexpected errors: {errors}"
+        definition_string = json.dumps(actual_data, indent=2, default=json_encoder)
+        definition_encoded = json.loads(definition_string)
+        assert definition_encoded == expected_data, \
+            f"{dir_name}: Data mismatch:\n" \
+            f"  Expected: \n{json.dumps(expected_data, indent=2, default=json_encoder)}\n\n" \
+            f"  Actual: \n{definition_string}"
+
+        if evaluation_data is not None:
+            definition_string = json.dumps(actual_eval_data, indent=2, default=json_encoder)
+            definition_encoded = json.loads(definition_string)
+            assert definition_encoded == evaluation_data, \
+                f"{dir_name}: Evaluation data mismatch:\n" \
+                f"  Expected: \n{json.dumps(evaluation_data, indent=2, default=json_encoder)}\n\n" \
+                f"  Actual: \n{definition_string}"
+
+    @staticmethod
+    def load_expected_data(source_file_name, dir_path):
+        expected_path = os.path.join(dir_path, source_file_name)
+        if not os.path.exists(expected_path):
+            return None
+
+        with open(expected_path, "r") as f:
+            expected_data = json.load(f)
+
+        # Convert to absolute path:   "buckets/bucket.tf[main.tf#0]"
+        #                              ^^^^^^^^^^^^^^^^^ ^^^^^^^
+        #                                    HERE       & HERE
+        #
+        resolved_pattern = re.compile(r"(.+)\[(.+)#(\d+)]")   # groups:  location (1), referrer (2), index (3)
+
+        # Expected files should have the filenames relative to their base directory, but the parser will
+        # use the absolute path. This loop with replace relative filenames with absolute.
+        keys = list(expected_data.keys())
+        for key in keys:
+            # NOTE: Sometimes keys have module referrers, sometimes they don't
+
+            match = resolved_pattern.match(key)
+            if match:
+                new_key = _make_module_ref_absolute(match, dir_path)
+            else:
+                if os.path.isabs(key):
+                    continue
+                new_key = os.path.join(dir_path, key)
+            expected_data[new_key] = expected_data[key]
+            del expected_data[key]
+
+        for resolved_list in jmespath.search("*.module[].*[].__resolved__", expected_data):
+            for list_index in range(0, len(resolved_list)):
+                match = resolved_pattern.match(resolved_list[list_index])
+                assert match is not None, f"Unexpected module resolved data: {resolved_list[list_index]}"
+                resolved_list[list_index] = _make_module_ref_absolute(match, dir_path)
+                # print(f"{match[0]} -> {resolved_list[list_index]}")
+
+        return expected_data
+
+
+def _make_module_ref_absolute(match, dir_path) -> str:
+    module_location = match[1]
+    if not os.path.isabs(module_location):
+        module_location = os.path.join(dir_path, module_location)
+
+    module_referrer = match[2]
+    if not os.path.isabs(module_referrer):
+        module_referrer = os.path.join(dir_path, module_referrer)
+    return f"{module_location}[{module_referrer}#{match[3]}]"
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/terraform/parser/test_terraform_hcl.py b/tests/terraform/parser/test_terraform_hcl.py
deleted file mode 100644
index f8cdbf8ced..0000000000
--- a/tests/terraform/parser/test_terraform_hcl.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import json
-import os
-import unittest
-
-from checkov.terraform.parser import Parser
-
-
-class TestTerraformHcl(unittest.TestCase):
-
-    def test_hcl1_parsing(self):
-        self._load_and_test('hcl1_example.tf', 'hcl1_example_expected.json')
-
-    def test_hcl2_parsing(self):
-        self._load_and_test('hcl2_example.tf', 'hcl2_example_expected.json')
-
-    def test_clean_definitions(self):
-        current_dir = os.path.dirname(os.path.realpath(__file__))
-        tf_definitions = Parser._parse_tf_definitions(f'{current_dir}/skip_bad_tf_example.tf')
-        assert len(tf_definitions['variable']) == 1
-        assert 'okay' in tf_definitions['variable'][0]
-        assert len(tf_definitions['module']) == 2
-        assert 'bar' in tf_definitions['module'][0]
-        assert 'okay' in tf_definitions['module'][1]
-
-    def _load_and_test(self, test_file, test_expected_file):
-        current_dir = os.path.dirname(os.path.realpath(__file__))
-
-        parser = Parser()
-        parse_errors = {}
-        result = parser.parse_file(f'{current_dir}/{test_file}', parse_errors)
-
-        with open(f'{current_dir}/{test_expected_file}', 'r') as f:
-            expected_result = json.load(f)
-        self.assertEqual(len(parse_errors.keys()), 0)
-        self.assertEqual(json.dumps(expected_result), json.dumps(result))
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/terraform/runner/test_runner.py b/tests/terraform/runner/test_runner.py
index d6c0089b5c..ab7bceb495 100644
--- a/tests/terraform/runner/test_runner.py
+++ b/tests/terraform/runner/test_runner.py
@@ -7,7 +7,7 @@
 from checkov.terraform.context_parsers.registry import parser_registry
 from checkov.terraform.runner import Runner
 from checkov.common.output.report import Report
-from checkov.terraform.parser import Parser
+from checkov.terraform.parser2 import Parser2
 
 
 class TestRunnerValid(unittest.TestCase):
@@ -142,24 +142,6 @@ def test_no_missing_ids(self):
             self.assertIn(f'CKV_AZURE_{i}', azure_checks,
                           msg=f'The new Azure violation should have the ID "CKV_AZURE_{i}"')
 
-    def test_evaluate_string_booleans(self):
-        current_dir = os.path.dirname(os.path.realpath(__file__))
-        valid_dir_path = current_dir + "/resources/hcl_0.11"
-        tf_file = f"{valid_dir_path}/main.tf"
-        runner = Runner()
-        runner.run(root_folder=valid_dir_path, external_checks_dir=None)
-        runner.evaluate_string_booleans()
-        print()
-        self.assertEqual(
-            dpath.get(runner.tf_definitions[tf_file], 'resource/0/aws_db_instance/test_db/apply_immediately/0'), True)
-        self.assertEqual(
-            dpath.get(runner.tf_definitions[tf_file], 'resource/0/aws_db_instance/test_db/backup_retention_period/0'),
-            True)
-        self.assertEqual(
-            dpath.get(runner.tf_definitions[tf_file], 'resource/0/aws_db_instance/test_db/storage_encrypted/0'), False)
-        self.assertEqual(dpath.get(runner.tf_definitions[tf_file], 'resource/0/aws_db_instance/test_db/multi_az/0'),
-                         False)
-
     def test_provider_uniqueness(self):
         current_dir = os.path.dirname(os.path.realpath(__file__))
         valid_dir_path = current_dir + "/resources/many_providers"
@@ -258,10 +240,10 @@ def __init__(self):
             def scan_resource_conf(self, conf, entity_type):
                 if entity_type == 'type_1':
                     test_self.assertIn('a', conf)
-                    test_self.assertEquals([1], conf['a'])
+                    test_self.assertEqual([1], conf['a'])
                 elif entity_type == 'type_2':
                     test_self.assertIn('b', conf)
-                    test_self.assertEquals([2], conf['b'])
+                    test_self.assertEqual([2], conf['b'])
                 else:
                     test_self.fail(f'Unexpected entity_type: {entity_type}. Expected type_1 or type_2, because no '
                                    f'other resources are defined in the files inside of {test_dir}.')
@@ -362,13 +344,36 @@ def test_external_definitions_context(self):
                     'current': {'start_line': 27, 'end_line': 0, 'code_lines': [], 'skipped_checks': []}}}}}
         tf_definitions = {}
         runner = Runner()
-        parser = Parser()
+        parser = Parser2()
         runner.tf_definitions = tf_definitions
-        parser.hcl2(directory=tf_dir_path, tf_definitions=tf_definitions)
+        parser.parse_directory(tf_dir_path, tf_definitions)
         report = Report('terraform')
         runner.check_tf_definition(root_folder=tf_dir_path, report=report, runner_filter=RunnerFilter(), external_definitions_context=external_definitions_context)
         self.assertGreaterEqual(len(report.passed_checks), 1)
 
+    def test_failure_in_resolved_module(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+        valid_dir_path = os.path.join(current_dir, "../parser/resources/parser_scenarios/module_matryoshka")
+        valid_dir_path = os.path.normpath(valid_dir_path)
+        runner = Runner()
+        checks_allowlist = ['CKV_AWS_20']
+        report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
+                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+        report_json = report.get_json()
+        self.assertTrue(isinstance(report_json, str))
+        self.assertIsNotNone(report_json)
+        self.assertIsNotNone(report.get_test_suites())
+        self.assertEqual(report.get_exit_code(soft_fail=False), 1)
+        self.assertEqual(report.get_exit_code(soft_fail=True), 0)
+
+        self.assertEqual(checks_allowlist[0], report.failed_checks[0].check_id)
+        self.assertEqual("/bucket1/bucket2/bucket3/bucket.tf", report.failed_checks[0].file_path)
+        self.assertEqual(1, len(report.failed_checks))
+
+        for record in report.failed_checks:
+            self.assertIn(record.check_id, checks_allowlist)
+
+
     def tearDown(self):
         parser_registry.definitions_context = {}
 
diff --git a/tests/terraform/test_parser2_internals.py b/tests/terraform/test_parser2_internals.py
new file mode 100644
index 0000000000..4952871298
--- /dev/null
+++ b/tests/terraform/test_parser2_internals.py
@@ -0,0 +1,9 @@
+import unittest
+
+from checkov.terraform import parser2
+
+
+class TestParser2Internals(unittest.TestCase):
+    def test_eval_string_to_list(self):
+        expected = ["a", "b", "c"]
+        assert parser2._eval_string('["a", "b", "c"]') == expected


Code review comments:
diff_hunk: @@ -0,0 +1,695 @@
+import json
+import logging
+import os
+import re
+from pathlib import Path
+from typing import Mapping, Optional, Dict, Any, List, Callable, Tuple
+
+import deep_merge
+import hcl2
+import jmespath
+
+from checkov.common.runners.base_runner import filter_ignored_directories
+from checkov.common.variables.context import EvaluationContext, VarReference
+from checkov.terraform.module_loading.registry import ModuleLoaderRegistry
+from checkov.terraform.module_loading.registry import module_loader_registry as default_ml_registry
+
+
+LOGGER = logging.getLogger(__name__)
+
+_VAR_PATTERN = re.compile(r'\${([^{}]+?)}')
+_SIMPLE_TYPES = frozenset(["string", "number", "bool"])
+
+_RESOURCE_REF_PATTERN = re.compile(r'[\d\w]+(\.[\d\w]+)+')
+
+
+class Parser2:
path: checkov/terraform/parser2.py
text: @user1:
@author  
I think it's safe to rename this class to `Parser` now (and the file name as well).

@author:
Done