diff --git a/.changelog/33933.txt b/.changelog/33933.txt new file mode 100644 index 00000000000..ae84f8196bc --- /dev/null +++ b/.changelog/33933.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `ja3_fingerprint` to `field_to_match` configuration blocks +``` \ No newline at end of file diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index eec40db1d9a..ed64fead136 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -525,6 +525,10 @@ func expandFieldToMatch(l []interface{}) *wafv2.FieldToMatch { f.SingleHeader = expandSingleHeader(m["single_header"].([]interface{})) } + if v, ok := m["ja3_fingerprint"]; ok && len(v.([]interface{})) > 0 { + f.JA3Fingerprint = expandJA3Fingerprint(v.([]interface{})) + } + if v, ok := m["single_query_argument"]; ok && len(v.([]interface{})) > 0 { f.SingleQueryArgument = expandSingleQueryArgument(m["single_query_argument"].([]interface{})) } @@ -641,6 +645,20 @@ func expandBody(l []interface{}) *wafv2.Body { return body } +func expandJA3Fingerprint(l []interface{}) *wafv2.JA3Fingerprint { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + ja3fingerprint := &wafv2.JA3Fingerprint{ + FallbackBehavior: aws.String(m["fallback_behavior"].(string)), + } + + return ja3fingerprint +} + func expandJSONMatchPattern(l []interface{}) *wafv2.JsonMatchPattern { if len(l) == 0 || l[0] == nil { return nil @@ -1901,6 +1919,10 @@ func flattenFieldToMatch(f *wafv2.FieldToMatch) interface{} { m["headers"] = flattenHeaders(f.Headers) } + if f.JA3Fingerprint != nil { + m["ja3_fingerprint"] = flattenJA3Fingerprint(f.JA3Fingerprint) + } + if f.JsonBody != nil { m["json_body"] = flattenJSONBody(f.JsonBody) } @@ -1986,6 +2008,18 @@ func flattenCookiesMatchPattern(c *wafv2.CookieMatchPattern) interface{} { return []interface{}{m} } +func flattenJA3Fingerprint(j *wafv2.JA3Fingerprint) interface{} { + if j == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "fallback_behavior": aws.StringValue(j.FallbackBehavior), + } + + return []interface{}{m} +} + func flattenJSONBody(b *wafv2.JsonBody) interface{} { if b == nil { return []interface{}{} diff --git a/internal/service/wafv2/schemas.go b/internal/service/wafv2/schemas.go index 652fb6fd334..ec60542b345 100644 --- a/internal/service/wafv2/schemas.go +++ b/internal/service/wafv2/schemas.go @@ -343,6 +343,7 @@ func fieldToMatchBaseSchema() *schema.Resource { "body": bodySchema(), "cookies": cookiesSchema(), "headers": headersSchema(), + "ja3_fingerprint": ja3fingerprintSchema(), "json_body": jsonBodySchema(), "method": emptySchema(), "query_string": emptySchema(), @@ -789,6 +790,23 @@ func cookiesMatchPatternSchema() *schema.Schema { } } +func ja3fingerprintSchema() *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, + ValidateFunc: validation.StringInSlice(wafv2.FallbackBehavior_Values(), false), + }, + }, + }, + } +} + func bodySchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, diff --git a/internal/service/wafv2/web_acl_test.go b/internal/service/wafv2/web_acl_test.go index 804559e88ef..ae394f97c5f 100644 --- a/internal/service/wafv2/web_acl_test.go +++ b/internal/service/wafv2/web_acl_test.go @@ -1156,6 +1156,54 @@ func TestAccWAFV2WebACL_ByteMatchStatement_basic(t *testing.T) { }) } +func TestAccWAFV2WebACL_ByteMatchStatement_ja3fingerprint(t *testing.T) { + ctx := acctest.Context(t) + var v wafv2.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, wafv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLConfig_byteMatchStatementJA3Fingerprint(webACLName, wafv2.FallbackBehaviorMatch), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, "name", webACLName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.0.byte_match_statement.0.field_to_match.0.ja3_fingerprint.#": "1", + "statement.0.byte_match_statement.0.field_to_match.0.ja3_fingerprint.0.fallback_behavior": "MATCH", + }), + ), + }, + { + Config: testAccWebACLConfig_byteMatchStatementJA3Fingerprint(webACLName, wafv2.FallbackBehaviorNoMatch), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLExists(ctx, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "wafv2", regexache.MustCompile(`regional/webacl/.+$`)), + resource.TestCheckResourceAttr(resourceName, "name", webACLName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "statement.0.byte_match_statement.0.field_to_match.0.ja3_fingerprint.#": "1", + "statement.0.byte_match_statement.0.field_to_match.0.ja3_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 wafv2.WebACL @@ -3090,6 +3138,57 @@ resource "aws_wafv2_web_acl" "test" { `, rName, positionalConstraint, searchString) } +func testAccWebACLConfig_byteMatchStatementJA3Fingerprint(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 { + ja3_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 59489c7d8cc..65f0ac567c5 100644 --- a/website/docs/r/wafv2_web_acl.html.markdown +++ b/website/docs/r/wafv2_web_acl.html.markdown @@ -814,12 +814,13 @@ The part of a web request that you want AWS WAF to inspect. Include the single ` The `field_to_match` block supports the following arguments: -~> **Note** Only one of `all_query_arguments`, `body`, `cookies`, `headers`, `json_body`, `method`, `query_string`, `single_header`, `single_query_argument`, or `uri_path` can be specified. An empty configuration block `{}` should be used when specifying `all_query_arguments`, `method`, or `query_string` attributes. +~> **Note** Only one of `all_query_arguments`, `body`, `cookies`, `headers`, `ja3_fingerprint`, `json_body`, `method`, `query_string`, `single_header`, `single_query_argument`, or `uri_path` can be specified. An empty configuration block `{}` should be used when specifying `all_query_arguments`, `method`, or `query_string` attributes. * `all_query_arguments` - (Optional) Inspect all query arguments. * `body` - (Optional) Inspect the request body, which immediately follows the request headers. See [`body`](#body-block) below for details. * `cookies` - (Optional) Inspect the cookies in the web request. See [`cookies`](#cookies-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. * `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. @@ -859,6 +860,12 @@ 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`. + ### `json_body` Block The `json_body` block supports the following arguments: