From 7da9582b819b3b784cb8ca9f51d6f23786d25bf1 Mon Sep 17 00:00:00 2001 From: Taylor <28880387+tsmithv11@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:19:42 -0800 Subject: [PATCH] feat(general): Update range includes to handle range values (#6867) * Update to include ranges * Fix mypy * Fix tests * Barak fixes --- .../range_includes_attribute_solver.py | 29 +++++-- .../3.Custom Policies/YAML Custom Policies.md | 80 +++++++++---------- .../JsonPathRangeIncludesListWRange.yaml | 16 ++++ .../RangeIncludesListWRange.yaml | 16 ++++ .../range_includes_solver/test_solver.py | 22 +++++ .../JsonPathRangeNotIncludesListWRange.yaml | 16 ++++ .../RangeNotIncludesListWRange.yaml | 16 ++++ .../range_not_includes_solver/test_solver.py | 22 +++++ 8 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/JsonPathRangeIncludesListWRange.yaml create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/RangeIncludesListWRange.yaml create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/JsonPathRangeNotIncludesListWRange.yaml create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/RangeNotIncludesListWRange.yaml diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/range_includes_attribute_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/range_includes_attribute_solver.py index 6e57a5039eb..db2e3cb7939 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/range_includes_attribute_solver.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/range_includes_attribute_solver.py @@ -11,9 +11,6 @@ def __init__( self, resource_types: List[str], attribute: Optional[str], value: Union[Any, List[Any]], is_jsonpath_check: bool = False ) -> None: - # Convert value to a list if it's not already one to unify handling - value = [force_int(v) if isinstance(v, (str, int)) else v for v in - (value if isinstance(value, list) else [value])] super().__init__(resource_types, attribute, value, is_jsonpath_check) def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bool: @@ -22,10 +19,32 @@ def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bo if attr is None: return False + processed_value = self._handle_range_values(self.value) + if isinstance(attr, list): - return any(self._check_value(value, attr_val) for attr_val in attr for value in self.value) + return any(self._check_value(value, attr_val) for attr_val in attr for value in processed_value) + + return any(self._check_value(value, attr) for value in processed_value) + + def _handle_range_values(self, value: Union[Any, List[Any]]) -> List[Any]: + # Convert value to a list if it's not already one to unify handling + value_list = value if isinstance(value, list) else [value] + + # Process each item in the value list + processed_value: List[Any] = [] + for v in value_list: + if isinstance(v, str) and '-' in v: + # Handle range strings + start_str, end_str = v.split('-') + start = force_int(start_str) + end = force_int(end_str) + if start is not None and end is not None: + processed_value.extend(range(start, end + 1)) + else: + # Handle single values + processed_value.append(force_int(v) if isinstance(v, (str, int)) else v) - return any(self._check_value(value, attr) for value in self.value) + return processed_value def _check_value(self, value: Any, attr: Any) -> bool: # expects one of the following values: diff --git a/docs/3.Custom Policies/YAML Custom Policies.md b/docs/3.Custom Policies/YAML Custom Policies.md index 1a67c670f56..c7d5493307b 100644 --- a/docs/3.Custom Policies/YAML Custom Policies.md +++ b/docs/3.Custom Policies/YAML Custom Policies.md @@ -99,47 +99,47 @@ definition: ### Attribute Condition: Operators -| Value in YAML | Description | Value types | Example | -|--------------------------------|--------------------------------------------------|-------------------|---------------------------------------------------| -| `equals` | Exact value match | String, Int, Bool | operator: "equals"
value: "t3.nano" | -| `not_equals` | Not equal to the value | String, Int, Bool | operator: "not_equals"
value: "t3.nano" | -| `regex_match` | The value must match the regular
expression | String (RegEx) | operator: "regex_match"
value: "^myex-.*" | -| `not_regex_match` | The value must not match the regular
expression | String (RegEx) | operator: "not_regex_match"
value: "^myex-.*" | -| `exists` | The attribute or connection appears in the
resource definition | None | attribute: "name"
operator: exists | -| `not_exists` | The attribute or connection does not
appear in the resource | None | attribute: "name"
operator: not_exists | -| `one_exists` | At least one connection of a specific type
exists | None | resource_types:
- aws_vpc
connected_resource_types:
- aws_flow_log
operator: one_exists
attribute: networking
cond_type: connection | -| `contains` | Checks if an attribute's value contains
the specified values, supporting nested structures | String | operator: "contains"
value:
-"value1" | -| `not_contains` | Checks if an attribute's value does not contain
the specified values, supporting nested structures | String | operator: "not_contains"
value:
-"value1" | -| `within` | Checks if the attribute is within a given list of values | (List) String | operator: within
- value1
- value2 | -| `not_within` | Checks if the attribute is not within a given list of values | (List) Strings | operator: not_within
value:
- 'value1'
- 'value2' | -| `starting_with` | The attribute must begin with the value | String | operator: starting_with
value: terraform-aws-modules | -| `not_starting_with` | The attribute must not begin with the value | String | operator: not_starting_with
value: terraform-aws-modules | -| `ending_with` | The value used by the attribute must end
with this string | String | operator: ending_with
value: "-good" | -| `not_ending_with` | The value used by the attribute must not
end with this string | String | operator: not_ending_with
value: "-bad" | -| `greater_than` | The value used by the attribute must be
greater than this value | String, Int | operator: greater_than
value: "100" | -| `greater_than_or_equal` | The value used by the attribute must be
greater than or equal to this value | String, Int | operator: less_than_or_equal
value: "100" | -| `less_than` | The value used by the attribute must be
less than this value | String, Int | operator: less_than
value: "100" | -| `less_than_or_equal` | The value used by the attribute must be
less than or equal to this value | String, Int | operator: less_than_or_equal
value: "100" | -| `subset` | The values used by the attribute must be
a subset of the listed values and not
outside of that | (List) String | operator: subset
value:
- "a"
- "b" | +| Value in YAML | Description | Value types | Example | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------|-------------------|---------------------------------------------------| +| `equals` | Exact value match | String, Int, Bool | operator: "equals"
value: "t3.nano" | +| `not_equals` | Not equal to the value | String, Int, Bool | operator: "not_equals"
value: "t3.nano" | +| `regex_match` | The value must match the regular
expression | String (RegEx) | operator: "regex_match"
value: "^myex-.*" | +| `not_regex_match` | The value must not match the regular
expression | String (RegEx) | operator: "not_regex_match"
value: "^myex-.*" | +| `exists` | The attribute or connection appears in the
resource definition | None | attribute: "name"
operator: exists | +| `not_exists` | The attribute or connection does not
appear in the resource | None | attribute: "name"
operator: not_exists | +| `one_exists` | At least one connection of a specific type
exists | None | resource_types:
- aws_vpc
connected_resource_types:
- aws_flow_log
operator: one_exists
attribute: networking
cond_type: connection | +| `contains` | Checks if an attribute's value contains
the specified values, supporting nested structures | String | operator: "contains"
value:
-"value1" | +| `not_contains` | Checks if an attribute's value does not contain
the specified values, supporting nested structures | String | operator: "not_contains"
value:
-"value1" | +| `within` | Checks if the attribute is within a given list of values | (List) String | operator: within
- value1
- value2 | +| `not_within` | Checks if the attribute is not within a given list of values | (List) Strings | operator: not_within
value:
- 'value1'
- 'value2' | +| `starting_with` | The attribute must begin with the value | String | operator: starting_with
value: terraform-aws-modules | +| `not_starting_with` | The attribute must not begin with the value | String | operator: not_starting_with
value: terraform-aws-modules | +| `ending_with` | The value used by the attribute must end
with this string | String | operator: ending_with
value: "-good" | +| `not_ending_with` | The value used by the attribute must not
end with this string | String | operator: not_ending_with
value: "-bad" | +| `greater_than` | The value used by the attribute must be
greater than this value | String, Int | operator: greater_than
value: "100" | +| `greater_than_or_equal` | The value used by the attribute must be
greater than or equal to this value | String, Int | operator: less_than_or_equal
value: "100" | +| `less_than` | The value used by the attribute must be
less than this value | String, Int | operator: less_than
value: "100" | +| `less_than_or_equal` | The value used by the attribute must be
less than or equal to this value | String, Int | operator: less_than_or_equal
value: "100" | +| `subset` | The values used by the attribute must be
a subset of the listed values and not
outside of that | (List) String | operator: subset
value:
- "a"
- "b" | | `not_subset` | The values used by the attribute must
not be any of a subset of the listed
values and not outside of that | (List) String | operator: not_subset
value:
- "a"
- "b" | -| `is_empty` | The attribute must not have a value | None | attribute: "audit_log_config.*.exempted_members"
operator: is_empty | -| `is_not_empty` | The attribute must have a value | None | attribute: "description"
operator: is_not_empty | -| `length_equals` | The list of attributes of that type must
be of this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_equals
value: "2" | -| `length_not_equals` | The list of attributes of that type must
not be of this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_not_equals
value: "2" | -| `length_less_than` | The list of attributes of that type must
be less than this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_less_than
value: "20" | -| `length_less_than_or_equal` | The list of attributes of that type must
be less than or equal to this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_less_than_or_equal
value: "20" | -| `length_greater_than` | The list of attributes of that type must
be greater than this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_greater_than
value: "20" | -| `length_greater_than_or_equal` | The list of attributes of that type must
be greater than or equal to this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_greater_than_or_equal
value: "20" | -| `is_false` | The value of the attribute must be false | None | operator: is_false | -| `is_true` | The value of the attribute must be true | None | operator: is_true | -| `intersects` | Given 2 values, check if those values
intersect | (List) Strings | attribute: "availability_zone"
operator: "intersects"
value: "us-" | -| `not_intersects` | Given 2 values, check if those values do
not intersect | (List) Strings | attribute: "availability_zone"
operator: "not_intersects"
value: "us-" | -| `equals_ignore_case` | The value of the attribute equals this
value, ignoring case for both | String | operator: "equals_ignore_case"
value: "INGRESS" | -| `not_equals_ignore_case` | The value of the attribute does not
equal this value, ignoring case for both | String | operator: "not_equals_ignore_case"
value: "INGRESS" | -| `range_includes` | The range of the value of the attribute
includes this single number | String, Int | operator: "range_includes"
value: 3000 | -| `range_not_includes` | The range of the value of the attribute
does not include this single number | String, Int | operator: "range_not_includes"
value: 3000 | -| `number_of_words_equals` | The number of words in the value of the
attribute is equal to this number | String, Int | operator: number_of_words_equals
value: 6 | -| `number_of_words_not_equals` | The number of words in the value of the
attribute is not equal to this number | String, Int | operator: number_of_words_not_equals
value: 6 | +| `is_empty` | The attribute must not have a value | None | attribute: "audit_log_config.*.exempted_members"
operator: is_empty | +| `is_not_empty` | The attribute must have a value | None | attribute: "description"
operator: is_not_empty | +| `length_equals` | The list of attributes of that type must
be of this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_equals
value: "2" | +| `length_not_equals` | The list of attributes of that type must
not be of this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_not_equals
value: "2" | +| `length_less_than` | The list of attributes of that type must
be less than this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_less_than
value: "20" | +| `length_less_than_or_equal` | The list of attributes of that type must
be less than or equal to this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_less_than_or_equal
value: "20" | +| `length_greater_than` | The list of attributes of that type must
be greater than this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_greater_than
value: "20" | +| `length_greater_than_or_equal` | The list of attributes of that type must
be greater than or equal to this number | String, Int | resource_types:
- aws_security_group
attribute: ingress
operator: length_greater_than_or_equal
value: "20" | +| `is_false` | The value of the attribute must be false | None | operator: is_false | +| `is_true` | The value of the attribute must be true | None | operator: is_true | +| `intersects` | Given 2 values, check if those values
intersect | (List) Strings | attribute: "availability_zone"
operator: "intersects"
value: "us-" | +| `not_intersects` | Given 2 values, check if those values do
not intersect | (List) Strings | attribute: "availability_zone"
operator: "not_intersects"
value: "us-" | +| `equals_ignore_case` | The value of the attribute equals this
value, ignoring case for both | String | operator: "equals_ignore_case"
value: "INGRESS" | +| `not_equals_ignore_case` | The value of the attribute does not
equal this value, ignoring case for both | String | operator: "not_equals_ignore_case"
value: "INGRESS" | +| `range_includes` | The range of the value or range of the
attribute includes this value or range | String, Int | operator: "range_includes"
value: 3000 | +| `range_not_includes` | The range of the value or range of the
attribute does not include this value or range | String, Int | operator: "range_not_includes"
value: 3000 | +| `number_of_words_equals` | The number of words in the value of the
attribute is equal to this number | String, Int | operator: number_of_words_equals
value: 6 | +| `number_of_words_not_equals` | The number of words in the value of the
attribute is not equal to this number | String, Int | operator: number_of_words_not_equals
value: 6 | All those operators are supporting JSONPath attribute expression by adding the `jsonpath_` prefix to the operator, for example - `jsonpath_length_equals` diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/JsonPathRangeIncludesListWRange.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/JsonPathRangeIncludesListWRange.yaml new file mode 100644 index 00000000000..1244ecca099 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/JsonPathRangeIncludesListWRange.yaml @@ -0,0 +1,16 @@ +metadata: + name: "example" + category: "GENERAL_SECURITY" + id: "JsonPathRangeIncludesListWRange" +scope: + provider: "AWS" +definition: + cond_type: "attribute" + resource_types: + - "test" + attribute: "range" + operator: "jsonpath_range_includes" + value: + - 200 + - 3000-4000 + - "400-500" diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/RangeIncludesListWRange.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/RangeIncludesListWRange.yaml new file mode 100644 index 00000000000..7443ec0d176 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/RangeIncludesListWRange.yaml @@ -0,0 +1,16 @@ +metadata: + name: "example" + category: "GENERAL_SECURITY" + id: "RangeIncludesListWRange" +scope: + provider: "AWS" +definition: + cond_type: "attribute" + resource_types: + - "test" + attribute: "range" + operator: "range_includes" + value: + - 200 + - 3000-4000 + - "400-500" diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/test_solver.py index ae1a2a0f809..f78f9fd07c4 100644 --- a/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/test_solver.py +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_includes_solver/test_solver.py @@ -72,4 +72,26 @@ def test_range_includes_list_jsonpath_solver(self): 'test.fail8', 'test.fail9'] expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} + self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id) + + def test_range_includes_list_w_list_solver(self): + root_folder = 'resources' + check_id = "RangeIncludesListWRange" + should_pass = ['test.pass1', 'test.pass2', 'test.pass3', 'test.pass4', 'test.pass5', 'test.pass6', 'test.pass7', + 'test.fail4'] + should_fail = ['test.fail1', 'test.fail2', 'test.fail3', 'test.fail5', 'test.fail6', 'test.fail7', 'test.fail8', + 'test.fail9'] + expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} + + self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id) + + def test_range_includes_list_w_list_jsonpath_solver(self): + root_folder = 'resources' + check_id = "JsonPathRangeIncludesListWRange" + should_pass = ['test.pass1', 'test.pass2', 'test.pass3', 'test.pass4', 'test.pass5', 'test.pass6', 'test.pass7', + 'test.fail4'] + should_fail = ['test.fail1', 'test.fail2', 'test.fail3', 'test.fail5', 'test.fail6', 'test.fail7', 'test.fail8', + 'test.fail9'] + expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} + self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id) \ No newline at end of file diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/JsonPathRangeNotIncludesListWRange.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/JsonPathRangeNotIncludesListWRange.yaml new file mode 100644 index 00000000000..09c4daa3152 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/JsonPathRangeNotIncludesListWRange.yaml @@ -0,0 +1,16 @@ +metadata: + name: "example" + category: "GENERAL_SECURITY" + id: "JsonPathRangeNotIncludesListWRange" +scope: + provider: "AWS" +definition: + cond_type: "attribute" + resource_types: + - "test" + attribute: "range" + operator: "jsonpath_range_not_includes" + value: + - 200 + - 3000-4000 + - "400-500" diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/RangeNotIncludesListWRange.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/RangeNotIncludesListWRange.yaml new file mode 100644 index 00000000000..567a4f7a98a --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/RangeNotIncludesListWRange.yaml @@ -0,0 +1,16 @@ +metadata: + name: "example" + category: "GENERAL_SECURITY" + id: "RangeNotIncludesListWRange" +scope: + provider: "AWS" +definition: + cond_type: "attribute" + resource_types: + - "test" + attribute: "range" + operator: "range_not_includes" + value: + - 200 + - 3000-4000 + - "400-500" diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/test_solver.py index 9c8739dead5..596af8c8943 100644 --- a/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/test_solver.py +++ b/tests/terraform/graph/checks_infra/attribute_solvers/range_not_includes_solver/test_solver.py @@ -73,3 +73,25 @@ def test_range_not_includes_list_jsonpath_solver(self): expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id) + + def test_range_not_includes_list_w_list_solver(self): + root_folder = 'resources' + check_id = "RangeNotIncludesListWRange" + should_fail = ['test.pass1', 'test.pass2', 'test.pass3', 'test.pass4', 'test.pass5', 'test.pass6', 'test.pass7', + 'test.fail4'] + should_pass = ['test.fail1', 'test.fail2', 'test.fail3', 'test.fail5', 'test.fail6', 'test.fail7', 'test.fail8', + 'test.fail9'] + expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} + + self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id) + + def test_range_not_includes_list_w_list_jsonpath_solver(self): + root_folder = 'resources' + check_id = "JsonPathRangeNotIncludesListWRange" + should_fail = ['test.pass1', 'test.pass2', 'test.pass3', 'test.pass4', 'test.pass5', 'test.pass6', 'test.pass7', + 'test.fail4'] + should_pass = ['test.fail1', 'test.fail2', 'test.fail3', 'test.fail5', 'test.fail6', 'test.fail7', 'test.fail8', + 'test.fail9'] + expected_results = {check_id: {"should_pass": should_pass, "should_fail": should_fail}} + + self.run_test(root_folder=root_folder, expected_results=expected_results, check_id=check_id)