Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resource: aws_securityhub_standards_control #14714

Merged
merged 12 commits into from
Jul 13, 2021
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