diff --git a/aws/provider.go b/aws/provider.go index f7eb3105605..a21e986df9e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -635,12 +635,13 @@ func Provider() *schema.Provider { "aws_glue_user_defined_function": resourceAwsGlueUserDefinedFunction(), "aws_glue_workflow": resourceAwsGlueWorkflow(), "aws_guardduty_detector": resourceAwsGuardDutyDetector(), - "aws_guardduty_publishing_destination": resourceAwsGuardDutyPublishingDestination(), + "aws_guardduty_filter": resourceAwsGuardDutyFilter(), "aws_guardduty_invite_accepter": resourceAwsGuardDutyInviteAccepter(), "aws_guardduty_ipset": resourceAwsGuardDutyIpset(), "aws_guardduty_member": resourceAwsGuardDutyMember(), "aws_guardduty_organization_admin_account": resourceAwsGuardDutyOrganizationAdminAccount(), "aws_guardduty_organization_configuration": resourceAwsGuardDutyOrganizationConfiguration(), + "aws_guardduty_publishing_destination": resourceAwsGuardDutyPublishingDestination(), "aws_guardduty_threatintelset": resourceAwsGuardDutyThreatintelset(), "aws_iam_access_key": resourceAwsIamAccessKey(), "aws_iam_account_alias": resourceAwsIamAccountAlias(), diff --git a/aws/resource_aws_guardduty_filter.go b/aws/resource_aws_guardduty_filter.go new file mode 100644 index 00000000000..aba50d68e6f --- /dev/null +++ b/aws/resource_aws_guardduty_filter.go @@ -0,0 +1,427 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +var guardDutyFilterCriterionValidateFunc = validation.Any( + validation.IsRFC3339Time, + validation.StringMatch(regexp.MustCompile(`^\d+$`), "must be an integer value"), +) + +func resourceAwsGuardDutyFilter() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGuardDutyFilterCreate, + Read: resourceAwsGuardDutyFilterRead, + Update: resourceAwsGuardDutyFilterUpdate, + Delete: resourceAwsGuardDutyFilterDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "detector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(3, 64), + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 512), + }, + "tags": tagsSchema(), + "finding_criteria": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "criterion": { + Type: schema.TypeSet, + MinItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field": { + Type: schema.TypeString, + Required: true, + }, + "equals": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "not_equals": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "greater_than": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: guardDutyFilterCriterionValidateFunc, + }, + "greater_than_or_equal": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: guardDutyFilterCriterionValidateFunc, + }, + "less_than": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: guardDutyFilterCriterionValidateFunc, + }, + "less_than_or_equal": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: guardDutyFilterCriterionValidateFunc, + }, + }, + }, + }, + }, + }, + }, + "action": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + guardduty.FilterActionNoop, + guardduty.FilterActionArchive, + }, false), + }, + "rank": { + Type: schema.TypeInt, + Required: true, + }, + }, + } +} + +func resourceAwsGuardDutyFilterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + input := guardduty.CreateFilterInput{ + Action: aws.String(d.Get("action").(string)), + Description: aws.String(d.Get("description").(string)), + DetectorId: aws.String(d.Get("detector_id").(string)), + Name: aws.String(d.Get("name").(string)), + Rank: aws.Int64(int64(d.Get("rank").(int))), + } + + var err error + input.FindingCriteria, err = expandFindingCriteria(d.Get("finding_criteria").([]interface{})) + if err != nil { + return err + } + + if v, ok := d.GetOk("tags"); ok { + tags := v.(map[string]interface{}) + if len(tags) > 0 { + input.Tags = keyvaluetags.New(tags).GuarddutyTags() + } + } + + log.Printf("[DEBUG] Creating GuardDuty Filter: %s", input) + output, err := conn.CreateFilter(&input) + if err != nil { + return fmt.Errorf("error creating GuardDuty Filter: %w", err) + } + + d.SetId(guardDutyFilterCreateID(d.Get("detector_id").(string), aws.StringValue(output.Name))) + + return resourceAwsGuardDutyFilterRead(d, meta) +} + +func resourceAwsGuardDutyFilterRead(d *schema.ResourceData, meta interface{}) error { + var detectorID, name string + var err error + + if _, ok := d.GetOk("detector_id"); !ok { + // If there is no "detector_id" passed, then it's an import. + detectorID, name, err = guardDutyFilterParseID(d.Id()) + if err != nil { + return err + } + } else { + detectorID = d.Get("detector_id").(string) + name = d.Get("name").(string) + } + + conn := meta.(*AWSClient).guarddutyconn + + input := guardduty.GetFilterInput{ + DetectorId: aws.String(detectorID), + FilterName: aws.String(name), + } + + log.Printf("[DEBUG] Reading GuardDuty Filter: %s", input) + filter, err := conn.GetFilter(&input) + + if err != nil { + if tfawserr.ErrMessageContains(err, guardduty.ErrCodeBadRequestException, "The request is rejected since no such resource found.") { + log.Printf("[WARN] GuardDuty detector %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading GuardDuty Filter '%s': %w", name, err) + } + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Service: "guardduty", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("detector/%s/filter/%s", detectorID, name), + }.String() + d.Set("arn", arn) + + err = d.Set("finding_criteria", flattenFindingCriteria(filter.FindingCriteria)) + if err != nil { + return fmt.Errorf("Setting GuardDuty Filter FindingCriteria failed: %w", err) + } + + d.Set("action", filter.Action) + d.Set("description", filter.Description) + d.Set("name", filter.Name) + d.Set("detector_id", detectorID) + d.Set("rank", filter.Rank) + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + d.Set("tags", keyvaluetags.GuarddutyKeyValueTags(filter.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()) + d.SetId(guardDutyFilterCreateID(detectorID, name)) + + return nil +} + +func resourceAwsGuardDutyFilterUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + if d.HasChanges("action", "description", "finding_criteria", "rank") { + input := guardduty.UpdateFilterInput{ + Action: aws.String(d.Get("action").(string)), + Description: aws.String(d.Get("description").(string)), + DetectorId: aws.String(d.Get("detector_id").(string)), + FilterName: aws.String(d.Get("name").(string)), + Rank: aws.Int64(int64(d.Get("rank").(int))), + } + + var err error + input.FindingCriteria, err = expandFindingCriteria(d.Get("finding_criteria").([]interface{})) + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating GuardDuty Filter: %s", input) + + _, err = conn.UpdateFilter(&input) + if err != nil { + return fmt.Errorf("error updating GuardDuty Filter %s: %w", d.Id(), err) + } + } + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.GuarddutyUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating GuardDuty Filter (%s) tags: %s", d.Get("arn").(string), err) + } + } + + return resourceAwsGuardDutyFilterRead(d, meta) +} + +func resourceAwsGuardDutyFilterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + detectorId := d.Get("detector_id").(string) + name := d.Get("name").(string) + + input := guardduty.DeleteFilterInput{ + FilterName: aws.String(name), + DetectorId: aws.String(detectorId), + } + + log.Printf("[DEBUG] Delete GuardDuty Filter: %s", input) + + _, err := conn.DeleteFilter(&input) + if tfawserr.ErrMessageContains(err, guardduty.ErrCodeBadRequestException, "The request is rejected since no such resource found.") { + return nil + } + if err != nil { + return fmt.Errorf("error deleting GuardDuty Filter %s: %w", d.Id(), err) + } + return nil +} + +const guardDutyFilterIDSeparator = ":" + +func guardDutyFilterCreateID(detectorID, filterName string) string { + return detectorID + guardDutyFilterIDSeparator + filterName +} + +func guardDutyFilterParseID(importedId string) (string, string, error) { + parts := strings.Split(importedId, guardDutyFilterIDSeparator) + if len(parts) != 2 { + return "", "", fmt.Errorf("GuardDuty filter ID must be of the form :. Got %q.", importedId) + } + return parts[0], parts[1], nil +} + +func expandFindingCriteria(raw []interface{}) (*guardduty.FindingCriteria, error) { + findingCriteria := raw[0].(map[string]interface{}) + inputFindingCriteria := findingCriteria["criterion"].(*schema.Set).List() + + criteria := map[string]*guardduty.Condition{} + for _, criterion := range inputFindingCriteria { + typedCriterion := criterion.(map[string]interface{}) + field := typedCriterion["field"].(string) + + condition := guardduty.Condition{} + if x, ok := typedCriterion["equals"]; ok { + if v, ok := x.([]interface{}); ok && len(v) > 0 { + foo := make([]*string, len(v)) + for i := range v { + s := v[i].(string) + foo[i] = &s + } + condition.Equals = foo + } + } + if x, ok := typedCriterion["not_equals"]; ok { + if v, ok := x.([]interface{}); ok && len(v) > 0 { + foo := make([]*string, len(v)) + for i := range v { + s := v[i].(string) + foo[i] = &s + } + condition.NotEquals = foo + } + } + if x, ok := typedCriterion["greater_than"]; ok { + if v, ok := x.(string); ok && v != "" { + i, err := expandConditionIntField(field, v) + if err != nil { + return nil, fmt.Errorf("error parsing condition %q for field %q: %w", "greater_than", field, err) + } + condition.GreaterThan = aws.Int64(i) + } + } + if x, ok := typedCriterion["greater_than_or_equal"]; ok { + if v, ok := x.(string); ok && v != "" { + i, err := expandConditionIntField(field, v) + if err != nil { + return nil, fmt.Errorf("error parsing condition %q for field %q: %w", "greater_than_or_equal", field, err) + } + condition.GreaterThanOrEqual = aws.Int64(i) + } + } + if x, ok := typedCriterion["less_than"]; ok { + if v, ok := x.(string); ok && v != "" { + i, err := expandConditionIntField(field, v) + if err != nil { + return nil, fmt.Errorf("error parsing condition %q for field %q: %w", "less_than", field, err) + } + condition.LessThan = aws.Int64(i) + } + } + if x, ok := typedCriterion["less_than_or_equal"]; ok { + if v, ok := x.(string); ok && v != "" { + i, err := expandConditionIntField(field, v) + if err != nil { + return nil, fmt.Errorf("error parsing condition %q for field %q: %w", "less_than_or_equal", field, err) + } + condition.LessThanOrEqual = aws.Int64(i) + } + } + criteria[field] = &condition + } + + return &guardduty.FindingCriteria{Criterion: criteria}, nil +} + +func expandConditionIntField(field, v string) (int64, error) { + if field == "updatedAt" { + date, err := time.Parse(time.RFC3339, v) + if err != nil { + return 0, err + } + return date.UnixNano() / 1000000, nil + } + + i, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, err + } + return i, nil +} + +func flattenFindingCriteria(findingCriteriaRemote *guardduty.FindingCriteria) []interface{} { + var flatCriteria []interface{} + + for field, conditions := range findingCriteriaRemote.Criterion { + criterion := map[string]interface{}{ + "field": field, + } + if len(conditions.Equals) > 0 { + criterion["equals"] = aws.StringValueSlice(conditions.Equals) + } + if len(conditions.NotEquals) > 0 { + criterion["not_equals"] = aws.StringValueSlice(conditions.NotEquals) + } + if v := aws.Int64Value(conditions.GreaterThan); v > 0 { + criterion["greater_than"] = flattenConditionIntField(field, v) + } + if v := aws.Int64Value(conditions.GreaterThanOrEqual); v > 0 { + criterion["greater_than_or_equal"] = flattenConditionIntField(field, v) + } + if v := aws.Int64Value(conditions.LessThan); v > 0 { + criterion["less_than"] = flattenConditionIntField(field, v) + } + if v := aws.Int64Value(conditions.LessThanOrEqual); v > 0 { + criterion["less_than_or_equal"] = flattenConditionIntField(field, v) + } + flatCriteria = append(flatCriteria, criterion) + } + + return []interface{}{ + map[string][]interface{}{ + "criterion": flatCriteria, + }, + } +} + +func flattenConditionIntField(field string, v int64) string { + if field == "updatedAt" { + seconds := v / 1000 + nanoseconds := v % 1000 + date := time.Unix(seconds, nanoseconds).UTC() + return date.Format(time.RFC3339) + } + return strconv.FormatInt(v, 10) +} diff --git a/aws/resource_aws_guardduty_filter_test.go b/aws/resource_aws_guardduty_filter_test.go new file mode 100644 index 00000000000..27b67e1ac62 --- /dev/null +++ b/aws/resource_aws_guardduty_filter_test.go @@ -0,0 +1,395 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfawsresource" +) + +func testAccAwsGuardDutyFilter_basic(t *testing.T) { + var v1, v2 guardduty.GetFilterOutput + resourceName := "aws_guardduty_filter.test" + detectorResourceName := "aws_guardduty_detector.test" + + startDate := "2020-01-01T00:00:00Z" + endDate := "2020-02-01T00:00:00Z" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyFilterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyFilterConfig_full(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "detector_id", detectorResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "name", "test-filter"), + resource.TestCheckResourceAttr(resourceName, "action", "ARCHIVE"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "rank", "1"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "guardduty", regexp.MustCompile("detector/[a-z0-9]{32}/filter/test-filter$")), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.#", "1"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.0.criterion.#", "3"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "finding_criteria.0.criterion.*", map[string]string{ + "field": "region", + "equals.#": "1", + "equals.0": "eu-west-1", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "finding_criteria.0.criterion.*", map[string]string{ + "field": "service.additionalInfo.threatListName", + "not_equals.#": "2", + "not_equals.0": "some-threat", + "not_equals.1": "another-threat", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "finding_criteria.0.criterion.*", map[string]string{ + "field": "updatedAt", + "greater_than_or_equal": startDate, + "less_than": endDate, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccGuardDutyFilterConfigNoop_full(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v2), + resource.TestCheckResourceAttrPair(resourceName, "detector_id", detectorResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "name", "test-filter"), + resource.TestCheckResourceAttr(resourceName, "action", "NOOP"), + resource.TestCheckResourceAttr(resourceName, "description", "This is a NOOP"), + resource.TestCheckResourceAttr(resourceName, "rank", "1"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.#", "1"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.0.criterion.#", "3"), + ), + }, + }, + }) +} + +func testAccAwsGuardDutyFilter_update(t *testing.T) { + var v1, v2 guardduty.GetFilterOutput + resourceName := "aws_guardduty_filter.test" + + startDate := "2020-01-01T00:00:00Z" + endDate := "2020-02-01T00:00:00Z" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyFilterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyFilterConfig_full(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.#", "1"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.0.criterion.#", "3"), + ), + }, + { + Config: testAccGuardDutyFilterConfig_update(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.#", "1"), + resource.TestCheckResourceAttr(resourceName, "finding_criteria.0.criterion.#", "2"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "finding_criteria.0.criterion.*", map[string]string{ + "field": "region", + "equals.#": "1", + "equals.0": "us-west-2", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "finding_criteria.0.criterion.*", map[string]string{ + "field": "service.additionalInfo.threatListName", + "not_equals.#": "2", + "not_equals.0": "some-threat", + "not_equals.1": "yet-another-threat", + }), + ), + }, + }, + }) +} + +func testAccAwsGuardDutyFilter_tags(t *testing.T) { + var v1, v2, v3 guardduty.GetFilterOutput + resourceName := "aws_guardduty_filter.test" + + startDate := "2020-01-01T00:00:00Z" + endDate := "2020-02-01T00:00:00Z" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyFilterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyFilterConfig_multipleTags(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", "test-filter"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "Value"), + ), + }, + { + Config: testAccGuardDutyFilterConfig_updateTags(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "Updated"), + ), + }, + { + Config: testAccGuardDutyFilterConfig_full(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v3), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func testAccAwsGuardDutyFilter_disappears(t *testing.T) { + var v guardduty.GetFilterOutput + resourceName := "aws_guardduty_filter.test" + + startDate := "2020-01-01T00:00:00Z" + endDate := "2020-02-01T00:00:00Z" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyFilterConfig_full(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyFilterExists(resourceName, &v), + testAccCheckResourceDisappears(testAccProvider, resourceAwsGuardDutyFilter(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsGuardDutyFilterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_guardduty_filter" { + continue + } + + detectorID, filterName, err := guardDutyFilterParseID(rs.Primary.ID) + if err != nil { + return err + } + + input := &guardduty.GetFilterInput{ + DetectorId: aws.String(detectorID), + FilterName: aws.String(filterName), + } + + _, err = conn.GetFilter(input) + if err != nil { + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "The request is rejected because the input detectorId is not owned by the current account.") { + return nil + } + return err + } + + return fmt.Errorf("Expected GuardDuty Filter to be destroyed, %s found", rs.Primary.Attributes["filter_name"]) + } + + return nil +} + +func testAccCheckAwsGuardDutyFilterExists(name string, filter *guardduty.GetFilterOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No GuardDuty filter is set") + } + + detectorID, name, err := guardDutyFilterParseID(rs.Primary.ID) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + input := guardduty.GetFilterInput{ + DetectorId: aws.String(detectorID), + FilterName: aws.String(name), + } + filter, err = conn.GetFilter(&input) + if err != nil { + return err + } + + return nil + } +} + +func testAccGuardDutyFilterConfig_full(startDate, endDate string) string { + return fmt.Sprintf(` +resource "aws_guardduty_filter" "test" { + detector_id = aws_guardduty_detector.test.id + name = "test-filter" + action = "ARCHIVE" + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["eu-west-1"] + } + + criterion { + field = "service.additionalInfo.threatListName" + not_equals = ["some-threat", "another-threat"] + } + + criterion { + field = "updatedAt" + greater_than_or_equal = %[1]q + less_than = %[2]q + } + } +} + +resource "aws_guardduty_detector" "test" { + enable = true +} +`, startDate, endDate) +} + +func testAccGuardDutyFilterConfigNoop_full(startDate, endDate string) string { + return fmt.Sprintf(` +resource "aws_guardduty_filter" "test" { + detector_id = aws_guardduty_detector.test.id + name = "test-filter" + action = "NOOP" + description = "This is a NOOP" + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["eu-west-1"] + } + + criterion { + field = "service.additionalInfo.threatListName" + not_equals = ["some-threat", "another-threat"] + } + + criterion { + field = "updatedAt" + greater_than_or_equal = %[1]q + less_than = %[2]q + } + } +} + +resource "aws_guardduty_detector" "test" { + enable = true +} +`, startDate, endDate) +} + +func testAccGuardDutyFilterConfig_multipleTags() string { + return ` +resource "aws_guardduty_filter" "test" { + detector_id = aws_guardduty_detector.test.id + name = "test-filter" + action = "ARCHIVE" + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["us-west-2"] + } + } + + tags = { + Name = "test-filter" + Key = "Value" + } +} + +resource "aws_guardduty_detector" "test" { + enable = true +} +` +} + +func testAccGuardDutyFilterConfig_update() string { + return ` +resource "aws_guardduty_filter" "test" { + detector_id = aws_guardduty_detector.test.id + name = "test-filter" + action = "ARCHIVE" + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["us-west-2"] + } + + criterion { + field = "service.additionalInfo.threatListName" + not_equals = ["some-threat", "yet-another-threat"] + } + } +} + +resource "aws_guardduty_detector" "test" { + enable = true +} +` +} + +func testAccGuardDutyFilterConfig_updateTags() string { + return ` +resource "aws_guardduty_filter" "test" { + detector_id = aws_guardduty_detector.test.id + name = "test-filter" + action = "ARCHIVE" + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["us-west-2"] + } + } + + tags = { + Key = "Updated" + } +} + +resource "aws_guardduty_detector" "test" { + enable = true +} +` +} diff --git a/aws/resource_aws_guardduty_test.go b/aws/resource_aws_guardduty_test.go index a52644524ab..596655cb7aa 100644 --- a/aws/resource_aws_guardduty_test.go +++ b/aws/resource_aws_guardduty_test.go @@ -13,6 +13,12 @@ func TestAccAWSGuardDuty_serial(t *testing.T) { "datasource_basic": testAccAWSGuarddutyDetectorDataSource_basic, "datasource_id": testAccAWSGuarddutyDetectorDataSource_Id, }, + "Filter": { + "basic": testAccAwsGuardDutyFilter_basic, + "update": testAccAwsGuardDutyFilter_update, + "tags": testAccAwsGuardDutyFilter_tags, + "disappears": testAccAwsGuardDutyFilter_disappears, + }, "InviteAccepter": { "basic": testAccAwsGuardDutyInviteAccepter_basic, }, diff --git a/website/docs/r/guardduty_filter.html.markdown b/website/docs/r/guardduty_filter.html.markdown new file mode 100644 index 00000000000..847655911ef --- /dev/null +++ b/website/docs/r/guardduty_filter.html.markdown @@ -0,0 +1,84 @@ +--- +layout: "aws" +subcategory: "GuardDuty" +page_title: "AWS: aws_guardduty_filter" +description: |- + Provides a resource to manage a GuardDuty filter +--- + +# Resource: aws_guardduty_filter + +Provides a resource to manage a GuardDuty filter. + +## Example Usage + +```hcl +resource "aws_guardduty_filter" "MyFilter" { + name = "MyFilter" + action = "ARCHIVE" + detector_id = aws_guardduty_detector.example.id + rank = 1 + + finding_criteria { + criterion { + field = "region" + equals = ["eu-west-1"] + } + + criterion { + field = "service.additionalInfo.threatListName" + not_equals = ["some-threat", "another-threat"] + } + + criterion { + field = "updatedAt" + greater_than = "2020-01-01T00:00:00Z" + less_than = "2020-02-01T00:00:00Z" + } + + criterion { + field = "severity" + greater_than_or_equal = "4" + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `detector_id` - (Required) ID of a GuardDuty detector, attached to your account. +* `name` - (Required) The name of your filter. +* `description` - (Optional) Description of the filter. +* `rank` - (Required) Specifies the position of the filter in the list of current filters. Also specifies the order in which this filter is applied to the findings. +* `action` - (Required) Specifies the action that is to be applied to the findings that match the filter. Can be one of `ARCHIVE` or `NOOP`. +* `tags` (Optional) - The tags that you want to add to the Filter resource. A tag consists of a key and a value. +* `finding_criteria` (Required) - Represents the criteria to be used in the filter for querying findings. Contains one or more `criterion` blocks, documented [below](#criterion). + +### criterion + +The `criterion` block suports the following: + +* `field` - (Required) The name of the field to be evaluated. The full list of field names can be found in [AWS documentation](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_filter-findings.html#filter_criteria). +* `equals` - (Optional) List of string values to be evaluated. +* `not_equals` - (Optional) List of string values to be evaluated. +* `greater_than` - (Optional) A value to be evaluated. Accepts either an integer or a date in [RFC 3339 format](https://tools.ietf.org/html/rfc3339#section-5.8). +* `greater_than_or_equal` - (Optional) A value to be evaluated. Accepts either an integer or a date in [RFC 3339 format](https://tools.ietf.org/html/rfc3339#section-5.8). +* `less_than` - (Optional) A value to be evaluated. Accepts either an integer or a date in [RFC 3339 format](https://tools.ietf.org/html/rfc3339#section-5.8). +* `less_than_or_equal` - (Optional) A value to be evaluated. Accepts either an integer or a date in [RFC 3339 format](https://tools.ietf.org/html/rfc3339#section-5.8). + +## Attributes Reference + +In addition to all arguments above, the following attribute is exported: + +* `arn` - The ARN of the GuardDuty filter. +* `id` - A compound field, consisting of the ID of the GuardDuty detector and the name of the filter. + +## Import + +GuardDuty filters can be imported using the detector ID and filter's name separated by underscore, e.g. + +``` +$ terraform import aws_guardduty_filter.MyFilter 00b00fd5aecc0ab60a708659477e9617_MyFilter +```