Skip to content

Commit

Permalink
Merge pull request #14714 from Tensho/securityhub-standard-controls
Browse files Browse the repository at this point in the history
New resource: aws_securityhub_standards_control
  • Loading branch information
ewbankkit authored Jul 13, 2021
2 parents 4e8dea0 + f92c7af commit 1d391eb
Show file tree
Hide file tree
Showing 13 changed files with 680 additions and 55 deletions.
3 changes: 3 additions & 0 deletions .changelog/14714.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_securityhub_standards_control
```
44 changes: 44 additions & 0 deletions aws/internal/service/securityhub/arn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package securityhub

import (
"fmt"
"strings"

"github.com/aws/aws-sdk-go/aws/arn"
)

const (
ARNSeparator = "/"
ARNService = "securityhub"
)

// StandardsControlARNToStandardsSubscriptionARN converts a security standard control ARN to a subscription ARN.
func StandardsControlARNToStandardsSubscriptionARN(inputARN string) (string, error) {
parsedARN, err := arn.Parse(inputARN)

if err != nil {
return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err)
}

if actual, expected := parsedARN.Service, ARNService; actual != expected {
return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual)
}

inputResourceParts := strings.Split(parsedARN.Resource, ARNSeparator)

if actual, expected := len(inputResourceParts), 3; actual < expected {
return "", fmt.Errorf("expected at least %d resource parts in ARN (%s), got: %d", expected, inputARN, actual)
}

outputResourceParts := append([]string{"subscription"}, inputResourceParts[1:len(inputResourceParts)-1]...)

outputARN := arn.ARN{
Partition: parsedARN.Partition,
Service: parsedARN.Service,
Region: parsedARN.Region,
AccountID: parsedARN.AccountID,
Resource: strings.Join(outputResourceParts, ARNSeparator),
}.String()

return outputARN, nil
}
65 changes: 65 additions & 0 deletions aws/internal/service/securityhub/arn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package securityhub_test

import (
"regexp"
"testing"

tfsecurityhub "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub"
)

func TestStandardsControlARNToStandardsSubscriptionARN(t *testing.T) {
testCases := []struct {
TestName string
InputARN string
ExpectedError *regexp.Regexp
ExpectedARN string
}{
{
TestName: "empty ARN",
InputARN: "",
ExpectedError: regexp.MustCompile(`error parsing ARN`),
},
{
TestName: "unparsable ARN",
InputARN: "test",
ExpectedError: regexp.MustCompile(`error parsing ARN`),
},
{
TestName: "invalid ARN service",
InputARN: "arn:aws:ec2:us-west-2:1234567890:control/cis-aws-foundations-benchmark/v/1.2.0/1.1",
ExpectedError: regexp.MustCompile(`expected service securityhub`),
},
{
TestName: "invalid ARN resource parts",
InputARN: "arn:aws:securityhub:us-west-2:1234567890:control/cis-aws-foundations-benchmark",
ExpectedError: regexp.MustCompile(`expected at least 3 resource parts`),
},
{
TestName: "valid ARN",
InputARN: "arn:aws:securityhub:us-west-2:1234567890:control/cis-aws-foundations-benchmark/v/1.2.0/1.1",
ExpectedARN: "arn:aws:securityhub:us-west-2:1234567890:subscription/cis-aws-foundations-benchmark/v/1.2.0",
},
}

for _, testCase := range testCases {
t.Run(testCase.TestName, func(t *testing.T) {
got, err := tfsecurityhub.StandardsControlARNToStandardsSubscriptionARN(testCase.InputARN)

if err == nil && testCase.ExpectedError != nil {
t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String())
}

if err != nil && testCase.ExpectedError == nil {
t.Fatalf("got unexpected error: %s", err)
}

if err != nil && !testCase.ExpectedError.MatchString(err.Error()) {
t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err)
}

if got != testCase.ExpectedARN {
t.Errorf("got %s, expected %s", got, testCase.ExpectedARN)
}
})
}
}
81 changes: 81 additions & 0 deletions aws/internal/service/securityhub/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/securityhub"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func AdminAccount(conn *securityhub.SecurityHub, adminAccountID string) (*securityhub.AdminAccount, error) {
Expand Down Expand Up @@ -51,3 +53,82 @@ func Insight(ctx context.Context, conn *securityhub.SecurityHub, arn string) (*s

return output.Insights[0], nil
}

func StandardsControlByStandardsSubscriptionARNAndStandardsControlARN(ctx context.Context, conn *securityhub.SecurityHub, standardsSubscriptionARN, standardsControlARN string) (*securityhub.StandardsControl, error) {
input := &securityhub.DescribeStandardsControlsInput{
StandardsSubscriptionArn: aws.String(standardsSubscriptionARN),
}
var output *securityhub.StandardsControl

err := conn.DescribeStandardsControlsPagesWithContext(ctx, input, func(page *securityhub.DescribeStandardsControlsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, control := range page.Controls {
if aws.StringValue(control.StandardsControlArn) == standardsControlARN {
output = control

return false
}
}

return !lastPage
})

if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output, nil
}

func StandardsSubscriptionByARN(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) {
input := &securityhub.GetEnabledStandardsInput{
StandardsSubscriptionArns: aws.StringSlice([]string{arn}),
}

output, err := conn.GetEnabledStandards(input)

if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if output == nil || len(output.StandardsSubscriptions) == 0 || output.StandardsSubscriptions[0] == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

// TODO Check for multiple results.
// TODO https://github.com/hashicorp/terraform-provider-aws/pull/17613.

subscription := output.StandardsSubscriptions[0]

if status := aws.StringValue(subscription.StandardsStatus); status == securityhub.StandardsStatusFailed {
return nil, &resource.NotFoundError{
Message: status,
LastRequest: input,
}
}

return subscription, nil
}
21 changes: 21 additions & 0 deletions aws/internal/service/securityhub/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/aws/aws-sdk-go/service/securityhub"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

const (
Expand All @@ -13,6 +14,8 @@ const (

// AdminStatus Unknown
AdminStatusUnknown = "Unknown"

StandardsStatusNotFound = "NotFound"
)

// AdminAccountAdminStatus fetches the AdminAccount and its AdminStatus
Expand All @@ -31,3 +34,21 @@ func AdminAccountAdminStatus(conn *securityhub.SecurityHub, adminAccountID strin
return adminAccount, aws.StringValue(adminAccount.Status), nil
}
}

func StandardsSubscriptionStatus(conn *securityhub.SecurityHub, arn string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := finder.StandardsSubscriptionByARN(conn, arn)

if tfresource.NotFound(err) {
// Return a fake result and status to deal with the INCOMPLETE subscription status
// being a target for both Create and Delete.
return "", StandardsStatusNotFound, nil
}

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.StandardsStatus), nil
}
}
37 changes: 37 additions & 0 deletions aws/internal/service/securityhub/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const (

// Maximum amount of time to wait for an AdminAccount to return NotFound
AdminAccountNotFoundTimeout = 5 * time.Minute

StandardsSubscriptionCreateTimeout = 3 * time.Minute
StandardsSubscriptionDeleteTimeout = 3 * time.Minute
)

// AdminAccountEnabled waits for an AdminAccount to return Enabled
Expand Down Expand Up @@ -50,3 +53,37 @@ func AdminAccountNotFound(conn *securityhub.SecurityHub, adminAccountID string)

return nil, err
}

func StandardsSubscriptionCreated(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{securityhub.StandardsStatusPending},
Target: []string{securityhub.StandardsStatusReady, securityhub.StandardsStatusIncomplete},
Refresh: StandardsSubscriptionStatus(conn, arn),
Timeout: StandardsSubscriptionCreateTimeout,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*securityhub.StandardsSubscription); ok {
return output, err
}

return nil, err
}

func StandardsSubscriptionDeleted(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{securityhub.StandardsStatusDeleting},
Target: []string{StandardsStatusNotFound, securityhub.StandardsStatusIncomplete},
Refresh: StandardsSubscriptionStatus(conn, arn),
Timeout: StandardsSubscriptionDeleteTimeout,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*securityhub.StandardsSubscription); ok {
return output, err
}

return nil, err
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ func Provider() *schema.Provider {
"aws_securityhub_member": resourceAwsSecurityHubMember(),
"aws_securityhub_organization_admin_account": resourceAwsSecurityHubOrganizationAdminAccount(),
"aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(),
"aws_securityhub_standards_control": resourceAwsSecurityHubStandardsControl(),
"aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(),
"aws_servicecatalog_budget_resource_association": resourceAwsServiceCatalogBudgetResourceAssociation(),
"aws_servicecatalog_constraint": resourceAwsServiceCatalogConstraint(),
Expand Down
Loading

0 comments on commit 1d391eb

Please sign in to comment.