diff --git a/fastly/block_fastly_waf_configuration_v1_active_rule_test.go b/fastly/block_fastly_waf_configuration_v1_active_rule_test.go index 0188936ed..23aa10331 100644 --- a/fastly/block_fastly_waf_configuration_v1_active_rule_test.go +++ b/fastly/block_fastly_waf_configuration_v1_active_rule_test.go @@ -94,43 +94,7 @@ func TestAccFastlyServiceWAFVersionV1FlattenWAFDeleteByModSecID(t *testing.T) { } } -func TestAccFastlyServiceWAFVersionV1AddWithRules(t *testing.T) { - var service gofastly.ServiceDetail - name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) - - rules := []gofastly.WAFActiveRule{ - { - ModSecID: 2029718, - Status: "log", - Revision: 1, - }, - { - ModSecID: 2037405, - Status: "log", - Revision: 1, - }, - } - wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) - rulesTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules) - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckServiceV1Destroy, - Steps: []resource.TestStep{ - { - Config: testAccFastlyServiceWAFVersionV1(name, wafVer), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceV1Exists(serviceRef, &service), - testAccCheckFastlyServiceWAFVersionV1CheckRules(&service, rules, 1), - ), - }, - }, - }) -} - -func TestAccFastlyServiceWAFVersionV1UpdateRules(t *testing.T) { +func TestAccFastlyServiceWAFVersionV1AddUpdateDeleteRules(t *testing.T) { var service gofastly.ServiceDetail name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) @@ -170,63 +134,10 @@ func TestAccFastlyServiceWAFVersionV1UpdateRules(t *testing.T) { } wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) rulesTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules1) - wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF1) - - rulesTF2 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules2) - wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF2) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckServiceV1Destroy, - Steps: []resource.TestStep{ - { - Config: testAccFastlyServiceWAFVersionV1(name, wafVer1), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceV1Exists(serviceRef, &service), - testAccCheckFastlyServiceWAFVersionV1CheckRules(&service, rules1, 1), - ), - }, - { - Config: testAccFastlyServiceWAFVersionV1(name, wafVer2), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceV1Exists(serviceRef, &service), - testAccCheckFastlyServiceWAFVersionV1CheckRules(&service, rules2, 2), - ), - }, - }, - }) -} - -func TestAccFastlyServiceWAFVersionV1DeleteRules(t *testing.T) { - var service gofastly.ServiceDetail - name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) - - rules1 := []gofastly.WAFActiveRule{ - { - ModSecID: 2029718, - Status: "log", - Revision: 1, - }, - { - ModSecID: 2037405, - Status: "log", - Revision: 1, - }, - } - rules2 := []gofastly.WAFActiveRule{ - { - ModSecID: 2029718, - Status: "block", - Revision: 1, - }, - } - wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) - rulesTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules1) - wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF1) + wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF1, "") rulesTF2 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules2) - wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF2) + wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF2, "") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -251,47 +162,6 @@ func TestAccFastlyServiceWAFVersionV1DeleteRules(t *testing.T) { }) } -func TestAccFastlyServiceWAFVersionV1ImportWithRules(t *testing.T) { - - var service gofastly.ServiceDetail - name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) - - rules := []gofastly.WAFActiveRule{ - { - ModSecID: 2029718, - Status: "log", - Revision: 1, - }, - { - ModSecID: 2037405, - Status: "log", - Revision: 1, - }, - } - - rulesTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules) - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(nil, rulesTF) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckServiceV1Destroy, - Steps: []resource.TestStep{ - { - Config: testAccFastlyServiceWAFVersionV1(name, wafVer), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceV1Exists(serviceRef, &service), - ), - }, - { - ResourceName: "fastly_service_waf_configuration.waf", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - func testAccCheckFastlyServiceWAFVersionV1CheckRules(service *gofastly.ServiceDetail, expected []gofastly.WAFActiveRule, wafVerNo int) resource.TestCheckFunc { return func(s *terraform.State) error { diff --git a/fastly/block_fastly_waf_configuration_v1_exclusion.go b/fastly/block_fastly_waf_configuration_v1_exclusion.go new file mode 100644 index 000000000..ed52b4209 --- /dev/null +++ b/fastly/block_fastly_waf_configuration_v1_exclusion.go @@ -0,0 +1,213 @@ +package fastly + +import ( + "fmt" + "log" + "strconv" + + gofastly "github.com/fastly/go-fastly/v2/fastly" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +var wafRuleExclusion = &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the exclusion.", + }, + "condition": { + Type: schema.TypeString, + Required: true, + Description: "A conditional expression in VCL used to determine if the condition is met.", + }, + "exclusion_type": { + Type: schema.TypeString, + Required: true, + Description: "The type of exclusion.", + ValidateFunc: validateExecutionType(), + }, + "modsec_rule_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "The modsec rule IDs to exclude.", + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + "number": { + Type: schema.TypeInt, + Computed: true, + Description: "A sequential ID assigned to the exclusion.", + }, + }, + }, +} + +func readWAFRuleExclusions(meta interface{}, d *schema.ResourceData, wafVersionNumber int) error { + conn := meta.(*FastlyClient).conn + wafID := d.Get("waf_id").(string) + + resp, e := conn.ListAllWAFRuleExclusions(&gofastly.ListAllWAFRuleExclusionsInput{ + WAFID: wafID, + WAFVersionNumber: wafVersionNumber, + Include: []string{"waf_rules"}, + }) + + if e != nil { + return e + } + + err := d.Set("rule_exclusion", flattenWAFRuleExclusions(resp.Items)) + + if err != nil { + log.Printf("[WARN] Error setting WAF rule exclusions for (%s): %s", d.Id(), err) + } + + return nil +} + +func flattenWAFRuleExclusions(exclusions []*gofastly.WAFRuleExclusion) []map[string]interface{} { + var result []map[string]interface{} + + for _, exclusion := range exclusions { + + m := make(map[string]interface{}) + if exclusion.Name != nil { + m["name"] = *exclusion.Name + } + if exclusion.Number != nil { + m["number"] = *exclusion.Number + } + if exclusion.Condition != nil { + m["condition"] = *exclusion.Condition + } + if exclusion.ExclusionType != nil { + m["exclusion_type"] = *exclusion.ExclusionType + } + + var rules []interface{} + for _, rule := range exclusion.Rules { + rules = append(rules, rule.ModSecID) + } + if len(rules) > 0 { + m["modsec_rule_ids"] = schema.NewSet(schema.HashInt, rules) + } + result = append(result, m) + } + + return result +} + +func updateWAFRuleExclusions(d *schema.ResourceData, meta interface{}, wafID string, wafVersionNumber int) error { + + os, ns := d.GetChange("rule_exclusion") + + if os == nil { + os = new(schema.Set) + } + if ns == nil { + ns = new(schema.Set) + } + + oss := os.(*schema.Set) + nss := ns.(*schema.Set) + + add := nss.Difference(oss).List() + remove := oss.Difference(nss).List() + + var err error + + err = deleteWAFRuleExclusion(remove, meta, wafID, wafVersionNumber) + if err != nil { + return err + } + + err = createWAFRuleExclusion(add, meta, wafID, wafVersionNumber) + if err != nil { + return err + } + + return nil +} + +func deleteWAFRuleExclusion(remove []interface{}, meta interface{}, wafID string, wafVersionNumber int) error { + conn := meta.(*FastlyClient).conn + + for _, aRaw := range remove { + a := aRaw.(map[string]interface{}) + + err := conn.DeleteWAFRuleExclusion(&gofastly.DeleteWAFRuleExclusionInput{ + Number: a["number"].(int), + WAFID: wafID, + WAFVersionNumber: wafVersionNumber, + }) + + if err != nil { + return err + } + } + + return nil +} + +func createWAFRuleExclusion(add []interface{}, meta interface{}, wafID string, wafVersionNumber int) error { + conn := meta.(*FastlyClient).conn + + for _, aRaw := range add { + a := aRaw.(map[string]interface{}) + + var rules []*gofastly.WAFRule + if a["exclusion_type"] == gofastly.WAFRuleExclusionTypeRule { + for _, ruleId := range a["modsec_rule_ids"].(*schema.Set).List() { + rules = append(rules, &gofastly.WAFRule{ + ID: strconv.Itoa(ruleId.(int)), + }) + } + } else { + rules = nil + } + + _, err := conn.CreateWAFRuleExclusion(&gofastly.CreateWAFRuleExclusionInput{ + WAFID: wafID, + WAFVersionNumber: wafVersionNumber, + WAFRuleExclusion: &gofastly.WAFRuleExclusion{ + Name: strToPtr(a["name"].(string)), + ExclusionType: strToPtr(a["exclusion_type"].(string)), + Condition: strToPtr(a["condition"].(string)), + Rules: rules, + }, + }) + + if err != nil { + return err + } + } + return nil +} + +func validateExecutionType() schema.SchemaValidateFunc { + return validation.StringInSlice( + []string{ + gofastly.WAFRuleExclusionTypeRule, + gofastly.WAFRuleExclusionTypeWAF, + }, + false, + ) +} + +func validateWAFRuleExclusion(d *schema.ResourceDiff) error { + for _, i := range d.Get("rule_exclusion").(*schema.Set).List() { + wafRuleExclusion := i.(map[string]interface{}) + + if wafRuleExclusion["exclusion_type"] == gofastly.WAFRuleExclusionTypeWAF && len(wafRuleExclusion["modsec_rule_ids"].(*schema.Set).List()) > 0 { + return fmt.Errorf("must not set \"modsec_rule_ids\" with \"waf\" exclusion type in exclusion \"%s\"", wafRuleExclusion["name"]) + } + if wafRuleExclusion["exclusion_type"] == gofastly.WAFRuleExclusionTypeRule && len(wafRuleExclusion["modsec_rule_ids"].(*schema.Set).List()) == 0 { + return fmt.Errorf("must set \"modsec_rule_ids\" with \"rule\" exclusion type in exclusion \"%s\"", wafRuleExclusion["name"]) + } + } + return nil +} diff --git a/fastly/block_fastly_waf_configuration_v1_exclusion_test.go b/fastly/block_fastly_waf_configuration_v1_exclusion_test.go new file mode 100644 index 000000000..e4132a144 --- /dev/null +++ b/fastly/block_fastly_waf_configuration_v1_exclusion_test.go @@ -0,0 +1,363 @@ +package fastly + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + "testing" + + gofastly "github.com/fastly/go-fastly/v2/fastly" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccFastlyServiceWAFVersionV1FlattenWAFRuleExclusions(t *testing.T) { + cases := []struct { + remote []*gofastly.WAFRuleExclusion + local []map[string]interface{} + }{ + { + remote: []*gofastly.WAFRuleExclusion{ + { + ID: "abc", + Number: intToPtr(1), + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.html\""), + Rules: []*gofastly.WAFRule{ + { + ID: "2029718", + ModSecID: 2029718, + }, + { + ID: "1010070", + ModSecID: 1010070, + }, + }, + }, + { + ID: "def", + Number: intToPtr(2), + Name: strToPtr("index php"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.php\""), + Rules: []*gofastly.WAFRule{ + { + ID: "1010070", + ModSecID: 1010070, + }, + }, + }, + { + ID: "ghi", + Number: intToPtr(3), + Name: strToPtr("index asp"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeWAF), + Condition: strToPtr("req.url.basename == \"index.asp\""), + }, + }, + local: []map[string]interface{}{ + { + "number": 1, + "name": "index page", + "exclusion_type": "rule", + "condition": "req.url.basename == \"index.html\"", + "modsec_rule_ids": schema.NewSet(schema.HashInt, []interface{}{2029718, 1010070}), + }, + { + "number": 2, + "name": "index php", + "exclusion_type": "rule", + "condition": "req.url.basename == \"index.php\"", + "modsec_rule_ids": schema.NewSet(schema.HashInt, []interface{}{1010070}), + }, + { + "number": 3, + "name": "index asp", + "exclusion_type": "waf", + "condition": "req.url.basename == \"index.asp\"", + }, + }, + }, + } + for _, c := range cases { + out := flattenWAFRuleExclusions(c.remote) + local := c.local + assertEqualsSliceOfMaps(t, out, local) + } +} + +func TestAccFastlyServiceWAFVersionV1Validation(t *testing.T) { + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + cases := []struct { + exclusions []gofastly.WAFRuleExclusion + expectedMessage string + }{ + { + exclusions: []gofastly.WAFRuleExclusion{ + { + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeWAF), + Condition: strToPtr("req.url.basename == \"index.html\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2029718, + }, + }, + }, + }, + expectedMessage: "must not set \"modsec_rule_ids\" with \"waf\" exclusion type in exclusion \"index page\"", + }, + { + exclusions: []gofastly.WAFRuleExclusion{ + { + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.html\""), + }, + }, + expectedMessage: "must set \"modsec_rule_ids\" with \"rule\" exclusion type in exclusion \"index page\"", + }, + } + + for _, c := range cases { + + wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) + exclusionsTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(c.exclusions) + + wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "", exclusionsTF1) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFastlyServiceWAFVersionV1(name, wafVer1), + ExpectError: regexp.MustCompile(c.expectedMessage), + }, + }, + }) + + } +} + +func TestAccFastlyServiceWAFVersionV1AddUpdateDeleteExclusions(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + rules := []gofastly.WAFActiveRule{ + { + ModSecID: 21032607, + Status: "log", + Revision: 1, + }, + { + ModSecID: 2029718, + Status: "log", + Revision: 1, + }, + { + ModSecID: 2037405, + Status: "log", + Revision: 1, + }, + } + + exclusions1 := []gofastly.WAFRuleExclusion{ + { + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.html\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2029718, + }, + { + ModSecID: 2037405, + }, + }, + }, + { + Name: strToPtr("index php"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.php\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2037405, + }, + }, + }, + { + Name: strToPtr("index asp"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeWAF), + Condition: strToPtr("req.url.basename == \"index.asp\""), + }, + } + + exclusions2 := []gofastly.WAFRuleExclusion{ + { + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.html\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 21032607, + }, + }, + }, + { + Name: strToPtr("index php"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.php\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2037405, + }, + }, + }, + { + Name: strToPtr("index new page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index-new.html\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2037405, + }, + }, + }, + } + + wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) + rulesTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules) + exclusionsTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions1) + exclusionsTF2 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions2) + + wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF, exclusionsTF1) + wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF, exclusionsTF2) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFastlyServiceWAFVersionV1(name, wafVer1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists(serviceRef, &service), + testAccCheckFastlyServiceWAFVersionV1CheckExclusions(&service, exclusions1, 1), + ), + }, + { + Config: testAccFastlyServiceWAFVersionV1(name, wafVer2), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists(serviceRef, &service), + testAccCheckFastlyServiceWAFVersionV1CheckExclusions(&service, exclusions2, 2), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceWAFVersionV1CheckExclusions(service *gofastly.ServiceDetail, expected []gofastly.WAFRuleExclusion, wafVerNo int) resource.TestCheckFunc { + return func(s *terraform.State) error { + + conn := testAccProvider.Meta().(*FastlyClient).conn + wafResp, err := conn.ListWAFs(&gofastly.ListWAFsInput{ + FilterService: service.ID, + FilterVersion: service.ActiveVersion.Number, + }) + if err != nil { + return fmt.Errorf("[ERR] Error looking up WAF records for (%s), version (%v): %s", service.Name, service.ActiveVersion.Number, err) + } + + if len(wafResp.Items) != 1 { + return fmt.Errorf("[ERR] Expected waf result size (%d), got (%d)", 1, len(wafResp.Items)) + } + + waf := wafResp.Items[0] + exclResp, err := conn.ListAllWAFRuleExclusions(&gofastly.ListAllWAFRuleExclusionsInput{ + WAFID: waf.ID, + WAFVersionNumber: wafVerNo, + Include: []string{"waf_rules"}, + }) + if err != nil { + return fmt.Errorf("[ERR] Error looking up WAF records for (%s), version (%v): %s", service.Name, service.ActiveVersion.Number, err) + } + + actual := exclResp.Items + if len(expected) != len(actual) { + return fmt.Errorf("Error matching rule exclusions slice sizes :\nexpected: %#v\ngot: %#v", len(expected), len(actual)) + } + + sort.Slice(expected, func(i, j int) bool { + return *expected[i].Name < *expected[j].Name + }) + + sort.Slice(actual, func(i, j int) bool { + return *actual[i].Name < *actual[j].Name + }) + + for i, expectedExcl := range expected { + actualExcl := actual[i] + + if *expectedExcl.Name != *actualExcl.Name { + return fmt.Errorf("Error matching Name:\nexpected: %#v\ngot: %#v", *expectedExcl.Name, *actualExcl.Name) + } + + if *expectedExcl.Condition != *actualExcl.Condition { + return fmt.Errorf("Error matching Condition:\nexpected: %#v\ngot: %#v", *expectedExcl.Condition, *actualExcl.Condition) + } + + if *expectedExcl.ExclusionType != *actualExcl.ExclusionType { + return fmt.Errorf("Error matching ExclusionType:\nexpected: %#v\ngot: %#v", *expectedExcl.ExclusionType, *actualExcl.ExclusionType) + } + + if len(expectedExcl.Rules) != len(actualExcl.Rules) { + return fmt.Errorf("Error matching rules slice sizes :\nexpected: %#v\ngot: %#v", len(expectedExcl.Rules), len(actualExcl.Rules)) + } + + sort.Slice(expectedExcl.Rules, func(i, j int) bool { + return expectedExcl.Rules[i].ModSecID < expectedExcl.Rules[j].ModSecID + }) + sort.Slice(actualExcl.Rules, func(i, j int) bool { + return actualExcl.Rules[i].ModSecID < actualExcl.Rules[j].ModSecID + }) + + for k, expectedRule := range expectedExcl.Rules { + actualRule := actualExcl.Rules[k] + if expectedRule.ModSecID != actualRule.ModSecID { + return fmt.Errorf("Error matching Rule ModsecId:\nexpected: %#v\ngot: %#v", expectedRule.ModSecID, actualRule.ModSecID) + } + } + } + + return nil + } +} + +func testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions []gofastly.WAFRuleExclusion) string { + + var result string + for _, excl := range exclusions { + var modsecIds []string + for _, r := range excl.Rules { + modsecIds = append(modsecIds, strconv.Itoa(r.ModSecID)) + } + + rule := fmt.Sprintf(` + rule_exclusion { + name = "%s" + condition = "%s" + exclusion_type = "%s" + modsec_rule_ids = [%s] + }`, *excl.Name, strings.ReplaceAll(*excl.Condition, "\"", "\\\""), *excl.ExclusionType, strings.Join(modsecIds, ",")) + result = result + rule + } + return result +} diff --git a/fastly/fastly_test.go b/fastly/fastly_test.go index 3d6414424..22aae8e2f 100644 --- a/fastly/fastly_test.go +++ b/fastly/fastly_test.go @@ -2,6 +2,9 @@ package fastly import ( "io/ioutil" + "reflect" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) import ( @@ -98,3 +101,28 @@ func TestEscapePercentSign(t *testing.T) { func appendNewLine(s string) string { return s + "\n" } + +// assertEqualsSliceOfMaps compares a slice of maps even if they include schema.Set values +func assertEqualsSliceOfMaps(t *testing.T, actualSlice []map[string]interface{}, expectedSlice []map[string]interface{}) { + for i, actualMap := range actualSlice { + var keysToBeRemoved []string + for key, value := range actualMap { + switch value.(type) { + case *schema.Set: + expected := expectedSlice[i][key] + keysToBeRemoved = append(keysToBeRemoved, key) + if !value.(*schema.Set).Equal(expected) { + t.Errorf("Expected sets %s to be equal: %#v\n got: %#v", key, expected, actualSlice) + } + } + } + for _, key := range keysToBeRemoved { + delete(actualMap, key) + delete(expectedSlice[i], key) + } + } + + if !reflect.DeepEqual(actualSlice, expectedSlice) { + t.Fatalf("Error matching:\nexpected: %#v\n got: %#v", expectedSlice, actualSlice) + } +} diff --git a/fastly/resource_fastly_service_waf_configuration.go b/fastly/resource_fastly_service_waf_configuration.go index 4b78e501e..f59f83803 100644 --- a/fastly/resource_fastly_service_waf_configuration.go +++ b/fastly/resource_fastly_service_waf_configuration.go @@ -20,7 +20,7 @@ func resourceServiceWAFConfigurationV1() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceServiceWAFConfigurationV1Import, }, - + CustomizeDiff: validateWAFConfigurationResource, Schema: map[string]*schema.Schema{ "waf_id": { Type: schema.TypeString, @@ -205,7 +205,8 @@ func resourceServiceWAFConfigurationV1() *schema.Resource { Description: "XSS attack threshold.", ValidateFunc: validation.IntAtLeast(1), }, - "rule": activeRule, + "rule": activeRule, + "rule_exclusion": wafRuleExclusion, }, } } @@ -252,6 +253,12 @@ func resourceServiceWAFConfigurationV1Update(d *schema.ResourceData, meta interf } } + if d.HasChange("rule_exclusion") { + if err := updateWAFRuleExclusions(d, meta, wafID, latestVersion.Number); err != nil { + return err + } + } + err = conn.DeployWAFVersion(&gofastly.DeployWAFVersionInput{ WAFID: wafID, WAFVersionNumber: latestVersion.Number, @@ -292,6 +299,10 @@ func resourceServiceWAFConfigurationV1Read(d *schema.ResourceData, meta interfac return err } + if err := readWAFRuleExclusions(meta, d, latestVersion.Number); err != nil { + return err + } + return nil } @@ -495,3 +506,8 @@ func determineLatestVersion(versions []*gofastly.WAFVersion) (*gofastly.WAFVersi return versions[0], nil } + +func validateWAFConfigurationResource(d *schema.ResourceDiff, meta interface{}) error { + err := validateWAFRuleExclusion(d) + return err +} diff --git a/fastly/resource_fastly_service_waf_configuration_test.go b/fastly/resource_fastly_service_waf_configuration_test.go index d8e175463..35611949a 100644 --- a/fastly/resource_fastly_service_waf_configuration_test.go +++ b/fastly/resource_fastly_service_waf_configuration_test.go @@ -67,7 +67,7 @@ func TestAccFastlyServiceWAFVersionV1Add(t *testing.T) { var service gofastly.ServiceDetail name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "") + wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "", "") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -89,7 +89,7 @@ func TestAccFastlyServiceWAFVersionV1AddExistingService(t *testing.T) { var service gofastly.ServiceDetail name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "") + wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "", "") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -118,10 +118,10 @@ func TestAccFastlyServiceWAFVersionV1Update(t *testing.T) { name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) wafVerInput1 := testAccFastlyServiceWAFVersionV1BuildConfig(20) - wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput1, "") + wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput1, "", "") wafVerInput2 := testAccFastlyServiceWAFVersionV1BuildConfig(22) - wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput2, "") + wafVer2 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput2, "", "") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -150,7 +150,7 @@ func TestAccFastlyServiceWAFVersionV1Delete(t *testing.T) { var service gofastly.ServiceDetail name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20) - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "") + wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "", "") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -184,10 +184,56 @@ func TestAccFastlyServiceWAFVersionV1Import(t *testing.T) { "http_violation_score_threshold": 10, } - wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(extraHCLMap, "") + rules := []gofastly.WAFActiveRule{ + { + ModSecID: 2029718, + Status: "log", + Revision: 1, + }, + { + ModSecID: 2037405, + Status: "log", + Revision: 1, + }, + } + + exclusions := []gofastly.WAFRuleExclusion{ + { + Name: strToPtr("index page"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.html\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2029718, + }, + { + ModSecID: 2037405, + }, + }, + }, + { + Name: strToPtr("index php"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeRule), + Condition: strToPtr("req.url.basename == \"index.php\""), + Rules: []*gofastly.WAFRule{ + { + ModSecID: 2037405, + }, + }, + }, + { + Name: strToPtr("index asp"), + ExclusionType: strToPtr(gofastly.WAFRuleExclusionTypeWAF), + Condition: strToPtr("req.url.basename == \"index.asp\""), + }, + } + + rulesTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules) + exclusionsTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions) + wafVer := testAccFastlyServiceWAFVersionV1ComposeConfiguration(extraHCLMap, rulesTF, exclusionsTF) wafSvcCfg := testAccFastlyServiceWAFVersionV1(name, wafVer) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckServiceV1Destroy, @@ -311,7 +357,7 @@ func testAccFastlyServiceWAFVersionV1GetVersionNumber(versions []*gofastly.WAFVe return gofastly.WAFVersion{}, fmt.Errorf("version number %d not found", number) } -func testAccFastlyServiceWAFVersionV1ComposeConfiguration(m map[string]interface{}, rules string) string { +func testAccFastlyServiceWAFVersionV1ComposeConfiguration(m map[string]interface{}, rules string, exclusions string) string { hcl := ` resource "fastly_service_waf_configuration" "waf" { @@ -331,8 +377,11 @@ func testAccFastlyServiceWAFVersionV1ComposeConfiguration(m map[string]interface `, k, v) } } - return hcl + fmt.Sprintf(`%s - }`, rules) + return hcl + fmt.Sprintf(` + %s + + %s + }`, rules, exclusions) } func testAccFastlyServiceWAFVersionV1(name, extraHCL string) string { diff --git a/go.mod b/go.mod index 11692313b..4e4475056 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect - github.com/fastly/go-fastly/v2 v2.0.0-alpha.1 + github.com/fastly/go-fastly/v2 v2.0.0-alpha.2 github.com/google/go-cmp v0.3.0 github.com/google/jsonapi v0.0.0-20180313013858-2dcc18f43696 // indirect github.com/hashicorp/terraform-plugin-sdk v1.1.0 diff --git a/go.sum b/go.sum index 53959dcf7..adab9be54 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/fastly/go-fastly/v2 v2.0.0-alpha.1 h1:ZlYscHQgWQKaDy5WN92z+qAqhex8x7X62JVOAtHCwMk= -github.com/fastly/go-fastly/v2 v2.0.0-alpha.1/go.mod h1:+gom+YR+9Q5I4biSk/ZjHQGWXxqpRxC3YDVYQcRpZwQ= +github.com/fastly/go-fastly/v2 v2.0.0-alpha.2 h1:bPTiOsujD7AcJ/CoR0Ht4z21yqd0s8DZV09TXqxwit0= +github.com/fastly/go-fastly/v2 v2.0.0-alpha.2/go.mod h1:+gom+YR+9Q5I4biSk/ZjHQGWXxqpRxC3YDVYQcRpZwQ= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/vendor/github.com/fastly/go-fastly/v2/fastly/errors.go b/vendor/github.com/fastly/go-fastly/v2/fastly/errors.go index 1be95f8aa..1bad4e731 100644 --- a/vendor/github.com/fastly/go-fastly/v2/fastly/errors.go +++ b/vendor/github.com/fastly/go-fastly/v2/fastly/errors.go @@ -117,6 +117,14 @@ var ErrMissingWAFVersionID = errors.New("missing required field 'WAFVersionID'") // requires a list of WAF active rules, but it is empty. var ErrMissingWAFActiveRuleList = errors.New("WAF active rules slice is empty") +// ErrMissingWAFRuleExclusionNumber is an error that is returned when an input struct +// requires a "WAFExclusionNumber" key, but one was not set. +var ErrMissingWAFRuleExclusionNumber = errors.New("missing required field 'WAFExclusionNumber'") + +// ErrMissingWAFRuleExclusion is an error that is returned when an input struct +// requires a "WAFRuleExclusion" key, but one was not set. +var ErrMissingWAFRuleExclusion = errors.New("missing required field 'WAFRuleExclusion'") + // ErrMissingOWASPID is an error that is returned was an input struct // requires a "OWASPID" key, but one was not set var ErrMissingOWASPID = errors.New("missing required field 'OWASPID'") diff --git a/vendor/github.com/fastly/go-fastly/v2/fastly/waf_rule_exclusion.go b/vendor/github.com/fastly/go-fastly/v2/fastly/waf_rule_exclusion.go new file mode 100644 index 000000000..0484cca91 --- /dev/null +++ b/vendor/github.com/fastly/go-fastly/v2/fastly/waf_rule_exclusion.go @@ -0,0 +1,304 @@ +package fastly + +import ( + "bytes" + "fmt" + "io" + "reflect" + "strconv" + "strings" + "time" + + "github.com/google/jsonapi" +) + +// WAFRuleExclusionType is used for reflection because JSONAPI wants to know what it's +// decoding into. +var WAFRuleExclusionType = reflect.TypeOf(new(WAFRuleExclusion)) + +const ( + // WAFRuleExclusionTypeRule is the type of WAF rule exclusions that excludes rules from the WAF based on certain conditions. + WAFRuleExclusionTypeRule = "rule" + // WAFRuleExclusionTypeWAF is the type of WAF rule exclusions that excludes WAF based on certain conditions. + WAFRuleExclusionTypeWAF = "waf" +) + +// WAFRuleExclusion is the information about a WAF rule exclusion object. +type WAFRuleExclusion struct { + ID string `jsonapi:"primary,waf_exclusion"` + Name *string `jsonapi:"attr,name"` + ExclusionType *string `jsonapi:"attr,exclusion_type"` + Condition *string `jsonapi:"attr,condition"` + Number *int `jsonapi:"attr,number"` + Rules []*WAFRule `jsonapi:"relation,waf_rules,omitempty"` + CreatedAt *time.Time `jsonapi:"attr,created_at,iso8601,omitempty"` + UpdatedAt *time.Time `jsonapi:"attr,updated_at,iso8601,omitempty"` +} + +// WAFRuleExclusionResponse represents a list of rule exclusions - full response. +type WAFRuleExclusionResponse struct { + Items []*WAFRuleExclusion + Info infoResponse +} + +// ListWAFRuleExclusionsInput used as input for listing a WAF's rule exclusions. +type ListWAFRuleExclusionsInput struct { + // The Web Application Firewall's ID. + WAFID string + // The Web Application Firewall's version number. + WAFVersionNumber int + // Limit results to exclusions with the specified exclusions type. + FilterExclusionType *string + // Limit results to exclusions with the specified exclusion name. + FilterName *string + // Limit results to exclusions that represent the specified ModSecurity modsec_rule_id. + FilterModSedID *string + // Limit the number of returned pages. + PageSize *int + // Request a specific page of exclusions. + PageNumber *int + // Include relationships. Optional. Permitted values: waf_rules. + Include []string +} + +// ListAllWAFRuleExclusionsInput used as input for listing all WAF rule exclusions. +type ListAllWAFRuleExclusionsInput struct { + // The Web Application Firewall's ID. + WAFID string + // The Web Application Firewall's version number. + WAFVersionNumber int + // Limit results to exclusions with the specified exclusions type. + FilterExclusionType *string + // Limit results to exclusions with the specified exclusion name. + FilterName *string + // Limit results to exclusions that represent the specified ModSecurity modsec_rule_id. + FilterModSedID *string + // Include relationships. Optional. Permitted values: waf_rules. + Include []string +} + +// CreateWAFRuleExclusionInput used as input to create a WAF rule exclusion. +type CreateWAFRuleExclusionInput struct { + // The Web Application Firewall's ID. + WAFID string + // The Web Application Firewall's version number. + WAFVersionNumber int + // The Web Application Firewall's exclusion + WAFRuleExclusion *WAFRuleExclusion +} + +// UpdateWAFRuleExclusionInput is used for exclusions updates. +type UpdateWAFRuleExclusionInput struct { + // The Web Application Firewall's ID. + WAFID string + // The Web Application Firewall's version number. + WAFVersionNumber int + // The exclusion number. + Number int + // The WAF rule exclusion + WAFRuleExclusion *WAFRuleExclusion +} + +// DeleteWAFRuleExclusionInput used as input for removing WAF rule exclusions. +type DeleteWAFRuleExclusionInput struct { + // The Web Application Firewall's ID. + WAFID string + // The Web Application Firewall's version number. + WAFVersionNumber int + // The rule exclusion number. + Number int +} + +func (i *ListWAFRuleExclusionsInput) formatFilters() map[string]string { + + include := strings.Join(i.Include, ",") + + result := map[string]string{} + pairings := map[string]interface{}{ + "filter[exclusion_type]": i.FilterExclusionType, + "filter[name]": i.FilterName, + "filter[waf_rules.modsec_rule_id]": i.FilterModSedID, + "page[size]": i.PageSize, + "page[number]": i.PageNumber, + "include": include, + } + + for key, value := range pairings { + switch value := value.(type) { + case string: + if value != "" { + result[key] = value + } + case *string: + if value != nil { + result[key] = *value + } + case *int: + if value != nil { + result[key] = strconv.Itoa(*value) + } + } + } + return result +} + +// ListWAFRuleExclusions returns the list of exclusions for a given WAF ID. +func (c *Client) ListWAFRuleExclusions(i *ListWAFRuleExclusionsInput) (*WAFRuleExclusionResponse, error) { + + if i.WAFID == "" { + return nil, ErrMissingWAFID + } + + if i.WAFVersionNumber == 0 { + return nil, ErrMissingWAFVersionNumber + } + + path := fmt.Sprintf("/waf/firewalls/%s/versions/%d/exclusions", i.WAFID, i.WAFVersionNumber) + resp, err := c.Get(path, &RequestOptions{ + Params: i.formatFilters(), + }) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + tee := io.TeeReader(resp.Body, &buf) + + info, err := getResponseInfo(tee) + if err != nil { + return nil, err + } + + data, err := jsonapi.UnmarshalManyPayload(bytes.NewReader(buf.Bytes()), WAFRuleExclusionType) + if err != nil { + return nil, err + } + + wafExclusions := make([]*WAFRuleExclusion, len(data)) + for i := range data { + typed, ok := data[i].(*WAFRuleExclusion) + if !ok { + return nil, fmt.Errorf("got back a non-WAFRuleExclusion response") + } + wafExclusions[i] = typed + } + return &WAFRuleExclusionResponse{ + Items: wafExclusions, + Info: info, + }, nil +} + +// ListAllWAFRuleExclusions returns the complete list of WAF rule exclusions for a given WAF ID. It iterates through +// all existing pages to ensure all WAF rule exclusions are returned at once. +func (c *Client) ListAllWAFRuleExclusions(i *ListAllWAFRuleExclusionsInput) (*WAFRuleExclusionResponse, error) { + + if i.WAFID == "" { + return nil, ErrMissingWAFID + } + + if i.WAFVersionNumber == 0 { + return nil, ErrMissingWAFVersionNumber + } + + currentPage := 1 + pageSize := WAFPaginationPageSize + result := &WAFRuleExclusionResponse{Items: []*WAFRuleExclusion{}} + for { + r, err := c.ListWAFRuleExclusions(&ListWAFRuleExclusionsInput{ + WAFID: i.WAFID, + WAFVersionNumber: i.WAFVersionNumber, + PageNumber: ¤tPage, + PageSize: &pageSize, + Include: i.Include, + FilterName: i.FilterName, + FilterModSedID: i.FilterModSedID, + FilterExclusionType: i.FilterExclusionType, + }) + if err != nil { + return r, err + } + + currentPage++ + result.Items = append(result.Items, r.Items...) + + if r.Info.Links.Next == "" || len(r.Items) == 0 { + return result, nil + } + } +} + +// CreateWAFRuleExclusion used to create a particular WAF rule exclusion. +func (c *Client) CreateWAFRuleExclusion(i *CreateWAFRuleExclusionInput) (*WAFRuleExclusion, error) { + + if i.WAFID == "" { + return nil, ErrMissingWAFID + } + + if i.WAFVersionNumber == 0 { + return nil, ErrMissingWAFVersionNumber + } + + if i.WAFRuleExclusion == nil { + return nil, ErrMissingWAFRuleExclusion + } + + path := fmt.Sprintf("/waf/firewalls/%s/versions/%d/exclusions", i.WAFID, i.WAFVersionNumber) + resp, err := c.PostJSONAPI(path, i.WAFRuleExclusion, nil) + if err != nil { + return nil, err + } + + var wafExclusion WAFRuleExclusion + if err := jsonapi.UnmarshalPayload(resp.Body, &wafExclusion); err != nil { + return nil, err + } + return &wafExclusion, nil +} + +// UpdateWAFRuleExclusion used to update a particular WAF rule exclusion. +func (c *Client) UpdateWAFRuleExclusion(i *UpdateWAFRuleExclusionInput) (*WAFRuleExclusion, error) { + if i.WAFID == "" { + return nil, ErrMissingWAFID + } + + if i.WAFVersionNumber == 0 { + return nil, ErrMissingWAFVersionNumber + } + + if i.Number == 0 { + return nil, ErrMissingWAFRuleExclusionNumber + } + + if i.WAFRuleExclusion == nil { + return nil, ErrMissingWAFRuleExclusion + } + + path := fmt.Sprintf("/waf/firewalls/%s/versions/%d/exclusions/%d", i.WAFID, i.WAFVersionNumber, i.Number) + resp, err := c.PatchJSONAPI(path, i.WAFRuleExclusion, nil) + if err != nil { + return nil, err + } + + var exc *WAFRuleExclusion + if err := decodeBodyMap(resp.Body, &exc); err != nil { + return nil, err + } + return exc, nil +} + +// DeleteWAFExclusions removes rules from a particular WAF. +func (c *Client) DeleteWAFRuleExclusion(i *DeleteWAFRuleExclusionInput) error { + if i.WAFID == "" { + return ErrMissingWAFID + } + if i.WAFVersionNumber == 0 { + return ErrMissingWAFVersionNumber + } + if i.Number == 0 { + return ErrMissingWAFRuleExclusionNumber + } + + path := fmt.Sprintf("/waf/firewalls/%s/versions/%d/exclusions/%d", i.WAFID, i.WAFVersionNumber, i.Number) + _, err := c.Delete(path, nil) + return err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index af0b8989c..dccb24d87 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -59,7 +59,7 @@ github.com/bgentry/go-netrc/netrc github.com/bgentry/speakeasy # github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew/spew -# github.com/fastly/go-fastly/v2 v2.0.0-alpha.1 +# github.com/fastly/go-fastly/v2 v2.0.0-alpha.2 ## explicit github.com/fastly/go-fastly/v2/fastly # github.com/fatih/color v1.7.0 diff --git a/website/docs/r/service_waf_configuration.html.markdown b/website/docs/r/service_waf_configuration.html.markdown index f6bbf95f0..4324b7c77 100644 --- a/website/docs/r/service_waf_configuration.html.markdown +++ b/website/docs/r/service_waf_configuration.html.markdown @@ -134,6 +134,74 @@ resource "fastly_service_waf_configuration" "waf" { } ``` +Usage with rule exclusions: + +```hcl +resource "fastly_service_v1" "demo" { + name = "demofastly" + + domain { + name = "example.com" + comment = "demo" + } + + backend { + address = "127.0.0.1" + name = "origin1" + port = 80 + } + + condition { + name = "WAF_Prefetch" + type = "PREFETCH" + statement = "req.backend.is_origin" + } + + # This condition will always be false + # adding it to the response object created below + # prevents Fastly from returning a 403 on all of your traffic. + condition { + name = "WAF_always_false" + statement = "false" + type = "REQUEST" + } + + response_object { + name = "WAF_Response" + status = "403" + response = "Forbidden" + content_type = "text/html" + content = "Forbidden" + request_condition = "WAF_always_false" + } + + waf { + prefetch_condition = "WAF_Prefetch" + response_object = "WAF_Response" + } + + force_destroy = true +} + +resource "fastly_service_waf_configuration" "waf" { + waf_id = fastly_service_v1.demo.waf[0].waf_id + http_violation_score_threshold = 100 + + rule { + modsec_rule_id = 2029718 + revision = 1 + status = "log" + } + + rule_exclusion { + name = "index page" + exclusion_type = "rule" + condition = "req.url.basename == \"index.html\"" + modsec_rule_ids = [2029718] + } +} +``` + Usage with rules from data source: ```hcl @@ -520,6 +588,7 @@ The following arguments are supported: * `warning_anomaly_score` - (Optional) Score value to add for warning anomalies. * `xss_score_threshold` - (Optional) XSS attack threshold. * `rule` - (Optional) The Web Application Firewall's active rules. +* `rule_exclusion` - (Optional) The Web Application Firewall's rule exclusions. The `rule` block supports: @@ -528,6 +597,16 @@ The `rule` block supports: * `modsec_rule_id` - (Required) The Web Application Firewall rule's modsecurity ID. * `revision` - (Optional) The Web Application Firewall rule's revision. The latest revision will be used if this is not provided. +The `rule_exclusion` block supports: + +* `name` - (Required) The name of rule exclusion. +* `exclusion_type` - (Required) The type of rule exclusion. Values are `rule` to exclude the specified rule(s), or `waf` to disable the Web Application Firewall. +* `condition` - (Required) A conditional expression in VCL used to determine if the condition is met. +* `modsec_rule_ids` - (Required) Set of modsecurity IDs to be excluded. No rules should be provided when `exclusion_type` is `waf`. The rules need to be configured on the Web Application Firewall to be excluded. + +The `rule_exclusion` block exports: + +* `number` - The numeric ID assigned to the WAF Rule Exclusion. ## Import diff --git a/website_src/docs/r/service_waf_configuration.html.markdown.tmpl b/website_src/docs/r/service_waf_configuration.html.markdown.tmpl index 1a4b7f973..51b8b32f2 100644 --- a/website_src/docs/r/service_waf_configuration.html.markdown.tmpl +++ b/website_src/docs/r/service_waf_configuration.html.markdown.tmpl @@ -134,6 +134,74 @@ resource "fastly_service_waf_configuration" "waf" { } ``` +Usage with rule exclusions: + +```hcl +resource "fastly_service_v1" "demo" { + name = "demofastly" + + domain { + name = "example.com" + comment = "demo" + } + + backend { + address = "127.0.0.1" + name = "origin1" + port = 80 + } + + condition { + name = "WAF_Prefetch" + type = "PREFETCH" + statement = "req.backend.is_origin" + } + + # This condition will always be false + # adding it to the response object created below + # prevents Fastly from returning a 403 on all of your traffic. + condition { + name = "WAF_always_false" + statement = "false" + type = "REQUEST" + } + + response_object { + name = "WAF_Response" + status = "403" + response = "Forbidden" + content_type = "text/html" + content = "Forbidden" + request_condition = "WAF_always_false" + } + + waf { + prefetch_condition = "WAF_Prefetch" + response_object = "WAF_Response" + } + + force_destroy = true +} + +resource "fastly_service_waf_configuration" "waf" { + waf_id = fastly_service_v1.demo.waf[0].waf_id + http_violation_score_threshold = 100 + + rule { + modsec_rule_id = 2029718 + revision = 1 + status = "log" + } + + rule_exclusion { + name = "index page" + exclusion_type = "rule" + condition = "req.url.basename == \"index.html\"" + modsec_rule_ids = [2029718] + } +} +``` + Usage with rules from data source: ```hcl @@ -520,6 +588,7 @@ The following arguments are supported: * `warning_anomaly_score` - (Optional) Score value to add for warning anomalies. * `xss_score_threshold` - (Optional) XSS attack threshold. * `rule` - (Optional) The Web Application Firewall's active rules. +* `rule_exclusion` - (Optional) The Web Application Firewall's rule exclusions. The `rule` block supports: @@ -528,6 +597,16 @@ The `rule` block supports: * `modsec_rule_id` - (Required) The Web Application Firewall rule's modsecurity ID. * `revision` - (Optional) The Web Application Firewall rule's revision. The latest revision will be used if this is not provided. +The `rule_exclusion` block supports: + +* `name` - (Required) The name of rule exclusion. +* `exclusion_type` - (Required) The type of rule exclusion. Values are `rule` to exclude the specified rule(s), or `waf` to disable the Web Application Firewall. +* `condition` - (Required) A conditional expression in VCL used to determine if the condition is met. +* `modsec_rule_ids` - (Required) Set of modsecurity IDs to be excluded. No rules should be provided when `exclusion_type` is `waf`. The rules need to be configured on the Web Application Firewall to be excluded. + +The `rule_exclusion` block exports: + +* `number` - The numeric ID assigned to the WAF Rule Exclusion. ## Import