Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ja3_fingerprint matching support to WAFv2 #33933

Merged
merged 2 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading