From 3ba318f3a6d6522cf835e13452fa7f916079e8b8 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 09:58:31 +0000 Subject: [PATCH 01/10] Add support for JA3 and JA4 Fingerprint rate limits --- internal/service/wafv2/flex.go | 54 +++++ internal/service/wafv2/schemas.go | 21 +- internal/service/wafv2/web_acl_test.go | 256 +++++++++++++++++++++ website/docs/r/wafv2_web_acl.html.markdown | 19 ++ 4 files changed, 349 insertions(+), 1 deletion(-) diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index 033a87783b5c..a7e79dbbce8f 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -1561,6 +1561,26 @@ func expandRateLimitHeader(l []interface{}) *awstypes.RateLimitHeader { } } +func expandRateLimitJa3Fingerprint(l []interface{}) *awstypes.RateLimitJA3Fingerprint { + if len(l) == 0 || l[0] == nil { + return nil + } + m := l[0].(map[string]interface{}) + return &awstypes.RateLimitJA3Fingerprint{ + FallbackBehavior: awstypes.FallbackBehavior(m["fallback_behavior"].(string)), + } +} + +func expandRateLimitJa4Fingerprint(l []interface{}) *awstypes.RateLimitJA4Fingerprint { + if len(l) == 0 || l[0] == nil { + return nil + } + m := l[0].(map[string]interface{}) + return &awstypes.RateLimitJA4Fingerprint{ + FallbackBehavior: awstypes.FallbackBehavior(m["fallback_behavior"].(string)), + } +} + func expandRateLimitLabelNamespace(l []interface{}) *awstypes.RateLimitLabelNamespace { if len(l) == 0 || l[0] == nil { return nil @@ -1626,6 +1646,12 @@ func expandRateBasedStatementCustomKeys(l []interface{}) []awstypes.RateBasedSta if v, ok := m["ip"]; ok && len(v.([]interface{})) > 0 { r.IP = &awstypes.RateLimitIP{} } + if v, ok := m["ja3_fingerprint"]; ok && len(v.([]interface{})) > 0 { + r.JA3Fingerprint = expandRateLimitJa3Fingerprint(v.([]interface{})) + } + if v, ok := m["ja4_fingerprint"]; ok && len(v.([]interface{})) > 0 { + r.JA4Fingerprint = expandRateLimitJa4Fingerprint(v.([]interface{})) + } if v, ok := m["label_namespace"]; ok { r.LabelNamespace = expandRateLimitLabelNamespace(v.([]interface{})) } @@ -2949,6 +2975,28 @@ func flattenRateLimitHeader(apiObject *awstypes.RateLimitHeader) []interface{} { } } +func flattenRateLimitJa3FingerPrint(apiObject *awstypes.RateLimitJA3Fingerprint) []interface{} { + if apiObject == nil { + return nil + } + return []interface{}{ + map[string]interface{}{ + "fallback_behavior": apiObject.FallbackBehavior, + }, + } +} + +func flattenRateLimitJa4FingerPrint(apiObject *awstypes.RateLimitJA4Fingerprint) []interface{} { + if apiObject == nil { + return nil + } + return []interface{}{ + map[string]interface{}{ + "fallback_behavior": apiObject.FallbackBehavior, + }, + } +} + func flattenRateLimitLabelNamespace(apiObject *awstypes.RateLimitLabelNamespace) []interface{} { if apiObject == nil { return nil @@ -3024,6 +3072,12 @@ func flattenRateBasedStatementCustomKeys(apiObject []awstypes.RateBasedStatement map[string]interface{}{}, } } + if o.JA3Fingerprint != nil { + tfMap["ja3_fingerprint"] = flattenRateLimitJa3FingerPrint(o.JA3Fingerprint) + } + if o.JA4Fingerprint != nil { + tfMap["ja4_fingerprint"] = flattenRateLimitJa4FingerPrint(o.JA4Fingerprint) + } if o.LabelNamespace != nil { tfMap["label_namespace"] = flattenRateLimitLabelNamespace(o.LabelNamespace) } diff --git a/internal/service/wafv2/schemas.go b/internal/service/wafv2/schemas.go index 8ac8b8d5342c..66a0102bc7a6 100644 --- a/internal/service/wafv2/schemas.go +++ b/internal/service/wafv2/schemas.go @@ -504,6 +504,23 @@ var forwardedIPConfigSchema = sync.OnceValue(func() *schema.Schema { } }) +var jaXFingerprintConfigSchema = sync.OnceValue(func() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fallback_behavior": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: enum.Validate[awstypes.FallbackBehavior](), + }, + }, + }, + } +}) + var textTransformationSchema = sync.OnceValue(func() *schema.Schema { return &schema.Schema{ Type: schema.TypeSet, @@ -1078,7 +1095,9 @@ func rateBasedStatementSchema(level int) *schema.Schema { }, }, }, - "ip": emptySchema(), + "ip": emptySchema(), + "ja3_fingerprint": jaXFingerprintConfigSchema(), + "ja4_fingerprint": jaXFingerprintConfigSchema(), "label_namespace": { Type: schema.TypeList, Optional: true, diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index 89af12aceb23..dc0bb8bf9c91 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -2062,6 +2062,124 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { }), ), }, + { + Config: testAccWebACLConfig_rateBasedStatement_customKeysBasic(webACLName, "cookie", "testcookie"), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "1", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.forwarded_ip_config.#": "0", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.0.text_transformation.#": "1", + }), + ), + }, + { + Config: testAccWebACLConfig_rateBasedStatement_customKeysJa3Fingerprint(webACLName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.forwarded_ip_config.#": "1", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.0.fallback_behaviour": "NO_MATCH", + }), + ), + }, + { + Config: testAccWebACLConfig_rateBasedStatement_customKeysBasic(webACLName, "cookie", "testcookie"), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "1", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.forwarded_ip_config.#": "0", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.0.text_transformation.#": "1", + }), + ), + }, + { + Config: testAccWebACLConfig_rateBasedStatement_customKeysJa4Fingerprint(webACLName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.forwarded_ip_config.#": "1", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.0.fallback_behaviour": "NO_MATCH", + }), + ), + }, { ResourceName: resourceName, ImportState: true, @@ -5392,6 +5510,144 @@ resource "aws_wafv2_web_acl" "test" { `, rName) } +func testAccWebACLConfig_rateBasedStatement_customKeysJa3Fingerprint(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + rule { + name = "rule-1" + priority = 1 + + action { + count {} + } + + statement { + rate_based_statement { + aggregate_key_type = "CUSTOM_KEYS" + limit = 50000 + + forwarded_ip_config { + fallback_behavior = "MATCH" + header_name = "x-forwarded-for" + } + + custom_key { + cookie { + name = "cookie-name" + + text_transformation { + type = "NONE" + priority = 0 + } + } + } + + custom_key { + ja3_fingerprint { + fallback_behavior = "NO_MATCH" + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-rule-metric-name" + sampled_requests_enabled = false + } + } + + tags = { + Tag1 = "Value1" + Tag2 = "Value2" + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } +} +`, rName) +} + +func testAccWebACLConfig_rateBasedStatement_customKeysJa4Fingerprint(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + rule { + name = "rule-1" + priority = 1 + + action { + count {} + } + + statement { + rate_based_statement { + aggregate_key_type = "CUSTOM_KEYS" + limit = 50000 + + forwarded_ip_config { + fallback_behavior = "MATCH" + header_name = "x-forwarded-for" + } + + custom_key { + cookie { + name = "cookie-name" + + text_transformation { + type = "NONE" + priority = 0 + } + } + } + + custom_key { + ja4_fingerprint { + fallback_behavior = "NO_MATCH" + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-rule-metric-name" + sampled_requests_enabled = false + } + } + + tags = { + Tag1 = "Value1" + Tag2 = "Value2" + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } +} +`, rName) +} + func testAccWebACLConfig_rateBasedStatementUpdate(rName string) string { return fmt.Sprintf(` resource "aws_wafv2_web_acl" "test" { diff --git a/website/docs/r/wafv2_web_acl.html.markdown b/website/docs/r/wafv2_web_acl.html.markdown index 9ed7ffe93e85..5d96a56099c4 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -1059,6 +1059,8 @@ The `custom_key` block supports the following arguments: * `http_method` - (Optional) Use the request's HTTP method as an aggregate key. See [RateLimit `http_method`](#ratelimit-http_method-block) below for details. * `header` - (Optional) Use the value of a header in the request as an aggregate key. See [RateLimit `header`](#ratelimit-header-block) below for details. * `ip` - (Optional) Use the request's originating IP address as an aggregate key. See [`RateLimit ip`](#ratelimit-ip-block) below for details. +* `ja3_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key.. See [`RateLimit ip`](#ratelimit-ja3_fingerprint-block) below for details. +* `ja4_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key.. See [`RateLimit ip`](#ratelimit-ja4_fingerprint-block) below for details. * `label_namespace` - (Optional) Use the specified label namespace as an aggregate key. See [RateLimit `label_namespace`](#ratelimit-label_namespace-block) below for details. * `query_argument` - (Optional) Use the specified query argument as an aggregate key. See [RateLimit `query_argument`](#ratelimit-query_argument-block) below for details. * `query_string` - (Optional) Use the request's query string as an aggregate key. See [RateLimit `query_string`](#ratelimit-query_string-block) below for details. @@ -1100,6 +1102,23 @@ Use the request's originating IP address as an aggregate key. Each distinct IP a The `ip` block is configured as an empty block `{}`. +### RateLimit `ja3_fingerprint` Block + +Use the JA3 fingerprint in the request as an aggregate key. Each distinct JA3 fingerprint contributes to the aggregation instance. You can use this key type once. + +The `ja3_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) - Match status to assign to the web request if there is insufficient TSL Client Hello information to compute the JA3 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + +### RateLimit `ja4_fingerprint` Block + +Use the JA3 fingerprint in the request as an aggregate key. Each distinct JA3 fingerprint contributes to the aggregation instance. You can use this key type once. + +The `ja4_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) - Match status to assign to the web request if there is insufficient TSL Client Hello information to compute the JA4 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + + ### RateLimit `label_namespace` Block Use the specified label namespace as an aggregate key. Each distinct fully qualified label name that has the specified label namespace contributes to the aggregation instance. If you use just one label namespace as your custom key, then each label name fully defines an aggregation instance. This uses only labels that have been added to the request by rules that are evaluated before this rate-based rule in the web ACL. For information about label namespaces and names, see Label syntax and naming requirements (https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-requirements.html) in the WAF Developer Guide. From 2dca4151490ae8559c10fd17d2206c2cf73fc866 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 10:06:27 +0000 Subject: [PATCH 02/10] Formatting --- website/docs/r/wafv2_web_acl.html.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/r/wafv2_web_acl.html.markdown b/website/docs/r/wafv2_web_acl.html.markdown index 5d96a56099c4..37dcc3f1aa3d 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -1118,7 +1118,6 @@ The `ja4_fingerprint` block supports the following arguments: * `fallback_behavior` - (Required) - Match status to assign to the web request if there is insufficient TSL Client Hello information to compute the JA4 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. - ### RateLimit `label_namespace` Block Use the specified label namespace as an aggregate key. Each distinct fully qualified label name that has the specified label namespace contributes to the aggregation instance. If you use just one label namespace as your custom key, then each label name fully defines an aggregation instance. This uses only labels that have been added to the request by rules that are evaluated before this rate-based rule in the web ACL. For information about label namespaces and names, see Label syntax and naming requirements (https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-requirements.html) in the WAF Developer Guide. From 6bc5f36ca6614fb66e3229c1886c5d4143f1cc30 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 10:56:33 +0000 Subject: [PATCH 03/10] Function names --- internal/service/wafv2/flex.go | 8 ++++---- internal/service/wafv2/schemas.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index a7e79dbbce8f..dbf7004bc309 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -2975,7 +2975,7 @@ func flattenRateLimitHeader(apiObject *awstypes.RateLimitHeader) []interface{} { } } -func flattenRateLimitJa3FingerPrint(apiObject *awstypes.RateLimitJA3Fingerprint) []interface{} { +func flattenRateLimitJa3Fingerprint(apiObject *awstypes.RateLimitJA3Fingerprint) []interface{} { if apiObject == nil { return nil } @@ -2986,7 +2986,7 @@ func flattenRateLimitJa3FingerPrint(apiObject *awstypes.RateLimitJA3Fingerprint) } } -func flattenRateLimitJa4FingerPrint(apiObject *awstypes.RateLimitJA4Fingerprint) []interface{} { +func flattenRateLimitJa4Fingerprint(apiObject *awstypes.RateLimitJA4Fingerprint) []interface{} { if apiObject == nil { return nil } @@ -3073,10 +3073,10 @@ func flattenRateBasedStatementCustomKeys(apiObject []awstypes.RateBasedStatement } } if o.JA3Fingerprint != nil { - tfMap["ja3_fingerprint"] = flattenRateLimitJa3FingerPrint(o.JA3Fingerprint) + tfMap["ja3_fingerprint"] = flattenRateLimitJa3Fingerprint(o.JA3Fingerprint) } if o.JA4Fingerprint != nil { - tfMap["ja4_fingerprint"] = flattenRateLimitJa4FingerPrint(o.JA4Fingerprint) + tfMap["ja4_fingerprint"] = flattenRateLimitJa4Fingerprint(o.JA4Fingerprint) } if o.LabelNamespace != nil { tfMap["label_namespace"] = flattenRateLimitLabelNamespace(o.LabelNamespace) diff --git a/internal/service/wafv2/schemas.go b/internal/service/wafv2/schemas.go index 66a0102bc7a6..fee835929453 100644 --- a/internal/service/wafv2/schemas.go +++ b/internal/service/wafv2/schemas.go @@ -504,7 +504,7 @@ var forwardedIPConfigSchema = sync.OnceValue(func() *schema.Schema { } }) -var jaXFingerprintConfigSchema = sync.OnceValue(func() *schema.Schema { +var rateLimitJAFingerprintConfigSchema = sync.OnceValue(func() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -1096,8 +1096,8 @@ func rateBasedStatementSchema(level int) *schema.Schema { }, }, "ip": emptySchema(), - "ja3_fingerprint": jaXFingerprintConfigSchema(), - "ja4_fingerprint": jaXFingerprintConfigSchema(), + "ja3_fingerprint": rateLimitJAFingerprintConfigSchema(), + "ja4_fingerprint": rateLimitJAFingerprintConfigSchema(), "label_namespace": { Type: schema.TypeList, Optional: true, From 7e010c6ea29b01f6e9d56ee8a9d85ea8c71627f8 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 11:20:52 +0000 Subject: [PATCH 04/10] Fix attribute name in test assertions --- internal/service/wafv2/web_acl_test.go | 84 +++++++++++--------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index dc0bb8bf9c91..b9c25327127a 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -2099,25 +2099,24 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ - "statement.#": "1", - "statement.0.rate_based_statement.#": "1", - "statement.0.rate_based_statement.0.custom_key.#": "2", - "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", - "statement.0.rate_based_statement.0.evaluation_window_sec": "300", - "statement.0.rate_based_statement.0.forwarded_ip_config.#": "1", - "statement.0.rate_based_statement.0.limit": "50000", - "statement.0.rate_based_statement.0.scope_down_statement.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", - "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", - "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.#": "1", - "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.0.fallback_behaviour": "NO_MATCH", + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.0.fallback_behavior": "NO_MATCH", }), ), }, @@ -2158,25 +2157,24 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ - "statement.#": "1", - "statement.0.rate_based_statement.#": "1", - "statement.0.rate_based_statement.0.custom_key.#": "2", - "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", - "statement.0.rate_based_statement.0.evaluation_window_sec": "300", - "statement.0.rate_based_statement.0.forwarded_ip_config.#": "1", - "statement.0.rate_based_statement.0.limit": "50000", - "statement.0.rate_based_statement.0.scope_down_statement.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", - "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", - "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.#": "1", - "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.0.fallback_behaviour": "NO_MATCH", + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.0.fallback_behavior": "NO_MATCH", }), ), }, @@ -5534,11 +5532,6 @@ resource "aws_wafv2_web_acl" "test" { aggregate_key_type = "CUSTOM_KEYS" limit = 50000 - forwarded_ip_config { - fallback_behavior = "MATCH" - header_name = "x-forwarded-for" - } - custom_key { cookie { name = "cookie-name" @@ -5603,11 +5596,6 @@ resource "aws_wafv2_web_acl" "test" { aggregate_key_type = "CUSTOM_KEYS" limit = 50000 - forwarded_ip_config { - fallback_behavior = "MATCH" - header_name = "x-forwarded-for" - } - custom_key { cookie { name = "cookie-name" From 944c3f911592485abc8c597d1d65fc41b7e44c43 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 12:51:47 +0000 Subject: [PATCH 05/10] Fix assertions to check right attribute --- internal/service/wafv2/web_acl_test.go | 72 +++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index b9c25327127a..8d81237ecaf7 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -2099,24 +2099,24 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ - "statement.#": "1", - "statement.0.rate_based_statement.#": "1", - "statement.0.rate_based_statement.0.custom_key.#": "2", - "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", - "statement.0.rate_based_statement.0.evaluation_window_sec": "300", - "statement.0.rate_based_statement.0.limit": "50000", - "statement.0.rate_based_statement.0.scope_down_statement.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", - "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", - "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.#": "1", - "statement.0.rate_based_statement.0.custom_key.1.ja3_signature.0.fallback_behavior": "NO_MATCH", + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja3_fingerprint.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja3_fingerprint.0.fallback_behavior": "NO_MATCH", }), ), }, @@ -2157,24 +2157,24 @@ func TestAccWAFV2WebACL_RateBased_customKeys(t *testing.T) { resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ - "statement.#": "1", - "statement.0.rate_based_statement.#": "1", - "statement.0.rate_based_statement.0.custom_key.#": "2", - "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", - "statement.0.rate_based_statement.0.evaluation_window_sec": "300", - "statement.0.rate_based_statement.0.limit": "50000", - "statement.0.rate_based_statement.0.scope_down_statement.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", - "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", - "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", - "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.#": "1", - "statement.0.rate_based_statement.0.custom_key.1.ja4_signature.0.fallback_behavior": "NO_MATCH", + "statement.#": "1", + "statement.0.rate_based_statement.#": "1", + "statement.0.rate_based_statement.0.custom_key.#": "2", + "statement.0.rate_based_statement.0.aggregate_key_type": "CUSTOM_KEYS", + "statement.0.rate_based_statement.0.evaluation_window_sec": "300", + "statement.0.rate_based_statement.0.limit": "50000", + "statement.0.rate_based_statement.0.scope_down_statement.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.cookie.#": "1", + "statement.0.rate_based_statement.0.custom_key.0.forwarded_ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.http_method.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.header.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.ip.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.label_namespace.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_argument.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.query_string.#": "0", + "statement.0.rate_based_statement.0.custom_key.0.uri_path.#": "0", + "statement.0.rate_based_statement.0.custom_key.1.ja4_fingerprint.#": "1", + "statement.0.rate_based_statement.0.custom_key.1.ja4_fingerprint.0.fallback_behavior": "NO_MATCH", }), ), }, From 83a89cac37233d24b57216388c8a6556462a6ccc Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 12:55:43 +0000 Subject: [PATCH 06/10] Changelog --- .changelog/41719.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/41719.txt diff --git a/.changelog/41719.txt b/.changelog/41719.txt new file mode 100644 index 000000000000..32ef23bdcbf6 --- /dev/null +++ b/.changelog/41719.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `ja3_fingerprint` and `ja4_fingerprint` arguments to `custom_key` for Rate Limit rules. +``` From fa1ad02c1e977c4efbaf7dee5eda767bb493a879 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 15:12:21 +0000 Subject: [PATCH 07/10] Support JA4 Match rules roo --- .changelog/41719.txt | 4 + internal/service/wafv2/flex.go | 34 ++++++++ internal/service/wafv2/schemas.go | 5 +- internal/service/wafv2/web_acl_test.go | 99 ++++++++++++++++++++++ website/docs/r/wafv2_web_acl.html.markdown | 7 ++ 5 files changed, 147 insertions(+), 2 deletions(-) diff --git a/.changelog/41719.txt b/.changelog/41719.txt index 32ef23bdcbf6..429e8535d68d 100644 --- a/.changelog/41719.txt +++ b/.changelog/41719.txt @@ -1,3 +1,7 @@ ```release-note:enhancement resource/aws_wafv2_web_acl: Add `ja3_fingerprint` and `ja4_fingerprint` arguments to `custom_key` for Rate Limit rules. ``` + +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `ja4_fingerprint` arguments to `custom_key` for Match rules. +``` diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index dbf7004bc309..e103ac6910f7 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -586,6 +586,10 @@ func expandFieldToMatch(l []interface{}) *awstypes.FieldToMatch { f.JA3Fingerprint = expandJA3Fingerprint(v.([]interface{})) } + if v, ok := m["ja4_fingerprint"]; ok && len(v.([]interface{})) > 0 { + f.JA4Fingerprint = expandJA4Fingerprint(v.([]interface{})) + } + if v, ok := m["single_query_argument"]; ok && len(v.([]interface{})) > 0 { f.SingleQueryArgument = expandSingleQueryArgument(m["single_query_argument"].([]interface{})) } @@ -716,6 +720,20 @@ func expandJA3Fingerprint(l []interface{}) *awstypes.JA3Fingerprint { return ja3fingerprint } +func expandJA4Fingerprint(l []interface{}) *awstypes.JA4Fingerprint { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + ja4fingerprint := &awstypes.JA4Fingerprint{ + FallbackBehavior: awstypes.FallbackBehavior(m["fallback_behavior"].(string)), + } + + return ja4fingerprint +} + func expandJSONMatchPattern(l []interface{}) *awstypes.JsonMatchPattern { if len(l) == 0 || l[0] == nil { return nil @@ -2150,6 +2168,10 @@ func flattenFieldToMatch(f *awstypes.FieldToMatch) interface{} { m["ja3_fingerprint"] = flattenJA3Fingerprint(f.JA3Fingerprint) } + if f.JA4Fingerprint != nil { + m["ja4_fingerprint"] = flattenJA4Fingerprint(f.JA4Fingerprint) + } + if f.JsonBody != nil { m["json_body"] = flattenJSONBody(f.JsonBody) } @@ -2247,6 +2269,18 @@ func flattenJA3Fingerprint(j *awstypes.JA3Fingerprint) interface{} { return []interface{}{m} } +func flattenJA4Fingerprint(j *awstypes.JA4Fingerprint) interface{} { + if j == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "fallback_behavior": j.FallbackBehavior, + } + + return []interface{}{m} +} + func flattenJSONBody(b *awstypes.JsonBody) interface{} { if b == nil { return []interface{}{} diff --git a/internal/service/wafv2/schemas.go b/internal/service/wafv2/schemas.go index fee835929453..01769b9a1f8d 100644 --- a/internal/service/wafv2/schemas.go +++ b/internal/service/wafv2/schemas.go @@ -379,7 +379,8 @@ var fieldToMatchBaseSchema = sync.OnceValue(func() *schema.Resource { "cookies": cookiesSchema(), "header_order": headerOrderSchema(), "headers": headersSchema(), - "ja3_fingerprint": ja3fingerprintSchema(), + "ja3_fingerprint": jaFingerprintSchema(), + "ja4_fingerprint": jaFingerprintSchema(), "json_body": jsonBodySchema(), "method": emptySchema(), "query_string": emptySchema(), @@ -877,7 +878,7 @@ func cookiesMatchPatternSchema() *schema.Schema { } } -func ja3fingerprintSchema() *schema.Schema { +func jaFingerprintSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index 8d81237ecaf7..f29969f3abc0 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -1233,6 +1233,54 @@ func TestAccWAFV2WebACL_ByteMatchStatement_ja3fingerprint(t *testing.T) { }) } +func TestAccWAFV2WebACL_ByteMatchStatement_ja4fingerprint(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.WebACL + webACLName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckScopeRegional(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLConfig_byteMatchStatementJA4Fingerprint(webACLName, string(awstypes.FallbackBehaviorMatch)), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.0.byte_match_statement.0.field_to_match.0.ja4_fingerprint.#": "1", + "statement.0.byte_match_statement.0.field_to_match.0.ja4_fingerprint.0.fallback_behavior": "MATCH", + }), + ), + }, + { + Config: testAccWebACLConfig_byteMatchStatementJA4Fingerprint(webACLName, string(awstypes.FallbackBehaviorNoMatch)), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, webACLName), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.0.byte_match_statement.0.field_to_match.0.ja4_fingerprint.#": "1", + "statement.0.byte_match_statement.0.field_to_match.0.ja4_fingerprint.0.fallback_behavior": "NO_MATCH", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLImportStateIdFunc(resourceName), + }, + }, + }) +} + func TestAccWAFV2WebACL_ByteMatchStatement_jsonBody(t *testing.T) { ctx := acctest.Context(t) var v awstypes.WebACL @@ -3541,6 +3589,57 @@ resource "aws_wafv2_web_acl" "test" { `, rName, fallbackBehavior) } +func testAccWebACLConfig_byteMatchStatementJA4Fingerprint(rName, fallbackBehavior string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + rule { + name = "rule-1" + priority = 1 + + action { + count {} + } + + statement { + byte_match_statement { + field_to_match { + ja4_fingerprint { + fallback_behavior = %[2]q + } + } + positional_constraint = "EXACTLY" + search_string = "abcdef1234567890abcdef1234567890" + text_transformation { + priority = 0 + type = "NONE" + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-rule-metric-name" + sampled_requests_enabled = false + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } +} +`, rName, fallbackBehavior) +} + func testAccWebACLConfig_byteMatchStatementJSONBody(rName, matchScope, invalidFallbackBehavior, oversizeHandling, matchPattern string) string { return fmt.Sprintf(` resource "aws_wafv2_web_acl" "test" { diff --git a/website/docs/r/wafv2_web_acl.html.markdown b/website/docs/r/wafv2_web_acl.html.markdown index 37dcc3f1aa3d..79e2a33c03b5 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -881,6 +881,7 @@ The `field_to_match` block supports the following arguments: * `header_order` - (Optional) Inspect a string containing the list of the request's header names, ordered as they appear in the web request that AWS WAF receives for inspection. See [`header_order`](#header_order-block) below for details. * `headers` - (Optional) Inspect the request headers. See [`headers`](#headers-block) below for details. * `ja3_fingerprint` - (Optional) Inspect the JA3 fingerprint. See [`ja3_fingerprint`](#ja3_fingerprint-block) below for details. +* `ja4_fingerprint` - (Optional) Inspect the JA3 fingerprint. See [`ja4_fingerprint`](#ja3_fingerprint-block) below for details. * `json_body` - (Optional) Inspect the request body as JSON. See [`json_body`](#json_body-block) for details. * `method` - (Optional) Inspect the HTTP method. The method indicates the type of operation that the request is asking the origin to perform. * `query_string` - (Optional) Inspect the query string. This is the part of a URL that appears after a `?` character, if any. @@ -934,6 +935,12 @@ The `ja3_fingerprint` block supports the following arguments: * `fallback_behavior` - (Required) The match status to assign to the web request if the request doesn't have a JA3 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. +### `ja4_fingerprint` Block + +The `ja4_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) The match status to assign to the web request if the request doesn't have a JA4 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + ### `json_body` Block The `json_body` block supports the following arguments: From a7f16cc6b96961bd5e6d28bd782ff02e2f5b4f81 Mon Sep 17 00:00:00 2001 From: Andrew Tulloch Date: Fri, 7 Mar 2025 15:16:24 +0000 Subject: [PATCH 08/10] Correct where ja4_fingerprint is added for match rules --- .changelog/41719.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/41719.txt b/.changelog/41719.txt index 429e8535d68d..0776e9feb567 100644 --- a/.changelog/41719.txt +++ b/.changelog/41719.txt @@ -3,5 +3,5 @@ resource/aws_wafv2_web_acl: Add `ja3_fingerprint` and `ja4_fingerprint` argument ``` ```release-note:enhancement -resource/aws_wafv2_web_acl: Add `ja4_fingerprint` arguments to `custom_key` for Match rules. +resource/aws_wafv2_web_acl: Add `ja4_fingerprint` arguments to `field_to_match` for Match rules. ``` From 5e17bb8ed27134d1652b15e722c8055574269fc9 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 10 Mar 2025 08:25:59 -0400 Subject: [PATCH 09/10] Add missing CHANGELOG entries. --- .changelog/41719.txt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.changelog/41719.txt b/.changelog/41719.txt index 0776e9feb567..5262ee376ed2 100644 --- a/.changelog/41719.txt +++ b/.changelog/41719.txt @@ -1,7 +1,15 @@ ```release-note:enhancement -resource/aws_wafv2_web_acl: Add `ja3_fingerprint` and `ja4_fingerprint` arguments to `custom_key` for Rate Limit rules. +resource/aws_wafv2_web_acl: Add `ja3_fingerprint` and `ja4_fingerprint` to `custom_key` configuration blocks ``` ```release-note:enhancement -resource/aws_wafv2_web_acl: Add `ja4_fingerprint` arguments to `field_to_match` for Match rules. +resource/aws_wafv2_rule_group: Add `ja3_fingerprint` and `ja4_fingerprint` to `custom_key` configuration blocks ``` + +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `ja4_fingerprint` to `field_to_match` configuration blocks +``` + +```release-note:enhancement +resource/aws_wafv2_rule_group: Add `ja4_fingerprint` to `field_to_match` configuration blocks +``` \ No newline at end of file From 4a7181d72941c82447b854c08d5a056b54742ca3 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 10 Mar 2025 08:26:54 -0400 Subject: [PATCH 10/10] r/aws_wafv2_rule_group: Document 'ja3_fingerprint' and 'ja4_fingerprint' configuration blocks. --- website/docs/r/wafv2_rule_group.html.markdown | 32 +++++++++++++++++++ website/docs/r/wafv2_web_acl.html.markdown | 4 +-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/website/docs/r/wafv2_rule_group.html.markdown b/website/docs/r/wafv2_rule_group.html.markdown index c7188276fa38..53436c128e80 100644 --- a/website/docs/r/wafv2_rule_group.html.markdown +++ b/website/docs/r/wafv2_rule_group.html.markdown @@ -588,6 +588,8 @@ An empty configuration block `{}` should be used when specifying `all_query_argu * `cookies` - (Optional) Inspect the cookies in the web request. See [Cookies](#cookies) below for details. * `header_order` - (Optional) Inspect the request headers. See [Header Order](#header-order) below for details. * `headers` - (Optional) Inspect the request headers. See [Headers](#headers) below for details. +* `ja3_fingerprint` - (Optional) Inspect the JA3 fingerprint. See [`ja3_fingerprint`](#ja3_fingerprint-block) below for details. +* `ja4_fingerprint` - (Optional) Inspect the JA3 fingerprint. See [`ja4_fingerprint`](#ja3_fingerprint-block) below for details. * `json_body` - (Optional) Inspect the request body as JSON. See [JSON Body](#json-body) for details. * `method` - (Optional) Inspect the HTTP method. The method indicates the type of operation that the request is asking the origin to perform. * `query_string` - (Optional) Inspect the query string. This is the part of a URL that appears after a `?` character, if any. @@ -637,6 +639,18 @@ The `headers` block supports the following arguments: * `match_scope` - (Required) The parts of the headers to inspect with the rule inspection criteria. If you specify `All`, AWS WAF inspects both keys and values. Valid values include the following: `ALL`, `Key`, `Value`. * `oversize_handling` - (Required) Oversize handling tells AWS WAF what to do with a web request when the request component that the rule inspects is over the limits. Valid values include the following: `CONTINUE`, `MATCH`, `NO_MATCH`. See the AWS [documentation](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-oversize-handling.html) for more information. +### `ja3_fingerprint` Block + +The `ja3_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) The match status to assign to the web request if the request doesn't have a JA3 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + +### `ja4_fingerprint` Block + +The `ja4_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) The match status to assign to the web request if the request doesn't have a JA4 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + ### JSON Body The `json_body` block supports the following arguments: @@ -719,6 +733,8 @@ The `custom_key` block supports the following arguments: * `http_method` - (Optional) Use the request's HTTP method as an aggregate key. See [RateLimit `http_method`](#ratelimit-http_method-block) below for details. * `header` - (Optional) Use the value of a header in the request as an aggregate key. See [RateLimit `header`](#ratelimit-header-block) below for details. * `ip` - (Optional) Use the request's originating IP address as an aggregate key. See [`RateLimit ip`](#ratelimit-ip-block) below for details. +* `ja3_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key. See [`RateLimit ip`](#ratelimit-ja3_fingerprint-block) below for details. +* `ja4_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key. See [`RateLimit ip`](#ratelimit-ja4_fingerprint-block) below for details. * `label_namespace` - (Optional) Use the specified label namespace as an aggregate key. See [RateLimit `label_namespace`](#ratelimit-label_namespace-block) below for details. * `query_argument` - (Optional) Use the specified query argument as an aggregate key. See [RateLimit `query_argument`](#ratelimit-query_argument-block) below for details. * `query_string` - (Optional) Use the request's query string as an aggregate key. See [RateLimit `query_string`](#ratelimit-query_string-block) below for details. @@ -760,6 +776,22 @@ Use the request's originating IP address as an aggregate key. Each distinct IP a The `ip` block is configured as an empty block `{}`. +### RateLimit `ja3_fingerprint` Block + +Use the JA3 fingerprint in the request as an aggregate key. Each distinct JA3 fingerprint contributes to the aggregation instance. You can use this key type once. + +The `ja3_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) - Match status to assign to the web request if there is insufficient TSL Client Hello information to compute the JA3 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + +### RateLimit `ja4_fingerprint` Block + +Use the JA3 fingerprint in the request as an aggregate key. Each distinct JA3 fingerprint contributes to the aggregation instance. You can use this key type once. + +The `ja4_fingerprint` block supports the following arguments: + +* `fallback_behavior` - (Required) - Match status to assign to the web request if there is insufficient TSL Client Hello information to compute the JA4 fingerprint. Valid values include: `MATCH` or `NO_MATCH`. + ### RateLimit `label_namespace` Block Use the specified label namespace as an aggregate key. Each distinct fully qualified label name that has the specified label namespace contributes to the aggregation instance. If you use just one label namespace as your custom key, then each label name fully defines an aggregation instance. This uses only labels that have been added to the request by rules that are evaluated before this rate-based rule in the web ACL. For information about label namespaces and names, see Label syntax and naming requirements (https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-requirements.html) in the WAF Developer Guide. diff --git a/website/docs/r/wafv2_web_acl.html.markdown b/website/docs/r/wafv2_web_acl.html.markdown index 79e2a33c03b5..712539a8682a 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -1066,8 +1066,8 @@ The `custom_key` block supports the following arguments: * `http_method` - (Optional) Use the request's HTTP method as an aggregate key. See [RateLimit `http_method`](#ratelimit-http_method-block) below for details. * `header` - (Optional) Use the value of a header in the request as an aggregate key. See [RateLimit `header`](#ratelimit-header-block) below for details. * `ip` - (Optional) Use the request's originating IP address as an aggregate key. See [`RateLimit ip`](#ratelimit-ip-block) below for details. -* `ja3_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key.. See [`RateLimit ip`](#ratelimit-ja3_fingerprint-block) below for details. -* `ja4_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key.. See [`RateLimit ip`](#ratelimit-ja4_fingerprint-block) below for details. +* `ja3_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key. See [`RateLimit ip`](#ratelimit-ja3_fingerprint-block) below for details. +* `ja4_fingerprint` - (Optional) Use the JA3 fingerprint in the request as an aggregate key. See [`RateLimit ip`](#ratelimit-ja4_fingerprint-block) below for details. * `label_namespace` - (Optional) Use the specified label namespace as an aggregate key. See [RateLimit `label_namespace`](#ratelimit-label_namespace-block) below for details. * `query_argument` - (Optional) Use the specified query argument as an aggregate key. See [RateLimit `query_argument`](#ratelimit-query_argument-block) below for details. * `query_string` - (Optional) Use the request's query string as an aggregate key. See [RateLimit `query_string`](#ratelimit-query_string-block) below for details.