diff --git a/azurerm/resource_arm_application_gateway.go b/azurerm/resource_arm_application_gateway.go index 84da5e763003..58bea99717ab 100644 --- a/azurerm/resource_arm_application_gateway.go +++ b/azurerm/resource_arm_application_gateway.go @@ -981,6 +981,39 @@ func resourceArmApplicationGateway() *schema.Resource { }, }, }, + "exclusion": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "match_variable": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "RequestHeaderNames", + "RequestArgNames", + "RequestCookieNames", + }, false), + }, + + "selector_match_operator": { + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "Equals", + "StartsWith", + "EndsWith", + "Contains", + }, false), + Optional: true, + }, + "selector": { + ValidateFunc: validate.NoEmptyStrings, + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, }, }, }, @@ -2730,6 +2763,7 @@ func expandApplicationGatewayWafConfig(d *schema.ResourceData) *network.Applicat RequestBodyCheck: utils.Bool(requestBodyCheck), MaxRequestBodySizeInKb: utils.Int32(int32(maxRequestBodySizeInKb)), DisabledRuleGroups: expandApplicationGatewayFirewallDisabledRuleGroup(v["disabled_rule_group"].([]interface{})), + Exclusions: expandApplicationGatewayFirewallExclusion(v["exclusion"].([]interface{})), } } @@ -2771,6 +2805,9 @@ func flattenApplicationGatewayWafConfig(input *network.ApplicationGatewayWebAppl output["max_request_body_size_kb"] = int(*input.MaxRequestBodySizeInKb) } + if input.Exclusions != nil { + output["exclusion"] = flattenApplicationGatewayFirewallExclusion(input.Exclusions) + } results = append(results, output) return results @@ -2828,6 +2865,53 @@ func flattenApplicationGateWayDisabledRuleGroups(input *[]network.ApplicationGat return ruleGroups } +func expandApplicationGatewayFirewallExclusion(d []interface{}) *[]network.ApplicationGatewayFirewallExclusion { + if len(d) == 0 { + return nil + } + + exclusions := make([]network.ApplicationGatewayFirewallExclusion, 0) + for _, exclusion := range d { + exclusionMap := exclusion.(map[string]interface{}) + + matchVariable := exclusionMap["match_variable"].(string) + selectorMatchOperator := exclusionMap["selector_match_operator"].(string) + selector := exclusionMap["selector"].(string) + + exclusionList := network.ApplicationGatewayFirewallExclusion{ + MatchVariable: utils.String(matchVariable), + SelectorMatchOperator: utils.String(selectorMatchOperator), + Selector: utils.String(selector), + } + + exclusions = append(exclusions, exclusionList) + } + + return &exclusions +} + +func flattenApplicationGatewayFirewallExclusion(input *[]network.ApplicationGatewayFirewallExclusion) []interface{} { + exclusionLists := make([]interface{}, 0) + for _, exclusionList := range *input { + exclusionListOutput := map[string]interface{}{} + + if exclusionList.MatchVariable != nil { + exclusionListOutput["match_variable"] = *exclusionList.MatchVariable + } + + if exclusionList.SelectorMatchOperator != nil { + exclusionListOutput["selector_match_operator"] = *exclusionList.SelectorMatchOperator + } + + if exclusionList.Selector != nil { + exclusionListOutput["selector"] = *exclusionList.Selector + } + exclusionLists = append(exclusionLists, exclusionListOutput) + + } + return exclusionLists +} + func expandApplicationGatewayCustomErrorConfigurations(vs []interface{}) *[]network.ApplicationGatewayCustomError { results := make([]network.ApplicationGatewayCustomError, 0) diff --git a/azurerm/resource_arm_application_gateway_test.go b/azurerm/resource_arm_application_gateway_test.go index 4b08b3f79e1e..3f62b1c5fbcf 100644 --- a/azurerm/resource_arm_application_gateway_test.go +++ b/azurerm/resource_arm_application_gateway_test.go @@ -662,6 +662,69 @@ func TestAccAzureRMApplicationGateway_webApplicationFirewall_disabledRuleGroups( }) } +func TestAccAzureRMApplicationGateway_webApplicationFirewall_exclusions(t *testing.T) { + resourceName := "azurerm_application_gateway.test" + ri := tf.AccRandTimeInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMApplicationGatewayDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApplicationGateway_webApplicationFirewall_exclusions_many(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApplicationGatewayExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "sku.0.name", "WAF_v2"), + resource.TestCheckResourceAttr(resourceName, "sku.0.tier", "WAF_v2"), + resource.TestCheckResourceAttr(resourceName, "sku.0.capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.firewall_mode", "Detection"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.rule_set_type", "OWASP"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.rule_set_version", "3.0"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.request_body_check", "true"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.max_request_body_size_kb", "128"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.file_upload_limit_mb", "100"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.match_variable", "RequestArgNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.selector_match_operator", "Equals"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.selector", "displayNameHtml"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.1.match_variable", "RequestCookieNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.1.selector_match_operator", "EndsWith"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.1.selector", "username"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.2.match_variable", "RequestHeaderNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.2.selector_match_operator", "StartsWith"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.2.selector", "ORIGIN"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.3.match_variable", "RequestHeaderNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.3.selector_match_operator", "Contains"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.3.selector", "ORIGIN"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.4.match_variable", "RequestHeaderNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.4.selector_match_operator", ""), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.4.selector", ""), + ), + }, + { + Config: testAccAzureRMApplicationGateway_webApplicationFirewall_exclusions_one(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApplicationGatewayExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "sku.0.name", "WAF_v2"), + resource.TestCheckResourceAttr(resourceName, "sku.0.tier", "WAF_v2"), + resource.TestCheckResourceAttr(resourceName, "sku.0.capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.firewall_mode", "Detection"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.rule_set_type", "OWASP"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.rule_set_version", "3.0"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.request_body_check", "true"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.max_request_body_size_kb", "128"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.file_upload_limit_mb", "100"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.match_variable", "RequestArgNames"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.selector_match_operator", "Equals"), + resource.TestCheckResourceAttr(resourceName, "waf_configuration.0.exclusion.0.selector", "displayNameHtml"), + ), + }, + }, + }) +} + func testCheckAzureRMApplicationGatewayExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -2572,6 +2635,219 @@ resource "azurerm_application_gateway" "test" { `, template, rInt, rInt) } +func testAccAzureRMApplicationGateway_webApplicationFirewall_exclusions_many(rInt int, location string) string { + template := testAccAzureRMApplicationGateway_template(rInt, location) + return fmt.Sprintf(` +%s + +# since these variables are re-used - a locals block makes this more maintainable +locals { + backend_address_pool_name = "${azurerm_virtual_network.test.name}-beap" + frontend_port_name = "${azurerm_virtual_network.test.name}-feport" + frontend_ip_configuration_name = "${azurerm_virtual_network.test.name}-feip" + http_setting_name = "${azurerm_virtual_network.test.name}-be-htst" + listener_name = "${azurerm_virtual_network.test.name}-httplstn" + request_routing_rule_name = "${azurerm_virtual_network.test.name}-rqrt" +} + + +resource "azurerm_public_ip" "test_standard" { + name = "acctest-pubip-%d-standard" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku = "Standard" + allocation_method = "Static" +} + +resource "azurerm_application_gateway" "test" { + name = "acctestag-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + + sku { + name = "WAF_v2" + tier = "WAF_v2" + capacity = 1 + } + + waf_configuration { + enabled = true + firewall_mode = "Detection" + rule_set_type = "OWASP" + rule_set_version = "3.0" + request_body_check = true + max_request_body_size_kb = 128 + file_upload_limit_mb = 100 + + exclusion { + match_variable = "RequestArgNames" + selector_match_operator = "Equals" + selector = "displayNameHtml" + } + + exclusion { + match_variable = "RequestCookieNames" + selector_match_operator = "EndsWith" + selector = "username" + } + + exclusion { + match_variable = "RequestHeaderNames" + selector_match_operator = "StartsWith" + selector = "ORIGIN" + } + + exclusion { + match_variable = "RequestHeaderNames" + selector_match_operator = "Contains" + selector = "ORIGIN" + } + + exclusion { + match_variable = "RequestHeaderNames" + } + } + + gateway_ip_configuration { + name = "my-gateway-ip-configuration" + subnet_id = "${azurerm_subnet.test.id}" + } + + frontend_port { + name = "${local.frontend_port_name}" + port = 80 + } + + frontend_ip_configuration { + name = "${local.frontend_ip_configuration_name}" + public_ip_address_id = "${azurerm_public_ip.test_standard.id}" + } + + backend_address_pool { + name = "${local.backend_address_pool_name}" + } + + backend_http_settings { + name = "${local.http_setting_name}" + cookie_based_affinity = "Disabled" + port = 80 + protocol = "Http" + request_timeout = 1 + } + + http_listener { + name = "${local.listener_name}" + frontend_ip_configuration_name = "${local.frontend_ip_configuration_name}" + frontend_port_name = "${local.frontend_port_name}" + protocol = "Http" + } + + request_routing_rule { + name = "${local.request_routing_rule_name}" + rule_type = "Basic" + http_listener_name = "${local.listener_name}" + backend_address_pool_name = "${local.backend_address_pool_name}" + backend_http_settings_name = "${local.http_setting_name}" + } +} +`, template, rInt, rInt) +} +func testAccAzureRMApplicationGateway_webApplicationFirewall_exclusions_one(rInt int, location string) string { + template := testAccAzureRMApplicationGateway_template(rInt, location) + return fmt.Sprintf(` +%s + +# since these variables are re-used - a locals block makes this more maintainable +locals { + backend_address_pool_name = "${azurerm_virtual_network.test.name}-beap" + frontend_port_name = "${azurerm_virtual_network.test.name}-feport" + frontend_ip_configuration_name = "${azurerm_virtual_network.test.name}-feip" + http_setting_name = "${azurerm_virtual_network.test.name}-be-htst" + listener_name = "${azurerm_virtual_network.test.name}-httplstn" + request_routing_rule_name = "${azurerm_virtual_network.test.name}-rqrt" +} + +resource "azurerm_public_ip" "test_standard" { + name = "acctest-pubip-%d-standard" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku = "Standard" + allocation_method = "Static" +} + +resource "azurerm_application_gateway" "test" { + name = "acctestag-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + + sku { + name = "WAF_v2" + tier = "WAF_v2" + capacity = 1 + } + + waf_configuration { + enabled = true + firewall_mode = "Detection" + rule_set_type = "OWASP" + rule_set_version = "3.0" + request_body_check = true + max_request_body_size_kb = 128 + file_upload_limit_mb = 100 + + exclusion { + match_variable = "RequestArgNames" + selector_match_operator = "Equals" + selector = "displayNameHtml" + } + + } + + gateway_ip_configuration { + name = "my-gateway-ip-configuration" + subnet_id = "${azurerm_subnet.test.id}" + } + + frontend_port { + name = "${local.frontend_port_name}" + port = 80 + } + + frontend_ip_configuration { + name = "${local.frontend_ip_configuration_name}" + public_ip_address_id = "${azurerm_public_ip.test_standard.id}" + } + + backend_address_pool { + name = "${local.backend_address_pool_name}" + } + + backend_http_settings { + name = "${local.http_setting_name}" + cookie_based_affinity = "Disabled" + port = 80 + protocol = "Http" + request_timeout = 1 + } + + http_listener { + name = "${local.listener_name}" + frontend_ip_configuration_name = "${local.frontend_ip_configuration_name}" + frontend_port_name = "${local.frontend_port_name}" + protocol = "Http" + } + + request_routing_rule { + name = "${local.request_routing_rule_name}" + rule_type = "Basic" + http_listener_name = "${local.listener_name}" + backend_address_pool_name = "${local.backend_address_pool_name}" + backend_http_settings_name = "${local.http_setting_name}" + } +} +`, template, rInt, rInt) +} + func testAccAzureRMApplicationGateway_template(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { diff --git a/website/docs/r/application_gateway.html.markdown b/website/docs/r/application_gateway.html.markdown index 38ce8f2af17a..93ea15f20050 100644 --- a/website/docs/r/application_gateway.html.markdown +++ b/website/docs/r/application_gateway.html.markdown @@ -401,6 +401,8 @@ A `waf_configuration` block supports the following: * `max_request_body_size_kb` - (Optional) The Maximum Request Body Size in KB. Accepted values are in the range `1`KB to `128`KB. Defaults to `128`KB. +* `exclusion` - (Optional) one or more `exclusion` blocks as defined below. + --- A `disabled_rule_group` block supports the following: @@ -411,6 +413,16 @@ A `disabled_rule_group` block supports the following: --- +A `exclusion` block supports the following: + +* `match_variable` - (Required) Match variable of the exclusion rule to exclude header, cookie or GET arguments. Possible values are `RequestHeaderNames`, `RequestArgNames` and `RequestCookieNames` + +* `selector_match_operator` - (Optional) Operator which will be used to search in the variable content. Possible values are `Equals`, `StartsWith`, `EndsWith`, `Contains`. If empty will exclude all traffic on this `match_variable` + +* `selector` - (Optional) String value which will be used for the filter operation. If empty will exclude all traffic on this `match_variable` + +--- + A `custom_error_configuration` block supports the following: * `status_code` - (Required) Status code of the application gateway customer error. Possible values are `HttpStatus403` and `HttpStatus502`