Repository: cloudtools/troposphere
Pull request #2014 (2022-03-03 23:20:20)

Reimplement WAFv2 Statement validation
A fix for #2012 would introduce another larger set of object copies to handle the ScoreDownStatement use case. Rather than perpetuate that set of changes I am proposing a breaking change to make the classes more uniform. Would be good to get feedback on this approach.

Tagging some committers/users of this service:
@linasdsilva @axelpavageau @marksidell @michael-k @nkrumm @jerry153fish 

The current WAFv2 works around a cyclic issue by using copies of objects
to allow hierarchical use. This change works around this issue by using
property validators instead. This does break backward compatibility with
the following class renames:
  - AndStatementOne, AndStatementTwo => AndStatement
  - NotStatementOne, NotStatementTwo => NotStatement
  - OrStatementOne, OrStatementTwo => OrStatement
  - RateBasedStatementOne, RateBasedStatementTwo => RateBasedStatement
  - StatementOne, StatementTwo, StatementThree => Statement


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d319237b..0d2323d9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,15 @@
+4.0.0-beta.1 (not released yet)
+-------------------------------
+Breaking Changes
+^^^^^^^^^^^^^^^^
+* AWS::WAFv2
+  - Renamed AndStatementOne, AndStatementTwo => AndStatement
+  - Renamed NotStatementOne, NotStatementTwo => NotStatement
+  - Renamed OrStatementOne, OrStatementTwo => OrStatement
+  - Renamed RateBasedStatementOne, RateBasedStatementTwo => RateBasedStatement
+  - Renamed StatementOne, StatementTwo, StatementThree => Statement
+
+
 4.0.0-beta.0 (2022*02*19)
 -------------------------
 This release has refactored the code to allow for auto-generation of the troposphere classes from the AWS
diff --git a/scripts/gen.py b/scripts/gen.py
index df4d011f..1652b177 100755
--- a/scripts/gen.py
+++ b/scripts/gen.py
@@ -410,6 +410,7 @@ class CodeGenerator:
         self.resources: Dict[str, ResourceType] = service.resources
         self.properties: Dict[str, PropertyType] = service.properties
         self.property_validators = service.property_validators
+        self.statement_found = False
 
     def generate(self, file=None) -> str:
         """Generated the troposphere source code."""
@@ -525,6 +526,13 @@ class CodeGenerator:
             # prevent recursive properties
             if property_name == name:
                 continue
+            # This is a horrible hack to fix an indirect recursion issue in WAFv2
+            # XXX - Need to implement a more durable solution to detect recursion
+            if self.service_name == "wafv2" and property_name == "Statement":
+                if self.statement_found:
+                    continue
+                else:
+                    self.statement_found = True
 
             try:
                 child = self._build_tree(
diff --git a/scripts/patches/wafv2.py b/scripts/patches/wafv2.py
index c22be8c3..b13c1a59 100644
--- a/scripts/patches/wafv2.py
+++ b/scripts/patches/wafv2.py
@@ -34,176 +34,6 @@ patches = [
         "path": "/ResourceTypes/AWS::WAFv2::RuleGroup/Properties/Rules/ItemType",
         "value": "RuleGroupRule",
     },
-    # backward compatibility - StatementOne
-    {
-        "op": "move",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.Statement",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne/Properties/AndStatement/Type",
-        "value": "AndStatementOne",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne/Properties/NotStatement/Type",
-        "value": "NotStatementOne",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne/Properties/OrStatement/Type",
-        "value": "OrStatementOne",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne/Properties/RateBasedStatement/Type",
-        "value": "RateBasedStatementOne",
-    },
-    {
-        "op": "move",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatement",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatementOne",
-    },
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatementTwo",
-    },
-    {
-        "op": "move",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatement",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatementOne",
-    },
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatementTwo",
-    },
-    {
-        "op": "move",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatement",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatementOne",
-    },
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatementTwo",
-    },
-    {
-        "op": "move",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatement",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatementOne",
-    },
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatementOne/Properties/Statements/ItemType",
-        "value": "StatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatementOne/Properties/Statement/Type",
-        "value": "StatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatementOne/Properties/Statements/ItemType",
-        "value": "StatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatementOne/Properties/ScopeDownStatement/Type",
-        "value": "StatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.AndStatementTwo/Properties/Statements/ItemType",
-        "value": "StatementThree",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.NotStatementTwo/Properties/Statement/Type",
-        "value": "StatementThree",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.OrStatementTwo/Properties/Statements/ItemType",
-        "value": "StatementThree",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.RateBasedStatementTwo/Properties/ScopeDownStatement/Type",
-        "value": "StatementThree",
-    },
-    # backward compatibility - StatementTwo
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementTwo/Properties/AndStatement/Type",
-        "value": "AndStatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementTwo/Properties/NotStatement/Type",
-        "value": "NotStatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementTwo/Properties/OrStatement/Type",
-        "value": "OrStatementTwo",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementTwo/Properties/RateBasedStatement/Type",
-        "value": "RateBasedStatementTwo",
-    },
-    # backward compatibility - StatementThree
-    {
-        "op": "copy",
-        "from": "/PropertyTypes/AWS::WAFv2::WebACL.StatementOne",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementThree",
-    },
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementThree/Properties/AndStatement",
-    },
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementThree/Properties/NotStatement",
-    },
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementThree/Properties/OrStatement",
-    },
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.StatementThree/Properties/RateBasedStatement",
-    },
-    # Insert StatementOne into RuleGroupRule and WebACLRule
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.RuleGroupRule/Properties/Statement/Type",
-        "value": "StatementOne",
-    },
-    {
-        "op": "replace",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.WebACLRule/Properties/Statement/Type",
-        "value": "StatementOne",
-    },
-    # backward compatibility - remove ManagedRuleGroupStatement ScopeDownStatement
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::WAFv2::WebACL.ManagedRuleGroupStatement/Properties/ScopeDownStatement",
-    },
     # Remove redundent RuleGroup properties
     {
         "op": "remove",
diff --git a/troposphere/validators/wafv2.py b/troposphere/validators/wafv2.py
index c407898d..a1336c95 100644
--- a/troposphere/validators/wafv2.py
+++ b/troposphere/validators/wafv2.py
@@ -4,6 +4,40 @@
 # See LICENSE file for full license.
 
 
+def validate_statement(statement):
+    """
+    Validate Transformation Type for WebACL TextTransformation
+    Property: RuleGroupRule.Statement
+    Property: WebACLRule.Statement
+    Property: ManagedRuleGroupStatement.ScopeDownStatement
+    Property: NotStatement.Statement
+    Property: RateBasedStatement.ScopeDownStatement
+    """
+
+    from .. import AWSHelperFn
+    from ..wafv2 import Statement
+
+    if not isinstance(statement, (Statement, AWSHelperFn)):
+        raise TypeError(f"{statement} is not a valid Statement")
+
+    return statement
+
+
+def validate_statements(statements):
+    """
+    Property: AndStatement.Statements
+    Property: OrStatement.Statements
+    """
+
+    if not isinstance(statements, list) or len(statements) != 2:
+        raise TypeError("Statements must be a list of 2 Statement elements")
+
+    for s in statements:
+        validate_statement(s)
+
+    return statements
+
+
 def validate_transformation_type(transformation_type):
     """
     Validate Transformation Type for WebACL TextTransformation
diff --git a/troposphere/wafv2.py b/troposphere/wafv2.py
index 6ec85485..d14c9194 100644
--- a/troposphere/wafv2.py
+++ b/troposphere/wafv2.py
@@ -13,6 +13,8 @@ from .validators.wafv2 import (
     validate_custom_response_bodies,
     validate_ipaddress_version,
     validate_positional_constraint,
+    validate_statement,
+    validate_statements,
     validate_transformation_type,
     wafv2_custom_body_response_content,
     wafv2_custom_body_response_content_type,
@@ -208,6 +210,16 @@ class RuleAction(AWSProperty):
     }
 
 
+class AndStatement(AWSProperty):
+    """
+    `AndStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-andstatement.html>`__
+    """
+
+    props: PropsDictType = {
+        "Statements": (validate_statements, True),
+    }
+
+
 class JsonMatchPattern(AWSProperty):
     """
     `JsonMatchPattern <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-jsonmatchpattern.html>`__
@@ -371,11 +383,45 @@ class ManagedRuleGroupStatement(AWSProperty):
         "ExcludedRules": ([ExcludedRule], False),
         "ManagedRuleGroupConfigs": ([ManagedRuleGroupConfig], False),
         "Name": (str, True),
+        "ScopeDownStatement": (validate_statement, False),
         "VendorName": (str, True),
         "Version": (str, False),
     }
 
 
+class NotStatement(AWSProperty):
+    """
+    `NotStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html>`__
+    """
+
+    props: PropsDictType = {
+        "Statement": (validate_statement, True),
+    }
+
+
+class OrStatement(AWSProperty):
+    """
+    `OrStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-orstatement.html>`__
+    """
+
+    props: PropsDictType = {
+        "Statements": (validate_statements, True),
+    }
+
+
+class RateBasedStatement(AWSProperty):
+    """
+    `RateBasedStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatement.html>`__
+    """
+
+    props: PropsDictType = {
+        "AggregateKeyType": (str, True),
+        "ForwardedIPConfig": (ForwardedIPConfiguration, False),
+        "Limit": (integer, True),
+        "ScopeDownStatement": (validate_statement, False),
+    }
+
+
 class RegexMatchStatement(AWSProperty):
     """
     `RegexMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-regexmatchstatement.html>`__
@@ -446,151 +492,21 @@ class XssMatchStatement(AWSProperty):
     }
 
 
-class StatementThree(AWSProperty):
-    """
-    `StatementThree <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
-    """
-
-    props: PropsDictType = {
-        "ByteMatchStatement": (ByteMatchStatement, False),
-        "GeoMatchStatement": (GeoMatchStatement, False),
-        "IPSetReferenceStatement": (IPSetReferenceStatement, False),
-        "LabelMatchStatement": (LabelMatchStatement, False),
-        "ManagedRuleGroupStatement": (ManagedRuleGroupStatement, False),
-        "RegexMatchStatement": (RegexMatchStatement, False),
-        "RegexPatternSetReferenceStatement": (RegexPatternSetReferenceStatement, False),
-        "RuleGroupReferenceStatement": (RuleGroupReferenceStatement, False),
-        "SizeConstraintStatement": (SizeConstraintStatement, False),
-        "SqliMatchStatement": (SqliMatchStatement, False),
-        "XssMatchStatement": (XssMatchStatement, False),
-    }
-
-
-class AndStatementTwo(AWSProperty):
-    """
-    `AndStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-andstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statements": ([StatementThree], True),
-    }
-
-
-class NotStatementTwo(AWSProperty):
-    """
-    `NotStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statement": (StatementThree, True),
-    }
-
-
-class OrStatementTwo(AWSProperty):
-    """
-    `OrStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-orstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statements": ([StatementThree], True),
-    }
-
-
-class RateBasedStatementTwo(AWSProperty):
-    """
-    `RateBasedStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "AggregateKeyType": (str, True),
-        "ForwardedIPConfig": (ForwardedIPConfiguration, False),
-        "Limit": (integer, True),
-        "ScopeDownStatement": (StatementThree, False),
-    }
-
-
-class StatementTwo(AWSProperty):
-    """
-    `StatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
-    """
-
-    props: PropsDictType = {
-        "AndStatement": (AndStatementTwo, False),
-        "ByteMatchStatement": (ByteMatchStatement, False),
-        "GeoMatchStatement": (GeoMatchStatement, False),
-        "IPSetReferenceStatement": (IPSetReferenceStatement, False),
-        "LabelMatchStatement": (LabelMatchStatement, False),
-        "ManagedRuleGroupStatement": (ManagedRuleGroupStatement, False),
-        "NotStatement": (NotStatementTwo, False),
-        "OrStatement": (OrStatementTwo, False),
-        "RateBasedStatement": (RateBasedStatementTwo, False),
-        "RegexMatchStatement": (RegexMatchStatement, False),
-        "RegexPatternSetReferenceStatement": (RegexPatternSetReferenceStatement, False),
-        "RuleGroupReferenceStatement": (RuleGroupReferenceStatement, False),
-        "SizeConstraintStatement": (SizeConstraintStatement, False),
-        "SqliMatchStatement": (SqliMatchStatement, False),
-        "XssMatchStatement": (XssMatchStatement, False),
-    }
-
-
-class AndStatementOne(AWSProperty):
-    """
-    `AndStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-andstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statements": ([StatementTwo], True),
-    }
-
-
-class NotStatementOne(AWSProperty):
-    """
-    `NotStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statement": (StatementTwo, True),
-    }
-
-
-class OrStatementOne(AWSProperty):
-    """
-    `OrStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-orstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "Statements": ([StatementTwo], True),
-    }
-
-
-class RateBasedStatementOne(AWSProperty):
-    """
-    `RateBasedStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatement.html>`__
-    """
-
-    props: PropsDictType = {
-        "AggregateKeyType": (str, True),
-        "ForwardedIPConfig": (ForwardedIPConfiguration, False),
-        "Limit": (integer, True),
-        "ScopeDownStatement": (StatementTwo, False),
-    }
-
-
-class StatementOne(AWSProperty):
+class Statement(AWSProperty):
     """
-    `StatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
+    `Statement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
     """
 
     props: PropsDictType = {
-        "AndStatement": (AndStatementOne, False),
+        "AndStatement": (AndStatement, False),
         "ByteMatchStatement": (ByteMatchStatement, False),
         "GeoMatchStatement": (GeoMatchStatement, False),
         "IPSetReferenceStatement": (IPSetReferenceStatement, False),
         "LabelMatchStatement": (LabelMatchStatement, False),
         "ManagedRuleGroupStatement": (ManagedRuleGroupStatement, False),
-        "NotStatement": (NotStatementOne, False),
-        "OrStatement": (OrStatementOne, False),
-        "RateBasedStatement": (RateBasedStatementOne, False),
+        "NotStatement": (NotStatement, False),
+        "OrStatement": (OrStatement, False),
+        "RateBasedStatement": (RateBasedStatement, False),
         "RegexMatchStatement": (RegexMatchStatement, False),
         "RegexPatternSetReferenceStatement": (RegexPatternSetReferenceStatement, False),
         "RuleGroupReferenceStatement": (RuleGroupReferenceStatement, False),
@@ -623,7 +539,7 @@ class RuleGroupRule(AWSProperty):
         "Name": (str, True),
         "Priority": (integer, True),
         "RuleLabels": ([Label], False),
-        "Statement": (StatementOne, True),
+        "Statement": (validate_statement, True),
         "VisibilityConfig": (VisibilityConfig, True),
     }
 
@@ -681,7 +597,7 @@ class WebACLRule(AWSProperty):
         "OverrideAction": (OverrideAction, False),
         "Priority": (integer, True),
         "RuleLabels": ([Label], False),
-        "Statement": (StatementOne, True),
+        "Statement": (validate_statement, True),
         "VisibilityConfig": (VisibilityConfig, True),
     }
 


Test patch:
diff --git a/tests/test_wafv2.py b/tests/test_wafv2.py
index 004da3db..06893bcf 100644
--- a/tests/test_wafv2.py
+++ b/tests/test_wafv2.py
@@ -1,8 +1,19 @@
 import unittest
 
 from troposphere.wafv2 import (
+    AndStatement,
+    ByteMatchStatement,
     CustomResponseBody,
+    FieldToMatch,
+    NotStatement,
+    Statement,
+    TextTransformation,
+    VisibilityConfig,
+    WebACLRule,
+    XssMatchStatement,
     validate_custom_response_bodies,
+    validate_statement,
+    validate_statements,
     wafv2_custom_body_response_content,
     wafv2_custom_body_response_content_type,
 )
@@ -41,6 +52,69 @@ class TestWafV2CustomBodies(unittest.TestCase):
             with self.assertRaises(ValueError):
                 wafv2_custom_body_response_content_type(s)
 
+    def test_statement_validator(self):
+        validate_statement(Statement())
+        with self.assertRaises(TypeError):
+            validate_statement("foo")
+        with self.assertRaises(TypeError):
+            validate_statement(10)
+
+    def test_statements_validator(self):
+        validate_statements([Statement(), Statement()])
+        with self.assertRaises(TypeError):
+            validate_statements(
+                [
+                    Statement(),
+                ]
+            )
+        with self.assertRaises(TypeError):
+            validate_statements("foo")
+        with self.assertRaises(TypeError):
+            validate_statements(["foo", "bar"])
+
+    def test_wafv2_snippet(self):
+        web_acl_rule = WebACLRule(
+            "WebACLRule",
+            Name="XSSprotection",
+            Priority=0,
+            VisibilityConfig=VisibilityConfig(
+                SampledRequestsEnabled=True,
+                CloudWatchMetricsEnabled=True,
+                MetricName="XSSprotection",
+            ),
+            Statement=Statement(
+                AndStatement=AndStatement(
+                    Statements=[
+                        Statement(
+                            XssMatchStatement=XssMatchStatement(
+                                FieldToMatch=FieldToMatch(UriPath={}),
+                                TextTransformations=[
+                                    TextTransformation(Type="URL_DECODE", Priority=0),
+                                ],
+                            ),
+                        ),
+                        Statement(
+                            NotStatement=NotStatement(
+                                Statement=Statement(
+                                    ByteMatchStatement=ByteMatchStatement(
+                                        FieldToMatch=FieldToMatch(UriPath={}),
+                                        PositionalConstraint="CONTAINS",
+                                        SearchString="xxxx",
+                                        TextTransformations=[
+                                            TextTransformation(
+                                                Type="LOWERCASE", Priority=0
+                                            ),
+                                        ],
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ],
+                ),
+            ),
+        )
+        web_acl_rule.to_dict()
+
 
 if __name__ == "__main__":
     unittest.main()