From f2b76e4a1b25812a58d7053eadb8383fecc98511 Mon Sep 17 00:00:00 2001 From: Atsushi Ishibashi Date: Wed, 31 Jan 2018 00:11:38 +0900 Subject: [PATCH] New Resource: aws_guardduty_threatintelset --- aws/provider.go | 1 + aws/resource_aws_guardduty_test.go | 4 + aws/resource_aws_guardduty_threatintelset.go | 211 ++++++++++++++++++ ...ource_aws_guardduty_threatintelset_test.go | 163 ++++++++++++++ aws/validators.go | 19 ++ aws/validators_test.go | 21 ++ website/aws.erb | 4 + .../r/guardduty_threatintelset.html.markdown | 63 ++++++ 8 files changed, 486 insertions(+) create mode 100644 aws/resource_aws_guardduty_threatintelset.go create mode 100644 aws/resource_aws_guardduty_threatintelset_test.go create mode 100644 website/docs/r/guardduty_threatintelset.html.markdown diff --git a/aws/provider.go b/aws/provider.go index 792cc9da789..4291b167d20 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -364,6 +364,7 @@ func Provider() terraform.ResourceProvider { "aws_guardduty_detector": resourceAwsGuardDutyDetector(), "aws_guardduty_ipset": resourceAwsGuardDutyIpset(), "aws_guardduty_member": resourceAwsGuardDutyMember(), + "aws_guardduty_threatintelset": resourceAwsGuardDutyThreatintelset(), "aws_iam_access_key": resourceAwsIamAccessKey(), "aws_iam_account_alias": resourceAwsIamAccountAlias(), "aws_iam_account_password_policy": resourceAwsIamAccountPasswordPolicy(), diff --git a/aws/resource_aws_guardduty_test.go b/aws/resource_aws_guardduty_test.go index 377c8fe4c0d..992beda06d5 100644 --- a/aws/resource_aws_guardduty_test.go +++ b/aws/resource_aws_guardduty_test.go @@ -14,6 +14,10 @@ func TestAccAWSGuardDuty(t *testing.T) { "basic": testAccAwsGuardDutyIpset_basic, "import": testAccAwsGuardDutyIpset_import, }, + "ThreatIntelSet": { + "basic": testAccAwsGuardDutyThreatintelset_basic, + "import": testAccAwsGuardDutyThreatintelset_import, + }, "Member": { "basic": testAccAwsGuardDutyMember_basic, "import": testAccAwsGuardDutyMember_import, diff --git a/aws/resource_aws_guardduty_threatintelset.go b/aws/resource_aws_guardduty_threatintelset.go new file mode 100644 index 00000000000..e559898068c --- /dev/null +++ b/aws/resource_aws_guardduty_threatintelset.go @@ -0,0 +1,211 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsGuardDutyThreatintelset() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGuardDutyThreatintelsetCreate, + Read: resourceAwsGuardDutyThreatintelsetRead, + Update: resourceAwsGuardDutyThreatintelsetUpdate, + Delete: resourceAwsGuardDutyThreatintelsetDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "detector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "format": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateGuardDutyThreatIntelSetFormat, + }, + "location": { + Type: schema.TypeString, + Required: true, + }, + "activate": { + Type: schema.TypeBool, + Required: true, + }, + }, + } +} + +func resourceAwsGuardDutyThreatintelsetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + detectorID := d.Get("detector_id").(string) + input := &guardduty.CreateThreatIntelSetInput{ + DetectorId: aws.String(detectorID), + Name: aws.String(d.Get("name").(string)), + Format: aws.String(d.Get("format").(string)), + Location: aws.String(d.Get("location").(string)), + Activate: aws.Bool(d.Get("activate").(bool)), + } + + resp, err := conn.CreateThreatIntelSet(input) + if err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{guardduty.ThreatIntelSetStatusActivating, guardduty.ThreatIntelSetStatusDeactivating}, + Target: []string{guardduty.ThreatIntelSetStatusActive, guardduty.ThreatIntelSetStatusInactive}, + Refresh: guardDutyThreatintelsetRefreshStatusFunc(conn, *resp.ThreatIntelSetId, detectorID), + Timeout: 5 * time.Minute, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("[WARN] Error waiting for GuardDuty ThreatIntelSet status to be \"%s\" or \"%s\": %s", + guardduty.ThreatIntelSetStatusActive, guardduty.ThreatIntelSetStatusInactive, err) + } + + d.SetId(fmt.Sprintf("%s:%s", detectorID, *resp.ThreatIntelSetId)) + return resourceAwsGuardDutyThreatintelsetRead(d, meta) +} + +func resourceAwsGuardDutyThreatintelsetRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + threatIntelSetId, detectorId, err := decodeGuardDutyThreatintelsetID(d.Id()) + if err != nil { + return err + } + input := &guardduty.GetThreatIntelSetInput{ + DetectorId: aws.String(detectorId), + ThreatIntelSetId: aws.String(threatIntelSetId), + } + + resp, err := conn.GetThreatIntelSet(input) + if err != nil { + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "The request is rejected because the input detectorId is not owned by the current account.") { + log.Printf("[WARN] GuardDuty ThreatIntelSet %q not found, removing from state", threatIntelSetId) + d.SetId("") + return nil + } + return err + } + + d.Set("detector_id", detectorId) + d.Set("format", resp.Format) + d.Set("location", resp.Location) + d.Set("name", resp.Name) + d.Set("activate", *resp.Status == guardduty.ThreatIntelSetStatusActive) + return nil +} + +func resourceAwsGuardDutyThreatintelsetUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + threatIntelSetID, detectorId, err := decodeGuardDutyThreatintelsetID(d.Id()) + if err != nil { + return err + } + input := &guardduty.UpdateThreatIntelSetInput{ + DetectorId: aws.String(detectorId), + ThreatIntelSetId: aws.String(threatIntelSetID), + } + + if d.HasChange("name") { + input.Name = aws.String(d.Get("name").(string)) + } + if d.HasChange("location") { + input.Location = aws.String(d.Get("location").(string)) + } + if d.HasChange("activate") { + input.Activate = aws.Bool(d.Get("activate").(bool)) + } + + _, err = conn.UpdateThreatIntelSet(input) + if err != nil { + return err + } + + return resourceAwsGuardDutyThreatintelsetRead(d, meta) +} + +func resourceAwsGuardDutyThreatintelsetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + threatIntelSetID, detectorId, err := decodeGuardDutyThreatintelsetID(d.Id()) + if err != nil { + return err + } + input := &guardduty.DeleteThreatIntelSetInput{ + DetectorId: aws.String(detectorId), + ThreatIntelSetId: aws.String(threatIntelSetID), + } + + _, err = conn.DeleteThreatIntelSet(input) + if err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ + guardduty.ThreatIntelSetStatusActive, + guardduty.ThreatIntelSetStatusActivating, + guardduty.ThreatIntelSetStatusInactive, + guardduty.ThreatIntelSetStatusDeactivating, + guardduty.ThreatIntelSetStatusDeletePending, + }, + Target: []string{guardduty.ThreatIntelSetStatusDeleted}, + Refresh: guardDutyThreatintelsetRefreshStatusFunc(conn, threatIntelSetID, detectorId), + Timeout: 5 * time.Minute, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("[WARN] Error waiting for GuardDuty ThreatIntelSet status to be \"%s\": %s", guardduty.ThreatIntelSetStatusDeleted, err) + } + + return nil +} + +func guardDutyThreatintelsetRefreshStatusFunc(conn *guardduty.GuardDuty, threatIntelSetID, detectorID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &guardduty.GetThreatIntelSetInput{ + DetectorId: aws.String(detectorID), + ThreatIntelSetId: aws.String(threatIntelSetID), + } + resp, err := conn.GetThreatIntelSet(input) + if err != nil { + return nil, "failed", err + } + return resp, *resp.Status, nil + } +} + +func decodeGuardDutyThreatintelsetID(id string) (threatIntelSetID, detectorID string, err error) { + parts := strings.Split(id, ":") + if len(parts) != 2 { + err = fmt.Errorf("GuardDuty ThreatIntelSet ID must be of the form :, was provided: %s", id) + return + } + threatIntelSetID = parts[1] + detectorID = parts[0] + return +} diff --git a/aws/resource_aws_guardduty_threatintelset_test.go b/aws/resource_aws_guardduty_threatintelset_test.go new file mode 100644 index 00000000000..1ba1652c0ce --- /dev/null +++ b/aws/resource_aws_guardduty_threatintelset_test.go @@ -0,0 +1,163 @@ +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/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func testAccAwsGuardDutyThreatintelset_basic(t *testing.T) { + bucketName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + keyName1 := fmt.Sprintf("tf-%s", acctest.RandString(5)) + keyName2 := fmt.Sprintf("tf-%s", acctest.RandString(5)) + threatintelsetName1 := fmt.Sprintf("tf-%s", acctest.RandString(5)) + threatintelsetName2 := fmt.Sprintf("tf-%s", acctest.RandString(5)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyThreatintelsetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyThreatintelsetConfig_basic(bucketName, keyName1, threatintelsetName1, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyThreatintelsetExists("aws_guardduty_threatintelset.test"), + resource.TestCheckResourceAttr("aws_guardduty_threatintelset.test", "name", threatintelsetName1), + resource.TestCheckResourceAttr("aws_guardduty_threatintelset.test", "activate", "true"), + resource.TestMatchResourceAttr( + "aws_guardduty_threatintelset.test", "location", regexp.MustCompile(fmt.Sprintf("%s/%s$", bucketName, keyName1)), + ), + ), + }, + { + Config: testAccGuardDutyThreatintelsetConfig_basic(bucketName, keyName2, threatintelsetName2, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyThreatintelsetExists("aws_guardduty_threatintelset.test"), + resource.TestCheckResourceAttr("aws_guardduty_threatintelset.test", "name", threatintelsetName2), + resource.TestCheckResourceAttr("aws_guardduty_threatintelset.test", "activate", "false"), + resource.TestMatchResourceAttr( + "aws_guardduty_threatintelset.test", "location", regexp.MustCompile(fmt.Sprintf("%s/%s$", bucketName, keyName2)), + ), + ), + }, + }, + }) +} + +func testAccAwsGuardDutyThreatintelset_import(t *testing.T) { + resourceName := "aws_guardduty_threatintelset.test" + bucketName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + keyName := fmt.Sprintf("tf-%s", acctest.RandString(5)) + threatintelsetName := fmt.Sprintf("tf-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyThreatintelsetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGuardDutyThreatintelsetConfig_basic(bucketName, keyName, threatintelsetName, true), + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsGuardDutyThreatintelsetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_guardduty_threatintelset" { + continue + } + + threatIntelSetId, detectorId, err := decodeGuardDutyThreatintelsetID(rs.Primary.ID) + if err != nil { + return err + } + input := &guardduty.GetThreatIntelSetInput{ + ThreatIntelSetId: aws.String(threatIntelSetId), + DetectorId: aws.String(detectorId), + } + + resp, err := conn.GetThreatIntelSet(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 + } + + if *resp.Status == guardduty.ThreatIntelSetStatusDeletePending || *resp.Status == guardduty.ThreatIntelSetStatusDeleted { + return nil + } + + return fmt.Errorf("Expected GuardDuty ThreatIntelSet to be destroyed, %s found", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAwsGuardDutyThreatintelsetExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + threatIntelSetId, detectorId, err := decodeGuardDutyThreatintelsetID(rs.Primary.ID) + if err != nil { + return err + } + + input := &guardduty.GetThreatIntelSetInput{ + DetectorId: aws.String(detectorId), + ThreatIntelSetId: aws.String(threatIntelSetId), + } + + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + _, err = conn.GetThreatIntelSet(input) + if err != nil { + return err + } + + return nil + } +} + +func testAccGuardDutyThreatintelsetConfig_basic(bucketName, keyName, threatintelsetName string, activate bool) string { + return fmt.Sprintf(` +%s + +resource "aws_s3_bucket" "test" { + acl = "private" + bucket = "%s" + force_destroy = true +} + +resource "aws_s3_bucket_object" "test" { + acl = "public-read" + content = "10.0.0.0/8\n" + bucket = "${aws_s3_bucket.test.id}" + key = "%s" +} + +resource "aws_guardduty_threatintelset" "test" { + name = "%s" + detector_id = "${aws_guardduty_detector.test.id}" + format = "TXT" + location = "https://s3.amazonaws.com/${aws_s3_bucket_object.test.bucket}/${aws_s3_bucket_object.test.key}" + activate = %t +} +`, testAccGuardDutyDetectorConfig_basic1, bucketName, keyName, threatintelsetName, activate) +} diff --git a/aws/validators.go b/aws/validators.go index 050da955d8c..b5241100823 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -2173,3 +2173,22 @@ func validateGuardDutyIpsetFormat(v interface{}, k string) (ws []string, errors errors = append(errors, fmt.Errorf("expected %s to be one of %v, got %s", k, validType, value)) return } + +func validateGuardDutyThreatIntelSetFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + validType := []string{ + guardduty.ThreatIntelSetFormatTxt, + guardduty.ThreatIntelSetFormatStix, + guardduty.ThreatIntelSetFormatOtxCsv, + guardduty.ThreatIntelSetFormatAlienVault, + guardduty.ThreatIntelSetFormatProofPoint, + guardduty.ThreatIntelSetFormatFireEye, + } + for _, str := range validType { + if value == str { + return + } + } + errors = append(errors, fmt.Errorf("expected %s to be one of %v, got %s", k, validType, value)) + return +} diff --git a/aws/validators_test.go b/aws/validators_test.go index fa0664d0c66..c4e234654c8 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -3063,3 +3063,24 @@ func TestValidateGuardDutyIpsetFormat(t *testing.T) { } } } + +func TestValidateGuardDutyThreatIntelSetFormat(t *testing.T) { + validTypes := []string{"TXT", "STIX", "OTX_CSV", "ALIEN_VAULT", "PROOF_POINT", "FIRE_EYE"} + for _, v := range validTypes { + _, errors := validateGuardDutyThreatIntelSetFormat(v, "") + if len(errors) != 0 { + t.Fatalf("%q should be a valid GuardDuty ThreatIntelSet Format: %q", v, errors) + } + } + + invalidTypes := []string{ + "hoge", + "txt", + } + for _, v := range invalidTypes { + _, errors := validateGuardDutyThreatIntelSetFormat(v, "") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid GuardDuty ThreatIntelSet Format", v) + } + } +} diff --git a/website/aws.erb b/website/aws.erb index 07daa5952ca..ae9d15d7924 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -965,6 +965,10 @@ > aws_guardduty_member + + > + aws_guardduty_threatintelset + diff --git a/website/docs/r/guardduty_threatintelset.html.markdown b/website/docs/r/guardduty_threatintelset.html.markdown new file mode 100644 index 00000000000..0eb09971a24 --- /dev/null +++ b/website/docs/r/guardduty_threatintelset.html.markdown @@ -0,0 +1,63 @@ +--- +layout: aws +page_title: 'AWS: aws_guardduty_threatintelset' +sidebar_current: docs-aws-resource-guardduty-threatintelset +description: Provides a resource to manage a GuardDuty ThreatIntelSet +--- + +# aws_guardduty_threatintelset + +Provides a resource to manage a GuardDuty ThreatIntelSet. + +~> **Note:** Currently in GuardDuty, users from member accounts cannot upload and further manage ThreatIntelSets. ThreatIntelSets that are uploaded by the master account are imposed on GuardDuty functionality in its member accounts. See the [GuardDuty API Documentation](https://docs.aws.amazon.com/guardduty/latest/ug/create-threat-intel-set.html) + +## Example Usage + +```hcl +resource "aws_guardduty_detector" "master" { + enable = true +} + +resource "aws_s3_bucket" "bucket" { + acl = "private" +} + +resource "aws_s3_bucket_object" "MyThreatIntelSet" { + acl = "public-read" + content = "10.0.0.0/8\n" + bucket = "${aws_s3_bucket.bucket.id}" + key = "MyThreatIntelSet" +} + +resource "aws_guardduty_threatintelset" "MyThreatIntelSet" { + activate = true + detector_id = "${aws_guardduty_detector.master.id}" + format = "TXT" + location = "https://s3.amazonaws.com/${aws_s3_bucket_object.MyThreatIntelSet.bucket}/${aws_s3_bucket_object.MyThreatIntelSet.key}" + name = "MyThreatIntelSet" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `activate` - (Required) Specifies whether GuardDuty is to start using the uploaded ThreatIntelSet. +* `detector_id` - (Required) The detector ID of the GuardDuty. +* `format` - (Required) The format of the file that contains the ThreatIntelSet. Valid values: `TXT` | `STIX` | `OTX_CSV` | `ALIEN_VAULT` | `PROOF_POINT` | `FIRE_EYE` +* `location` - (Required) The URI of the file that contains the ThreatIntelSet. +* `name` - (Required) The friendly name to identify the ThreatIntelSet. + +## Attributes Reference + +The following additional attributes are exported: + +* `id` - The ID of the GuardDuty ThreatIntelSet and the detector ID. Format: `:` + +## Import + +GuardDuty ThreatIntelSet can be imported using the the master GuardDuty detector ID and ThreatIntelSetID, e.g. + +``` +$ terraform import aws_guardduty_threatintelset.MyThreatIntelSet 00b00fd5aecc0ab60a708659477e9617:123456789012 +```