From 080d8c9ae4e977e6dcf4544dda58071104ab9118 Mon Sep 17 00:00:00 2001 From: Mor Kadosh Date: Sun, 6 Nov 2022 13:26:46 +0200 Subject: [PATCH 1/4] feat: add support for number_of_words_equals and number_of_words_not_equals attribute solvers --- checkov/common/checks_infra/checks_parser.py | 8 +++++-- .../solvers/attribute_solvers/__init__.py | 2 ++ .../number_of_words_equals_solver.py | 17 ++++++++++++++ .../number_of_words_not_equals_solver.py | 11 ++++++++++ checkov/common/graph/checks_infra/enums.py | 2 ++ .../3.Custom Policies/YAML Custom Policies.md | 2 ++ .../jsonpath_equals_solver/example.tf | 11 ++++++++++ .../NumberOfWordsEquals.yaml | 9 ++++++++ .../number_of_words_equals_solver/__init__.py | 0 .../test_solver.py | 22 +++++++++++++++++++ .../NumberOfWordsEquals.yaml | 9 ++++++++ .../__init__.py | 0 .../test_solver.py | 22 +++++++++++++++++++ .../terraform/graph/resources/lengths/main.tf | 2 +- 14 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py create mode 100644 checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/jsonpath_equals_solver/example.tf create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/NumberOfWordsEquals.yaml create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/__init__.py create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/NumberOfWordsEquals.yaml create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/__init__.py create mode 100644 tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py diff --git a/checkov/common/checks_infra/checks_parser.py b/checkov/common/checks_infra/checks_parser.py index 9a143695ae4..275fe6b7dfe 100644 --- a/checkov/common/checks_infra/checks_parser.py +++ b/checkov/common/checks_infra/checks_parser.py @@ -46,7 +46,9 @@ EqualsIgnoreCaseAttributeSolver, NotEqualsIgnoreCaseAttributeSolver, RangeIncludesAttributeSolver, - RangeNotIncludesAttributeSolver + RangeNotIncludesAttributeSolver, + NumberOfWordsEqualsAttributeSolver, + NumberOfWordsNotEqualsAttributeSolver ) from checkov.common.checks_infra.solvers.connections_solvers.connection_one_exists_solver import \ ConnectionOneExistsSolver @@ -100,7 +102,9 @@ "equals_ignore_case": EqualsIgnoreCaseAttributeSolver, "not_equals_ignore_case": NotEqualsIgnoreCaseAttributeSolver, "range_includes": RangeIncludesAttributeSolver, - "range_not_includes": RangeNotIncludesAttributeSolver + "range_not_includes": RangeNotIncludesAttributeSolver, + "number_of_words_equals": NumberOfWordsEqualsAttributeSolver, + "number_of_words_not_equals": NumberOfWordsNotEqualsAttributeSolver, } operators_to_complex_solver_classes: dict[str, Type[BaseComplexSolver]] = { diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py b/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py index 5ae8afda648..a5b0db22969 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py @@ -34,3 +34,5 @@ from checkov.common.checks_infra.solvers.attribute_solvers.not_equals_ignore_case_attribute_solver import NotEqualsIgnoreCaseAttributeSolver # noqa from checkov.common.checks_infra.solvers.attribute_solvers.range_includes_attribute_solver import RangeIncludesAttributeSolver # noqa from checkov.common.checks_infra.solvers.attribute_solvers.range_not_includes_attribute_solver import RangeNotIncludesAttributeSolver # noqa +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_solver import NumberOfWordsEqualsAttributeSolver # noqa +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_not_equals_solver import NumberOfWordsNotEqualsAttributeSolver # noqa diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py new file mode 100644 index 00000000000..4be048b7f52 --- /dev/null +++ b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py @@ -0,0 +1,17 @@ +from typing import Optional, Any, Dict + +from checkov.common.graph.checks_infra.enums import Operators +from checkov.common.checks_infra.solvers.attribute_solvers.base_attribute_solver import BaseAttributeSolver + + +class NumberOfWordsEqualsAttributeSolver(BaseAttributeSolver): + operator = Operators.NUMBER_OF_WORDS_EQUALS # noqa: CCE003 # a static attribute + + def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bool: + vertex_attr = vertex.get(attribute) # type:ignore[arg-type] # due to attribute can be None + + if not isinstance(vertex_attr, str): + return False + words = vertex_attr.split(" ") + + return len(words) == self.value diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py new file mode 100644 index 00000000000..d210f911ab4 --- /dev/null +++ b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py @@ -0,0 +1,11 @@ +from typing import Optional, Any, Dict + +from checkov.common.graph.checks_infra.enums import Operators +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_solver import NumberOfWordsEqualsAttributeSolver + + +class NumberOfWordsNotEqualsAttributeSolver(NumberOfWordsEqualsAttributeSolver): + operator = Operators.NUMBER_OF_WORDS_NOT_EQUALS # noqa: CCE003 # a static attribute + + def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bool: + return not super()._get_operation(vertex, attribute) diff --git a/checkov/common/graph/checks_infra/enums.py b/checkov/common/graph/checks_infra/enums.py index c2d977049d7..4c204d038ad 100644 --- a/checkov/common/graph/checks_infra/enums.py +++ b/checkov/common/graph/checks_infra/enums.py @@ -66,3 +66,5 @@ class Operators: NOT_EQUALS_IGNORE_CASE = 'not_equals_ignore_case' RANGE_INCLUDES = 'range_includes' RANGE_NOT_INCLUDES = 'range_not_includes' + NUMBER_OF_WORDS_EQUALS = 'number_of_words_equals' + NUMBER_OF_WORDS_NOT_EQUALS = 'number_of_words_not_equals' diff --git a/docs/3.Custom Policies/YAML Custom Policies.md b/docs/3.Custom Policies/YAML Custom Policies.md index 4b5c846ffd2..cd007c92fc6 100644 --- a/docs/3.Custom Policies/YAML Custom Policies.md +++ b/docs/3.Custom Policies/YAML Custom Policies.md @@ -127,6 +127,8 @@ definition: | Not Equals Ignore Case | `not_equals_ignore_case` | | Range Includes | `range_includes` | | Range Not Includes | `range_not_includes` | +| Number of words Equals | `number_of_words_equals` | +| Number of words not Equals | `number_of_words_not_equals` | 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/jsonpath_equals_solver/example.tf b/tests/terraform/graph/checks_infra/attribute_solvers/jsonpath_equals_solver/example.tf new file mode 100644 index 00000000000..c5070b8e17c --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/jsonpath_equals_solver/example.tf @@ -0,0 +1,11 @@ +resource "aws_security_group" "web-node" { + # security group is open to the world in SSH port + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [ + "0.0.0.0/0" + ] + } +} \ No newline at end of file diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/NumberOfWordsEquals.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/NumberOfWordsEquals.yaml new file mode 100644 index 00000000000..643689260f3 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/NumberOfWordsEquals.yaml @@ -0,0 +1,9 @@ +metadata: + id: "NumberOfWordsEquals" +definition: + cond_type: "attribute" + resource_types: + - aws_security_group + attribute: ingress.description + operator: number_of_words_equals + value: 6 diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/__init__.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py new file mode 100644 index 00000000000..87b76e9abac --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py @@ -0,0 +1,22 @@ +import os + +from tests.terraform.graph.checks_infra.test_base import TestBaseSolver + +TEST_DIRNAME = os.path.dirname(os.path.realpath(__file__)) + + +class TestNumberOfNotWordsEquals(TestBaseSolver): + def setUp(self): + self.checks_dir = TEST_DIRNAME + super(TestNumberOfNotWordsEquals, self).setUp() + + def test_number_of_words_equals(self): + # this is just a basic check to make sure the operator works + # we'll check all the other combinations more directly (because coming up with all the policy combos is painful) + root_folder = '../../../resources/lengths' + check_id = "NumberOfWordsEquals" + should_pass = ['aws_security_group.sg1'] + should_fail = ['aws_security_group.sg2'] + 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) diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/NumberOfWordsEquals.yaml b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/NumberOfWordsEquals.yaml new file mode 100644 index 00000000000..c18f2bfd070 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/NumberOfWordsEquals.yaml @@ -0,0 +1,9 @@ +metadata: + id: "NumberOfWordsNotEquals" +definition: + cond_type: "attribute" + resource_types: + - aws_security_group + attribute: ingress.description + operator: number_of_words_not_equals + value: 6 diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/__init__.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py new file mode 100644 index 00000000000..97f9ca45420 --- /dev/null +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py @@ -0,0 +1,22 @@ +import os + +from tests.terraform.graph.checks_infra.test_base import TestBaseSolver + +TEST_DIRNAME = os.path.dirname(os.path.realpath(__file__)) + + +class TestNumberOfWordsEquals(TestBaseSolver): + def setUp(self): + self.checks_dir = TEST_DIRNAME + super(TestNumberOfWordsEquals, self).setUp() + + def test_number_of_words_not_equals(self): + # this is just a basic check to make sure the operator works + # we'll check all the other combinations more directly (because coming up with all the policy combos is painful) + root_folder = '../../../resources/lengths' + check_id = "NumberOfWordsNotEquals" + should_pass = ['aws_security_group.sg2'] + should_fail = ['aws_security_group.sg1'] + 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) diff --git a/tests/terraform/graph/resources/lengths/main.tf b/tests/terraform/graph/resources/lengths/main.tf index cc99e581d08..574c1ebbdfb 100644 --- a/tests/terraform/graph/resources/lengths/main.tf +++ b/tests/terraform/graph/resources/lengths/main.tf @@ -33,7 +33,7 @@ resource "aws_security_group" "sg2" { } ingress { - description = "Access to Bastion Host Security Group" + description = "Access to Bastion Host SG" from_port = "5432" protocol = "tcp" security_groups = ["sg-id-0"] From f7ccfdb6cef09e4d03a36f868a1d8d8dffd77efb Mon Sep 17 00:00:00 2001 From: Mor Kadosh Date: Sun, 6 Nov 2022 16:29:33 +0200 Subject: [PATCH 2/4] fix: fixed solver type issue --- .../attribute_solvers/number_of_words_equals_solver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py index 4be048b7f52..02b310c6bff 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py @@ -2,6 +2,7 @@ from checkov.common.graph.checks_infra.enums import Operators from checkov.common.checks_infra.solvers.attribute_solvers.base_attribute_solver import BaseAttributeSolver +from checkov.common.util.type_forcers import force_int class NumberOfWordsEqualsAttributeSolver(BaseAttributeSolver): @@ -13,5 +14,6 @@ def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bo if not isinstance(vertex_attr, str): return False words = vertex_attr.split(" ") + value_numeric = force_int(self.value) - return len(words) == self.value + return len(words) == value_numeric From b364ccc4d12a079f1cc37511fc9d702a06efd3c1 Mon Sep 17 00:00:00 2001 From: Mor Kadosh Date: Mon, 7 Nov 2022 08:26:51 +0200 Subject: [PATCH 3/4] fix: using split() instead of split(" ") to handle multiple whitespaces as well --- .../number_of_words_equals_solver.py | 2 +- .../test_solver.py | 2 +- .../test_solver.py | 2 +- .../terraform/graph/resources/lengths/main.tf | 2 +- .../graph/resources/number_of_words/main.tf | 43 +++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 tests/terraform/graph/resources/number_of_words/main.tf diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py index 02b310c6bff..7e8294c4c69 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py @@ -13,7 +13,7 @@ def _get_operation(self, vertex: Dict[str, Any], attribute: Optional[str]) -> bo if not isinstance(vertex_attr, str): return False - words = vertex_attr.split(" ") + words = vertex_attr.split() value_numeric = force_int(self.value) return len(words) == value_numeric diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py index 87b76e9abac..ac7bf2d75e2 100644 --- a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_equals_solver/test_solver.py @@ -13,7 +13,7 @@ def setUp(self): def test_number_of_words_equals(self): # this is just a basic check to make sure the operator works # we'll check all the other combinations more directly (because coming up with all the policy combos is painful) - root_folder = '../../../resources/lengths' + root_folder = '../../../resources/number_of_words' check_id = "NumberOfWordsEquals" should_pass = ['aws_security_group.sg1'] should_fail = ['aws_security_group.sg2'] diff --git a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py index 97f9ca45420..612c61e635e 100644 --- a/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py +++ b/tests/terraform/graph/checks_infra/attribute_solvers/number_of_words_not_equals_solver/test_solver.py @@ -13,7 +13,7 @@ def setUp(self): def test_number_of_words_not_equals(self): # this is just a basic check to make sure the operator works # we'll check all the other combinations more directly (because coming up with all the policy combos is painful) - root_folder = '../../../resources/lengths' + root_folder = '../../../resources/number_of_words' check_id = "NumberOfWordsNotEquals" should_pass = ['aws_security_group.sg2'] should_fail = ['aws_security_group.sg1'] diff --git a/tests/terraform/graph/resources/lengths/main.tf b/tests/terraform/graph/resources/lengths/main.tf index 574c1ebbdfb..cc99e581d08 100644 --- a/tests/terraform/graph/resources/lengths/main.tf +++ b/tests/terraform/graph/resources/lengths/main.tf @@ -33,7 +33,7 @@ resource "aws_security_group" "sg2" { } ingress { - description = "Access to Bastion Host SG" + description = "Access to Bastion Host Security Group" from_port = "5432" protocol = "tcp" security_groups = ["sg-id-0"] diff --git a/tests/terraform/graph/resources/number_of_words/main.tf b/tests/terraform/graph/resources/number_of_words/main.tf new file mode 100644 index 00000000000..368a3788835 --- /dev/null +++ b/tests/terraform/graph/resources/number_of_words/main.tf @@ -0,0 +1,43 @@ +resource "aws_security_group" "sg1" { + description = "sg1" + + egress { + description = "Self Reference" + cidr_blocks = ["0.0.0.0/0", "25.0.9.19/92"] + from_port = "0" + protocol = "-1" + self = "false" + to_port = "0" + } + + ingress { + description = "Access to Bastion Host Security Group" + from_port = "5432" + protocol = "tcp" + security_groups = ["sg-id-0"] + self = "false" + to_port = "8182" + } +} + +resource "aws_security_group" "sg2" { + description = "security_group_2" + + egress { + description = "Self Reference" + cidr_blocks = ["0.0.0.0/0"] + from_port = "0" + protocol = "-1" + self = "false" + to_port = "0" + } + + ingress { + description = "Access to SG" + from_port = "5432" + protocol = "tcp" + security_groups = ["sg-id-0"] + self = "false" + to_port = "1234" + } +} From 2d0a2aa1c8c5ffed8431fb4faa605544317766d2 Mon Sep 17 00:00:00 2001 From: Mor Kadosh Date: Mon, 7 Nov 2022 14:39:12 +0200 Subject: [PATCH 4/4] chore: renamed the attribute solver file names --- .../common/checks_infra/solvers/attribute_solvers/__init__.py | 4 ++-- ...s_solver.py => number_of_words_equals_attribute_solver.py} | 0 ...lver.py => number_of_words_not_equals_attribute_solver.py} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename checkov/common/checks_infra/solvers/attribute_solvers/{number_of_words_equals_solver.py => number_of_words_equals_attribute_solver.py} (100%) rename checkov/common/checks_infra/solvers/attribute_solvers/{number_of_words_not_equals_solver.py => number_of_words_not_equals_attribute_solver.py} (86%) diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py b/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py index a5b0db22969..dce91770ba7 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/__init__.py @@ -34,5 +34,5 @@ from checkov.common.checks_infra.solvers.attribute_solvers.not_equals_ignore_case_attribute_solver import NotEqualsIgnoreCaseAttributeSolver # noqa from checkov.common.checks_infra.solvers.attribute_solvers.range_includes_attribute_solver import RangeIncludesAttributeSolver # noqa from checkov.common.checks_infra.solvers.attribute_solvers.range_not_includes_attribute_solver import RangeNotIncludesAttributeSolver # noqa -from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_solver import NumberOfWordsEqualsAttributeSolver # noqa -from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_not_equals_solver import NumberOfWordsNotEqualsAttributeSolver # noqa +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_attribute_solver import NumberOfWordsEqualsAttributeSolver # noqa +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_not_equals_attribute_solver import NumberOfWordsNotEqualsAttributeSolver # noqa diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_attribute_solver.py similarity index 100% rename from checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_solver.py rename to checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_equals_attribute_solver.py diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_attribute_solver.py similarity index 86% rename from checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py rename to checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_attribute_solver.py index d210f911ab4..190efc33242 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_solver.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/number_of_words_not_equals_attribute_solver.py @@ -1,7 +1,7 @@ from typing import Optional, Any, Dict from checkov.common.graph.checks_infra.enums import Operators -from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_solver import NumberOfWordsEqualsAttributeSolver +from checkov.common.checks_infra.solvers.attribute_solvers.number_of_words_equals_attribute_solver import NumberOfWordsEqualsAttributeSolver class NumberOfWordsNotEqualsAttributeSolver(NumberOfWordsEqualsAttributeSolver):