Skip to content

Commit

Permalink
Merge pull request #39511 from kamilturek/f-aws-standards-control-ass…
Browse files Browse the repository at this point in the history
…ociations

r/aws_securityhub_standards_control_association: new resource
  • Loading branch information
ewbankkit authored Sep 27, 2024
2 parents 85686f4 + 31b2871 commit 034169b
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 27 deletions.
3 changes: 3 additions & 0 deletions .changelog/39511.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_securityhub_standards_control_association
```
2 changes: 2 additions & 0 deletions internal/service/securityhub/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ResourceOrganizationConfiguration = resourceOrganizationConfiguration
ResourceProductSubscription = resourceProductSubscription
ResourceStandardsControl = resourceStandardsControl
ResourceStandardsControlAssociation = newStandardsControlAssociationResource
ResourceStandardsSubscription = resourceStandardsSubscription

AccountHubARN = accountHubARN
Expand All @@ -33,6 +34,7 @@ var (
FindMemberByAccountID = findMemberByAccountID
FindOrganizationConfiguration = findOrganizationConfiguration
FindProductSubscriptionByARN = findProductSubscriptionByARN
FindStandardsControlAssociationByTwoPartKey = findStandardsControlAssociationByTwoPartKey
FindStandardsControlByTwoPartKey = findStandardsControlByTwoPartKey
FindStandardsSubscriptionByARN = findStandardsSubscriptionByARN
StandardsControlARNToStandardsSubscriptionARN = standardsControlARNToStandardsSubscriptionARN
Expand Down
3 changes: 3 additions & 0 deletions internal/service/securityhub/securityhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func TestAccSecurityHub_serial(t *testing.T) {
"DisabledControlStatus": testAccStandardsControl_disabledControlStatus,
"EnabledControlStatusAndDisabledReason": testAccStandardsControl_enabledControlStatusAndDisabledReason,
},
"StandardsControlAssociation": {
acctest.CtBasic: testAccStandardsControlAssociation_basic,
},
"StandardsControlAssociationsDataSource": {
acctest.CtBasic: testAccStandardsControlAssociationsDataSource_basic,
},
Expand Down
4 changes: 4 additions & 0 deletions internal/service/securityhub/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

318 changes: 318 additions & 0 deletions internal/service/securityhub/standards_control_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package securityhub

import (
"context"
"errors"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/securityhub"
awstypes "github.com/aws/aws-sdk-go-v2/service/securityhub/types"
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag"
autoflex "github.com/hashicorp/terraform-provider-aws/internal/flex"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
"github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkResource("aws_securityhub_standards_control_association", name="Standards Control Association")
func newStandardsControlAssociationResource(_ context.Context) (resource.ResourceWithConfigure, error) {
r := &standardsControlAssociationResource{}

return r, nil
}

type standardsControlAssociationResource struct {
framework.ResourceWithConfigure
framework.WithNoOpDelete
}

func (*standardsControlAssociationResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = "aws_securityhub_standards_control_association"
}

func (r *standardsControlAssociationResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"association_status": schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.AssociationStatus](),
Required: true,
},
names.AttrID: framework.IDAttribute(),
"security_control_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"standards_arn": schema.StringAttribute{
CustomType: fwtypes.ARNType,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"updated_reason": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
},
}
}

func (r *standardsControlAssociationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var data standardsControlAssociationResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().SecurityHubClient(ctx)

input := &securityhub.BatchUpdateStandardsControlAssociationsInput{
StandardsControlAssociationUpdates: []awstypes.StandardsControlAssociationUpdate{
{
AssociationStatus: awstypes.AssociationStatus(data.AssociationStatus.ValueString()),
SecurityControlId: data.SecurityControlID.ValueStringPointer(),
StandardsArn: data.StandardsARN.ValueStringPointer(),
UpdatedReason: data.UpdatedReason.ValueStringPointer(),
},
},
}

output, err := conn.BatchUpdateStandardsControlAssociations(ctx, input)

if err == nil {
err = unprocessedAssociationUpdatesError(output.UnprocessedAssociationUpdates)
}

if err != nil {
response.Diagnostics.AddError("creating Standards Control Association", err.Error())

return
}

// Set values for unknowns.
data.setID()

response.Diagnostics.Append(response.State.Set(ctx, data)...)
}

func (r *standardsControlAssociationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
var data standardsControlAssociationResourceModel
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

if err := data.InitFromID(ctx); err != nil {
response.Diagnostics.AddError("parsing resource ID", err.Error())

return
}

conn := r.Meta().SecurityHubClient(ctx)

securityControlID, standardsARN := data.SecurityControlID.ValueString(), data.StandardsARN.ValueString()
output, err := findStandardsControlAssociationByTwoPartKey(ctx, conn, securityControlID, standardsARN)

if tfresource.NotFound(err) {
response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
response.State.RemoveResource(ctx)
return
}

if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("reading SecurityHub Standards Control Association (%s/%s)", securityControlID, standardsARN), err.Error())

return
}

response.Diagnostics.Append(flex.Flatten(ctx, output, &data)...)
if response.Diagnostics.HasError() {
return
}

response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

func (r *standardsControlAssociationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var data standardsControlAssociationResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().SecurityHubClient(ctx)

input := &securityhub.BatchUpdateStandardsControlAssociationsInput{
StandardsControlAssociationUpdates: []awstypes.StandardsControlAssociationUpdate{
{
AssociationStatus: awstypes.AssociationStatus(data.AssociationStatus.ValueString()),
SecurityControlId: data.SecurityControlID.ValueStringPointer(),
StandardsArn: data.StandardsARN.ValueStringPointer(),
UpdatedReason: data.UpdatedReason.ValueStringPointer(),
},
},
}

output, err := conn.BatchUpdateStandardsControlAssociations(ctx, input)

if err == nil {
err = unprocessedAssociationUpdatesError(output.UnprocessedAssociationUpdates)
}

if err != nil {
response.Diagnostics.AddError("updating Standards Control Association", err.Error())

return
}

response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

func (r *standardsControlAssociationResource) ValidateConfig(ctx context.Context, request resource.ValidateConfigRequest, response *resource.ValidateConfigResponse) {
var data standardsControlAssociationResourceModel
response.Diagnostics.Append(request.Config.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

if data.AssociationStatus == fwtypes.StringEnumValue(awstypes.AssociationStatusEnabled) {
return
}

if !data.UpdatedReason.IsNull() {
return
}

response.Diagnostics.Append(
fwdiag.NewAttributeRequiredWhenError(
path.Root("updated_reason"),
path.Root("association_status"),
data.AssociationStatus.ValueString(),
),
)
}

type standardsControlAssociationResourceModel struct {
AssociationStatus fwtypes.StringEnum[awstypes.AssociationStatus] `tfsdk:"association_status"`
ID types.String `tfsdk:"id"`
SecurityControlID types.String `tfsdk:"security_control_id"`
StandardsARN fwtypes.ARN `tfsdk:"standards_arn"`
UpdatedReason types.String `tfsdk:"updated_reason"`
}

const (
standardsControlAssociationResourceIDPartCount = 2
)

func (m *standardsControlAssociationResourceModel) InitFromID(ctx context.Context) error {
parts, err := autoflex.ExpandResourceId(m.ID.ValueString(), standardsControlAssociationResourceIDPartCount, false)
if err != nil {
return err
}

m.SecurityControlID = types.StringValue(parts[0])
m.StandardsARN = fwtypes.ARNValue(parts[1])

return nil
}

func (m *standardsControlAssociationResourceModel) setID() {
id, _ := standardsControlAssociationCreateResourceID(m.SecurityControlID.ValueString(), m.StandardsARN.ValueString())
m.ID = types.StringValue(id)
}

func standardsControlAssociationCreateResourceID(securityControlID, standardsARN string) (string, error) {
return autoflex.FlattenResourceId([]string{securityControlID, standardsARN}, standardsControlAssociationResourceIDPartCount, false)
}

func findStandardsControlAssociationByTwoPartKey(ctx context.Context, conn *securityhub.Client, securityControlID string, standardsARN string) (*awstypes.StandardsControlAssociationSummary, error) {
input := &securityhub.ListStandardsControlAssociationsInput{
SecurityControlId: aws.String(securityControlID),
}

return findStandardsControlAssociation(ctx, conn, input, func(v *awstypes.StandardsControlAssociationSummary) bool {
return aws.ToString(v.StandardsArn) == standardsARN
})
}

func findStandardsControlAssociation(ctx context.Context, conn *securityhub.Client, input *securityhub.ListStandardsControlAssociationsInput, filter tfslices.Predicate[*awstypes.StandardsControlAssociationSummary]) (*awstypes.StandardsControlAssociationSummary, error) {
output, err := findStandardsControlAssociations(ctx, conn, input, filter)

if err != nil {
return nil, err
}

return tfresource.AssertSingleValueResult(output)
}

func findStandardsControlAssociations(ctx context.Context, conn *securityhub.Client, input *securityhub.ListStandardsControlAssociationsInput, filter tfslices.Predicate[*awstypes.StandardsControlAssociationSummary]) ([]awstypes.StandardsControlAssociationSummary, error) {
var output []awstypes.StandardsControlAssociationSummary

pages := securityhub.NewListStandardsControlAssociationsPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)

if tfawserr.ErrCodeEquals(err, errCodeResourceNotFoundException) || tfawserr.ErrMessageContains(err, errCodeInvalidAccessException, "not subscribed to AWS Security Hub") {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

for _, v := range page.StandardsControlAssociationSummaries {
if filter(&v) {
output = append(output, v)
}
}
}

return output, nil
}

func unprocessedAssociationUpdatesError(apiObjects []awstypes.UnprocessedStandardsControlAssociationUpdate) error {
var errs []error

for _, apiObject := range apiObjects {
err := unprocessedAssociationUpdateError(&apiObject)
if v := apiObject.StandardsControlAssociationUpdate; v != nil {
id, _ := standardsControlAssociationCreateResourceID(aws.ToString(v.SecurityControlId), aws.ToString(v.StandardsArn))
err = fmt.Errorf("%s: %w", id, err)
}
errs = append(errs, err)
}

return errors.Join(errs...)
}

func unprocessedAssociationUpdateError(apiObject *awstypes.UnprocessedStandardsControlAssociationUpdate) error {
if apiObject == nil {
return nil
}

return fmt.Errorf("%s: %s", apiObject.ErrorCode, aws.ToString(apiObject.ErrorReason))
}
Loading

0 comments on commit 034169b

Please sign in to comment.