TF parser support for ternary conditions
* Work drop
* Support ternaries with latest parser work
* Logic correction in "false" case
Also disables some tests for some currently unhandled cases.
* Multi-line test case (HCL2 parser doesn't like)
* Merge fix
MODIFY
checkov/terraform/parser.py
checkov/terraform/parser.py
@@ -535,6 +535,11 @@ def _handle_single_var_pattern(orig_variable: str, var_value_and_file_map: Dict[
module_data_retrieval: Callable[[str], Dict[str, Any]],
eval_map_by_var_name: Dict[str, EvaluationContext],
context, orig_variable_full, root_directory: str) -> Any:
+
+ ternary_info = _is_ternary(orig_variable)
+ if ternary_info:
+ return _process_ternary(orig_variable, ternary_info[0], ternary_info[1])
+
if orig_variable.startswith("module."):
if not module_list:
return orig_variable
@@ -814,6 +819,36 @@ def _remove_module_dependency_in_path(path):
return path
+def _is_ternary(value: str) -> Optional[Tuple[int,int]]:
+ """
+ Determines whether or not the given string is *probably* a ternary operation
+ :return: If the expression does represent a possibly-processable ternary expression, a tuple
+ containing the index of the question mark and colon will be returned.
+ """
+ if not value:
+ return None
+ question_index = value.find("?")
+ if question_index < 1 or value.count("?") > 1:
+ return None
+ colon_index = value.find(":")
+ if colon_index < question_index or value.count(":") > 1:
+ return None
+ return question_index, colon_index
+
+
+def _process_ternary(value: str, question_index: int, colon_index: int) -> str:
+ condition = value[:question_index].strip()
+
+ # Fast & easy case is simple boolean
+ condition_lower = condition.lower()
+ if condition_lower == "true":
+ return _to_native_value(value[question_index + 1: colon_index].strip())
+ elif condition_lower == "false":
+ return _to_native_value(value[colon_index + 1:].strip())
+
+ # Otherwise, this isn't evaluated enough
+ return value
+
def _safe_index(sequence_hopefully, index) -> Optional[Any]:
try:
return sequence_hopefully[index]

MODIFY
checkov/terraform/parser_var_blocks.py
checkov/terraform/parser_var_blocks.py
@@ -110,6 +110,20 @@ def find_var_blocks(value: str) -> List[VarBlockMatch]:
elif c == "(": # do we care?
if not ParserMode.is_string(current_mode):
mode_stack.append(ParserMode.PARAMS)
+ elif c == "?" and current_mode == ParserMode.EVAL: # ternary
+ # If what's been processed in the ternary so far is "true" or "false" (boolean or string type)
+ # then nothing special will happen here and only the full expression will be returned.
+ # Anything else will be treated as an unresolved variable block.
+ start_pos = eval_start_pos_stack[-1] # DO NOT pop: there's no separate eval start indicator
+ eval_string = value[start_pos: index].strip()
+
+ # HACK ALERT: For the cases with the trailing quotes, see:
+ # test_hcl2_load_assumptions.py -> test_weird_ternary_string_clipping
+ if eval_string not in {"true", "false", '"true"', '"false"', 'true"', 'false"'}:
+ # REMINDER: The eval string is not wrapped in a eval markers since they didn't really
+ # appear in the original value. If they're put in, substitution doesn't
+ # work properly.
+ to_return.append(VarBlockMatch(eval_string, eval_string))
preceding_string_escape = False

ADD
tests/terraform/parser/resources/parser_scenarios/ternaries/expected.json
+{
+ "main.tf": {
+ "locals": [
+ {
+ "a": ["a"],
+ "b": ["b"],
+ "empty": [""],
+ "bool_true": ["correct"],
+ "bool_false": ["correct"],
+ "type": ["bool"],
+ "default": [true]
+ }
+ ]
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/ternaries/main.tf
+locals {
+ a = "a"
+ b = "b"
+ empty = ""
+
+ bool_true = true ? "correct" : "wrong"
+ bool_false = false ? "wrong" : "correct"
+
+// local_true = true
+ // TODO: HCL2 parser doesn't like the following line
+// multiline = (local.local_true) ?
+// "correct" : "wrong"
+
+ // TODO: See test_hcl2_load_assumptions.py -> test_weird_ternary_string_clipping
+ // Doesn't currently pull the ternary correctly since it's evaluated inside the string.
+// bool_string_true = "true" ? "correct" : "wrong"
+// bool_string_false = "false" ? "wrong" : "correct"
+
+ // TODO: Comparison cases...
+// compare_string_true = "a" == "a" ? "correct" : "wrong"
+// compare_string_false = "a" != "a" ? "wrong" : "correct"
+//
+// compare_num_true = 1 == 1 ? "correct" : "wrong"
+// compare_num_false = 1 != 1 ? "correct" : "wrong"
+//
+// # NOTE: I don't think evals in locals is valid in TF, but the parser will eval it
+// default_not_taken = local.a != "" ? local.a : "default value"
+// default_taken = local.empty != "" ? local.a : "default value"
+
+ type = bool
+ default = true
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/ternary_793/expected.json
+{
+ "main.tf": {
+ "variable": [
+ {
+ "metadata_http_tokens_required": {
+ "type": ["bool"],
+ "default": [true],
+ "description": ["Whether or not the metadata service requires session tokens"]
+ }
+ }
+ ],
+ "resource": [
+ {
+ "aws_instance": {
+ "foo": {
+ "ami": ["ami-005e54dee72cc1d00"],
+ "instance_type": ["t2.micro"],
+ "root_block_device": [
+ {
+ "encrypted": [true]
+ }
+ ],
+ "metadata_options": [
+ {
+ "http_tokens": ["required"]
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/ternary_793/main.tf
+variable "metadata_http_tokens_required" {
+ type = bool
+ default = true
+ description = "Whether or not the metadata service requires session tokens"
+}
+
+resource "aws_instance" "foo" {
+ ami = "ami-005e54dee72cc1d00" # us-west-2
+ instance_type = "t2.micro"
+
+ root_block_device {
+ encrypted = true
+ }
+
+ metadata_options {
+ http_tokens = (var.metadata_http_tokens_required) ? "required" : "optional"
+ }
+}
\ No newline at end of file

MODIFY
tests/terraform/parser/test_hcl2_load_assumptions.py
tests/terraform/parser/test_hcl2_load_assumptions.py
@@ -7,6 +7,27 @@ 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_ternary(self):
+ # Ternary and removal of parens are interesting things here
+ tf = '''
+ resource "aws_instance" "foo" {
+ metadata_options {
+ http_tokens = (var.metadata_http_tokens_required) ? "required" : "optional"
+ }
+ }'''
+ expect = {
+ "resource": [{
+ "aws_instance": {
+ "foo": {
+ "metadata_options": [{
+ "http_tokens": ['${var.metadata_http_tokens_required ? "required" : "optional"}']
+ }]
+ }
+ }
+ }]
+ }
+ self.go(tf, expect)
+
def test_tfvars(self):
tf = '''
VERSIONING = true
@@ -255,3 +276,20 @@ class TestHCL2LoadAssumptions(unittest.TestCase):
f"{json.dumps(expected_result, indent=2)}\n" \
f"** ACTUAL **\n" \
f"{json.dumps(actual_result, indent=2)}"
+
+ def test_math(self):
+ tf = "four = 2 + 2"
+ expect = {
+ "four": ["${2 + 2}"]
+ }
+ self.go(tf, expect)
+
+ def test_weird_ternary_string_clipping(self):
+ tf = 'bool_string_false = "false" ? "wrong" : "correct"'
+ expect = {
+ "bool_string_false": ["false\" ? \"wrong\" : \"correct"]
+ # -- --
+ # | |
+ # missing quotes on outer tokens :-(
+ }
+ self.go(tf, expect)

MODIFY
tests/terraform/parser/test_parser_scenarios.py
tests/terraform/parser/test_parser_scenarios.py
@@ -105,6 +105,12 @@ class TestParserScenarios(unittest.TestCase):
def test_json_807(self):
self.go("json_807")
+ def test_ternaries(self):
+ self.go("ternaries")
+
+ def test_ternary_793(self):
+ self.go("ternary_793")
+
def test_tfvars(self):
self.go("tfvars")

MODIFY
tests/terraform/parser/test_parser_var_blocks.py
tests/terraform/parser/test_parser_var_blocks.py
@@ -121,7 +121,46 @@ class TestParserInternals(unittest.TestCase):
VBM("${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}",
"merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})")
]
- )
+ ),
+
+ # Ternaries
+ (
+ '${var.metadata_http_tokens_required ? "required" : "optional"}',
+ [
+ VBM('var.metadata_http_tokens_required', 'var.metadata_http_tokens_required'),
+ VBM('${var.metadata_http_tokens_required ? "required" : "optional"}',
+ 'var.metadata_http_tokens_required ? "required" : "optional"')
+ ]
+ ),
+ (
+ '${1 + 1 == 2 ? "required" : "optional"}',
+ [
+ VBM('1 + 1 == 2', '1 + 1 == 2'),
+ VBM('${1 + 1 == 2 ? "required" : "optional"}', '1 + 1 == 2 ? "required" : "optional"')
+ ]
+ ),
+ (
+ '${true ? "required" : "optional"}',
+ [
+ VBM('${true ? "required" : "optional"}', 'true ? "required" : "optional"')
+ ]
+ ),
+ (
+ '${false ? "required" : "optional"}',
+ [
+ VBM('${false ? "required" : "optional"}', 'false ? "required" : "optional"')
+ ]
+ ),
+ # TODO: var -> comparison -> ternary
+ # (
+ # '${local.empty != "" ? local.a : "default value"}',
+ # [
+ # VBM("local.empty", "local.empty"),
+ # VBM('local.empty != ""', 'local.empty != ""'),
+ # VBM('${local.empty != "" ? local.a : "default value"}',
+ # 'local.empty != "" ? local.a : "default value"')
+ # ]
+ # )
]
for case in cases:
actual = find_var_blocks(case[0])

TF parser support for ternary conditions (#855)

* Work drop

* Support ternaries with latest parser work

* Logic correction in "false" case

Also disables some tests for some currently unhandled cases.

* Multi-line test case (HCL2 parser doesn't like)

* Merge fix

Co-authored-by: Nimrod Kor <nimrodkor@gmail.com>