Repository: cloudtools/troposphere
Pull request #1996 (2021-12-29 21:33:44)

Auto-generate WAFv2
Here is a first pass at auto-generating WAFv2. Wanted to get some input on this change as there were quite a few backward compatibility changes needed along with some removed properties and changes to required fields.

@dalgarin @jerry153fish @axelpavageau @marksidell 

markpeek: @sid21rane LabelMatchStatement is included in this PR via StatementOne, StatementTwo and StatementThree properties. These are used due to the circular definitions in WAFv2 and are implemented per [AWS::WAFv2::RuleGroup Statement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html).
sid21rane: Thanks Mark,

Please can I request for LabelMatchStatement under StatementOne, Two and
Three for AWS::WAFv2::WebACL as well.


Regards,
Siddharth Rane

On Thu, 30 Dec 2021, 21:10 Mark Peek, ***@***.***> wrote:

> @sid21rane <https://github.com/sid21rane> LabelMatchStatement is included
> in this PR via StatementOne, StatementTwo and StatementThree properties.
> These are used due to the circular definitions in WAFv2 and are implemented
> per AWS::WAFv2::RuleGroup Statement
> <https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>
> .
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/cloudtools/troposphere/pull/1996#issuecomment-1003080953>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AXCXQNNPET3Y4HUFCCSKIWTUTR4NDANCNFSM5K6UCCHA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

markpeek: @sid21rane yes, as I commented above LabelMatchStatement is already in each of those classes for this PR.
markpeek: Oh, sorry, misunderstood your request. WebACL and RuleGroup use a common set of Statements so it is already there. There are additions to the WebACL version which I took care of this morning.
sid21rane: Great then. Thanks Mark. When can we expect the PR to go live?


Regards,
Siddharth Rane

On Thu, 30 Dec 2021, 23:58 Mark Peek, ***@***.***> wrote:

> @sid21rane <https://github.com/sid21rane> yes, as I commented above
> LabelMatchStatement is already in each of those classes for this PR.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/cloudtools/troposphere/pull/1996#issuecomment-1003137843>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AXCXQNKCSW6QSMCLD23JQUDUTSQE5ANCNFSM5K6UCCHA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>


Patch:
diff --git a/scripts/patches/wafv2.py b/scripts/patches/wafv2.py
new file mode 100644
index 00000000..027bff5d
--- /dev/null
+++ b/scripts/patches/wafv2.py
@@ -0,0 +1,207 @@
+patches = [
+    # LoggingConfiguration FieldToMatch is different from WebACL or RuleGroup FieldToMatch
+    {
+        "op": "move",
+        "from": "/PropertyTypes/AWS::WAFv2::LoggingConfiguration.FieldToMatch",
+        "path": "/PropertyTypes/AWS::WAFv2::LoggingConfiguration.LoggingConfigurationFieldToMatch",
+    },
+    {
+        "op": "replace",
+        "path": "/ResourceTypes/AWS::WAFv2::LoggingConfiguration/Properties/RedactedFields/ItemType",
+        "value": "LoggingConfigurationFieldToMatch",
+    },
+    # backward compatibility
+    {
+        "op": "move",
+        "from": "/PropertyTypes/AWS::WAFv2::WebACL.Rule",
+        "path": "/PropertyTypes/AWS::WAFv2::WebACL.WebACLRule",
+    },
+    # backward compatibility
+    {
+        "op": "replace",
+        "path": "/ResourceTypes/AWS::WAFv2::WebACL/Properties/Rules/ItemType",
+        "value": "WebACLRule",
+    },
+    # backward compatibility
+    {
+        "op": "move",
+        "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.Rule",
+        "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.RuleGroupRule",
+    },
+    # backward compatibility
+    {
+        "op": "replace",
+        "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",
+    },
+]
diff --git a/troposphere/validators/__init__.py b/troposphere/validators/__init__.py
index 01f0a925..c49232af 100644
--- a/troposphere/validators/__init__.py
+++ b/troposphere/validators/__init__.py
@@ -682,21 +682,3 @@ def ecs_efs_encryption_status(status):
             % (", ".join(valid_status))
         )
     return status
-
-
-def wafv2_custom_body_response_content(content):
-    """validate wafv2 custom body response content. Any character between 1 to 10240"""
-    if not content:
-        raise ValueError("Content must not be empty")
-    if len(content) > 10240:
-        raise ValueError("Content maximum length must not exceed 10240")
-
-    return content
-
-
-def wafv2_custom_body_response_content_type(content_type):
-    """validate wafv2 custom response content type"""
-    valid_types = ["APPLICATION_JSON", "TEXT_HTML", "TEXT_PLAIN"]
-    if content_type not in valid_types:
-        raise ValueError('ContentType must be one of: "%s"' % (", ".join(valid_types)))
-    return content_type
diff --git a/troposphere/validators/wafv2.py b/troposphere/validators/wafv2.py
new file mode 100644
index 00000000..c407898d
--- /dev/null
+++ b/troposphere/validators/wafv2.py
@@ -0,0 +1,147 @@
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
+# All rights reserved.
+#
+# See LICENSE file for full license.
+
+
+def validate_transformation_type(transformation_type):
+    """
+    Validate Transformation Type for WebACL TextTransformation
+    Property: TextTransformation.Type
+    """
+
+    VALID_TRANSFORMATION_TYPES = (
+        "BASE64_DECODE",
+        "BASE64_DECODE_EXT",
+        "CMD_LINE",
+        "COMPRESS_WHITE_SPACE",
+        "CSS_DECODE",
+        "ESCAPE_SEQ_DECODE",
+        "HEX_DECODE",
+        "HTML_ENTITY_DECODE",
+        "JS_DECODE",
+        "LOWERCASE",
+        "MD5",
+        "NONE",
+        "NORMALIZE_PATH",
+        "NORMALIZE_PATH_WIN",
+        "REMOVE_NULLS",
+        "REPLACE_COMMENTS",
+        "REPLACE_NULLS",
+        "SQL_HEX_DECODE",
+        "URL_DECODE",
+        "URL_DECODE_UNI",
+        "UTF8_TO_UNICODE",
+    )
+
+    if transformation_type not in VALID_TRANSFORMATION_TYPES:
+        raise ValueError(
+            "WebACL TextTransformation must be one of: %s"
+            % ", ".join(VALID_TRANSFORMATION_TYPES)
+        )
+    return transformation_type
+
+
+def validate_comparison_operator(comparison_operator):
+    """
+    Validate Comparison Operator for WebACL SizeConstraintStatement
+    Property: SizeConstraintStatement.ComparisonOperator
+    """
+
+    VALID_COMPARISON_OPERATORS = (
+        "EQ",
+        "GE",
+        "GT",
+        "LE",
+        "LT",
+        "NE",
+    )
+
+    if comparison_operator not in VALID_COMPARISON_OPERATORS:
+        raise ValueError(
+            "WebACL SizeConstraintStatement must be one of: %s"
+            % ", ".join(VALID_COMPARISON_OPERATORS)
+        )
+    return comparison_operator
+
+
+def validate_ipaddress_version(ipaddress_version):
+    """
+    Validate IPAddress version for IPSet
+    Property: IPSet.IPAddressVersion
+    """
+
+    VALID_IP_VERSION = ("IPV4", "IPV6")
+
+    if ipaddress_version not in VALID_IP_VERSION:
+        raise ValueError(
+            "IPSet IPAddressVersion must be one of: %s" % ", ".join(VALID_IP_VERSION)
+        )
+    return ipaddress_version
+
+
+def validate_positional_constraint(positional_constraint):
+    """
+    Validate positional constraint for ByteMatchStatement
+    Property: ByteMatchStatement.PositionalConstraint
+    """
+
+    VALID_POSITIONAL_CONSTRAINTS = (
+        "CONTAINS",
+        "CONTAINS_WORD",
+        "ENDS_WITH",
+        "EXACTLY",
+        "STARTS_WITH",
+    )
+
+    if positional_constraint not in VALID_POSITIONAL_CONSTRAINTS:
+        raise ValueError(
+            "ByteMatchStatement PositionalConstraint must be one of: %s"
+            % ", ".join(VALID_POSITIONAL_CONSTRAINTS)  # NOQA
+        )
+    return positional_constraint
+
+
+def validate_custom_response_bodies(custom_response_bodies):
+    """
+    Validate custom response bodies
+    Property: RuleGroup.CustomResponseBodies
+    Property: WebACL.CustomResponseBodies
+    """
+
+    from ..wafv2 import CustomResponseBody
+
+    if not isinstance(custom_response_bodies, dict):
+        raise ValueError("CustomResponseBodies must be dict")
+
+    for k, v in custom_response_bodies.items():
+        if not isinstance(v, CustomResponseBody):
+            raise ValueError("value of %s must be type of CustomResponseBody" % (k))
+
+    return custom_response_bodies
+
+
+def wafv2_custom_body_response_content(content):
+    """
+    Validate wafv2 custom body response content. Any character between 1 to 10240
+    Property: CustomResponseBody.Content
+    """
+
+    if not content:
+        raise ValueError("Content must not be empty")
+    if len(content) > 10240:
+        raise ValueError("Content maximum length must not exceed 10240")
+
+    return content
+
+
+def wafv2_custom_body_response_content_type(content_type):
+    """
+    validate wafv2 custom response content type
+    Property: CustomResponseBody.ContentType
+    """
+
+    valid_types = ["APPLICATION_JSON", "TEXT_HTML", "TEXT_PLAIN"]
+    if content_type not in valid_types:
+        raise ValueError('ContentType must be one of: "%s"' % (", ".join(valid_types)))
+    return content_type
diff --git a/troposphere/wafv2.py b/troposphere/wafv2.py
index d42d384d..f3a7c03a 100644
--- a/troposphere/wafv2.py
+++ b/troposphere/wafv2.py
@@ -1,188 +1,284 @@
-# Copyright (c) 2012-2019, Mark Peek <mark@peek.org>
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
 # All rights reserved.
 #
 # See LICENSE file for full license.
+#
+# *** Do not modify - this file is autogenerated ***
+# Resource specification version: 51.0.0
+
 
 from . import AWSObject, AWSProperty, Tags
-from .validators import (
-    boolean,
-    integer,
-    json_checker,
+from .validators import boolean, double, integer
+from .validators.wafv2 import (
+    validate_comparison_operator,
+    validate_custom_response_bodies,
+    validate_ipaddress_version,
+    validate_positional_constraint,
+    validate_transformation_type,
     wafv2_custom_body_response_content,
     wafv2_custom_body_response_content_type,
 )
 
-VALID_TRANSFORMATION_TYPES = (
-    "CMD_LINE",
-    "COMPRESS_WHITE_SPACE",
-    "HTML_ENTITY_DECODE",
-    "LOWERCASE",
-    "NONE",
-    "URL_DECODE",
-)
-VALID_COMPARISON_OPERATORS = ("EQ", "GE", "GT", "LE", "LT", "NE")
-VALID_IP_VERSION = ("IPV4", "IPV6")
-VALID_POSITIONAL_CONSTRAINTS = (
-    "CONTAINS",
-    "CONTAINS_WORD",
-    "ENDS_WITH",
-    "EXACTLY",
-    "STARTS_WITH",
-)
 
+class IPSet(AWSObject):
+    """
+    `IPSet <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-ipset.html>`__
+    """
 
-def validate_transformation_type(transformation_type):
-    """Validate Transformation Type for WebACL TextTransformation"""
+    resource_type = "AWS::WAFv2::IPSet"
 
-    if transformation_type not in VALID_TRANSFORMATION_TYPES:
-        raise ValueError(
-            "WebACL TextTransformation must be one of: %s"
-            % ", ".join(VALID_TRANSFORMATION_TYPES)
-        )
-    return transformation_type
+    props = {
+        "Addresses": ([str], True),
+        "Description": (str, False),
+        "IPAddressVersion": (validate_ipaddress_version, True),
+        "Name": (str, False),
+        "Scope": (str, True),
+        "Tags": (Tags, False),
+    }
 
 
-def validate_comparison_operator(comparison_operator):
-    """Validate Comparison Operator for WebACL SizeConstraintStatement"""
+class LoggingConfigurationFieldToMatch(AWSProperty):
+    """
+    `LoggingConfigurationFieldToMatch <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-loggingconfiguration-fieldtomatch.html>`__
+    """
 
-    if comparison_operator not in VALID_COMPARISON_OPERATORS:
-        raise ValueError(
-            "WebACL SizeConstraintStatement must be one of: %s"
-            % ", ".join(VALID_COMPARISON_OPERATORS)
-        )
-    return comparison_operator
+    props = {
+        "JsonBody": (dict, False),
+        "Method": (dict, False),
+        "QueryString": (dict, False),
+        "SingleHeader": (dict, False),
+        "UriPath": (dict, False),
+    }
 
 
-def validate_ipaddress_version(ipaddress_version):
-    """Validate IPAddress version for IPSet"""
+class LoggingConfiguration(AWSObject):
+    """
+    `LoggingConfiguration <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-loggingconfiguration.html>`__
+    """
 
-    if ipaddress_version not in VALID_IP_VERSION:
-        raise ValueError(
-            "IPSet IPAddressVersion must be one of: %s" % ", ".join(VALID_IP_VERSION)
-        )
-    return ipaddress_version
+    resource_type = "AWS::WAFv2::LoggingConfiguration"
 
+    props = {
+        "LogDestinationConfigs": ([str], True),
+        "LoggingFilter": (dict, False),
+        "RedactedFields": ([LoggingConfigurationFieldToMatch], False),
+        "ResourceArn": (str, True),
+    }
 
-def validate_positional_constraint(positional_constraint):
-    """Validate positional constraint for ByteMatchStatement"""
 
-    if positional_constraint not in VALID_POSITIONAL_CONSTRAINTS:
-        raise ValueError(
-            "ByteMatchStatement PositionalConstraint must be one of: %s"
-            % ", ".join(VALID_POSITIONAL_CONSTRAINTS)  # NOQA
-        )
-    return positional_constraint
+class RegexPatternSet(AWSObject):
+    """
+    `RegexPatternSet <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-regexpatternset.html>`__
+    """
 
+    resource_type = "AWS::WAFv2::RegexPatternSet"
 
-def validate_custom_response_bodies(custom_response_bodies):
-    """validate custom response bodies"""
-    if not isinstance(custom_response_bodies, dict):
-        raise ValueError("CustomResponseBodies must be dict")
+    props = {
+        "Description": (str, False),
+        "Name": (str, False),
+        "RegularExpressionList": ([str], True),
+        "Scope": (str, True),
+        "Tags": (Tags, False),
+    }
 
-    for k, v in custom_response_bodies.items():
-        if not isinstance(v, CustomResponseBody):
-            raise ValueError("value of %s must be type of CustomResponseBody" % (k))
 
-    return custom_response_bodies
+class CustomResponseBody(AWSProperty):
+    """
+    `CustomResponseBody <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-customresponsebody.html>`__
+    """
 
+    props = {
+        "Content": (wafv2_custom_body_response_content, True),
+        "ContentType": (wafv2_custom_body_response_content_type, True),
+    }
 
-class ExcludedRule(AWSProperty):
-    props = {"Name": (str, False)}
 
+class ImmunityTimeProperty(AWSProperty):
+    """
+    `ImmunityTimeProperty <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-immunitytimeproperty.html>`__
+    """
 
-class RuleGroupReferenceStatement(AWSProperty):
-    props = {"Arn": (str, False), "ExcludedRules": ([ExcludedRule], False)}
+    props = {
+        "ImmunityTime": (integer, True),
+    }
 
 
-class TextTransformation(AWSProperty):
+class CaptchaConfig(AWSProperty):
+    """
+    `CaptchaConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-captchaconfig.html>`__
+    """
+
     props = {
-        "Priority": (integer, False),
-        "Type": (validate_transformation_type, False),
+        "ImmunityTimeProperty": (ImmunityTimeProperty, False),
     }
 
 
-class SingleHeader(AWSProperty):
-    props = {"Name": (str, False)}
+class Label(AWSProperty):
+    """
+    `Label <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-label.html>`__
+    """
 
+    props = {
+        "Name": (str, True),
+    }
 
-class SingleQueryArgument(AWSProperty):
-    props = {"Name": (str, False)}
 
+class CustomHTTPHeader(AWSProperty):
+    """
+    `CustomHTTPHeader <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-customhttpheader.html>`__
+    """
 
-class Body(AWSProperty):
-    props = {}
+    props = {
+        "Name": (str, True),
+        "Value": (str, True),
+    }
 
 
-class Method(AWSProperty):
-    props = {}
+class CustomRequestHandling(AWSProperty):
+    """
+    `CustomRequestHandling <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-customrequesthandling.html>`__
+    """
 
+    props = {
+        "InsertHeaders": ([CustomHTTPHeader], True),
+    }
 
-class AllQueryArguments(AWSProperty):
-    props = {}
 
+class AllowAction(AWSProperty):
+    """
+    `AllowAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-allowaction.html>`__
+    """
 
-class QueryString(AWSProperty):
-    props = {}
+    props = {
+        "CustomRequestHandling": (CustomRequestHandling, False),
+    }
 
 
-class UriPath(AWSProperty):
-    props = {}
+class CustomResponse(AWSProperty):
+    """
+    `CustomResponse <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-customresponse.html>`__
+    """
 
+    props = {
+        "CustomResponseBodyKey": (str, False),
+        "ResponseCode": (integer, True),
+        "ResponseHeaders": ([CustomHTTPHeader], False),
+    }
+
+
+class BlockAction(AWSProperty):
+    """
+    `BlockAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-blockaction.html>`__
+    """
 
-class FieldToMatch(AWSProperty):
     props = {
-        "AllQueryArguments": (AllQueryArguments, False),
-        "Body": (Body, False),
-        "Method": (Method, False),
-        "QueryString": (QueryString, False),
-        "SingleHeader": (SingleHeader, False),
-        "SingleQueryArgument": (SingleQueryArgument, False),
-        "UriPath": (UriPath, False),
+        "CustomResponse": (CustomResponse, False),
     }
 
 
-class RegexPatternSetReferenceStatement(AWSProperty):
+class CaptchaAction(AWSProperty):
+    """
+    `CaptchaAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-captchaaction.html>`__
+    """
+
     props = {
-        "Arn": (str, True),
-        "FieldToMatch": (FieldToMatch, True),
-        "TextTransformations": ([TextTransformation], True),
+        "CustomRequestHandling": (CustomRequestHandling, False),
     }
 
 
-class XssMatchStatement(AWSProperty):
+class CountAction(AWSProperty):
+    """
+    `CountAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-countaction.html>`__
+    """
+
     props = {
-        "FieldToMatch": (FieldToMatch, True),
-        "TextTransformations": ([TextTransformation], True),
+        "CustomRequestHandling": (CustomRequestHandling, False),
     }
 
 
-class SqliMatchStatement(AWSProperty):
+class RuleAction(AWSProperty):
+    """
+    `RuleAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ruleaction.html>`__
+    """
+
     props = {
-        "FieldToMatch": (FieldToMatch, True),
-        "TextTransformations": ([TextTransformation], True),
+        "Allow": (AllowAction, False),
+        "Block": (BlockAction, False),
+        "Captcha": (CaptchaAction, False),
+        "Count": (CountAction, False),
     }
 
 
-class SizeConstraintStatement(AWSProperty):
+class JsonMatchPattern(AWSProperty):
+    """
+    `JsonMatchPattern <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-jsonmatchpattern.html>`__
+    """
+
     props = {
-        "ComparisonOperator": (validate_comparison_operator, True),
-        "FieldToMatch": (FieldToMatch, True),
-        "Size": (integer, True),
-        "TextTransformations": ([TextTransformation], True),
+        "All": (dict, False),
+        "IncludedPaths": ([str], False),
+    }
+
+
+class JsonBody(AWSProperty):
+    """
+    `JsonBody <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-jsonbody.html>`__
+    """
+
+    props = {
+        "InvalidFallbackBehavior": (str, False),
+        "MatchPattern": (JsonMatchPattern, True),
+        "MatchScope": (str, True),
+    }
+
+
+class FieldToMatch(AWSProperty):
+    """
+    `FieldToMatch <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-fieldtomatch.html>`__
+    """
+
+    props = {
+        "AllQueryArguments": (dict, False),
+        "Body": (dict, False),
+        "JsonBody": (JsonBody, False),
+        "Method": (dict, False),
+        "QueryString": (dict, False),
+        "SingleHeader": (dict, False),
+        "SingleQueryArgument": (dict, False),
+        "UriPath": (dict, False),
+    }
+
+
+class TextTransformation(AWSProperty):
+    """
+    `TextTransformation <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-texttransformation.html>`__
+    """
+
+    props = {
+        "Priority": (integer, True),
+        "Type": (validate_transformation_type, True),
     }
 
 
 class ByteMatchStatement(AWSProperty):
+    """
+    `ByteMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-bytematchstatement.html>`__
+    """
+
     props = {
         "FieldToMatch": (FieldToMatch, True),
         "PositionalConstraint": (validate_positional_constraint, True),
-        "SearchString": (str, True),
+        "SearchString": (str, False),
         "SearchStringBase64": (str, False),
         "TextTransformations": ([TextTransformation], True),
     }
 
 
 class ForwardedIPConfiguration(AWSProperty):
+    """
+    `ForwardedIPConfiguration <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-forwardedipconfiguration.html>`__
+    """
+
     props = {
         "FallbackBehavior": (str, True),
         "HeaderName": (str, True),
@@ -190,6 +286,10 @@ class ForwardedIPConfiguration(AWSProperty):
 
 
 class GeoMatchStatement(AWSProperty):
+    """
+    `GeoMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-geomatchstatement.html>`__
+    """
+
     props = {
         "CountryCodes": ([str], False),
         "ForwardedIPConfig": (ForwardedIPConfiguration, False),
@@ -197,6 +297,10 @@ class GeoMatchStatement(AWSProperty):
 
 
 class IPSetForwardedIPConfiguration(AWSProperty):
+    """
+    `IPSetForwardedIPConfiguration <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetforwardedipconfiguration.html>`__
+    """
+
     props = {
         "FallbackBehavior": (str, True),
         "HeaderName": (str, True),
@@ -205,27 +309,132 @@ class IPSetForwardedIPConfiguration(AWSProperty):
 
 
 class IPSetReferenceStatement(AWSProperty):
+    """
+    `IPSetReferenceStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ipsetreferencestatement.html>`__
+    """
+
     props = {
-        "Arn": (str, False),
+        "Arn": (str, True),
         "IPSetForwardedIPConfig": (IPSetForwardedIPConfiguration, False),
     }
 
 
+class LabelMatchStatement(AWSProperty):
+    """
+    `LabelMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-labelmatchstatement.html>`__
+    """
+
+    props = {
+        "Key": (str, True),
+        "Scope": (str, True),
+    }
+
+
+class ExcludedRule(AWSProperty):
+    """
+    `ExcludedRule <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-excludedrule.html>`__
+    """
+
+    props = {
+        "Name": (str, True),
+    }
+
+
 class ManagedRuleGroupStatement(AWSProperty):
+    """
+    `ManagedRuleGroupStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupstatement.html>`__
+    """
+
     props = {
         "ExcludedRules": ([ExcludedRule], False),
-        "Name": (str, False),
-        "VendorName": (str, False),
+        "Name": (str, True),
+        "VendorName": (str, True),
         "Version": (str, False),
     }
 
 
+class RegexMatchStatement(AWSProperty):
+    """
+    `RegexMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-regexmatchstatement.html>`__
+    """
+
+    props = {
+        "FieldToMatch": (FieldToMatch, True),
+        "RegexString": (str, True),
+        "TextTransformations": ([TextTransformation], True),
+    }
+
+
+class RegexPatternSetReferenceStatement(AWSProperty):
+    """
+    `RegexPatternSetReferenceStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-regexpatternsetreferencestatement.html>`__
+    """
+
+    props = {
+        "Arn": (str, True),
+        "FieldToMatch": (FieldToMatch, True),
+        "TextTransformations": ([TextTransformation], True),
+    }
+
+
+class RuleGroupReferenceStatement(AWSProperty):
+    """
+    `RuleGroupReferenceStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-rulegroupreferencestatement.html>`__
+    """
+
+    props = {
+        "Arn": (str, True),
+        "ExcludedRules": ([ExcludedRule], False),
+    }
+
+
+class SizeConstraintStatement(AWSProperty):
+    """
+    `SizeConstraintStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-sizeconstraintstatement.html>`__
+    """
+
+    props = {
+        "ComparisonOperator": (validate_comparison_operator, True),
+        "FieldToMatch": (FieldToMatch, True),
+        "Size": (double, True),
+        "TextTransformations": ([TextTransformation], True),
+    }
+
+
+class SqliMatchStatement(AWSProperty):
+    """
+    `SqliMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-sqlimatchstatement.html>`__
+    """
+
+    props = {
+        "FieldToMatch": (FieldToMatch, True),
+        "TextTransformations": ([TextTransformation], True),
+    }
+
+
+class XssMatchStatement(AWSProperty):
+    """
+    `XssMatchStatement <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-xssmatchstatement.html>`__
+    """
+
+    props = {
+        "FieldToMatch": (FieldToMatch, True),
+        "TextTransformations": ([TextTransformation], True),
+    }
+
+
 class StatementThree(AWSProperty):
+    """
+    `StatementThree <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
+    """
+
     props = {
         "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),
@@ -235,36 +444,64 @@ class StatementThree(AWSProperty):
 
 
 class AndStatementTwo(AWSProperty):
-    props = {"Statements": ([StatementThree], False)}
+    """
+    `AndStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-andstatement.html>`__
+    """
+
+    props = {
+        "Statements": ([StatementThree], True),
+    }
 
 
 class NotStatementTwo(AWSProperty):
-    props = {"Statement": (StatementThree, False)}
+    """
+    `NotStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html>`__
+    """
+
+    props = {
+        "Statement": (StatementThree, True),
+    }
 
 
 class OrStatementTwo(AWSProperty):
-    props = {"Statements": ([StatementThree], False)}
+    """
+    `OrStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-orstatement.html>`__
+    """
+
+    props = {
+        "Statements": ([StatementThree], True),
+    }
 
 
 class RateBasedStatementTwo(AWSProperty):
+    """
+    `RateBasedStatementTwo <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatement.html>`__
+    """
+
     props = {
-        "AggregateKeyType": (str, False),
+        "AggregateKeyType": (str, True),
         "ForwardedIPConfig": (ForwardedIPConfiguration, False),
-        "Limit": (integer, False),
-        "ScopeDownStatement": StatementThree,
+        "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 = {
         "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),
@@ -274,36 +511,64 @@ class StatementTwo(AWSProperty):
 
 
 class AndStatementOne(AWSProperty):
-    props = {"Statements": ([StatementTwo], False)}
+    """
+    `AndStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-andstatement.html>`__
+    """
+
+    props = {
+        "Statements": ([StatementTwo], True),
+    }
 
 
 class NotStatementOne(AWSProperty):
-    props = {"Statement": (StatementTwo, False)}
+    """
+    `NotStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-notstatement.html>`__
+    """
+
+    props = {
+        "Statement": (StatementTwo, True),
+    }
 
 
 class OrStatementOne(AWSProperty):
-    props = {"Statements": ([StatementTwo], False)}
+    """
+    `OrStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-orstatement.html>`__
+    """
+
+    props = {
+        "Statements": ([StatementTwo], True),
+    }
 
 
 class RateBasedStatementOne(AWSProperty):
+    """
+    `RateBasedStatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ratebasedstatement.html>`__
+    """
+
     props = {
-        "AggregateKeyType": (str, False),
+        "AggregateKeyType": (str, True),
         "ForwardedIPConfig": (ForwardedIPConfiguration, False),
-        "Limit": (integer, False),
+        "Limit": (integer, True),
         "ScopeDownStatement": (StatementTwo, False),
     }
 
 
 class StatementOne(AWSProperty):
+    """
+    `StatementOne <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-statement.html>`__
+    """
+
     props = {
         "AndStatement": (AndStatementOne, 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),
+        "RegexMatchStatement": (RegexMatchStatement, False),
         "RegexPatternSetReferenceStatement": (RegexPatternSetReferenceStatement, False),
         "RuleGroupReferenceStatement": (RuleGroupReferenceStatement, False),
         "SizeConstraintStatement": (SizeConstraintStatement, False),
@@ -313,149 +578,119 @@ class StatementOne(AWSProperty):
 
 
 class VisibilityConfig(AWSProperty):
-    props = {
-        "CloudWatchMetricsEnabled": (boolean, False),
-        "MetricName": (str, False),
-        "SampledRequestsEnabled": (boolean, False),
-    }
-
-
-class AllowAction(AWSProperty):
-    props = {}
-
-
-class BlockAction(AWSProperty):
-    props = {}
+    """
+    `VisibilityConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-visibilityconfig.html>`__
+    """
 
-
-class CountAction(AWSProperty):
-    props = {}
-
-
-class NoneAction(AWSProperty):
-    props = {}
-
-
-class RuleAction(AWSProperty):
     props = {
-        "Allow": (AllowAction, False),
-        "Block": (BlockAction, False),
-        "Count": (CountAction, False),
+        "CloudWatchMetricsEnabled": (boolean, True),
+        "MetricName": (str, True),
+        "SampledRequestsEnabled": (boolean, True),
     }
 
 
-class OverrideAction(AWSProperty):
-    props = {
-        "Count": (CountAction, False),
-        "None": (NoneAction, False),
-    }
-
+class RuleGroupRule(AWSProperty):
+    """
+    `RuleGroupRule <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-rule.html>`__
+    """
 
-class WebACLRule(AWSProperty):
     props = {
         "Action": (RuleAction, False),
-        "Name": (str, False),
-        "OverrideAction": (OverrideAction, False),
-        "Priority": (integer, False),
-        "Statement": (StatementOne, False),
-        "VisibilityConfig": (VisibilityConfig, False),
+        "CaptchaConfig": (CaptchaConfig, False),
+        "Name": (str, True),
+        "Priority": (integer, True),
+        "RuleLabels": ([Label], False),
+        "Statement": (StatementOne, True),
+        "VisibilityConfig": (VisibilityConfig, True),
     }
 
 
-class DefaultAction(AWSProperty):
-    props = {
-        "Allow": (AllowAction, False),
-        "Block": (BlockAction, False),
-    }
-
+class RuleGroup(AWSObject):
+    """
+    `RuleGroup <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-rulegroup.html>`__
+    """
 
-class WebACL(AWSObject):
-    resource_type = "AWS::WAFv2::WebACL"
+    resource_type = "AWS::WAFv2::RuleGroup"
 
     props = {
+        "Capacity": (integer, True),
         "CustomResponseBodies": (validate_custom_response_bodies, False),
-        "DefaultAction": (DefaultAction, False),
         "Description": (str, False),
         "Name": (str, False),
-        "Rules": ([WebACLRule], False),
+        "Rules": ([RuleGroupRule], False),
         "Scope": (str, True),
         "Tags": (Tags, False),
-        "VisibilityConfig": (VisibilityConfig, False),
+        "VisibilityConfig": (VisibilityConfig, True),
     }
 
 
-class IPSet(AWSObject):
-    resource_type = "AWS::WAFv2::IPSet"
+class DefaultAction(AWSProperty):
+    """
+    `DefaultAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-defaultaction.html>`__
+    """
 
     props = {
-        "Addresses": ([str], False),
-        "Description": (str, False),
-        "IPAddressVersion": (validate_ipaddress_version, False),
-        "Name": (str, False),
-        "Scope": (str, True),
-        "Tags": (Tags, False),
+        "Allow": (AllowAction, False),
+        "Block": (BlockAction, False),
     }
 
 
-class LoggingConfiguration(AWSObject):
-    resource_type = "AWS::WAFv2::LoggingConfiguration"
+class OverrideAction(AWSProperty):
+    """
+    `OverrideAction <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-overrideaction.html>`__
+    """
 
     props = {
-        "LogDestinationConfigs": ([str], True),
-        "LoggingFilter": (json_checker, False),
-        "RedactedFields": ([FieldToMatch], False),
-        "ResourceArn": (str, True),
+        "Count": (dict, False),
+        "None": (dict, False),
     }
 
 
-class RegexPatternSet(AWSObject):
-    resource_type = "AWS::WAFv2::RegexPatternSet"
-
-    props = {
-        "Description": (str, False),
-        "Name": (str, False),
-        "RegularExpressionList": ([str], True),
-        "Scope": (str, True),
-        "Tags": (Tags, False),
-    }
-
+class WebACLRule(AWSProperty):
+    """
+    `WebACLRule <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-rule.html>`__
+    """
 
-class RuleGroupRule(AWSProperty):
     props = {
         "Action": (RuleAction, False),
-        "Name": (str, False),
-        "Priority": (integer, False),
-        "Statement": (StatementOne, False),
-        "VisibilityConfig": (VisibilityConfig, False),
+        "CaptchaConfig": (CaptchaConfig, False),
+        "Name": (str, True),
+        "OverrideAction": (OverrideAction, False),
+        "Priority": (integer, True),
+        "RuleLabels": ([Label], False),
+        "Statement": (StatementOne, True),
+        "VisibilityConfig": (VisibilityConfig, True),
     }
 
 
-class RuleGroup(AWSObject):
-    resource_type = "AWS::WAFv2::RuleGroup"
+class WebACL(AWSObject):
+    """
+    `WebACL <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html>`__
+    """
+
+    resource_type = "AWS::WAFv2::WebACL"
 
     props = {
-        "Capacity": (integer, False),
+        "CaptchaConfig": (CaptchaConfig, False),
         "CustomResponseBodies": (validate_custom_response_bodies, False),
+        "DefaultAction": (DefaultAction, True),
         "Description": (str, False),
         "Name": (str, False),
-        "Rules": ([RuleGroupRule], False),
-        "Scope": (str, False),
+        "Rules": ([WebACLRule], False),
+        "Scope": (str, True),
         "Tags": (Tags, False),
-        "VisibilityConfig": (VisibilityConfig, False),
+        "VisibilityConfig": (VisibilityConfig, True),
     }
 
 
 class WebACLAssociation(AWSObject):
+    """
+    `WebACLAssociation <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webaclassociation.html>`__
+    """
+
     resource_type = "AWS::WAFv2::WebACLAssociation"
 
     props = {
         "ResourceArn": (str, True),
         "WebACLArn": (str, True),
     }
-
-
-class CustomResponseBody(AWSObject):
-    props = {
-        "Content": (wafv2_custom_body_response_content, True),
-        "ContentType": (wafv2_custom_body_response_content_type, True),
-    }


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 4af3ac0b..f02f86ea 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -28,8 +28,6 @@ from troposphere.validators import (
     task_type,
     tg_healthcheck_port,
     waf_action_type,
-    wafv2_custom_body_response_content,
-    wafv2_custom_body_response_content_type,
 )
 
 
@@ -283,24 +281,6 @@ class TestValidators(unittest.TestCase):
             with self.assertRaises(ValueError):
                 waf_action_type(s)
 
-    def test_wafv2_custom_body_response_content(self):
-        for s in [
-            "{'hello': 'world'}",
-            "<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Test</h1><p>Test.</p></body></html>",
-            "Health",
-        ]:
-            wafv2_custom_body_response_content(s)
-        for s in ["", "a" * 10241]:
-            with self.assertRaises(ValueError):
-                wafv2_custom_body_response_content(s)
-
-    def test_wafv2_custom_body_response_content_type(self):
-        for s in ["APPLICATION_JSON", "TEXT_HTML", "TEXT_PLAIN"]:
-            wafv2_custom_body_response_content_type(s)
-        for s in ["", "APPLICATION", "HTML", "TEXT"]:
-            with self.assertRaises(ValueError):
-                wafv2_custom_body_response_content_type(s)
-
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_wafv2.py b/tests/test_wafv2.py
new file mode 100644
index 00000000..004da3db
--- /dev/null
+++ b/tests/test_wafv2.py
@@ -0,0 +1,46 @@
+import unittest
+
+from troposphere.wafv2 import (
+    CustomResponseBody,
+    validate_custom_response_bodies,
+    wafv2_custom_body_response_content,
+    wafv2_custom_body_response_content_type,
+)
+
+
+class TestWafV2CustomBodies(unittest.TestCase):
+    def test_valid_empty_dict(self):
+        validate_custom_response_bodies({})
+
+    def test_valid_dict(self):
+        validate_custom_response_bodies({"foo": CustomResponseBody()})
+
+    def test_not_a_dict(self):
+        with self.assertRaises(ValueError):
+            validate_custom_response_bodies("foo")
+
+    def test_not_containing_a_custom_response_body(self):
+        with self.assertRaises(ValueError):
+            validate_custom_response_bodies({"foo": "bar"})
+
+    def test_wafv2_custom_body_response_content(self):
+        for s in [
+            "{'hello': 'world'}",
+            "<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Test</h1><p>Test.</p></body></html>",
+            "Health",
+        ]:
+            wafv2_custom_body_response_content(s)
+        for s in ["", "a" * 10241]:
+            with self.assertRaises(ValueError):
+                wafv2_custom_body_response_content(s)
+
+    def test_wafv2_custom_body_response_content_type(self):
+        for s in ["APPLICATION_JSON", "TEXT_HTML", "TEXT_PLAIN"]:
+            wafv2_custom_body_response_content_type(s)
+        for s in ["", "APPLICATION", "HTML", "TEXT"]:
+            with self.assertRaises(ValueError):
+                wafv2_custom_body_response_content_type(s)
+
+
+if __name__ == "__main__":
+    unittest.main()