Skip to content

Commit

Permalink
Merge pull request #33933 from dpirotte/f-aws-wafv2-ja3fingerprint
Browse files Browse the repository at this point in the history
Add `ja3_fingerprint` matching support to WAFv2
  • Loading branch information
ewbankkit authored Oct 13, 2023
2 parents a53e897 + 216cfc2 commit 3168fff
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/33933.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_wafv2_web_acl: Add `ja3_fingerprint` to `field_to_match` configuration blocks
```
34 changes: 34 additions & 0 deletions internal/service/wafv2/flex.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}))
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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{}{}
Expand Down
18 changes: 18 additions & 0 deletions internal/service/wafv2/schemas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
99 changes: 99 additions & 0 deletions internal/service/wafv2/web_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" {
Expand Down
9 changes: 8 additions & 1 deletion website/docs/r/wafv2_web_acl.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 3168fff

Please sign in to comment.