Skip to content

Commit

Permalink
feat(general): intersects/not_intersects operators (solvers) (#3482)
Browse files Browse the repository at this point in the history
* feat: support for intersects/not_intersects solvers

* fix: fixed mypy tests

* chore: CR comments - removed logging, use Collection as type assertion

Co-authored-by: Mor Kadosh <[email protected]>
  • Loading branch information
kadoshms and Mor Kadosh authored Sep 8, 2022
1 parent 7c0729f commit be3710f
Show file tree
Hide file tree
Showing 20 changed files with 252 additions and 2 deletions.
4 changes: 4 additions & 0 deletions checkov/common/checks_infra/checks_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
LengthGreaterThanOrEqualAttributeSolver,
IsTrueAttributeSolver,
IsFalseAttributeSolver,
IntersectsAttributeSolver,
NotIntersectsAttributeSolver
)
from checkov.common.checks_infra.solvers.connections_solvers.connection_one_exists_solver import \
ConnectionOneExistsSolver
Expand Down Expand Up @@ -89,6 +91,8 @@
"length_less_than_or_equal": LengthLessThanOrEqualAttributeSolver,
"is_true": IsTrueAttributeSolver,
"is_false": IsFalseAttributeSolver,
"intersects": IntersectsAttributeSolver,
"not_intersects": NotIntersectsAttributeSolver
}

operators_to_complex_solver_classes: dict[str, Type[BaseComplexSolver]] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
from checkov.common.checks_infra.solvers.attribute_solvers.length_less_than_or_equal_attribute_solver import LengthLessThanOrEqualAttributeSolver # noqa
from checkov.common.checks_infra.solvers.attribute_solvers.is_true_attribute_solver import IsTrueAttributeSolver # noqa
from checkov.common.checks_infra.solvers.attribute_solvers.is_false_attribute_solver import IsFalseAttributeSolver # noqa
from checkov.common.checks_infra.solvers.attribute_solvers.intersects_attribute_solver import IntersectsAttributeSolver # noqa
from checkov.common.checks_infra.solvers.attribute_solvers.not_intersects_attribute_solver import NotIntersectsAttributeSolver # noqa
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Optional, Any, Dict, Collection
from collections.abc import Iterable
from checkov.common.graph.checks_infra.enums import Operators
from checkov.common.checks_infra.solvers.attribute_solvers.base_attribute_solver import BaseAttributeSolver


class IntersectsAttributeSolver(BaseAttributeSolver):
operator = Operators.INTERSECTS # noqa: CCE003 # a static attribute

def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bool: # type:ignore[override]
attr = vertex.get(attribute) # type:ignore[arg-type] # due to attribute can be None

# if this value contains an underendered variable, then we cannot evaluate the check,
# so return True (since we cannot return UNKNOWN)
if self._is_variable_dependant(attr, vertex['source_']):
return True

if isinstance(self.value, str) and isinstance(attr, Iterable):
return self.value in attr

if isinstance(self.value, Collection) and isinstance(attr, Iterable):
return any(i in self.value for i in attr)

return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Optional, Any, Dict

from checkov.common.graph.checks_infra.enums import Operators
from .intersects_attribute_solver import IntersectsAttributeSolver


class NotIntersectsAttributeSolver(IntersectsAttributeSolver):
operator = Operators.NOT_INTERSECTS # noqa: CCE003 # a static attribute

def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bool: # type:ignore[override]
return not super()._get_operation(vertex, attribute)
2 changes: 2 additions & 0 deletions checkov/common/graph/checks_infra/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ class Operators:
LENGTH_LESS_THAN_OR_EQUAL = 'length_less_than_or_equal'
IS_TRUE = 'is_true'
IS_FALSE = 'is_false'
INTERSECTS = 'intersects'
NOT_INTERSECTS = 'not_intersects'
4 changes: 3 additions & 1 deletion docs/3.Custom Policies/YAML Custom Policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ definition:
| Length Greater Than Or Equal | `length_greater_than_or_equal` |
| Is False | `is_false` |
| Is True | `is_true` |
| Intersects | `intersects` |
| Not Intersects | `not_intersects` |

All those operators are supporting JSONPath attribute expression by adding the `jsonpath_` prefix to the operator, for example - `jsonpath_length_equals`

Expand All @@ -119,7 +121,7 @@ All those operators are supporting JSONPath attribute expression by adding the `
| `cond_type` | string | Must be `attribute` |
| `resource_type` | collection of strings | Use either `all` or `[resource types from list]` |
| `attribute` | string | Attribute of defined resource types. For example, `automated_snapshot_retention_period` |
| `operator` | string | - `equals`, `not_equals`, `regex_match`, `not_regex_match`, `exists`, `not exists`, `any`, `contains`, `not_contains`, `within`, `starting_with`, `not_starting_with`, `ending_with`, `not_ending_with`, `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`, `is_empty`, `is_not_empty`, `length_equals`, `length_not_equals`, `length_greater_than`, `length_greater_than_or_equal`, `length_less_than`, `length_less_than_or_equal`, `is_true`, `is_false` |
| `operator` | string | - `equals`, `not_equals`, `regex_match`, `not_regex_match`, `exists`, `not exists`, `any`, `contains`, `not_contains`, `within`, `starting_with`, `not_starting_with`, `ending_with`, `not_ending_with`, `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`, `is_empty`, `is_not_empty`, `length_equals`, `length_not_equals`, `length_greater_than`, `length_greater_than_or_equal`, `length_less_than`, `length_less_than_or_equal`, `is_true`, `is_false`, `intersects`, `not_intersects` |
| `value` (not relevant for operator: `exists`/`not_exists`) | string | User input. |


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
metadata:
id: "ArrayIntersect"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_xyz"
attribute: "arr"
operator: "intersects"
value:
- "allowed1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
metadata:
id: "MixedValue"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_default_security_group"
attribute: "ingress.cidr_blocks"
operator: "intersects"
value: "0.0.0.0/0"

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
metadata:
id: "NoneAttribute"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_subnet"
attribute: "foo"
operator: "intersects"
value:
- "bar"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
metadata:
id: "PublicVMs"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_default_security_group"
attribute: "ingress.cidr_blocks"
operator: "intersects"
value:
- "0.0.0.0/0"

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
metadata:
id: "StringAttribute"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_subnet"
attribute: "availability_zone"
operator: "intersects"
value: "us-"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
metadata:
id: "TagsIntersect"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "all"
attribute: "tags"
operator: "intersects"
value:
- "acme"
- "foo"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os

from tests.terraform.graph.checks_infra.test_base import TestBaseSolver

TEST_DIRNAME = os.path.dirname(os.path.realpath(__file__))


class TestIntersectsSolver(TestBaseSolver):
def setUp(self):
self.checks_dir = TEST_DIRNAME
super(TestIntersectsSolver, self).setUp()

def test_simple_array_intersection1(self):
root_folder = '../../../resources/public_virtual_machines'
check_id = "PublicVMs"
should_pass = ['aws_default_security_group.default_security_group_open']
should_fail = ['aws_default_security_group.default_security_group_closed']
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_simple_array_intersection2(self):
root_folder = '../../../resources/array_test'
check_id = "ArrayIntersect"
should_pass = ['aws_xyz.pass1', 'aws_xyz.pass2']
should_fail = ['aws_xyz.fail2', 'aws_xyz.fail3', 'aws_xyz.pass3']
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_none_attribute(self):
root_folder = '../../../resources/public_virtual_machines'
check_id = "NoneAttribute"
should_pass = []
should_fail = ['aws_subnet.subnet_public_ip', 'aws_subnet.subnet_not_public_ip']
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_string_attribute(self):
root_folder = '../../../resources/public_virtual_machines'
check_id = "StringAttribute"
should_pass = ['aws_subnet.subnet_public_ip']
should_fail = ['aws_subnet.subnet_not_public_ip']
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_mixed_value(self):
root_folder = '../../../resources/public_virtual_machines'
check_id = "MixedValue"
should_pass = ['aws_default_security_group.default_security_group_open']
should_fail = ['aws_default_security_group.default_security_group_closed']
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_tags_intersection(self):
root_folder = '../../../resources/tag_includes'
check_id = "TagsIntersect"
should_pass = ['aws_subnet.acme_subnet']
should_fail = ['aws_instance.some_instance', 'aws_s3_bucket.acme_s3_bucket']
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
metadata:
id: "ArrayNotIntersect"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_xyz"
attribute: "arr"
operator: "not_intersects"
value:
- "notallowed"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
metadata:
id: "PublicVMs"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "aws_default_security_group"
attribute: "ingress.cidr_blocks"
operator: "not_intersects"
value:
- "0.0.0.0/0"

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
metadata:
id: "TagsNotIntersect"
scope:
provider: "AWS"
definition:
cond_type: "attribute"
resource_types:
- "all"
attribute: "tags"
operator: "intersects"
value:
- "acme"
- "bar"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os

from tests.terraform.graph.checks_infra.test_base import TestBaseSolver

TEST_DIRNAME = os.path.dirname(os.path.realpath(__file__))


class TestNotIntersectsSolver(TestBaseSolver):
def setUp(self):
self.checks_dir = TEST_DIRNAME
super(TestNotIntersectsSolver, self).setUp()

def test_simple_array_no_intersection1(self):
root_folder = '../../../resources/public_virtual_machines'
check_id = "PublicVMs"
should_pass = ['aws_default_security_group.default_security_group_closed']
should_fail = ['aws_default_security_group.default_security_group_open']
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_simple_array_no_intersection2(self):
root_folder = '../../../resources/array_test'
check_id = "ArrayNotIntersect"
should_pass = ['aws_xyz.pass1', 'aws_xyz.pass2']
should_fail = ['aws_xyz.fail2', 'aws_xyz.fail3', 'aws_xyz.pass3']
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)
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ resource "aws_subnet" "subnet_public_ip" {
resource "aws_subnet" "subnet_not_public_ip" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
availability_zone = "eu-central-1"

tags = {
Name = "second-tf-example"
Expand Down

0 comments on commit be3710f

Please sign in to comment.