We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[checks][python] How to determine which fields are changing in a resource
While you can use `TF_PLAN_RESOURCE_CHANGE_ACTIONS` to see "did a resource change", I'm wanting to know "what field(s) changed"?

The plan data definitely exposes this, so it should be possible, at the very least. Is this already available and just not explicitly documented? If not, is that on the roadmap to add?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `checkov/terraform/plan_parser.py`
Content:
```
1 from __future__ import annotations
2 
3 import itertools
4 import json
5 import logging
6 from typing import Optional, Tuple, Dict, List, Any, cast
7 
8 from checkov.common.graph.graph_builder import CustomAttributes
9 from checkov.common.parsers.node import ListNode
10 from checkov.common.util.type_forcers import force_list
11 from checkov.terraform.context_parsers.tf_plan import parse
12 
13 SIMPLE_TYPES = (str, int, float, bool)
14 TF_PLAN_RESOURCE_ADDRESS = "__address__"
15 TF_PLAN_RESOURCE_CHANGE_ACTIONS = "__change_actions__"
16 
17 RESOURCE_TYPES_JSONIFY = {
18     "aws_batch_job_definition": "container_properties",
19     "aws_ecs_task_definition": "container_definitions",
20     "aws_iam_policy": "policy",
21     "aws_iam_role": "assume_role_policy",
22     "aws_iam_role_policy": "policy",
23     "aws_iam_group_policy": "policy",
24     "aws_iam_user_policy": "policy",
25     "aws_ssoadmin_permission_set_inline_policy": "inline_policy",
26 }
27 
28 
29 def _is_simple_type(obj: Any) -> bool:
30     if obj is None:
31         return True
32     if isinstance(obj, SIMPLE_TYPES):
33         return True
34     return False
35 
36 
37 def _is_list_of_simple_types(obj: Any) -> bool:
38     if not isinstance(obj, list):
39         return False
40     for i in obj:
41         if not _is_simple_type(i):
42             return False
43     return True
44 
45 
46 def _is_list_of_dicts(obj: Any) -> bool:
47     if not isinstance(obj, list):
48         return False
49     for i in obj:
50         if isinstance(i, dict):
51             return True
52     return False
53 
54 
55 def _hclify(
56     obj: dict[str, Any],
57     conf: dict[str, Any] | None = None,
58     parent_key: str | None = None,
59     resource_type: str | None = None,
60 ) -> dict[str, list[Any]]:
61     ret_dict = {}
62 
63     if not isinstance(obj, dict):
64         raise Exception("this method receives only dicts")
65 
66     if hasattr(obj, "start_mark") and hasattr(obj, "end_mark"):
67         obj["start_line"] = obj.start_mark.line
68         obj["end_line"] = obj.end_mark.line
69     for key, value in obj.items():
70         if _is_simple_type(value) or _is_list_of_simple_types(value):
71             if parent_key == "tags":
72                 ret_dict[key] = value
73             else:
74                 # only wrap non-lists into a list
75                 ret_dict[key] = _clean_simple_type_list([value])
76 
77         if _is_list_of_dicts(value):
78             child_list = []
79             conf_val = conf.get(key, []) if conf else []
80             if not isinstance(conf_val, list):
81                 # this occurs, when a resource in the current state has no value for that argument
82                 conf_val = [conf_val]
83 
84             for internal_val, internal_conf_val in itertools.zip_longest(value, conf_val):
85                 if isinstance(internal_val, dict):
86                     child_list.append(_hclify(internal_val, internal_conf_val, parent_key=key))
87             if key == "tags":
88                 ret_dict[key] = [child_list]
89             else:
90                 ret_dict[key] = child_list
91         if isinstance(value, dict):
92             child_dict = _hclify(value, parent_key=key)
93             if parent_key == "tags":
94                 ret_dict[key] = child_dict
95             else:
96                 ret_dict[key] = [child_dict]
97     if conf and isinstance(conf, dict):
98         _add_references(obj=obj, conf=conf, return_resource=ret_dict)
99 
100     if resource_type and resource_type in RESOURCE_TYPES_JSONIFY:
101         # values shouldn't be encapsulated in lists
102         dict_value = jsonify(obj=obj, resource_type=resource_type)
103         if dict_value is not None:
104             ret_dict[RESOURCE_TYPES_JSONIFY[resource_type]] = force_list(dict_value)
105 
106     return ret_dict
107 
108 
109 def jsonify(obj: dict[str, Any], resource_type: str) -> dict[str, Any] | None:
110     """Tries to create a dict from a string of a supported resource type attribute"""
111 
112     jsonify_key = RESOURCE_TYPES_JSONIFY[resource_type]
113     if jsonify_key in obj:
114         try:
115             return cast("dict[str, Any]", json.loads(obj[jsonify_key]))
116         except json.JSONDecodeError:
117             logging.debug(
118                 f"Attribute {jsonify_key} of resource type {resource_type} is not json encoded {obj[jsonify_key]}"
119             )
120 
121     return None
122 
123 
124 def _prepare_resource_block(
125     resource: dict[str, Any], conf: dict[str, Any] | None, resource_changes: dict[str, dict[str, Any]]
126 ) -> tuple[dict[str, dict[str, Any]], str, bool]:
127     """hclify resource if pre-conditions met.
128 
129     :param resource: tf planned_values resource block
130     :param conf: tf configuration resource block
131     :param resource_changes: tf resource_changes block
132 
133     :returns:
134         - resource_block: a list of strings representing the header columns
135         - prepared: whether conditions met to prepare data
136     """
137 
138     resource_block: Dict[str, Dict[str, Any]] = {}
139     resource_type = resource["type"]
140     resource_block[resource_type] = {}
141     prepared = False
142     mode = ""
143     block_type = ""
144     if "mode" in resource:
145         mode = resource["mode"]
146         block_type = "data" if mode == "data" else "resource"
147 
148     # Rare cases where data block appears in resources with same name as resource block and only partial values
149     # and where *_module resources don't have values field
150     if mode in ("managed", "data"):
151         expressions = conf.get("expressions") if conf else None
152 
153         resource_conf = _hclify(
154             obj=resource.get("values", {"start_line": 0, "end_line": 0}),
155             conf=expressions,
156             resource_type=resource_type,
157         )
158         resource_address: str | None = resource.get("address")
159         resource_conf[TF_PLAN_RESOURCE_ADDRESS] = resource_address  # type:ignore[assignment]  # special field
160 
161         changes = resource_changes.get(resource_address)  # type:ignore[arg-type]  # becaus eit can be None
162         if changes:
163             resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get("change", {}).get("actions") or []
164 
165         resource_block[resource_type][resource.get("name", "default")] = resource_conf
166         prepared = True
167     return resource_block, block_type, prepared
168 
169 
170 def _find_child_modules(
171     child_modules: ListNode, resource_changes: dict[str, dict[str, Any]], root_module_conf: dict[str, Any]
172 ) -> dict[str, list[dict[str, dict[str, Any]]]]:
173     """ Find all child modules if any. Including any amount of nested child modules.
174 
175     :param child_modules: list of terraform child_module objects
176     :param resource_changes: a resource address to resource changes dict
177     :param root_module_conf: configuration block of the root module
178     :returns:
179         list of terraform resource blocks
180     """
181 
182     blocks: dict[str, list[dict[str, dict[str, Any]]]] = {"resource": [], "data": []}
183     for child_module in child_modules:
184         nested_child_modules = child_module.get("child_modules", [])
185         if nested_child_modules:
186             nested_blocks = _find_child_modules(
187                 child_modules=nested_child_modules,
188                 resource_changes=resource_changes,
189                 root_module_conf=root_module_conf
190             )
191             for block_type, resource_blocks in nested_blocks.items():
192                 blocks[block_type].extend(resource_blocks)
193 
194         module_address = child_module.get("address", "")
195         module_call_resources = _get_module_call_resources(
196             module_address=module_address,
197             root_module_conf=root_module_conf,
198         )
199 
200         for resource in child_module.get("resources", []):
201             module_call_conf = None
202             if module_address and module_call_resources:
203                 module_call_conf = next(
204                     (
205                         module_call_resource
206                         for module_call_resource in module_call_resources
207                         if f"{module_address}.{module_call_resource['address']}" == resource["address"]
208                     ),
209                     None
210                 )
211 
212             resource_block, block_type, prepared = _prepare_resource_block(
213                 resource=resource,
214                 conf=module_call_conf,
215                 resource_changes=resource_changes,
216             )
217             if prepared is True:
218                 if block_type == "resource":
219                     blocks["resource"].append(resource_block)
220                 elif block_type == "data":
221                     blocks["data"].append(resource_block)
222     return blocks
223 
224 
225 def _get_module_call_resources(module_address: str, root_module_conf: dict[str, Any]) -> list[dict[str, Any]]:
226     """Extracts the resources from the 'module_calls' block under 'configuration'"""
227 
228     for module_name in module_address.split("."):
229         if module_name == "module":
230             # module names are always prefixed with 'module.', therefore skip it
231             continue
232         root_module_conf = root_module_conf.get("module_calls", {}).get(module_name, {}).get("module", {})
233 
234     return root_module_conf.get("resources", [])
235 
236 
237 def _get_resource_changes(template: dict[str, Any]) -> dict[str, dict[str, Any]]:
238     """Returns a resource address to resource changes dict"""
239 
240     resource_changes_map = {}
241 
242     resource_changes = template.get("resource_changes")
243     if resource_changes and isinstance(resource_changes, list):
244         resource_changes_map = {
245             change.get("address", ""): change
246             for change in resource_changes
247         }
248 
249     return resource_changes_map
250 
251 
252 def _add_references(obj: dict[str, Any], conf: dict[str, Any], return_resource: dict[str, Any]) -> None:
253     """Adds references to the resources in the TF plan definition"""
254 
255     for conf_key, conf_value in conf.items():
256         if not isinstance(conf_value, dict) or "references" not in conf_value:
257             # only interested in dict with a "references" key
258             continue
259 
260         ref = next((x for x in conf_value["references"] or [] if not x.startswith(("var.", "local."))), None)
261         if ref:
262             if conf_key not in obj:
263                 return_resource[conf_key] = [ref]
264             elif obj[conf_key] is None:
265                 return_resource[conf_key] = [ref]
266             elif isinstance(obj[conf_key], list) and any(obj_value is None for obj_value in obj[conf_key]):
267                 return_resource[conf_key] = [[obj_value for obj_value in obj[conf_key] if obj_value is not None] + [ref]]
268 
269             return_resource.setdefault(CustomAttributes.REFERENCES, []).append(conf_value["references"])
270 
271 
272 def parse_tf_plan(tf_plan_file: str, out_parsing_errors: Dict[str, str]) -> Tuple[Optional[Dict[str, Any]], Optional[List[Tuple[int, str]]]]:
273     """
274     :type tf_plan_file: str - path to plan file
275     :rtype: tf_definition dictionary and template_lines of the plan file
276     """
277     tf_definition: Dict[str, Any] = {"resource": [], "data": []}
278     template, template_lines = parse(tf_plan_file, out_parsing_errors)
279     if not template:
280         return None, None
281 
282     resource_changes = _get_resource_changes(template=template)
283 
284     for resource in template.get("planned_values", {}).get("root_module", {}).get("resources", []):
285         conf = next(
286             (
287                 x
288                 for x in template.get("configuration", {}).get("root_module", {}).get("resources", [])
289                 if x["type"] == resource["type"] and x["name"] == resource["name"]
290             ),
291             None,
292         )
293         resource_block, block_type, prepared = _prepare_resource_block(
294             resource=resource,
295             conf=conf,
296             resource_changes=resource_changes,
297         )
298         if prepared is True:
299             if block_type == "resource":
300                 tf_definition["resource"].append(resource_block)
301             elif block_type == "data":
302                 tf_definition["data"].append(resource_block)
303     child_modules = template.get("planned_values", {}).get("root_module", {}).get("child_modules", [])
304     root_module_conf = template.get("configuration", {}).get("root_module", {})
305     # Terraform supports modules within modules so we need to search
306     # in nested modules to find all resource blocks
307     module_blocks = _find_child_modules(
308         child_modules=child_modules,
309         resource_changes=resource_changes,
310         root_module_conf=root_module_conf,
311     )
312     for block_type, resource_blocks in module_blocks.items():
313         tf_definition[block_type].extend(resource_blocks)
314     return tf_definition, template_lines
315 
316 
317 def _clean_simple_type_list(value_list: List[Any]) -> List[Any]:
318     """
319     Given a list of simple types return a cleaned list of simple types.
320     Converts booleans that are input as strings back to booleans to maintain consistent expectations for later evaluation.
321     Sometimes Terraform Plan will output Map values as strings regardless of boolean input.
322     """
323     for i in range(len(value_list)):
324         if isinstance(value_list[i], str):
325             lower_case_value = value_list[i].lower()
326             if lower_case_value == "true":
327                 value_list[i] = True
328             if lower_case_value == "false":
329                 value_list[i] = False
330     return value_list
331
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py
--- a/checkov/terraform/plan_parser.py
+++ b/checkov/terraform/plan_parser.py
@@ -7,12 +7,14 @@
 
 from checkov.common.graph.graph_builder import CustomAttributes
 from checkov.common.parsers.node import ListNode
+from checkov.common.util.consts import LINE_FIELD_NAMES
 from checkov.common.util.type_forcers import force_list
 from checkov.terraform.context_parsers.tf_plan import parse
 
 SIMPLE_TYPES = (str, int, float, bool)
 TF_PLAN_RESOURCE_ADDRESS = "__address__"
 TF_PLAN_RESOURCE_CHANGE_ACTIONS = "__change_actions__"
+TF_PLAN_RESOURCE_CHANGE_KEYS = "__change_keys__"
 
 RESOURCE_TYPES_JSONIFY = {
     "aws_batch_job_definition": "container_properties",
@@ -129,7 +131,6 @@
     :param resource: tf planned_values resource block
     :param conf: tf configuration resource block
     :param resource_changes: tf resource_changes block
-
     :returns:
         - resource_block: a list of strings representing the header columns
         - prepared: whether conditions met to prepare data
@@ -161,6 +162,7 @@
         changes = resource_changes.get(resource_address)  # type:ignore[arg-type]  # becaus eit can be None
         if changes:
             resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get("change", {}).get("actions") or []
+            resource_conf[TF_PLAN_RESOURCE_CHANGE_KEYS] = changes.get(TF_PLAN_RESOURCE_CHANGE_KEYS) or []
 
         resource_block[resource_type][resource.get("name", "default")] = resource_conf
         prepared = True
@@ -186,7 +188,7 @@
             nested_blocks = _find_child_modules(
                 child_modules=nested_child_modules,
                 resource_changes=resource_changes,
-                root_module_conf=root_module_conf
+                root_module_conf=root_module_conf,
             )
             for block_type, resource_blocks in nested_blocks.items():
                 blocks[block_type].extend(resource_blocks)
@@ -238,13 +240,24 @@
     """Returns a resource address to resource changes dict"""
 
     resource_changes_map = {}
-
     resource_changes = template.get("resource_changes")
+
     if resource_changes and isinstance(resource_changes, list):
-        resource_changes_map = {
-            change.get("address", ""): change
-            for change in resource_changes
-        }
+        for resource in resource_changes:
+            resource_changes_map[resource["address"]] = resource
+            changes = []
+
+            # before + after are None when resources are created/destroyed, so make them safe
+            change_before = resource["change"]["before"] or {}
+            change_after = resource["change"]["after"] or {}
+
+            for field, value in change_before.items():
+                if field in LINE_FIELD_NAMES:
+                    continue  # don't care about line #s
+                if value != change_after.get(field):
+                    changes.append(field)
+
+            resource_changes_map[resource["address"]][TF_PLAN_RESOURCE_CHANGE_KEYS] = changes
 
     return resource_changes_map

{"golden_diff": "diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py\n--- a/checkov/terraform/plan_parser.py\n+++ b/checkov/terraform/plan_parser.py\n@@ -7,12 +7,14 @@\n \n from checkov.common.graph.graph_builder import CustomAttributes\n from checkov.common.parsers.node import ListNode\n+from checkov.common.util.consts import LINE_FIELD_NAMES\n from checkov.common.util.type_forcers import force_list\n from checkov.terraform.context_parsers.tf_plan import parse\n \n SIMPLE_TYPES = (str, int, float, bool)\n TF_PLAN_RESOURCE_ADDRESS = \"__address__\"\n TF_PLAN_RESOURCE_CHANGE_ACTIONS = \"__change_actions__\"\n+TF_PLAN_RESOURCE_CHANGE_KEYS = \"__change_keys__\"\n \n RESOURCE_TYPES_JSONIFY = {\n     \"aws_batch_job_definition\": \"container_properties\",\n@@ -129,7 +131,6 @@\n     :param resource: tf planned_values resource block\n     :param conf: tf configuration resource block\n     :param resource_changes: tf resource_changes block\n-\n     :returns:\n         - resource_block: a list of strings representing the header columns\n         - prepared: whether conditions met to prepare data\n@@ -161,6 +162,7 @@\n         changes = resource_changes.get(resource_address)  # type:ignore[arg-type]  # becaus eit can be None\n         if changes:\n             resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get(\"change\", {}).get(\"actions\") or []\n+            resource_conf[TF_PLAN_RESOURCE_CHANGE_KEYS] = changes.get(TF_PLAN_RESOURCE_CHANGE_KEYS) or []\n \n         resource_block[resource_type][resource.get(\"name\", \"default\")] = resource_conf\n         prepared = True\n@@ -186,7 +188,7 @@\n             nested_blocks = _find_child_modules(\n                 child_modules=nested_child_modules,\n                 resource_changes=resource_changes,\n-                root_module_conf=root_module_conf\n+                root_module_conf=root_module_conf,\n             )\n             for block_type, resource_blocks in nested_blocks.items():\n                 blocks[block_type].extend(resource_blocks)\n@@ -238,13 +240,24 @@\n     \"\"\"Returns a resource address to resource changes dict\"\"\"\n \n     resource_changes_map = {}\n-\n     resource_changes = template.get(\"resource_changes\")\n+\n     if resource_changes and isinstance(resource_changes, list):\n-        resource_changes_map = {\n-            change.get(\"address\", \"\"): change\n-            for change in resource_changes\n-        }\n+        for resource in resource_changes:\n+            resource_changes_map[resource[\"address\"]] = resource\n+            changes = []\n+\n+            # before + after are None when resources are created/destroyed, so make them safe\n+            change_before = resource[\"change\"][\"before\"] or {}\n+            change_after = resource[\"change\"][\"after\"] or {}\n+\n+            for field, value in change_before.items():\n+                if field in LINE_FIELD_NAMES:\n+                    continue  # don't care about line #s\n+                if value != change_after.get(field):\n+                    changes.append(field)\n+\n+            resource_changes_map[resource[\"address\"]][TF_PLAN_RESOURCE_CHANGE_KEYS] = changes\n \n     return resource_changes_map\n", "issue": "[checks][python] How to determine which fields are changing in a resource\nWhile you can use `TF_PLAN_RESOURCE_CHANGE_ACTIONS` to see \"did a resource change\", I'm wanting to know \"what field(s) changed\"?\r\n\r\nThe plan data definitely exposes this, so it should be possible, at the very least. Is this already available and just not explicitly documented? If not, is that on the roadmap to add?\n", "before_files": [{"content": "from __future__ import annotations\n\nimport itertools\nimport json\nimport logging\nfrom typing import Optional, Tuple, Dict, List, Any, cast\n\nfrom checkov.common.graph.graph_builder import CustomAttributes\nfrom checkov.common.parsers.node import ListNode\nfrom checkov.common.util.type_forcers import force_list\nfrom checkov.terraform.context_parsers.tf_plan import parse\n\nSIMPLE_TYPES = (str, int, float, bool)\nTF_PLAN_RESOURCE_ADDRESS = \"__address__\"\nTF_PLAN_RESOURCE_CHANGE_ACTIONS = \"__change_actions__\"\n\nRESOURCE_TYPES_JSONIFY = {\n    \"aws_batch_job_definition\": \"container_properties\",\n    \"aws_ecs_task_definition\": \"container_definitions\",\n    \"aws_iam_policy\": \"policy\",\n    \"aws_iam_role\": \"assume_role_policy\",\n    \"aws_iam_role_policy\": \"policy\",\n    \"aws_iam_group_policy\": \"policy\",\n    \"aws_iam_user_policy\": \"policy\",\n    \"aws_ssoadmin_permission_set_inline_policy\": \"inline_policy\",\n}\n\n\ndef _is_simple_type(obj: Any) -> bool:\n    if obj is None:\n        return True\n    if isinstance(obj, SIMPLE_TYPES):\n        return True\n    return False\n\n\ndef _is_list_of_simple_types(obj: Any) -> bool:\n    if not isinstance(obj, list):\n        return False\n    for i in obj:\n        if not _is_simple_type(i):\n            return False\n    return True\n\n\ndef _is_list_of_dicts(obj: Any) -> bool:\n    if not isinstance(obj, list):\n        return False\n    for i in obj:\n        if isinstance(i, dict):\n            return True\n    return False\n\n\ndef _hclify(\n    obj: dict[str, Any],\n    conf: dict[str, Any] | None = None,\n    parent_key: str | None = None,\n    resource_type: str | None = None,\n) -> dict[str, list[Any]]:\n    ret_dict = {}\n\n    if not isinstance(obj, dict):\n        raise Exception(\"this method receives only dicts\")\n\n    if hasattr(obj, \"start_mark\") and hasattr(obj, \"end_mark\"):\n        obj[\"start_line\"] = obj.start_mark.line\n        obj[\"end_line\"] = obj.end_mark.line\n    for key, value in obj.items():\n        if _is_simple_type(value) or _is_list_of_simple_types(value):\n            if parent_key == \"tags\":\n                ret_dict[key] = value\n            else:\n                # only wrap non-lists into a list\n                ret_dict[key] = _clean_simple_type_list([value])\n\n        if _is_list_of_dicts(value):\n            child_list = []\n            conf_val = conf.get(key, []) if conf else []\n            if not isinstance(conf_val, list):\n                # this occurs, when a resource in the current state has no value for that argument\n                conf_val = [conf_val]\n\n            for internal_val, internal_conf_val in itertools.zip_longest(value, conf_val):\n                if isinstance(internal_val, dict):\n                    child_list.append(_hclify(internal_val, internal_conf_val, parent_key=key))\n            if key == \"tags\":\n                ret_dict[key] = [child_list]\n            else:\n                ret_dict[key] = child_list\n        if isinstance(value, dict):\n            child_dict = _hclify(value, parent_key=key)\n            if parent_key == \"tags\":\n                ret_dict[key] = child_dict\n            else:\n                ret_dict[key] = [child_dict]\n    if conf and isinstance(conf, dict):\n        _add_references(obj=obj, conf=conf, return_resource=ret_dict)\n\n    if resource_type and resource_type in RESOURCE_TYPES_JSONIFY:\n        # values shouldn't be encapsulated in lists\n        dict_value = jsonify(obj=obj, resource_type=resource_type)\n        if dict_value is not None:\n            ret_dict[RESOURCE_TYPES_JSONIFY[resource_type]] = force_list(dict_value)\n\n    return ret_dict\n\n\ndef jsonify(obj: dict[str, Any], resource_type: str) -> dict[str, Any] | None:\n    \"\"\"Tries to create a dict from a string of a supported resource type attribute\"\"\"\n\n    jsonify_key = RESOURCE_TYPES_JSONIFY[resource_type]\n    if jsonify_key in obj:\n        try:\n            return cast(\"dict[str, Any]\", json.loads(obj[jsonify_key]))\n        except json.JSONDecodeError:\n            logging.debug(\n                f\"Attribute {jsonify_key} of resource type {resource_type} is not json encoded {obj[jsonify_key]}\"\n            )\n\n    return None\n\n\ndef _prepare_resource_block(\n    resource: dict[str, Any], conf: dict[str, Any] | None, resource_changes: dict[str, dict[str, Any]]\n) -> tuple[dict[str, dict[str, Any]], str, bool]:\n    \"\"\"hclify resource if pre-conditions met.\n\n    :param resource: tf planned_values resource block\n    :param conf: tf configuration resource block\n    :param resource_changes: tf resource_changes block\n\n    :returns:\n        - resource_block: a list of strings representing the header columns\n        - prepared: whether conditions met to prepare data\n    \"\"\"\n\n    resource_block: Dict[str, Dict[str, Any]] = {}\n    resource_type = resource[\"type\"]\n    resource_block[resource_type] = {}\n    prepared = False\n    mode = \"\"\n    block_type = \"\"\n    if \"mode\" in resource:\n        mode = resource[\"mode\"]\n        block_type = \"data\" if mode == \"data\" else \"resource\"\n\n    # Rare cases where data block appears in resources with same name as resource block and only partial values\n    # and where *_module resources don't have values field\n    if mode in (\"managed\", \"data\"):\n        expressions = conf.get(\"expressions\") if conf else None\n\n        resource_conf = _hclify(\n            obj=resource.get(\"values\", {\"start_line\": 0, \"end_line\": 0}),\n            conf=expressions,\n            resource_type=resource_type,\n        )\n        resource_address: str | None = resource.get(\"address\")\n        resource_conf[TF_PLAN_RESOURCE_ADDRESS] = resource_address  # type:ignore[assignment]  # special field\n\n        changes = resource_changes.get(resource_address)  # type:ignore[arg-type]  # becaus eit can be None\n        if changes:\n            resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get(\"change\", {}).get(\"actions\") or []\n\n        resource_block[resource_type][resource.get(\"name\", \"default\")] = resource_conf\n        prepared = True\n    return resource_block, block_type, prepared\n\n\ndef _find_child_modules(\n    child_modules: ListNode, resource_changes: dict[str, dict[str, Any]], root_module_conf: dict[str, Any]\n) -> dict[str, list[dict[str, dict[str, Any]]]]:\n    \"\"\" Find all child modules if any. Including any amount of nested child modules.\n\n    :param child_modules: list of terraform child_module objects\n    :param resource_changes: a resource address to resource changes dict\n    :param root_module_conf: configuration block of the root module\n    :returns:\n        list of terraform resource blocks\n    \"\"\"\n\n    blocks: dict[str, list[dict[str, dict[str, Any]]]] = {\"resource\": [], \"data\": []}\n    for child_module in child_modules:\n        nested_child_modules = child_module.get(\"child_modules\", [])\n        if nested_child_modules:\n            nested_blocks = _find_child_modules(\n                child_modules=nested_child_modules,\n                resource_changes=resource_changes,\n                root_module_conf=root_module_conf\n            )\n            for block_type, resource_blocks in nested_blocks.items():\n                blocks[block_type].extend(resource_blocks)\n\n        module_address = child_module.get(\"address\", \"\")\n        module_call_resources = _get_module_call_resources(\n            module_address=module_address,\n            root_module_conf=root_module_conf,\n        )\n\n        for resource in child_module.get(\"resources\", []):\n            module_call_conf = None\n            if module_address and module_call_resources:\n                module_call_conf = next(\n                    (\n                        module_call_resource\n                        for module_call_resource in module_call_resources\n                        if f\"{module_address}.{module_call_resource['address']}\" == resource[\"address\"]\n                    ),\n                    None\n                )\n\n            resource_block, block_type, prepared = _prepare_resource_block(\n                resource=resource,\n                conf=module_call_conf,\n                resource_changes=resource_changes,\n            )\n            if prepared is True:\n                if block_type == \"resource\":\n                    blocks[\"resource\"].append(resource_block)\n                elif block_type == \"data\":\n                    blocks[\"data\"].append(resource_block)\n    return blocks\n\n\ndef _get_module_call_resources(module_address: str, root_module_conf: dict[str, Any]) -> list[dict[str, Any]]:\n    \"\"\"Extracts the resources from the 'module_calls' block under 'configuration'\"\"\"\n\n    for module_name in module_address.split(\".\"):\n        if module_name == \"module\":\n            # module names are always prefixed with 'module.', therefore skip it\n            continue\n        root_module_conf = root_module_conf.get(\"module_calls\", {}).get(module_name, {}).get(\"module\", {})\n\n    return root_module_conf.get(\"resources\", [])\n\n\ndef _get_resource_changes(template: dict[str, Any]) -> dict[str, dict[str, Any]]:\n    \"\"\"Returns a resource address to resource changes dict\"\"\"\n\n    resource_changes_map = {}\n\n    resource_changes = template.get(\"resource_changes\")\n    if resource_changes and isinstance(resource_changes, list):\n        resource_changes_map = {\n            change.get(\"address\", \"\"): change\n            for change in resource_changes\n        }\n\n    return resource_changes_map\n\n\ndef _add_references(obj: dict[str, Any], conf: dict[str, Any], return_resource: dict[str, Any]) -> None:\n    \"\"\"Adds references to the resources in the TF plan definition\"\"\"\n\n    for conf_key, conf_value in conf.items():\n        if not isinstance(conf_value, dict) or \"references\" not in conf_value:\n            # only interested in dict with a \"references\" key\n            continue\n\n        ref = next((x for x in conf_value[\"references\"] or [] if not x.startswith((\"var.\", \"local.\"))), None)\n        if ref:\n            if conf_key not in obj:\n                return_resource[conf_key] = [ref]\n            elif obj[conf_key] is None:\n                return_resource[conf_key] = [ref]\n            elif isinstance(obj[conf_key], list) and any(obj_value is None for obj_value in obj[conf_key]):\n                return_resource[conf_key] = [[obj_value for obj_value in obj[conf_key] if obj_value is not None] + [ref]]\n\n            return_resource.setdefault(CustomAttributes.REFERENCES, []).append(conf_value[\"references\"])\n\n\ndef parse_tf_plan(tf_plan_file: str, out_parsing_errors: Dict[str, str]) -> Tuple[Optional[Dict[str, Any]], Optional[List[Tuple[int, str]]]]:\n    \"\"\"\n    :type tf_plan_file: str - path to plan file\n    :rtype: tf_definition dictionary and template_lines of the plan file\n    \"\"\"\n    tf_definition: Dict[str, Any] = {\"resource\": [], \"data\": []}\n    template, template_lines = parse(tf_plan_file, out_parsing_errors)\n    if not template:\n        return None, None\n\n    resource_changes = _get_resource_changes(template=template)\n\n    for resource in template.get(\"planned_values\", {}).get(\"root_module\", {}).get(\"resources\", []):\n        conf = next(\n            (\n                x\n                for x in template.get(\"configuration\", {}).get(\"root_module\", {}).get(\"resources\", [])\n                if x[\"type\"] == resource[\"type\"] and x[\"name\"] == resource[\"name\"]\n            ),\n            None,\n        )\n        resource_block, block_type, prepared = _prepare_resource_block(\n            resource=resource,\n            conf=conf,\n            resource_changes=resource_changes,\n        )\n        if prepared is True:\n            if block_type == \"resource\":\n                tf_definition[\"resource\"].append(resource_block)\n            elif block_type == \"data\":\n                tf_definition[\"data\"].append(resource_block)\n    child_modules = template.get(\"planned_values\", {}).get(\"root_module\", {}).get(\"child_modules\", [])\n    root_module_conf = template.get(\"configuration\", {}).get(\"root_module\", {})\n    # Terraform supports modules within modules so we need to search\n    # in nested modules to find all resource blocks\n    module_blocks = _find_child_modules(\n        child_modules=child_modules,\n        resource_changes=resource_changes,\n        root_module_conf=root_module_conf,\n    )\n    for block_type, resource_blocks in module_blocks.items():\n        tf_definition[block_type].extend(resource_blocks)\n    return tf_definition, template_lines\n\n\ndef _clean_simple_type_list(value_list: List[Any]) -> List[Any]:\n    \"\"\"\n    Given a list of simple types return a cleaned list of simple types.\n    Converts booleans that are input as strings back to booleans to maintain consistent expectations for later evaluation.\n    Sometimes Terraform Plan will output Map values as strings regardless of boolean input.\n    \"\"\"\n    for i in range(len(value_list)):\n        if isinstance(value_list[i], str):\n            lower_case_value = value_list[i].lower()\n            if lower_case_value == \"true\":\n                value_list[i] = True\n            if lower_case_value == \"false\":\n                value_list[i] = False\n    return value_list\n", "path": "checkov/terraform/plan_parser.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport itertools\nimport json\nimport logging\nfrom typing import Optional, Tuple, Dict, List, Any, cast\n\nfrom checkov.common.graph.graph_builder import CustomAttributes\nfrom checkov.common.parsers.node import ListNode\nfrom checkov.common.util.consts import LINE_FIELD_NAMES\nfrom checkov.common.util.type_forcers import force_list\nfrom checkov.terraform.context_parsers.tf_plan import parse\n\nSIMPLE_TYPES = (str, int, float, bool)\nTF_PLAN_RESOURCE_ADDRESS = \"__address__\"\nTF_PLAN_RESOURCE_CHANGE_ACTIONS = \"__change_actions__\"\nTF_PLAN_RESOURCE_CHANGE_KEYS = \"__change_keys__\"\n\nRESOURCE_TYPES_JSONIFY = {\n    \"aws_batch_job_definition\": \"container_properties\",\n    \"aws_ecs_task_definition\": \"container_definitions\",\n    \"aws_iam_policy\": \"policy\",\n    \"aws_iam_role\": \"assume_role_policy\",\n    \"aws_iam_role_policy\": \"policy\",\n    \"aws_iam_group_policy\": \"policy\",\n    \"aws_iam_user_policy\": \"policy\",\n    \"aws_ssoadmin_permission_set_inline_policy\": \"inline_policy\",\n}\n\n\ndef _is_simple_type(obj: Any) -> bool:\n    if obj is None:\n        return True\n    if isinstance(obj, SIMPLE_TYPES):\n        return True\n    return False\n\n\ndef _is_list_of_simple_types(obj: Any) -> bool:\n    if not isinstance(obj, list):\n        return False\n    for i in obj:\n        if not _is_simple_type(i):\n            return False\n    return True\n\n\ndef _is_list_of_dicts(obj: Any) -> bool:\n    if not isinstance(obj, list):\n        return False\n    for i in obj:\n        if isinstance(i, dict):\n            return True\n    return False\n\n\ndef _hclify(\n    obj: dict[str, Any],\n    conf: dict[str, Any] | None = None,\n    parent_key: str | None = None,\n    resource_type: str | None = None,\n) -> dict[str, list[Any]]:\n    ret_dict = {}\n\n    if not isinstance(obj, dict):\n        raise Exception(\"this method receives only dicts\")\n\n    if hasattr(obj, \"start_mark\") and hasattr(obj, \"end_mark\"):\n        obj[\"start_line\"] = obj.start_mark.line\n        obj[\"end_line\"] = obj.end_mark.line\n    for key, value in obj.items():\n        if _is_simple_type(value) or _is_list_of_simple_types(value):\n            if parent_key == \"tags\":\n                ret_dict[key] = value\n            else:\n                # only wrap non-lists into a list\n                ret_dict[key] = _clean_simple_type_list([value])\n\n        if _is_list_of_dicts(value):\n            child_list = []\n            conf_val = conf.get(key, []) if conf else []\n            if not isinstance(conf_val, list):\n                # this occurs, when a resource in the current state has no value for that argument\n                conf_val = [conf_val]\n\n            for internal_val, internal_conf_val in itertools.zip_longest(value, conf_val):\n                if isinstance(internal_val, dict):\n                    child_list.append(_hclify(internal_val, internal_conf_val, parent_key=key))\n            if key == \"tags\":\n                ret_dict[key] = [child_list]\n            else:\n                ret_dict[key] = child_list\n        if isinstance(value, dict):\n            child_dict = _hclify(value, parent_key=key)\n            if parent_key == \"tags\":\n                ret_dict[key] = child_dict\n            else:\n                ret_dict[key] = [child_dict]\n    if conf and isinstance(conf, dict):\n        _add_references(obj=obj, conf=conf, return_resource=ret_dict)\n\n    if resource_type and resource_type in RESOURCE_TYPES_JSONIFY:\n        # values shouldn't be encapsulated in lists\n        dict_value = jsonify(obj=obj, resource_type=resource_type)\n        if dict_value is not None:\n            ret_dict[RESOURCE_TYPES_JSONIFY[resource_type]] = force_list(dict_value)\n\n    return ret_dict\n\n\ndef jsonify(obj: dict[str, Any], resource_type: str) -> dict[str, Any] | None:\n    \"\"\"Tries to create a dict from a string of a supported resource type attribute\"\"\"\n\n    jsonify_key = RESOURCE_TYPES_JSONIFY[resource_type]\n    if jsonify_key in obj:\n        try:\n            return cast(\"dict[str, Any]\", json.loads(obj[jsonify_key]))\n        except json.JSONDecodeError:\n            logging.debug(\n                f\"Attribute {jsonify_key} of resource type {resource_type} is not json encoded {obj[jsonify_key]}\"\n            )\n\n    return None\n\n\ndef _prepare_resource_block(\n    resource: dict[str, Any], conf: dict[str, Any] | None, resource_changes: dict[str, dict[str, Any]]\n) -> tuple[dict[str, dict[str, Any]], str, bool]:\n    \"\"\"hclify resource if pre-conditions met.\n\n    :param resource: tf planned_values resource block\n    :param conf: tf configuration resource block\n    :param resource_changes: tf resource_changes block\n    :returns:\n        - resource_block: a list of strings representing the header columns\n        - prepared: whether conditions met to prepare data\n    \"\"\"\n\n    resource_block: Dict[str, Dict[str, Any]] = {}\n    resource_type = resource[\"type\"]\n    resource_block[resource_type] = {}\n    prepared = False\n    mode = \"\"\n    block_type = \"\"\n    if \"mode\" in resource:\n        mode = resource[\"mode\"]\n        block_type = \"data\" if mode == \"data\" else \"resource\"\n\n    # Rare cases where data block appears in resources with same name as resource block and only partial values\n    # and where *_module resources don't have values field\n    if mode in (\"managed\", \"data\"):\n        expressions = conf.get(\"expressions\") if conf else None\n\n        resource_conf = _hclify(\n            obj=resource.get(\"values\", {\"start_line\": 0, \"end_line\": 0}),\n            conf=expressions,\n            resource_type=resource_type,\n        )\n        resource_address: str | None = resource.get(\"address\")\n        resource_conf[TF_PLAN_RESOURCE_ADDRESS] = resource_address  # type:ignore[assignment]  # special field\n\n        changes = resource_changes.get(resource_address)  # type:ignore[arg-type]  # becaus eit can be None\n        if changes:\n            resource_conf[TF_PLAN_RESOURCE_CHANGE_ACTIONS] = changes.get(\"change\", {}).get(\"actions\") or []\n            resource_conf[TF_PLAN_RESOURCE_CHANGE_KEYS] = changes.get(TF_PLAN_RESOURCE_CHANGE_KEYS) or []\n\n        resource_block[resource_type][resource.get(\"name\", \"default\")] = resource_conf\n        prepared = True\n    return resource_block, block_type, prepared\n\n\ndef _find_child_modules(\n    child_modules: ListNode, resource_changes: dict[str, dict[str, Any]], root_module_conf: dict[str, Any]\n) -> dict[str, list[dict[str, dict[str, Any]]]]:\n    \"\"\" Find all child modules if any. Including any amount of nested child modules.\n\n    :param child_modules: list of terraform child_module objects\n    :param resource_changes: a resource address to resource changes dict\n    :param root_module_conf: configuration block of the root module\n    :returns:\n        list of terraform resource blocks\n    \"\"\"\n\n    blocks: dict[str, list[dict[str, dict[str, Any]]]] = {\"resource\": [], \"data\": []}\n    for child_module in child_modules:\n        nested_child_modules = child_module.get(\"child_modules\", [])\n        if nested_child_modules:\n            nested_blocks = _find_child_modules(\n                child_modules=nested_child_modules,\n                resource_changes=resource_changes,\n                root_module_conf=root_module_conf,\n            )\n            for block_type, resource_blocks in nested_blocks.items():\n                blocks[block_type].extend(resource_blocks)\n\n        module_address = child_module.get(\"address\", \"\")\n        module_call_resources = _get_module_call_resources(\n            module_address=module_address,\n            root_module_conf=root_module_conf,\n        )\n\n        for resource in child_module.get(\"resources\", []):\n            module_call_conf = None\n            if module_address and module_call_resources:\n                module_call_conf = next(\n                    (\n                        module_call_resource\n                        for module_call_resource in module_call_resources\n                        if f\"{module_address}.{module_call_resource['address']}\" == resource[\"address\"]\n                    ),\n                    None\n                )\n\n            resource_block, block_type, prepared = _prepare_resource_block(\n                resource=resource,\n                conf=module_call_conf,\n                resource_changes=resource_changes,\n            )\n            if prepared is True:\n                if block_type == \"resource\":\n                    blocks[\"resource\"].append(resource_block)\n                elif block_type == \"data\":\n                    blocks[\"data\"].append(resource_block)\n    return blocks\n\n\ndef _get_module_call_resources(module_address: str, root_module_conf: dict[str, Any]) -> list[dict[str, Any]]:\n    \"\"\"Extracts the resources from the 'module_calls' block under 'configuration'\"\"\"\n\n    for module_name in module_address.split(\".\"):\n        if module_name == \"module\":\n            # module names are always prefixed with 'module.', therefore skip it\n            continue\n        root_module_conf = root_module_conf.get(\"module_calls\", {}).get(module_name, {}).get(\"module\", {})\n\n    return root_module_conf.get(\"resources\", [])\n\n\ndef _get_resource_changes(template: dict[str, Any]) -> dict[str, dict[str, Any]]:\n    \"\"\"Returns a resource address to resource changes dict\"\"\"\n\n    resource_changes_map = {}\n    resource_changes = template.get(\"resource_changes\")\n\n    if resource_changes and isinstance(resource_changes, list):\n        for resource in resource_changes:\n            resource_changes_map[resource[\"address\"]] = resource\n            changes = []\n\n            # before + after are None when resources are created/destroyed, so make them safe\n            change_before = resource[\"change\"][\"before\"] or {}\n            change_after = resource[\"change\"][\"after\"] or {}\n\n            for field, value in change_before.items():\n                if field in LINE_FIELD_NAMES:\n                    continue  # don't care about line #s\n                if value != change_after.get(field):\n                    changes.append(field)\n\n            resource_changes_map[resource[\"address\"]][TF_PLAN_RESOURCE_CHANGE_KEYS] = changes\n\n    return resource_changes_map\n\n\ndef _add_references(obj: dict[str, Any], conf: dict[str, Any], return_resource: dict[str, Any]) -> None:\n    \"\"\"Adds references to the resources in the TF plan definition\"\"\"\n\n    for conf_key, conf_value in conf.items():\n        if not isinstance(conf_value, dict) or \"references\" not in conf_value:\n            # only interested in dict with a \"references\" key\n            continue\n\n        ref = next((x for x in conf_value[\"references\"] or [] if not x.startswith((\"var.\", \"local.\"))), None)\n        if ref:\n            if conf_key not in obj:\n                return_resource[conf_key] = [ref]\n            elif obj[conf_key] is None:\n                return_resource[conf_key] = [ref]\n            elif isinstance(obj[conf_key], list) and any(obj_value is None for obj_value in obj[conf_key]):\n                return_resource[conf_key] = [[obj_value for obj_value in obj[conf_key] if obj_value is not None] + [ref]]\n\n            return_resource.setdefault(CustomAttributes.REFERENCES, []).append(conf_value[\"references\"])\n\n\ndef parse_tf_plan(tf_plan_file: str, out_parsing_errors: Dict[str, str]) -> Tuple[Optional[Dict[str, Any]], Optional[List[Tuple[int, str]]]]:\n    \"\"\"\n    :type tf_plan_file: str - path to plan file\n    :rtype: tf_definition dictionary and template_lines of the plan file\n    \"\"\"\n    tf_definition: Dict[str, Any] = {\"resource\": [], \"data\": []}\n    template, template_lines = parse(tf_plan_file, out_parsing_errors)\n    if not template:\n        return None, None\n\n    resource_changes = _get_resource_changes(template=template)\n\n    for resource in template.get(\"planned_values\", {}).get(\"root_module\", {}).get(\"resources\", []):\n        conf = next(\n            (\n                x\n                for x in template.get(\"configuration\", {}).get(\"root_module\", {}).get(\"resources\", [])\n                if x[\"type\"] == resource[\"type\"] and x[\"name\"] == resource[\"name\"]\n            ),\n            None,\n        )\n        resource_block, block_type, prepared = _prepare_resource_block(\n            resource=resource,\n            conf=conf,\n            resource_changes=resource_changes,\n        )\n        if prepared is True:\n            if block_type == \"resource\":\n                tf_definition[\"resource\"].append(resource_block)\n            elif block_type == \"data\":\n                tf_definition[\"data\"].append(resource_block)\n    child_modules = template.get(\"planned_values\", {}).get(\"root_module\", {}).get(\"child_modules\", [])\n    root_module_conf = template.get(\"configuration\", {}).get(\"root_module\", {})\n    # Terraform supports modules within modules so we need to search\n    # in nested modules to find all resource blocks\n    module_blocks = _find_child_modules(\n        child_modules=child_modules,\n        resource_changes=resource_changes,\n        root_module_conf=root_module_conf,\n    )\n    for block_type, resource_blocks in module_blocks.items():\n        tf_definition[block_type].extend(resource_blocks)\n    return tf_definition, template_lines\n\n\ndef _clean_simple_type_list(value_list: List[Any]) -> List[Any]:\n    \"\"\"\n    Given a list of simple types return a cleaned list of simple types.\n    Converts booleans that are input as strings back to booleans to maintain consistent expectations for later evaluation.\n    Sometimes Terraform Plan will output Map values as strings regardless of boolean input.\n    \"\"\"\n    for i in range(len(value_list)):\n        if isinstance(value_list[i], str):\n            lower_case_value = value_list[i].lower()\n            if lower_case_value == \"true\":\n                value_list[i] = True\n            if lower_case_value == \"false\":\n                value_list[i] = False\n    return value_list\n", "path": "checkov/terraform/plan_parser.py"}]}