From c336db202927d2447331c23045b90731a08ae737 Mon Sep 17 00:00:00 2001 From: Mateus Pimenta <1920261+matpimenta@users.noreply.github.com> Date: Mon, 12 Oct 2020 13:16:26 +0100 Subject: [PATCH] Adding WAF Rule Exclusions to WAF Configuration resource (#328) * Adding WAF Exclusions to WAF Configuration resource During this implementation, I've refactored some of the WAF acceptance tests to reduce overlapping: 1. TestAccFastlyServiceWAFVersionV1ImportWithRules was merged into TestAccFastlyServiceWAFVersionV1Import. This tests was also enhanced to include WAF exclusions 1. TestAccFastlyServiceWAFVersionV1AddWithRules and TestAccFastlyServiceWAFVersionV1DeleteRules was merged into TestAccFastlyServiceWAFVersionV1UpdateRules. The update tests was performing add, update and delete already. TestAccFastlyServiceWAFVersionV1UpdateRules was renamed to TestAccFastlyServiceWAFVersionV1AddUpdateDeleteRules to make it clearer Co-authored-by: Zsolt Balvanyos <5632209+ZsoltBalvanyos@users.noreply.github.com> * Changing WAF Exclusion terminology to WAF Rule Exclusion * Changing go imports to group them by stdlib and external. Pointing go-fastly to v2.0.0-alpha.2. Changing provider to use new struct format (include is now a slice instead of a string). Cleaned up go.sum Co-authored-by: Zsolt Balvanyos <5632209+ZsoltBalvanyos@users.noreply.github.com> --- ...y_waf_configuration_v1_active_rule_test.go | 136 +------ ...k_fastly_waf_configuration_v1_exclusion.go | 213 ++++++++++ ...tly_waf_configuration_v1_exclusion_test.go | 363 ++++++++++++++++++ fastly/fastly_test.go | 28 ++ ...source_fastly_service_waf_configuration.go | 20 +- ...e_fastly_service_waf_configuration_test.go | 69 +++- go.mod | 2 +- go.sum | 4 +- .../fastly/go-fastly/v2/fastly/errors.go | 8 + .../go-fastly/v2/fastly/waf_rule_exclusion.go | 304 +++++++++++++++ vendor/modules.txt | 2 +- .../r/service_waf_configuration.html.markdown | 79 ++++ ...rvice_waf_configuration.html.markdown.tmpl | 79 ++++ 13 files changed, 1158 insertions(+), 149 deletions(-) create mode 100644 fastly/block_fastly_waf_configuration_v1_exclusion.go create mode 100644 fastly/block_fastly_waf_configuration_v1_exclusion_test.go create mode 100644 vendor/github.com/fastly/go-fastly/v2/fastly/waf_rule_exclusion.go 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