diff --git a/aws/provider.go b/aws/provider.go index accff62cdb8..5207c0293a7 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -721,6 +721,7 @@ func Provider() terraform.ResourceProvider { "aws_default_security_group": resourceAwsDefaultSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_securityhub_account": resourceAwsSecurityHubAccount(), + "aws_securityhub_member": resourceAwsSecurityHubMember(), "aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(), "aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(), "aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(), diff --git a/aws/resource_aws_securityhub_member.go b/aws/resource_aws_securityhub_member.go new file mode 100644 index 00000000000..6eaaec33739 --- /dev/null +++ b/aws/resource_aws_securityhub_member.go @@ -0,0 +1,158 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +const ( + SecurityHubMemberStatusCreated = "Created" + SecurityHubMemberStatusInvited = "Invited" + SecurityHubMemberStatusAssociated = "Associated" + SecurityHubMemberStatusResigned = "Resigned" + SecurityHubMemberStatusRemoved = "Removed" +) + +func resourceAwsSecurityHubMember() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubMemberCreate, + Read: resourceAwsSecurityHubMemberRead, + Delete: resourceAwsSecurityHubMemberDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "invite": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "master_id": { + Type: schema.TypeString, + Computed: true, + }, + "member_status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsSecurityHubMemberCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Creating Security Hub member %s", d.Get("account_id").(string)) + + resp, err := conn.CreateMembers(&securityhub.CreateMembersInput{ + AccountDetails: []*securityhub.AccountDetails{ + { + AccountId: aws.String(d.Get("account_id").(string)), + Email: aws.String(d.Get("email").(string)), + }, + }, + }) + + if err != nil { + return fmt.Errorf("Error creating Security Hub member %s: %s", d.Get("account_id").(string), err) + } + + if len(resp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error creating Security Hub member %s: UnprocessedAccounts is not empty", d.Get("account_id").(string)) + } + + d.SetId(d.Get("account_id").(string)) + + if d.Get("invite").(bool) { + log.Printf("[INFO] Inviting Security Hub member %s", d.Id()) + iresp, err := conn.InviteMembers(&securityhub.InviteMembersInput{ + AccountIds: []*string{aws.String(d.Get("account_id").(string))}, + }) + + if err != nil { + return fmt.Errorf("Error inviting Security Hub member %s: %s", d.Id(), err) + } + + if len(iresp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error inviting Security Hub member %s: UnprocessedAccounts is not empty", d.Id()) + } + } + + return resourceAwsSecurityHubMemberRead(d, meta) +} + +func resourceAwsSecurityHubMemberRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + log.Printf("[DEBUG] Reading Security Hub member %s", d.Id()) + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(d.Id())}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub member (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err + } + + if len(resp.Members) == 0 { + log.Printf("[WARN] Security Hub member (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + member := resp.Members[0] + + d.Set("account_id", member.AccountId) + d.Set("email", member.Email) + d.Set("master_id", member.MasterId) + + status := aws.StringValue(member.MemberStatus) + d.Set("member_status", status) + + invited := status == SecurityHubMemberStatusInvited || status == SecurityHubMemberStatusAssociated || status == SecurityHubMemberStatusResigned + d.Set("invite", invited) + + return nil +} + +func resourceAwsSecurityHubMemberDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + log.Printf("[DEBUG] Deleting Security Hub member: %s", d.Id()) + + resp, err := conn.DeleteMembers(&securityhub.DeleteMembersInput{ + AccountIds: []*string{aws.String(d.Id())}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Security Hub member (%s) not found", d.Id()) + return nil + } + return fmt.Errorf("Error deleting Security Hub member %s: %s", d.Id(), err) + } + + if len(resp.UnprocessedAccounts) > 0 { + return fmt.Errorf("Error deleting Security Hub member %s: UnprocessedAccounts is not empty", d.Get("account_id").(string)) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_member_test.go b/aws/resource_aws_securityhub_member_test.go new file mode 100644 index 00000000000..82f5672ab19 --- /dev/null +++ b/aws/resource_aws_securityhub_member_test.go @@ -0,0 +1,142 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func testAccAWSSecurityHubMember_basic(t *testing.T) { + var member securityhub.Member + resourceName := "aws_securityhub_member.example" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubMemberConfig_basic("111111111111", "example@example.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSSecurityHubMember_invite(t *testing.T) { + var member securityhub.Member + resourceName := "aws_securityhub_member.example" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSecurityHubMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSecurityHubMemberConfig_invite("111111111111", "example@example.com", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSecurityHubMemberExists(resourceName, &member), + resource.TestCheckResourceAttr(resourceName, "member_status", "Invited"), + resource.TestCheckResourceAttr(resourceName, "invite", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSSecurityHubMemberExists(n string, member *securityhub.Member) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if len(resp.Members) == 0 { + return fmt.Errorf("Security Hub member %s not found", rs.Primary.ID) + } + + member = resp.Members[0] + + return nil + } +} + +func testAccCheckAWSSecurityHubMemberDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_member" { + continue + } + + resp, err := conn.GetMembers(&securityhub.GetMembersInput{ + AccountIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + if isAWSErr(err, securityhub.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + + if len(resp.Members) != 0 { + return fmt.Errorf("Security Hub member still exists") + } + + return nil + } + + return nil +} + +func testAccAWSSecurityHubMemberConfig_basic(accountId, email string) string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_member" "example" { + depends_on = ["aws_securityhub_account.example"] + account_id = "%s" + email = "%s" +} +`, accountId, email) +} + +func testAccAWSSecurityHubMemberConfig_invite(accountId, email string, invite bool) string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_member" "example" { + depends_on = ["aws_securityhub_account.example"] + account_id = "%s" + email = "%s" + invite = %t +} +`, accountId, email, invite) +} diff --git a/aws/resource_aws_securityhub_test.go b/aws/resource_aws_securityhub_test.go index a00828248a5..a6151c71392 100644 --- a/aws/resource_aws_securityhub_test.go +++ b/aws/resource_aws_securityhub_test.go @@ -9,6 +9,10 @@ func TestAccAWSSecurityHub(t *testing.T) { "Account": { "basic": testAccAWSSecurityHubAccount_basic, }, + "Member": { + "basic": testAccAWSSecurityHubMember_basic, + "invite": testAccAWSSecurityHubMember_invite, + }, "ProductSubscription": { "basic": testAccAWSSecurityHubProductSubscription_basic, }, diff --git a/website/aws.erb b/website/aws.erb index 685aff5734a..3c9b2c8f25b 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2613,6 +2613,9 @@
  • aws_securityhub_account
  • +
  • + aws_securityhub_member +
  • aws_securityhub_product_subscription
  • diff --git a/website/docs/r/securityhub_member.markdown b/website/docs/r/securityhub_member.markdown new file mode 100644 index 00000000000..c156563a217 --- /dev/null +++ b/website/docs/r/securityhub_member.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "Security Hub" +layout: "aws" +page_title: "AWS: aws_securityhub_member" +description: |- + Provides a Security Hub member resource. +--- + +# Resource: aws_securityhub_member + +Provides a Security Hub member resource. + +## Example Usage + +```hcl +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_member" "example" { + depends_on = ["aws_securityhub_account.example"] + account_id = "123456789012" + email = "example@example.com" + invite = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Required) The ID of the member AWS account. +* `email` - (Required) The email of the member AWS account. +* `invite` - (Optional) Boolean whether to invite the account to Security Hub as a member. Defaults to `false`. + +## Attributes Reference + +The following attributes are exported in addition to the arguments listed above: + +* `id` - The ID of the member AWS account (matches `account_id`). +* `master_id` - The ID of the master Security Hub AWS account. +* `member_status` - The status of the relationship between the member account and its master account. + +## Import + +Security Hub members can be imported using their account ID, e.g. + +``` +$ terraform import aws_securityhub_member.example 123456789012 +```