From c128b022b3aa35a9f2e2fb3bea85885bf331b5af Mon Sep 17 00:00:00 2001 From: Andres Montalban Date: Tue, 17 Sep 2024 09:58:22 -0300 Subject: [PATCH] feat: Support label_match_statement --- examples/complete/main.tf | 51 ++++++++++++++++++++++ rules.tf | 89 +++++++++++++++++++++++++++++++++++++++ variables.tf | 81 ++++++++++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 5b238ad..1590912 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -398,5 +398,56 @@ module "waf" { } ] + label_match_statement_rules = [ + { + name = "block_sanctioned_ukraine_regions_Luhansk" + action = "block" + priority = 201 + + statement = { + key = "awswaf:clientip:geo:region:UA-09" + scope = "LABEL" + } + + visibility_config = { + cloudwatch_metrics_enabled = true + sampled_requests_enabled = true + metric_name = "block_sanctioned_ukraine_regions" + } + }, + { + name = "block_sanctioned_ukraine_regions_Donetsk" + action = "block" + priority = 202 + + statement = { + key = "awswaf:clientip:geo:region:UA-14" + scope = "LABEL" + } + + visibility_config = { + cloudwatch_metrics_enabled = true + sampled_requests_enabled = true + metric_name = "block_sanctioned_ukraine_regions" + } + }, + { + name = "block_sanctioned_ukraine_regions_Crimea" + action = "block" + priority = 203 + + statement = { + key = "awswaf:clientip:geo:region:UA-43" + scope = "LABEL" + } + + visibility_config = { + cloudwatch_metrics_enabled = true + sampled_requests_enabled = true + metric_name = "block_sanctioned_ukraine_regions" + } + }, + ] + context = module.this.context } diff --git a/rules.tf b/rules.tf index 4bc0ba8..232fcb5 100644 --- a/rules.tf +++ b/rules.tf @@ -23,6 +23,14 @@ locals { ) => rule } : {} + label_match_statement_rules = local.enabled && var.label_match_statement_rules != null ? { + for rule in flatten(var.label_match_statement_rules) : + format("%s-%s", + lookup(rule, "name", null) != null ? rule.name : format("%s-label-match-%d", module.this.id, rule.priority), + rule.action, + ) => rule + } : {} + ip_set_reference_statement_rules = local.enabled && var.ip_set_reference_statement_rules != null ? { for indx, rule in flatten(var.ip_set_reference_statement_rules) : format("%s-%s", @@ -406,6 +414,87 @@ resource "aws_wafv2_web_acl" "default" { } } + dynamic "rule" { + for_each = local.label_match_statement_rules + + content { + name = rule.value.name + priority = rule.value.priority + + action { + dynamic "allow" { + for_each = rule.value.action == "allow" ? [1] : [] + content {} + } + dynamic "block" { + for_each = rule.value.action == "block" ? [1] : [] + content { + dynamic "custom_response" { + for_each = lookup(rule.value, "custom_response", null) != null ? [1] : [] + content { + response_code = rule.value.custom_response.response_code + custom_response_body_key = lookup(rule.value.custom_response, "custom_response_body_key", null) + dynamic "response_header" { + for_each = lookup(rule.value.custom_response, "response_header", null) != null ? [1] : [] + content { + name = rule.value.custom_response.response_header.name + value = rule.value.custom_response.response_header.value + } + } + } + } + } + } + dynamic "count" { + for_each = rule.value.action == "count" ? [1] : [] + content {} + } + dynamic "captcha" { + for_each = rule.value.action == "captcha" ? [1] : [] + content {} + } + } + + statement { + dynamic "label_match_statement" { + for_each = lookup(rule.value, "statement", null) != null ? [rule.value.statement] : [] + + content { + key = label_match_statement.value.key + scope = label_match_statement.value.scope + } + } + } + + dynamic "visibility_config" { + for_each = lookup(rule.value, "visibility_config", null) != null ? [rule.value.visibility_config] : [] + + content { + cloudwatch_metrics_enabled = lookup(visibility_config.value, "cloudwatch_metrics_enabled", true) + metric_name = visibility_config.value.metric_name + sampled_requests_enabled = lookup(visibility_config.value, "sampled_requests_enabled", true) + } + } + + dynamic "captcha_config" { + for_each = lookup(rule.value, "captcha_config", null) != null ? [rule.value.captcha_config] : [] + + content { + immunity_time_property { + immunity_time = captcha_config.value.immunity_time_property.immunity_time + } + } + } + + dynamic "rule_label" { + for_each = lookup(rule.value, "rule_label", null) != null ? rule.value.rule_label : [] + content { + name = rule_label.value + } + } + } + } + dynamic "rule" { for_each = local.ip_set_reference_statement_rules diff --git a/variables.tf b/variables.tf index 32a38d2..a3867c0 100644 --- a/variables.tf +++ b/variables.tf @@ -280,6 +280,85 @@ variable "geo_match_statement_rules" { DOC } +variable "label_match_statement_rules" { + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + custom_response = optional(object({ + response_code = string + custom_response_body_key = string + response_header = optional(object({ + name = string + value = string + }), null) + }), null) + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) + default = null + description = <<-DOC + A rule statement used to identify web requests based on labels. + + This can be useful to either block or allow specific regions inside a country. + + action: + The action that AWS WAF should take on a web request when it matches the rule's statement. + name: + A friendly name of the rule. + priority: + If you define more than one Rule in a WebACL, + AWS WAF evaluates each request against the rules in order based on the value of priority. + AWS WAF processes rules with lower priority first. + + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement. + + custom_response: + response_code: + The HTTP status code to return when the request is blocked. + custom_response_body_key: + The key of the custom response body to use in the response. + response_header: + The response header to send with the response. + + statement: + key: + Specify whether you want to match using the label name or just the namespace. Valid values are `LABEL` or `NAMESPACE`. + scope: + String to match against. + + visibility_config: + Defines and enables Amazon CloudWatch metrics and web request sample collection. + + cloudwatch_metrics_enabled: + Whether the associated resource sends metrics to CloudWatch. + metric_name: + A friendly name of the CloudWatch metric. + sampled_requests_enabled: + Whether AWS WAF should store a sampling of the web requests that match the rules. + DOC +} + variable "ip_set_reference_statement_rules" { type = list(object({ name = string @@ -580,7 +659,7 @@ variable "rate_based_statement_rules" { field_to_match: Part of a web request that you want AWS WAF to inspect. positional_constraint: - Area within the portion of a web request that you want AWS WAF to search for search_string. + Area within the portion of a web request that you want AWS WAF to search for search_string. Valid values include the following: `EXACTLY`, `STARTS_WITH`, `ENDS_WITH`, `CONTAINS`, `CONTAINS_WORD`. search_string: String value that you want AWS WAF to search for.