Skip to content

Commit fd2dd28

Browse files
fix(cloudformation): render variables in cfn vertices config (#7423)
* render cfn vertices config varaibles * mypy * export common function * export function * lint * move unit tests * rename file
1 parent 354b388 commit fd2dd28

8 files changed

Lines changed: 270 additions & 200 deletions

File tree

checkov/cloudformation/graph_builder/local_graph.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
from checkov.common.graph.graph_builder import Edge
1717
from checkov.common.graph.graph_builder.local_graph import LocalGraph
1818
from checkov.common.util.consts import START_LINE, END_LINE
19-
from checkov.common.util.data_structures_utils import search_deep_keys
19+
from checkov.common.util.data_structures_utils import search_deep_keys, pickle_deepcopy
2020
from checkov.cloudformation.graph_builder.graph_components.generic_resource_encryption import ENCRYPTION_BY_RESOURCE_TYPE
21+
from checkov.common.graph.graph_builder.utils import filter_sub_keys
22+
from checkov.terraform.graph_builder.local_graph import update_dictionary_attribute
23+
2124

2225
if TYPE_CHECKING:
2326
from checkov.common.graph.graph_builder.graph_components.blocks import Block
@@ -56,6 +59,7 @@ def build_graph(self, render_variables: bool) -> None:
5659
logging.info(f"Rendering variables, graph has {len(self.vertices)} vertices and {len(self.edges)} edges")
5760
renderer = CloudformationVariableRenderer(self)
5861
renderer.render_variables_from_local_graph()
62+
self.update_vertices_configs()
5963
self.update_vertices_breadcrumbs()
6064
self.calculate_encryption_attribute(ENCRYPTION_BY_RESOURCE_TYPE)
6165

@@ -391,15 +395,37 @@ def _is_of_type(cfndict: dict[str, Any], identifier: Any, *template_sections: Te
391395
return False
392396

393397
def update_vertices_configs(self) -> None:
394-
# not used
395-
pass
398+
for vertex in self.vertices:
399+
changed_attributes = list(vertex.changed_attributes.keys())
400+
if changed_attributes:
401+
self.update_vertex_config(vertex, changed_attributes)
396402

397403
@staticmethod
398404
def update_vertex_config(
399405
vertex: Block, changed_attributes: list[str] | dict[str, Any], dynamic_blocks: bool = False
400406
) -> None:
401-
# not used
402-
pass
407+
if not changed_attributes:
408+
return
409+
410+
if not isinstance(vertex.config, dict):
411+
return
412+
413+
updated_config = pickle_deepcopy(vertex.config)
414+
if isinstance(changed_attributes, dict):
415+
attributes_to_update = list(changed_attributes.keys())
416+
else:
417+
attributes_to_update = changed_attributes
418+
419+
attributes_to_update = filter_sub_keys(attributes_to_update)
420+
421+
for attribute in attributes_to_update:
422+
if attribute not in vertex.attributes:
423+
continue
424+
425+
new_value = vertex.attributes[attribute]
426+
update_dictionary_attribute(updated_config, attribute, new_value)
427+
428+
vertex.config = updated_config
403429

404430

405431
def get_only_dict_items(origin_dict: Union[Dict[str, Any], Any]) -> Dict[str, Dict[str, Any]]:

checkov/common/graph/graph_builder/utils.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import concurrent
44
import hashlib
5-
from typing import Any, Callable
5+
from typing import Any, Callable, overload, Union, List, Dict
66
import concurrent.futures
77

8+
from checkov.common.util.data_structures_utils import pickle_deepcopy
9+
from checkov.common.util.type_forcers import force_int
10+
811

912
def calculate_hash(data: Any) -> str:
1013
sha256 = hashlib.sha256(str(data).encode("utf-8"))
@@ -67,3 +70,127 @@ def adjust_value(element_name: str, value: Any) -> Any:
6770
return adjust_value(".".join(key_parts[1:]), new_value)
6871

6972
return value
73+
74+
75+
def to_list(data: Any) -> list[Any] | dict[str, Any]:
76+
if isinstance(data, list) and len(data) == 1 and (isinstance(data[0], str) or isinstance(data[0], int)):
77+
return data
78+
elif isinstance(data, list):
79+
return [to_list(x) for x in data]
80+
elif isinstance(data, dict):
81+
return {key: to_list(val) for key, val in data.items()}
82+
else:
83+
return [data]
84+
85+
86+
@overload
87+
def update_dictionary_attribute(
88+
config: dict[str, Any], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
89+
) -> dict[str, Any]:
90+
...
91+
92+
93+
@overload
94+
def update_dictionary_attribute(
95+
config: list[Any], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
96+
) -> list[Any]:
97+
...
98+
99+
100+
def update_dictionary_attribute(
101+
config: Union[List[Any], Dict[str, Any]], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
102+
) -> Union[List[Any], Dict[str, Any]]:
103+
key_parts = key_to_update.split(".")
104+
if '"' in key_to_update:
105+
key_parts = join_double_quote_surrounded_dot_split(str_parts=key_parts)
106+
107+
if isinstance(config, dict) and isinstance(key_parts, list):
108+
key = key_parts[0]
109+
inner_config = config.get(key)
110+
111+
if inner_config is not None:
112+
if len(key_parts) == 1:
113+
if isinstance(inner_config, list) and not isinstance(new_value, list):
114+
new_value = [new_value]
115+
config[key] = to_list(new_value) if dynamic_blocks else new_value
116+
return config
117+
else:
118+
config[key] = update_dictionary_attribute(
119+
inner_config, ".".join(key_parts[1:]), new_value, dynamic_blocks=dynamic_blocks
120+
)
121+
else:
122+
for key in config:
123+
config[key] = update_dictionary_attribute(
124+
config[key], key_to_update, new_value, dynamic_blocks=dynamic_blocks
125+
)
126+
if isinstance(config, list):
127+
return update_list_attribute(
128+
config=config,
129+
key_parts=key_parts,
130+
key_to_update=key_to_update,
131+
new_value=new_value,
132+
dynamic_blocks=dynamic_blocks,
133+
)
134+
return config
135+
136+
137+
def update_list_attribute(
138+
config: list[Any], key_parts: list[str], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
139+
) -> list[Any] | dict[str, Any]:
140+
"""Updates a list attribute in the given config"""
141+
142+
if not config:
143+
# happens when we can't correctly evaluate something, because of strange defaults or 'for_each' blocks
144+
return config
145+
146+
if len(key_parts) == 1 and len(config) == 1:
147+
idx = force_int(key_parts[0])
148+
# Avoid changing the config and cause side effects
149+
inner_config = pickle_deepcopy(config[0])
150+
151+
if idx is not None and isinstance(inner_config, list):
152+
if not inner_config:
153+
# happens when config = [[]]
154+
return config
155+
156+
inner_config[idx] = new_value
157+
return [inner_config]
158+
entry_to_update = int(key_parts[0]) if key_parts[0].isnumeric() else -1
159+
for i, config_value in enumerate(config):
160+
if entry_to_update == -1:
161+
config[i] = update_dictionary_attribute(config=config_value, key_to_update=key_to_update, new_value=new_value, dynamic_blocks=dynamic_blocks)
162+
elif entry_to_update == i:
163+
config[i] = update_dictionary_attribute(config=config_value, key_to_update=".".join(key_parts[1:]), new_value=new_value, dynamic_blocks=dynamic_blocks)
164+
165+
return config
166+
167+
168+
def join_double_quote_surrounded_dot_split(str_parts: list[str]) -> list[str]:
169+
"""Joins back split strings which enclosed a dot by double quotes
170+
171+
ex.
172+
173+
['google_project_iam_binding', 'role["roles/logging', 'admin"]'] -> ['google_project_iam_binding', 'role["roles/logging.admin"]']
174+
175+
If someone finds a better solution feel free to replace it!
176+
"""
177+
178+
new_str_parts = []
179+
joined_str_parts: list[str] = []
180+
for part in str_parts:
181+
if not joined_str_parts:
182+
if '"' not in part:
183+
new_str_parts.append(part)
184+
elif part.count('"') >= 2:
185+
new_str_parts.append(part)
186+
else:
187+
joined_str_parts.append(part)
188+
continue
189+
190+
joined_str_parts.append(part)
191+
192+
if '"' in part:
193+
new_str_parts.append(".".join(joined_str_parts))
194+
joined_str_parts = []
195+
196+
return new_str_parts

checkov/terraform/graph_builder/local_graph.py

Lines changed: 4 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
from collections import defaultdict
77
from functools import partial
88
from pathlib import Path
9-
from typing import List, Optional, Union, Any, Dict, overload, TypedDict, cast
9+
from typing import List, Optional, Union, Any, Dict, TypedDict, cast
1010

1111
import checkov.terraform.graph_builder.foreach.consts
1212
from checkov.common.graph.graph_builder import Edge
1313
from checkov.common.graph.graph_builder import reserved_attribute_names
1414
from checkov.common.graph.graph_builder.graph_components.attribute_names import CustomAttributes
1515
from checkov.common.graph.graph_builder.local_graph import LocalGraph
16-
from checkov.common.graph.graph_builder.utils import calculate_hash, join_trimmed_strings, filter_sub_keys
16+
from checkov.common.graph.graph_builder.utils import calculate_hash, join_trimmed_strings, filter_sub_keys, \
17+
update_dictionary_attribute
1718
from checkov.common.runners.base_runner import strtobool
1819
from checkov.common.typing import TFDefinitionKeyType
1920
from checkov.common.util.data_structures_utils import pickle_deepcopy
20-
from checkov.common.util.type_forcers import force_int
2121
from checkov.terraform.graph_builder.foreach.builder import ForeachBuilder
2222
from checkov.terraform.graph_builder.foreach.consts import VIRTUAL_RESOURCE
2323
from checkov.terraform.graph_builder.variable_rendering.vertex_reference import TerraformVertexReference
@@ -31,8 +31,7 @@
3131
get_attribute_is_leaf,
3232
get_referenced_vertices_in_value,
3333
attribute_has_nested_attributes,
34-
remove_index_pattern_from_str,
35-
join_double_quote_surrounded_dot_split, )
34+
remove_index_pattern_from_str, )
3635
from checkov.terraform.graph_builder.foreach.utils import get_terraform_foreach_or_count_key, \
3736
get_sanitized_terraform_resource_id
3837
from checkov.terraform.graph_builder.utils import is_local_path
@@ -857,99 +856,6 @@ def _build_virtual_resources_edges(self, origin_node_index: int, vertex: Terrafo
857856
self.create_edge(i, origin_node_index, VIRTUAL_RESOURCE)
858857

859858

860-
def to_list(data: Any) -> list[Any] | dict[str, Any]:
861-
if isinstance(data, list) and len(data) == 1 and (isinstance(data[0], str) or isinstance(data[0], int)):
862-
return data
863-
elif isinstance(data, list):
864-
return [to_list(x) for x in data]
865-
elif isinstance(data, dict):
866-
return {key: to_list(val) for key, val in data.items()}
867-
else:
868-
return [data]
869-
870-
871-
@overload
872-
def update_dictionary_attribute(
873-
config: dict[str, Any], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
874-
) -> dict[str, Any]:
875-
...
876-
877-
878-
@overload
879-
def update_dictionary_attribute(
880-
config: list[Any], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
881-
) -> list[Any]:
882-
...
883-
884-
885-
def update_dictionary_attribute(
886-
config: Union[List[Any], Dict[str, Any]], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
887-
) -> Union[List[Any], Dict[str, Any]]:
888-
key_parts = key_to_update.split(".")
889-
if '"' in key_to_update:
890-
key_parts = join_double_quote_surrounded_dot_split(str_parts=key_parts)
891-
892-
if isinstance(config, dict) and isinstance(key_parts, list):
893-
key = key_parts[0]
894-
inner_config = config.get(key)
895-
896-
if inner_config is not None:
897-
if len(key_parts) == 1:
898-
if isinstance(inner_config, list) and not isinstance(new_value, list):
899-
new_value = [new_value]
900-
config[key] = to_list(new_value) if dynamic_blocks else new_value
901-
return config
902-
else:
903-
config[key] = update_dictionary_attribute(
904-
inner_config, ".".join(key_parts[1:]), new_value, dynamic_blocks=dynamic_blocks
905-
)
906-
else:
907-
for key in config:
908-
config[key] = update_dictionary_attribute(
909-
config[key], key_to_update, new_value, dynamic_blocks=dynamic_blocks
910-
)
911-
if isinstance(config, list):
912-
return update_list_attribute(
913-
config=config,
914-
key_parts=key_parts,
915-
key_to_update=key_to_update,
916-
new_value=new_value,
917-
dynamic_blocks=dynamic_blocks,
918-
)
919-
return config
920-
921-
922-
def update_list_attribute(
923-
config: list[Any], key_parts: list[str], key_to_update: str, new_value: Any, dynamic_blocks: bool = False
924-
) -> list[Any] | dict[str, Any]:
925-
"""Updates a list attribute in the given config"""
926-
927-
if not config:
928-
# happens when we can't correctly evaluate something, because of strange defaults or 'for_each' blocks
929-
return config
930-
931-
if len(key_parts) == 1 and len(config) == 1:
932-
idx = force_int(key_parts[0])
933-
# Avoid changing the config and cause side effects
934-
inner_config = pickle_deepcopy(config[0])
935-
936-
if idx is not None and isinstance(inner_config, list):
937-
if not inner_config:
938-
# happens when config = [[]]
939-
return config
940-
941-
inner_config[idx] = new_value
942-
return [inner_config]
943-
entry_to_update = int(key_parts[0]) if key_parts[0].isnumeric() else -1
944-
for i, config_value in enumerate(config):
945-
if entry_to_update == -1:
946-
config[i] = update_dictionary_attribute(config=config_value, key_to_update=key_to_update, new_value=new_value, dynamic_blocks=dynamic_blocks)
947-
elif entry_to_update == i:
948-
config[i] = update_dictionary_attribute(config=config_value, key_to_update=".".join(key_parts[1:]), new_value=new_value, dynamic_blocks=dynamic_blocks)
949-
950-
return config
951-
952-
953859
def get_vertex_as_tf_module(block: TerraformBlock) -> TFModule:
954860
block_name = get_sanitized_terraform_resource_id(block.name)
955861
return TFModule(path=block.path, name=block_name, nested_tf_module=block.source_module_object, foreach_idx=block.for_each_index)

checkov/terraform/graph_builder/utils.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -382,34 +382,3 @@ def get_attribute_is_leaf(vertex: TerraformBlock) -> Dict[str, bool]:
382382
if other in attribute_is_leaf:
383383
attribute_is_leaf[other] = False
384384
return attribute_is_leaf
385-
386-
387-
def join_double_quote_surrounded_dot_split(str_parts: list[str]) -> list[str]:
388-
"""Joins back split strings which enclosed a dot by double quotes
389-
390-
ex.
391-
392-
['google_project_iam_binding', 'role["roles/logging', 'admin"]'] -> ['google_project_iam_binding', 'role["roles/logging.admin"]']
393-
394-
If someone finds a better solution feel free to replace it!
395-
"""
396-
397-
new_str_parts = []
398-
joined_str_parts: list[str] = []
399-
for part in str_parts:
400-
if not joined_str_parts:
401-
if '"' not in part:
402-
new_str_parts.append(part)
403-
elif part.count('"') >= 2:
404-
new_str_parts.append(part)
405-
else:
406-
joined_str_parts.append(part)
407-
continue
408-
409-
joined_str_parts.append(part)
410-
411-
if '"' in part:
412-
new_str_parts.append(".".join(joined_str_parts))
413-
joined_str_parts = []
414-
415-
return new_str_parts

0 commit comments

Comments
 (0)