Skip to content

Commit

Permalink
New Resource: aws_securityhub_member (#6975)
Browse files Browse the repository at this point in the history
Output from acceptance testing:

```
--- PASS: TestAccAWSSecurityHub (89.60s)
    --- PASS: TestAccAWSSecurityHub/Account (12.62s)
        --- PASS: TestAccAWSSecurityHub/Account/basic (12.62s)
    --- PASS: TestAccAWSSecurityHub/Member (19.02s)
        --- PASS: TestAccAWSSecurityHub/Member/basic (9.65s)
        --- PASS: TestAccAWSSecurityHub/Member/invite (9.37s)
    --- PASS: TestAccAWSSecurityHub/ProductSubscription (20.25s)
        --- PASS: TestAccAWSSecurityHub/ProductSubscription/basic (20.25s)
    --- PASS: TestAccAWSSecurityHub/StandardsSubscription (37.71s)
        --- PASS: TestAccAWSSecurityHub/StandardsSubscription/basic (37.71s)
```
  • Loading branch information
gazoakley authored Mar 17, 2020
1 parent 2475992 commit 6ed264f
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 0 deletions.
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,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(),
Expand Down
158 changes: 158 additions & 0 deletions aws/resource_aws_securityhub_member.go
Original file line number Diff line number Diff line change
@@ -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
}
142 changes: 142 additions & 0 deletions aws/resource_aws_securityhub_member_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 4 additions & 0 deletions aws/resource_aws_securityhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ func TestAccAWSSecurityHub(t *testing.T) {
"Account": {
"basic": testAccAWSSecurityHubAccount_basic,
},
"Member": {
"basic": testAccAWSSecurityHubMember_basic,
"invite": testAccAWSSecurityHubMember_invite,
},
"ProductSubscription": {
"basic": testAccAWSSecurityHubProductSubscription_basic,
},
Expand Down
3 changes: 3 additions & 0 deletions website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,9 @@
<li>
<a href="/docs/providers/aws/r/securityhub_account.html">aws_securityhub_account</a>
</li>
<li>
<a href="/docs/providers/aws/r/securityhub_member.html">aws_securityhub_member</a>
</li>
<li>
<a href="/docs/providers/aws/r/securityhub_product_subscription.html">aws_securityhub_product_subscription</a>
</li>
Expand Down
48 changes: 48 additions & 0 deletions website/docs/r/securityhub_member.markdown
Original file line number Diff line number Diff line change
@@ -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
```

0 comments on commit 6ed264f

Please sign in to comment.