diff --git a/.changelog/35625.txt b/.changelog/35625.txt new file mode 100644 index 00000000000..303be785c45 --- /dev/null +++ b/.changelog/35625.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_guardduty_member_detector_feature +``` \ No newline at end of file diff --git a/internal/conns/awsclient_gen.go b/internal/conns/awsclient_gen.go index 7ff63b248b7..8720a74c74c 100644 --- a/internal/conns/awsclient_gen.go +++ b/internal/conns/awsclient_gen.go @@ -91,6 +91,7 @@ import ( glacier_sdkv2 "github.com/aws/aws-sdk-go-v2/service/glacier" globalaccelerator_sdkv2 "github.com/aws/aws-sdk-go-v2/service/globalaccelerator" groundstation_sdkv2 "github.com/aws/aws-sdk-go-v2/service/groundstation" + guardduty_sdkv2 "github.com/aws/aws-sdk-go-v2/service/guardduty" healthlake_sdkv2 "github.com/aws/aws-sdk-go-v2/service/healthlake" iam_sdkv2 "github.com/aws/aws-sdk-go-v2/service/iam" identitystore_sdkv2 "github.com/aws/aws-sdk-go-v2/service/identitystore" @@ -722,6 +723,10 @@ func (c *AWSClient) GuardDutyConn(ctx context.Context) *guardduty_sdkv1.GuardDut return errs.Must(conn[*guardduty_sdkv1.GuardDuty](ctx, c, names.GuardDuty, make(map[string]any))) } +func (c *AWSClient) GuardDutyClient(ctx context.Context) *guardduty_sdkv2.Client { + return errs.Must(client[*guardduty_sdkv2.Client](ctx, c, names.GuardDuty, make(map[string]any))) +} + func (c *AWSClient) HealthLakeClient(ctx context.Context) *healthlake_sdkv2.Client { return errs.Must(client[*healthlake_sdkv2.Client](ctx, c, names.HealthLake, make(map[string]any))) } diff --git a/internal/service/guardduty/guardduty_test.go b/internal/service/guardduty/guardduty_test.go index 1bbf0bef073..e8faefe1bfc 100644 --- a/internal/service/guardduty/guardduty_test.go +++ b/internal/service/guardduty/guardduty_test.go @@ -64,6 +64,11 @@ func TestAccGuardDuty_serial(t *testing.T) { "additional_configuration": testAccOrganizationConfigurationFeature_additionalConfiguration, "multiple": testAccOrganizationConfigurationFeature_multiple, }, + "MemberDetectorFeature": { + "basic": testAccMemberDetectorFeature_basic, + "additional_configuration": testAccMemberDetectorFeature_additionalConfiguration, + "multiple": testAccMemberDetectorFeature_multiple, + }, "ThreatIntelSet": { acctest.CtBasic: testAccThreatIntelSet_basic, "tags": testAccThreatIntelSet_tags, @@ -101,6 +106,17 @@ func testAccMemberFromEnv(t *testing.T) (string, string) { return accountID, email } +func testAccMemberAccountFromEnv(t *testing.T) string { + accountID := os.Getenv("AWS_GUARDDUTY_MEMBER_ACCOUNT_ID") + if accountID == "" { + t.Skip( + "Environment variable AWS_GUARDDUTY_MEMBER_ACCOUNT_ID is not set. " + + "To properly test GuardDuty member accounts, " + + "a valid AWS account ID must be provided.") + } + return accountID +} + // testAccPreCheckDetectorExists verifies the current account has a single active GuardDuty detector configured. func testAccPreCheckDetectorExists(ctx context.Context, t *testing.T) { conn := acctest.Provider.Meta().(*conns.AWSClient).GuardDutyConn(ctx) diff --git a/internal/service/guardduty/member_detector_feature.go b/internal/service/guardduty/member_detector_feature.go new file mode 100644 index 00000000000..105874dd79a --- /dev/null +++ b/internal/service/guardduty/member_detector_feature.go @@ -0,0 +1,344 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package guardduty + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/guardduty" + awstypes "github.com/aws/aws-sdk-go-v2/service/guardduty/types" + "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-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + fwvalidators "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Member Detector Feature") +func newResourceMemberDetectorFeature(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceMemberDetectorFeature{} + return r, nil +} + +const ( + memberDetectorFeatureResourceIDPartCount = 3 + memberDetectorFeatureResourceName = "Member Detector Feature" + memberDetectorFeatureResourceTypeName = "aws_guardduty_member_detector_feature" +) + +type resourceMemberDetectorFeature struct { + framework.ResourceWithConfigure +} + +func (r *resourceMemberDetectorFeature) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = memberDetectorFeatureResourceTypeName +} + +func (r *resourceMemberDetectorFeature) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "account_id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + fwvalidators.AWSAccountID(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "detector_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrID: framework.IDAttribute(), + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.DetectorFeature](), + }, + }, + "status": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.FeatureStatus](), + }, + }, + }, + Blocks: map[string]schema.Block{ + "additional_configuration": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[additionalConfigurationModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.OrgFeatureAdditionalConfiguration](), + }, + }, + "status": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.FeatureStatus](), + }, + }, + }, + }, + }, + }, + } +} + +func (r *resourceMemberDetectorFeature) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().GuardDutyClient(ctx) + + var plan resourceMemberDetectorFeatureModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := createUpdateMemberDetectorsInput(plan) + + if !plan.AdditionalConfiguration.IsNull() { + resp.Diagnostics.Append(fwflex.Expand(ctx, &plan.AdditionalConfiguration, &in.Features[0].AdditionalConfiguration)...) + if resp.Diagnostics.HasError() { + return + } + } + + _, err := updateMemberDetectorFeature(ctx, conn, in) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.GuardDuty, create.ErrActionCreating, memberDetectorFeatureResourceName, plan.Name.ValueString(), err), + err.Error(), + ) + return + } + + // Set the ID and save the state + plan.setID() + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceMemberDetectorFeature) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data resourceMemberDetectorFeatureModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + resp.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().GuardDutyClient(ctx) + + output, err := FindMemberDetectorFeatureByThreePartKey(ctx, conn, data.DetectorID.ValueString(), data.AccountID.ValueString(), data.Name.ValueString()) + + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + + return + } + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.GuardDuty, create.ErrActionReading, memberDetectorFeatureResourceName, data.ID.ValueString(), err), + err.Error(), + ) + + return + } + + resp.Diagnostics.Append(fwflex.Flatten(ctx, output.AdditionalConfiguration, &data.AdditionalConfiguration)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *resourceMemberDetectorFeature) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var old, new resourceMemberDetectorFeatureModel + + resp.Diagnostics.Append(req.State.Get(ctx, &old)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.Plan.Get(ctx, &new)...) + + if resp.Diagnostics.HasError() { + return + } + + conn := r.Meta().GuardDutyClient(ctx) + + in := createUpdateMemberDetectorsInput(new) + + if !new.AdditionalConfiguration.IsNull() { + resp.Diagnostics.Append(fwflex.Expand(ctx, new.AdditionalConfiguration, &in.Features[0].AdditionalConfiguration)...) + if resp.Diagnostics.HasError() { + return + } + } + + _, err := updateMemberDetectorFeature(ctx, conn, in) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.GuardDuty, create.ErrActionUpdating, memberDetectorFeatureResourceName, new.ID.ValueString(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &new)...) +} + +func (r *resourceMemberDetectorFeature) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op +} + +// ==== HELPERS ==== +func createUpdateMemberDetectorsInput(plan resourceMemberDetectorFeatureModel) *guardduty.UpdateMemberDetectorsInput { + in := &guardduty.UpdateMemberDetectorsInput{ + AccountIds: []string{plan.AccountID.ValueString()}, + DetectorId: aws.String(plan.DetectorID.ValueString()), + Features: []awstypes.MemberFeaturesConfiguration{ + { + Name: awstypes.OrgFeature(plan.Name.ValueString()), + Status: awstypes.FeatureStatus(plan.Status.ValueString()), + }, + }, + } + return in +} + +func updateMemberDetectorFeature(ctx context.Context, conn *guardduty.Client, in *guardduty.UpdateMemberDetectorsInput) (*guardduty.UpdateMemberDetectorsOutput, error) { + conns.GlobalMutexKV.Lock(*in.DetectorId) + defer conns.GlobalMutexKV.Unlock(*in.DetectorId) + + out, err := conn.UpdateMemberDetectors(ctx, in) + if err != nil { + return out, err + } + + if out == nil { + return nil, errors.New("empty output") + } + + // For example: + // {"unprocessedAccounts":[{"result":"The request is rejected because the given account ID is not an associated member of account the current account.","accountId":"123456789012"}]} + if len(out.UnprocessedAccounts) > 0 { + return out, errors.New(*(out.UnprocessedAccounts[0].Result)) + } + + return out, err +} + +// ==== FINDERS ==== +func FindMemberDetectorFeatureByThreePartKey(ctx context.Context, client *guardduty.Client, detectorID, accountID, name string) (*awstypes.MemberFeaturesConfigurationResult, error) { + output, err := findMemberConfigurationByDetectorAndAccountID(ctx, client, detectorID, accountID) + + if err != nil { + return nil, err + } + + for _, feature := range output.Features { + if string(feature.Name) == name { + return &feature, nil + } + } + + return nil, fmt.Errorf("no MemberFeaturesConfigurationResult found with name %s", name) + +} + +func findMemberConfigurationByDetectorAndAccountID(ctx context.Context, client *guardduty.Client, detectorID string, accountID string) (*awstypes.MemberDataSourceConfiguration, error) { + input := &guardduty.GetMemberDetectorsInput{ + DetectorId: aws.String(detectorID), + AccountIds: []string{accountID}, + } + + output, err := client.GetMemberDetectors(ctx, input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if output.MemberDataSourceConfigurations == nil || len(output.MemberDataSourceConfigurations) == 0 { + return nil, tfresource.NewEmptyResultError(input) + } + + return &output.MemberDataSourceConfigurations[0], nil +} + +// ==== MODEL ==== +type resourceMemberDetectorFeatureModel struct { + AccountID types.String `tfsdk:"account_id"` + AdditionalConfiguration fwtypes.ListNestedObjectValueOf[additionalConfigurationModel] `tfsdk:"additional_configuration"` + DetectorID types.String `tfsdk:"detector_id"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Status types.String `tfsdk:"status"` +} + +type additionalConfigurationModel struct { + Name types.String `tfsdk:"name"` + Status types.String `tfsdk:"status"` +} + +func (data *resourceMemberDetectorFeatureModel) InitFromID() error { + id := data.ID.ValueString() + parts, err := flex.ExpandResourceId(id, memberDetectorFeatureResourceIDPartCount, false) + + if err != nil { + return err + } + + data.DetectorID = types.StringValue(parts[0]) + data.AccountID = types.StringValue(parts[1]) + data.Name = types.StringValue(parts[2]) + + return nil +} + +func (data *resourceMemberDetectorFeatureModel) setID() { + data.ID = types.StringValue(errs.Must(flex.FlattenResourceId([]string{data.DetectorID.ValueString(), data.AccountID.ValueString(), data.Name.ValueString()}, memberDetectorFeatureResourceIDPartCount, false))) +} diff --git a/internal/service/guardduty/member_detector_feature_test.go b/internal/service/guardduty/member_detector_feature_test.go new file mode 100644 index 00000000000..4e34b9080d6 --- /dev/null +++ b/internal/service/guardduty/member_detector_feature_test.go @@ -0,0 +1,206 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package guardduty_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfguardduty "github.com/hashicorp/terraform-provider-aws/internal/service/guardduty" +) + +func testAccMemberDetectorFeature_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_guardduty_member_detector_feature.test" + accountID := testAccMemberAccountFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheckDetectorExists(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, guardduty.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccMemberDetectorFeatureConfig_basic("RDS_LOGIN_EVENTS", "ENABLED", accountID), + Check: resource.ComposeAggregateTestCheckFunc( + testAccMemberDetectorFeatureExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.#", "0"), + resource.TestCheckResourceAttr(resourceName, "status", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "detector_id"), + resource.TestCheckResourceAttr(resourceName, "name", "RDS_LOGIN_EVENTS"), + resource.TestCheckResourceAttr(resourceName, "account_id", accountID), + ), + }, + }, + }) +} + +func testAccMemberDetectorFeature_additionalConfiguration(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_guardduty_member_detector_feature.test" + accountID := testAccMemberAccountFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheckDetectorExists(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, guardduty.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccMemberDetectorFeatureConfig_additionalConfiguration(accountID, "DISABLED", "ENABLED"), + Check: resource.ComposeTestCheckFunc( + testAccMemberDetectorFeatureExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "status", "ENABLED"), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.#", "2"), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.0.status", "DISABLED"), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.0.name", "EKS_ADDON_MANAGEMENT"), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.1.status", "ENABLED"), + resource.TestCheckResourceAttr(resourceName, "additional_configuration.1.name", "ECS_FARGATE_AGENT_MANAGEMENT"), + resource.TestCheckResourceAttr(resourceName, "name", "RUNTIME_MONITORING"), + resource.TestCheckResourceAttr(resourceName, "account_id", accountID), + ), + }, + }, + }) +} + +func testAccMemberDetectorFeature_multiple(t *testing.T) { + ctx := acctest.Context(t) + resource1Name := "aws_guardduty_member_detector_feature.test1" + resource2Name := "aws_guardduty_member_detector_feature.test2" + resource3Name := "aws_guardduty_member_detector_feature.test3" + accountID := testAccMemberAccountFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheckDetectorExists(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, guardduty.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccMemberDetectorFeatureConfig_multiple(accountID, "ENABLED", "DISABLED", "ENABLED"), + Check: resource.ComposeTestCheckFunc( + testAccMemberDetectorFeatureExists(ctx, resource1Name), + testAccMemberDetectorFeatureExists(ctx, resource2Name), + testAccMemberDetectorFeatureExists(ctx, resource3Name), + resource.TestCheckResourceAttr(resource1Name, "additional_configuration.#", "1"), + resource.TestCheckResourceAttr(resource1Name, "additional_configuration.0.status", "ENABLED"), + resource.TestCheckResourceAttr(resource1Name, "additional_configuration.0.name", "EKS_ADDON_MANAGEMENT"), + resource.TestCheckResourceAttr(resource1Name, "status", "ENABLED"), + resource.TestCheckResourceAttr(resource1Name, "name", "EKS_RUNTIME_MONITORING"), + resource.TestCheckResourceAttr(resource1Name, "account_id", accountID), + resource.TestCheckResourceAttr(resource2Name, "additional_configuration.#", "0"), + resource.TestCheckResourceAttr(resource2Name, "status", "DISABLED"), + resource.TestCheckResourceAttr(resource2Name, "name", "S3_DATA_EVENTS"), + resource.TestCheckResourceAttr(resource2Name, "account_id", accountID), + resource.TestCheckResourceAttr(resource3Name, "additional_configuration.#", "0"), + resource.TestCheckResourceAttr(resource3Name, "status", "ENABLED"), + resource.TestCheckResourceAttr(resource3Name, "name", "LAMBDA_NETWORK_LOGS"), + resource.TestCheckResourceAttr(resource3Name, "account_id", accountID), + ), + }, + }, + }) +} + +func testAccMemberDetectorFeatureExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).GuardDutyClient(ctx) + + _, err := tfguardduty.FindMemberDetectorFeatureByThreePartKey(ctx, conn, rs.Primary.Attributes["detector_id"], rs.Primary.Attributes["account_id"], rs.Primary.Attributes["name"]) + + return err + } +} + +func testAccMemberDetectorFeatureConfig_basic(name, status, accountID string) string { + return acctest.ConfigCompose(testAccMemberDetectorFeatureConfig_base, fmt.Sprintf(` +resource "aws_guardduty_member_detector_feature" "test" { + detector_id = data.aws_guardduty_detector.test.id + name = "%[1]s" + status = "%[2]s" + account_id = "%[3]s" +} +`, name, status, accountID)) +} + +func testAccMemberDetectorFeatureConfig_additionalConfiguration(accountID, eksStatus, ecsStatus string) string { + return acctest.ConfigCompose(testAccMemberDetectorFeatureConfig_base, fmt.Sprintf(` +resource "aws_guardduty_member_detector_feature" "test" { + detector_id = data.aws_guardduty_detector.test.id + name = "RUNTIME_MONITORING" + status = "ENABLED" + account_id = "%[1]s" + + additional_configuration { + name = "EKS_ADDON_MANAGEMENT" + status = "%[2]s" + } + + additional_configuration { + name = "ECS_FARGATE_AGENT_MANAGEMENT" + status = "%[3]s" + } +} +`, accountID, eksStatus, ecsStatus)) +} + +func testAccMemberDetectorFeatureConfig_multiple(accountID, status1, status2, status3 string) string { + return acctest.ConfigCompose(testAccMemberDetectorFeatureConfig_base, fmt.Sprintf(` +resource "aws_guardduty_member_detector_feature" "test1" { + detector_id = data.aws_guardduty_detector.test.id + name = "EKS_RUNTIME_MONITORING" + status = "%[2]s" + account_id = "%[1]s" + + additional_configuration { + name = "EKS_ADDON_MANAGEMENT" + status = "%[2]s" + } +} + +resource "aws_guardduty_member_detector_feature" "test2" { + detector_id = data.aws_guardduty_detector.test.id + name = "S3_DATA_EVENTS" + status = "%[3]s" + account_id = "%[1]s" +} + +resource "aws_guardduty_member_detector_feature" "test3" { + detector_id = data.aws_guardduty_detector.test.id + name = "LAMBDA_NETWORK_LOGS" + status = "%[4]s" + account_id = "%[1]s" +} + +`, accountID, status1, status2, status3)) +} + +const testAccMemberDetectorFeatureConfig_base = ` + +data "aws_guardduty_detector" "test" {} + +` diff --git a/internal/service/guardduty/service_endpoints_gen_test.go b/internal/service/guardduty/service_endpoints_gen_test.go index 8dfd48daf27..7c50f087414 100644 --- a/internal/service/guardduty/service_endpoints_gen_test.go +++ b/internal/service/guardduty/service_endpoints_gen_test.go @@ -4,17 +4,22 @@ package guardduty_test import ( "context" + "errors" "fmt" "maps" - "net/url" "os" "path/filepath" + "reflect" "strings" "testing" + aws_sdkv2 "github.com/aws/aws-sdk-go-v2/aws" + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + guardduty_sdkv2 "github.com/aws/aws-sdk-go-v2/service/guardduty" aws_sdkv1 "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/endpoints" guardduty_sdkv1 "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" "github.com/google/go-cmp/cmp" "github.com/hashicorp/aws-sdk-go-base/v2/servicemocks" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -229,52 +234,88 @@ func TestEndpointConfiguration(t *testing.T) { //nolint:paralleltest // uses t.S }, } - for name, testcase := range testcases { //nolint:paralleltest // uses t.Setenv - testcase := testcase + t.Run("v1", func(t *testing.T) { + for name, testcase := range testcases { //nolint:paralleltest // uses t.Setenv + testcase := testcase - t.Run(name, func(t *testing.T) { - testEndpointCase(t, providerRegion, testcase, callService) - }) - } + t.Run(name, func(t *testing.T) { + testEndpointCase(t, providerRegion, testcase, callServiceV1) + }) + } + }) + + t.Run("v2", func(t *testing.T) { + for name, testcase := range testcases { //nolint:paralleltest // uses t.Setenv + testcase := testcase + + t.Run(name, func(t *testing.T) { + testEndpointCase(t, providerRegion, testcase, callServiceV2) + }) + } + }) } func defaultEndpoint(region string) string { - r := endpoints.DefaultResolver() + r := guardduty_sdkv2.NewDefaultEndpointResolverV2() - ep, err := r.EndpointFor(guardduty_sdkv1.EndpointsID, region) + ep, err := r.ResolveEndpoint(context.Background(), guardduty_sdkv2.EndpointParameters{ + Region: aws_sdkv2.String(region), + }) if err != nil { return err.Error() } - url, _ := url.Parse(ep.URL) - - if url.Path == "" { - url.Path = "/" + if ep.URI.Path == "" { + ep.URI.Path = "/" } - return url.String() + return ep.URI.String() } func defaultFIPSEndpoint(region string) string { - r := endpoints.DefaultResolver() + r := guardduty_sdkv2.NewDefaultEndpointResolverV2() - ep, err := r.EndpointFor(guardduty_sdkv1.EndpointsID, region, func(opt *endpoints.Options) { - opt.UseFIPSEndpoint = endpoints.FIPSEndpointStateEnabled + ep, err := r.ResolveEndpoint(context.Background(), guardduty_sdkv2.EndpointParameters{ + Region: aws_sdkv2.String(region), + UseFIPS: aws_sdkv2.Bool(true), }) if err != nil { return err.Error() } - url, _ := url.Parse(ep.URL) + if ep.URI.Path == "" { + ep.URI.Path = "/" + } + + return ep.URI.String() +} - if url.Path == "" { - url.Path = "/" +func callServiceV2(ctx context.Context, t *testing.T, meta *conns.AWSClient) apiCallParams { + t.Helper() + + client := meta.GuardDutyClient(ctx) + + var result apiCallParams + + _, err := client.ListDetectors(ctx, &guardduty_sdkv2.ListDetectorsInput{}, + func(opts *guardduty_sdkv2.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &result.endpoint), + addRetrieveRegionMiddleware(&result.region), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) } - return url.String() + return result } -func callService(ctx context.Context, t *testing.T, meta *conns.AWSClient) apiCallParams { +func callServiceV1(ctx context.Context, t *testing.T, meta *conns.AWSClient) apiCallParams { t.Helper() client := meta.GuardDutyConn(ctx) @@ -443,6 +484,89 @@ func testEndpointCase(t *testing.T, region string, testcase endpointTestCase, ca } } +func addRetrieveEndpointURLMiddleware(t *testing.T, endpoint *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + retrieveEndpointURLMiddleware(t, endpoint), + middleware.After, + ) + } +} + +func retrieveEndpointURLMiddleware(t *testing.T, endpoint *string) middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Retrieve Endpoint", + func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + t.Helper() + + request, ok := in.Request.(*smithyhttp.Request) + if !ok { + t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request)) + } + + url := request.URL + url.RawQuery = "" + url.Path = "/" + + *endpoint = url.String() + + return next.HandleFinalize(ctx, in) + }) +} + +func addRetrieveRegionMiddleware(region *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Serialize.Add( + retrieveRegionMiddleware(region), + middleware.After, + ) + } +} + +func retrieveRegionMiddleware(region *string) middleware.SerializeMiddleware { + return middleware.SerializeMiddlewareFunc( + "Test: Retrieve Region", + func(ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler) (middleware.SerializeOutput, middleware.Metadata, error) { + *region = awsmiddleware.GetRegion(ctx) + + return next.HandleSerialize(ctx, in) + }, + ) +} + +var errCancelOperation = fmt.Errorf("Test: Canceling request") + +func addCancelRequestMiddleware() func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + cancelRequestMiddleware(), + middleware.After, + ) + } +} + +// cancelRequestMiddleware creates a Smithy middleware that intercepts the request before sending and cancels it +func cancelRequestMiddleware() middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Cancel Requests", + func(_ context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + return middleware.FinalizeOutput{}, middleware.Metadata{}, errCancelOperation + }) +} + +func fullTypeName(i interface{}) string { + return fullValueTypeName(reflect.ValueOf(i)) +} + +func fullValueTypeName(v reflect.Value) string { + if v.Kind() == reflect.Ptr { + return "*" + fullValueTypeName(reflect.Indirect(v)) + } + + requestType := v.Type() + return fmt.Sprintf("%s.%s", requestType.PkgPath(), requestType.Name()) +} + func generateSharedConfigFile(config configFile) string { var buf strings.Builder diff --git a/internal/service/guardduty/service_package_gen.go b/internal/service/guardduty/service_package_gen.go index d30127908b8..ff5f3811217 100644 --- a/internal/service/guardduty/service_package_gen.go +++ b/internal/service/guardduty/service_package_gen.go @@ -5,6 +5,8 @@ package guardduty import ( "context" + aws_sdkv2 "github.com/aws/aws-sdk-go-v2/aws" + guardduty_sdkv2 "github.com/aws/aws-sdk-go-v2/service/guardduty" aws_sdkv1 "github.com/aws/aws-sdk-go/aws" endpoints_sdkv1 "github.com/aws/aws-sdk-go/aws/endpoints" session_sdkv1 "github.com/aws/aws-sdk-go/aws/session" @@ -27,7 +29,12 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceMemberDetectorFeature, + Name: "Member Detector Feature", + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { @@ -132,6 +139,25 @@ func (p *servicePackage) NewConn(ctx context.Context, config map[string]any) (*g return guardduty_sdkv1.New(sess.Copy(&cfg)), nil } +// NewClient returns a new AWS SDK for Go v2 client for this service package's AWS API. +func (p *servicePackage) NewClient(ctx context.Context, config map[string]any) (*guardduty_sdkv2.Client, error) { + cfg := *(config["aws_sdkv2_config"].(*aws_sdkv2.Config)) + + return guardduty_sdkv2.NewFromConfig(cfg, func(o *guardduty_sdkv2.Options) { + if endpoint := config[names.AttrEndpoint].(string); endpoint != "" { + tflog.Debug(ctx, "setting endpoint", map[string]any{ + "tf_aws.endpoint": endpoint, + }) + o.BaseEndpoint = aws_sdkv2.String(endpoint) + + if o.EndpointOptions.UseFIPSEndpoint == aws_sdkv2.FIPSEndpointStateEnabled { + tflog.Debug(ctx, "endpoint set, ignoring UseFIPSEndpoint setting") + o.EndpointOptions.UseFIPSEndpoint = aws_sdkv2.FIPSEndpointStateDisabled + } + } + }), nil +} + func ServicePackage(ctx context.Context) conns.ServicePackage { return &servicePackage{} } diff --git a/names/data/names_data.csv b/names/data/names_data.csv index da7ff7f9837..92f32462a8c 100644 --- a/names/data/names_data.csv +++ b/names/data/names_data.csv @@ -175,7 +175,7 @@ globalaccelerator,globalaccelerator,globalaccelerator,globalaccelerator,,globala glue,glue,glue,glue,,glue,,,Glue,Glue,,1,,,aws_glue_,,glue_,Glue,AWS,,,,,,,Glue,ListRegistries,,, databrew,databrew,gluedatabrew,databrew,,databrew,,gluedatabrew,DataBrew,GlueDataBrew,,1,,,aws_databrew_,,databrew_,Glue DataBrew,AWS,,x,,,,,DataBrew,,,, groundstation,groundstation,groundstation,groundstation,,groundstation,,,GroundStation,GroundStation,,,2,,aws_groundstation_,,groundstation_,Ground Station,AWS,,,,,,,GroundStation,ListConfigs,,, -guardduty,guardduty,guardduty,guardduty,,guardduty,,,GuardDuty,GuardDuty,,1,,,aws_guardduty_,,guardduty_,GuardDuty,Amazon,,,,,,,GuardDuty,ListDetectors,,, +guardduty,guardduty,guardduty,guardduty,,guardduty,,,GuardDuty,GuardDuty,,1,2,,aws_guardduty_,,guardduty_,GuardDuty,Amazon,,,,,,,GuardDuty,ListDetectors,,, health,health,health,health,,health,,,Health,Health,,1,,,aws_health_,,health_,Health,AWS,,x,,,,,Health,,,, healthlake,healthlake,healthlake,healthlake,,healthlake,,,HealthLake,HealthLake,,,2,,aws_healthlake_,,healthlake_,HealthLake,Amazon,,,,,,,HealthLake,ListFHIRDatastores,,, honeycode,honeycode,honeycode,honeycode,,honeycode,,,Honeycode,Honeycode,,1,,,aws_honeycode_,,honeycode_,Honeycode,Amazon,,x,,,,,Honeycode,,,, diff --git a/website/docs/r/guardduty_member_detector_feature.html.markdown b/website/docs/r/guardduty_member_detector_feature.html.markdown new file mode 100644 index 00000000000..c01424444e2 --- /dev/null +++ b/website/docs/r/guardduty_member_detector_feature.html.markdown @@ -0,0 +1,59 @@ +--- +subcategory: "GuardDuty" +layout: "aws" +page_title: "AWS: aws_guardduty_member_detector_feature" +description: |- + Provides a resource to manage an Amazon GuardDuty member account detector feature +--- + +# Resource: aws_guardduty_member_detector_feature + +Provides a resource to manage a single Amazon GuardDuty [detector feature](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty-features-activation-model.html#guardduty-features) for a member account. + +~> **NOTE:** Deleting this resource does not disable the detector feature in the member account, the resource in simply removed from state instead. + +## Example Usage + +```terraform +resource "aws_guardduty_detector" "example" { + enable = true +} + +resource "aws_guardduty_member_detector_feature" "runtime_monitoring" { + detector_id = aws_guardduty_detector.example.id + account_id = "123456789012" + name = "RUNTIME_MONITORING" + status = "ENABLED" + + additional_configuration { + name = "EKS_ADDON_MANAGEMENT" + status = "ENABLED" + } + + additional_configuration { + name = "ECS_FARGATE_AGENT_MANAGEMENT" + status = "ENABLED" + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `detector_id` - (Required) Amazon GuardDuty detector ID. +* `account_id` - (Required) Member account ID to be updated. +* `name` - (Required) The name of the detector feature. Valid values: `S3_DATA_EVENTS`, `EKS_AUDIT_LOGS`, `EBS_MALWARE_PROTECTION`, `RDS_LOGIN_EVENTS`, `EKS_RUNTIME_MONITORING`,`RUNTIME_MONITORING`, `LAMBDA_NETWORK_LOGS`. +* `status` - (Required) The status of the detector feature. Valid values: `ENABLED`, `DISABLED`. +* `additional_configuration` - (Optional) Additional feature configuration block. See [below](#additional-configuration). + +### Additional Configuration + +The `additional_configuration` block supports the following: + +* `name` - (Required) The name of the additional configuration. Valid values: `EKS_ADDON_MANAGEMENT`, `ECS_FARGATE_AGENT_MANAGEMENT`. +* `status` - (Required) The status of the additional configuration. Valid values: `ENABLED`, `DISABLED`. + +## Attribute Reference + +This resource exports no additional attributes.