We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
WAFv2 AndStatement and OrStatement incorrect validation
Hi guys,

https://github.com/cloudtools/troposphere/blob/f287fa8999ef2a5f5e301ba5c0af6421471e230b/troposphere/validators/wafv2.py#L32-L33

In the validator for WAFv2 statements, there is a check to see if the number of statements if _exactly_ 2 when it should be looking to see if there are _at least_ 2 statements given.  

> "You provide more than one [Statement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html#cfn-wafv2-webacl-notstatement-statement) within the AndStatement."
[Source](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-andstatement.html)

AWS requires there to be more than one statement, but does not restrict them to only pairs.

A small change in the logic of the `if` statement and the `TypeError` message is all that is needed.

Thank you


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `troposphere/validators/wafv2.py`
Content:
```
1 # Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
2 # All rights reserved.
3 #
4 # See LICENSE file for full license.
5 
6 
7 def validate_statement(statement):
8     """
9     Validate Transformation Type for WebACL TextTransformation
10     Property: RuleGroupRule.Statement
11     Property: WebACLRule.Statement
12     Property: ManagedRuleGroupStatement.ScopeDownStatement
13     Property: NotStatement.Statement
14     Property: RateBasedStatement.ScopeDownStatement
15     """
16 
17     from .. import AWSHelperFn
18     from ..wafv2 import Statement
19 
20     if not isinstance(statement, (Statement, AWSHelperFn)):
21         raise TypeError(f"{statement} is not a valid Statement")
22 
23     return statement
24 
25 
26 def validate_statements(statements):
27     """
28     Property: AndStatement.Statements
29     Property: OrStatement.Statements
30     """
31 
32     if not isinstance(statements, list) or len(statements) != 2:
33         raise TypeError("Statements must be a list of 2 Statement elements")
34 
35     for s in statements:
36         validate_statement(s)
37 
38     return statements
39 
40 
41 def validate_transformation_type(transformation_type):
42     """
43     Validate Transformation Type for WebACL TextTransformation
44     Property: TextTransformation.Type
45     """
46 
47     VALID_TRANSFORMATION_TYPES = (
48         "BASE64_DECODE",
49         "BASE64_DECODE_EXT",
50         "CMD_LINE",
51         "COMPRESS_WHITE_SPACE",
52         "CSS_DECODE",
53         "ESCAPE_SEQ_DECODE",
54         "HEX_DECODE",
55         "HTML_ENTITY_DECODE",
56         "JS_DECODE",
57         "LOWERCASE",
58         "MD5",
59         "NONE",
60         "NORMALIZE_PATH",
61         "NORMALIZE_PATH_WIN",
62         "REMOVE_NULLS",
63         "REPLACE_COMMENTS",
64         "REPLACE_NULLS",
65         "SQL_HEX_DECODE",
66         "URL_DECODE",
67         "URL_DECODE_UNI",
68         "UTF8_TO_UNICODE",
69     )
70 
71     if transformation_type not in VALID_TRANSFORMATION_TYPES:
72         raise ValueError(
73             "WebACL TextTransformation must be one of: %s"
74             % ", ".join(VALID_TRANSFORMATION_TYPES)
75         )
76     return transformation_type
77 
78 
79 def validate_comparison_operator(comparison_operator):
80     """
81     Validate Comparison Operator for WebACL SizeConstraintStatement
82     Property: SizeConstraintStatement.ComparisonOperator
83     """
84 
85     VALID_COMPARISON_OPERATORS = (
86         "EQ",
87         "GE",
88         "GT",
89         "LE",
90         "LT",
91         "NE",
92     )
93 
94     if comparison_operator not in VALID_COMPARISON_OPERATORS:
95         raise ValueError(
96             "WebACL SizeConstraintStatement must be one of: %s"
97             % ", ".join(VALID_COMPARISON_OPERATORS)
98         )
99     return comparison_operator
100 
101 
102 def validate_ipaddress_version(ipaddress_version):
103     """
104     Validate IPAddress version for IPSet
105     Property: IPSet.IPAddressVersion
106     """
107 
108     VALID_IP_VERSION = ("IPV4", "IPV6")
109 
110     if ipaddress_version not in VALID_IP_VERSION:
111         raise ValueError(
112             "IPSet IPAddressVersion must be one of: %s" % ", ".join(VALID_IP_VERSION)
113         )
114     return ipaddress_version
115 
116 
117 def validate_positional_constraint(positional_constraint):
118     """
119     Validate positional constraint for ByteMatchStatement
120     Property: ByteMatchStatement.PositionalConstraint
121     """
122 
123     VALID_POSITIONAL_CONSTRAINTS = (
124         "CONTAINS",
125         "CONTAINS_WORD",
126         "ENDS_WITH",
127         "EXACTLY",
128         "STARTS_WITH",
129     )
130 
131     if positional_constraint not in VALID_POSITIONAL_CONSTRAINTS:
132         raise ValueError(
133             "ByteMatchStatement PositionalConstraint must be one of: %s"
134             % ", ".join(VALID_POSITIONAL_CONSTRAINTS)  # NOQA
135         )
136     return positional_constraint
137 
138 
139 def validate_custom_response_bodies(custom_response_bodies):
140     """
141     Validate custom response bodies
142     Property: RuleGroup.CustomResponseBodies
143     Property: WebACL.CustomResponseBodies
144     """
145 
146     from ..wafv2 import CustomResponseBody
147 
148     if not isinstance(custom_response_bodies, dict):
149         raise ValueError("CustomResponseBodies must be dict")
150 
151     for k, v in custom_response_bodies.items():
152         if not isinstance(v, CustomResponseBody):
153             raise ValueError("value of %s must be type of CustomResponseBody" % (k))
154 
155     return custom_response_bodies
156 
157 
158 def wafv2_custom_body_response_content(content):
159     """
160     Validate wafv2 custom body response content. Any character between 1 to 10240
161     Property: CustomResponseBody.Content
162     """
163 
164     if not content:
165         raise ValueError("Content must not be empty")
166     if len(content) > 10240:
167         raise ValueError("Content maximum length must not exceed 10240")
168 
169     return content
170 
171 
172 def wafv2_custom_body_response_content_type(content_type):
173     """
174     validate wafv2 custom response content type
175     Property: CustomResponseBody.ContentType
176     """
177 
178     valid_types = ["APPLICATION_JSON", "TEXT_HTML", "TEXT_PLAIN"]
179     if content_type not in valid_types:
180         raise ValueError('ContentType must be one of: "%s"' % (", ".join(valid_types)))
181     return content_type
182
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/troposphere/validators/wafv2.py b/troposphere/validators/wafv2.py
--- a/troposphere/validators/wafv2.py
+++ b/troposphere/validators/wafv2.py
@@ -29,8 +29,8 @@
     Property: OrStatement.Statements
     """
 
-    if not isinstance(statements, list) or len(statements) != 2:
-        raise TypeError("Statements must be a list of 2 Statement elements")
+    if not isinstance(statements, list) or len(statements) < 2:
+        raise TypeError("Statements must be a list of at least 2 Statement elements")
 
     for s in statements:
         validate_statement(s)

{"golden_diff": "diff --git a/troposphere/validators/wafv2.py b/troposphere/validators/wafv2.py\n--- a/troposphere/validators/wafv2.py\n+++ b/troposphere/validators/wafv2.py\n@@ -29,8 +29,8 @@\n     Property: OrStatement.Statements\n     \"\"\"\n \n-    if not isinstance(statements, list) or len(statements) != 2:\n-        raise TypeError(\"Statements must be a list of 2 Statement elements\")\n+    if not isinstance(statements, list) or len(statements) < 2:\n+        raise TypeError(\"Statements must be a list of at least 2 Statement elements\")\n \n     for s in statements:\n         validate_statement(s)\n", "issue": "WAFv2 AndStatement and OrStatement incorrect validation\nHi guys,\r\n\r\nhttps://github.com/cloudtools/troposphere/blob/f287fa8999ef2a5f5e301ba5c0af6421471e230b/troposphere/validators/wafv2.py#L32-L33\r\n\r\nIn the validator for WAFv2 statements, there is a check to see if the number of statements if _exactly_ 2 when it should be looking to see if there are _at least_ 2 statements given.  \r\n\r\n> \"You provide more than one [Statement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html#cfn-wafv2-webacl-notstatement-statement) within the AndStatement.\"\r\n[Source](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-andstatement.html)\r\n\r\nAWS requires there to be more than one statement, but does not restrict them to only pairs.\r\n\r\nA small change in the logic of the `if` statement and the `TypeError` message is all that is needed.\r\n\r\nThank you\r\n\n", "before_files": [{"content": "# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>\n# All rights reserved.\n#\n# See LICENSE file for full license.\n\n\ndef validate_statement(statement):\n    \"\"\"\n    Validate Transformation Type for WebACL TextTransformation\n    Property: RuleGroupRule.Statement\n    Property: WebACLRule.Statement\n    Property: ManagedRuleGroupStatement.ScopeDownStatement\n    Property: NotStatement.Statement\n    Property: RateBasedStatement.ScopeDownStatement\n    \"\"\"\n\n    from .. import AWSHelperFn\n    from ..wafv2 import Statement\n\n    if not isinstance(statement, (Statement, AWSHelperFn)):\n        raise TypeError(f\"{statement} is not a valid Statement\")\n\n    return statement\n\n\ndef validate_statements(statements):\n    \"\"\"\n    Property: AndStatement.Statements\n    Property: OrStatement.Statements\n    \"\"\"\n\n    if not isinstance(statements, list) or len(statements) != 2:\n        raise TypeError(\"Statements must be a list of 2 Statement elements\")\n\n    for s in statements:\n        validate_statement(s)\n\n    return statements\n\n\ndef validate_transformation_type(transformation_type):\n    \"\"\"\n    Validate Transformation Type for WebACL TextTransformation\n    Property: TextTransformation.Type\n    \"\"\"\n\n    VALID_TRANSFORMATION_TYPES = (\n        \"BASE64_DECODE\",\n        \"BASE64_DECODE_EXT\",\n        \"CMD_LINE\",\n        \"COMPRESS_WHITE_SPACE\",\n        \"CSS_DECODE\",\n        \"ESCAPE_SEQ_DECODE\",\n        \"HEX_DECODE\",\n        \"HTML_ENTITY_DECODE\",\n        \"JS_DECODE\",\n        \"LOWERCASE\",\n        \"MD5\",\n        \"NONE\",\n        \"NORMALIZE_PATH\",\n        \"NORMALIZE_PATH_WIN\",\n        \"REMOVE_NULLS\",\n        \"REPLACE_COMMENTS\",\n        \"REPLACE_NULLS\",\n        \"SQL_HEX_DECODE\",\n        \"URL_DECODE\",\n        \"URL_DECODE_UNI\",\n        \"UTF8_TO_UNICODE\",\n    )\n\n    if transformation_type not in VALID_TRANSFORMATION_TYPES:\n        raise ValueError(\n            \"WebACL TextTransformation must be one of: %s\"\n            % \", \".join(VALID_TRANSFORMATION_TYPES)\n        )\n    return transformation_type\n\n\ndef validate_comparison_operator(comparison_operator):\n    \"\"\"\n    Validate Comparison Operator for WebACL SizeConstraintStatement\n    Property: SizeConstraintStatement.ComparisonOperator\n    \"\"\"\n\n    VALID_COMPARISON_OPERATORS = (\n        \"EQ\",\n        \"GE\",\n        \"GT\",\n        \"LE\",\n        \"LT\",\n        \"NE\",\n    )\n\n    if comparison_operator not in VALID_COMPARISON_OPERATORS:\n        raise ValueError(\n            \"WebACL SizeConstraintStatement must be one of: %s\"\n            % \", \".join(VALID_COMPARISON_OPERATORS)\n        )\n    return comparison_operator\n\n\ndef validate_ipaddress_version(ipaddress_version):\n    \"\"\"\n    Validate IPAddress version for IPSet\n    Property: IPSet.IPAddressVersion\n    \"\"\"\n\n    VALID_IP_VERSION = (\"IPV4\", \"IPV6\")\n\n    if ipaddress_version not in VALID_IP_VERSION:\n        raise ValueError(\n            \"IPSet IPAddressVersion must be one of: %s\" % \", \".join(VALID_IP_VERSION)\n        )\n    return ipaddress_version\n\n\ndef validate_positional_constraint(positional_constraint):\n    \"\"\"\n    Validate positional constraint for ByteMatchStatement\n    Property: ByteMatchStatement.PositionalConstraint\n    \"\"\"\n\n    VALID_POSITIONAL_CONSTRAINTS = (\n        \"CONTAINS\",\n        \"CONTAINS_WORD\",\n        \"ENDS_WITH\",\n        \"EXACTLY\",\n        \"STARTS_WITH\",\n    )\n\n    if positional_constraint not in VALID_POSITIONAL_CONSTRAINTS:\n        raise ValueError(\n            \"ByteMatchStatement PositionalConstraint must be one of: %s\"\n            % \", \".join(VALID_POSITIONAL_CONSTRAINTS)  # NOQA\n        )\n    return positional_constraint\n\n\ndef validate_custom_response_bodies(custom_response_bodies):\n    \"\"\"\n    Validate custom response bodies\n    Property: RuleGroup.CustomResponseBodies\n    Property: WebACL.CustomResponseBodies\n    \"\"\"\n\n    from ..wafv2 import CustomResponseBody\n\n    if not isinstance(custom_response_bodies, dict):\n        raise ValueError(\"CustomResponseBodies must be dict\")\n\n    for k, v in custom_response_bodies.items():\n        if not isinstance(v, CustomResponseBody):\n            raise ValueError(\"value of %s must be type of CustomResponseBody\" % (k))\n\n    return custom_response_bodies\n\n\ndef wafv2_custom_body_response_content(content):\n    \"\"\"\n    Validate wafv2 custom body response content. Any character between 1 to 10240\n    Property: CustomResponseBody.Content\n    \"\"\"\n\n    if not content:\n        raise ValueError(\"Content must not be empty\")\n    if len(content) > 10240:\n        raise ValueError(\"Content maximum length must not exceed 10240\")\n\n    return content\n\n\ndef wafv2_custom_body_response_content_type(content_type):\n    \"\"\"\n    validate wafv2 custom response content type\n    Property: CustomResponseBody.ContentType\n    \"\"\"\n\n    valid_types = [\"APPLICATION_JSON\", \"TEXT_HTML\", \"TEXT_PLAIN\"]\n    if content_type not in valid_types:\n        raise ValueError('ContentType must be one of: \"%s\"' % (\", \".join(valid_types)))\n    return content_type\n", "path": "troposphere/validators/wafv2.py"}], "after_files": [{"content": "# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>\n# All rights reserved.\n#\n# See LICENSE file for full license.\n\n\ndef validate_statement(statement):\n    \"\"\"\n    Validate Transformation Type for WebACL TextTransformation\n    Property: RuleGroupRule.Statement\n    Property: WebACLRule.Statement\n    Property: ManagedRuleGroupStatement.ScopeDownStatement\n    Property: NotStatement.Statement\n    Property: RateBasedStatement.ScopeDownStatement\n    \"\"\"\n\n    from .. import AWSHelperFn\n    from ..wafv2 import Statement\n\n    if not isinstance(statement, (Statement, AWSHelperFn)):\n        raise TypeError(f\"{statement} is not a valid Statement\")\n\n    return statement\n\n\ndef validate_statements(statements):\n    \"\"\"\n    Property: AndStatement.Statements\n    Property: OrStatement.Statements\n    \"\"\"\n\n    if not isinstance(statements, list) or len(statements) < 2:\n        raise TypeError(\"Statements must be a list of at least 2 Statement elements\")\n\n    for s in statements:\n        validate_statement(s)\n\n    return statements\n\n\ndef validate_transformation_type(transformation_type):\n    \"\"\"\n    Validate Transformation Type for WebACL TextTransformation\n    Property: TextTransformation.Type\n    \"\"\"\n\n    VALID_TRANSFORMATION_TYPES = (\n        \"BASE64_DECODE\",\n        \"BASE64_DECODE_EXT\",\n        \"CMD_LINE\",\n        \"COMPRESS_WHITE_SPACE\",\n        \"CSS_DECODE\",\n        \"ESCAPE_SEQ_DECODE\",\n        \"HEX_DECODE\",\n        \"HTML_ENTITY_DECODE\",\n        \"JS_DECODE\",\n        \"LOWERCASE\",\n        \"MD5\",\n        \"NONE\",\n        \"NORMALIZE_PATH\",\n        \"NORMALIZE_PATH_WIN\",\n        \"REMOVE_NULLS\",\n        \"REPLACE_COMMENTS\",\n        \"REPLACE_NULLS\",\n        \"SQL_HEX_DECODE\",\n        \"URL_DECODE\",\n        \"URL_DECODE_UNI\",\n        \"UTF8_TO_UNICODE\",\n    )\n\n    if transformation_type not in VALID_TRANSFORMATION_TYPES:\n        raise ValueError(\n            \"WebACL TextTransformation must be one of: %s\"\n            % \", \".join(VALID_TRANSFORMATION_TYPES)\n        )\n    return transformation_type\n\n\ndef validate_comparison_operator(comparison_operator):\n    \"\"\"\n    Validate Comparison Operator for WebACL SizeConstraintStatement\n    Property: SizeConstraintStatement.ComparisonOperator\n    \"\"\"\n\n    VALID_COMPARISON_OPERATORS = (\n        \"EQ\",\n        \"GE\",\n        \"GT\",\n        \"LE\",\n        \"LT\",\n        \"NE\",\n    )\n\n    if comparison_operator not in VALID_COMPARISON_OPERATORS:\n        raise ValueError(\n            \"WebACL SizeConstraintStatement must be one of: %s\"\n            % \", \".join(VALID_COMPARISON_OPERATORS)\n        )\n    return comparison_operator\n\n\ndef validate_ipaddress_version(ipaddress_version):\n    \"\"\"\n    Validate IPAddress version for IPSet\n    Property: IPSet.IPAddressVersion\n    \"\"\"\n\n    VALID_IP_VERSION = (\"IPV4\", \"IPV6\")\n\n    if ipaddress_version not in VALID_IP_VERSION:\n        raise ValueError(\n            \"IPSet IPAddressVersion must be one of: %s\" % \", \".join(VALID_IP_VERSION)\n        )\n    return ipaddress_version\n\n\ndef validate_positional_constraint(positional_constraint):\n    \"\"\"\n    Validate positional constraint for ByteMatchStatement\n    Property: ByteMatchStatement.PositionalConstraint\n    \"\"\"\n\n    VALID_POSITIONAL_CONSTRAINTS = (\n        \"CONTAINS\",\n        \"CONTAINS_WORD\",\n        \"ENDS_WITH\",\n        \"EXACTLY\",\n        \"STARTS_WITH\",\n    )\n\n    if positional_constraint not in VALID_POSITIONAL_CONSTRAINTS:\n        raise ValueError(\n            \"ByteMatchStatement PositionalConstraint must be one of: %s\"\n            % \", \".join(VALID_POSITIONAL_CONSTRAINTS)  # NOQA\n        )\n    return positional_constraint\n\n\ndef validate_custom_response_bodies(custom_response_bodies):\n    \"\"\"\n    Validate custom response bodies\n    Property: RuleGroup.CustomResponseBodies\n    Property: WebACL.CustomResponseBodies\n    \"\"\"\n\n    from ..wafv2 import CustomResponseBody\n\n    if not isinstance(custom_response_bodies, dict):\n        raise ValueError(\"CustomResponseBodies must be dict\")\n\n    for k, v in custom_response_bodies.items():\n        if not isinstance(v, CustomResponseBody):\n            raise ValueError(\"value of %s must be type of CustomResponseBody\" % (k))\n\n    return custom_response_bodies\n\n\ndef wafv2_custom_body_response_content(content):\n    \"\"\"\n    Validate wafv2 custom body response content. Any character between 1 to 10240\n    Property: CustomResponseBody.Content\n    \"\"\"\n\n    if not content:\n        raise ValueError(\"Content must not be empty\")\n    if len(content) > 10240:\n        raise ValueError(\"Content maximum length must not exceed 10240\")\n\n    return content\n\n\ndef wafv2_custom_body_response_content_type(content_type):\n    \"\"\"\n    validate wafv2 custom response content type\n    Property: CustomResponseBody.ContentType\n    \"\"\"\n\n    valid_types = [\"APPLICATION_JSON\", \"TEXT_HTML\", \"TEXT_PLAIN\"]\n    if content_type not in valid_types:\n        raise ValueError('ContentType must be one of: \"%s\"' % (\", \".join(valid_types)))\n    return content_type\n", "path": "troposphere/validators/wafv2.py"}]}