diff --git a/.changelog/36268.txt b/.changelog/36268.txt new file mode 100644 index 000000000000..d844c7157551 --- /dev/null +++ b/.changelog/36268.txt @@ -0,0 +1,31 @@ +```release-note:bug +resource/aws_securitylake_subscriber: Allow more than one log source +``` + +```release-note:bug +resource/aws_securitylake_aws_log_source: Correctly handles unspecified `source_version` +``` + +```release-note:bug +resource/aws_securitylake_aws_log_source: Prevents errors when creating multiple log sources concurrently +``` + +```release-note:bug +resource/aws_securitylake_custom_log_source: Validates length of `source_name` parameter +``` + +```release-note:bug +resource/aws_securitylake_custom_log_source: Prevents errors when creating multiple log sources concurrently +``` + +```release-note:bug +resource/aws_securitylake_subscriber: Correctly handles unspecified `access_type` +``` + +```release-note:bug +resource/aws_securitylake_subscriber: Correctly requires `source_name` parameter for `aws_log_source_resource` and `custom_log_source_resource` +``` + +```release-note:bug +resource/aws_securitylake_subscriber: Correctly handles unspecified `source_version` parameter for `aws_log_source_resource` and `custom_log_source_resource` +``` diff --git a/internal/service/securitylake/aws_log_source.go b/internal/service/securitylake/aws_log_source.go index 728b7e195f17..fbc0227e6d4a 100644 --- a/internal/service/securitylake/aws_log_source.go +++ b/internal/service/securitylake/aws_log_source.go @@ -117,7 +117,9 @@ func (r *awsLogSourceResource) Create(ctx context.Context, request resource.Crea return } - _, err := conn.CreateAwsLogSource(ctx, input) + _, err := retryDataLakeConflictWithMutex(ctx, func() (*securitylake.CreateAwsLogSourceOutput, error) { + return conn.CreateAwsLogSource(ctx, input) + }) if err != nil { response.Diagnostics.AddError("creating Security Lake AWS Log Source", err.Error()) @@ -144,6 +146,7 @@ func (r *awsLogSourceResource) Create(ctx context.Context, request resource.Crea sourceData.Accounts.SetValue = fwflex.FlattenFrameworkStringValueSet(ctx, logSource.Accounts) sourceData.SourceVersion = fwflex.StringToFramework(ctx, logSource.SourceVersion) + data.Source = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, sourceData) response.Diagnostics.Append(response.State.Set(ctx, data)...) } @@ -212,7 +215,9 @@ func (r *awsLogSourceResource) Delete(ctx context.Context, request resource.Dele input.Sources = []awstypes.AwsLogSourceConfiguration{*logSource} } - _, err := conn.DeleteAwsLogSource(ctx, input) + _, err := retryDataLakeConflictWithMutex(ctx, func() (*securitylake.DeleteAwsLogSourceOutput, error) { + return conn.DeleteAwsLogSource(ctx, input) + }) if errs.IsA[*awstypes.ResourceNotFoundException](err) { return diff --git a/internal/service/securitylake/aws_log_source_test.go b/internal/service/securitylake/aws_log_source_test.go index 94ce12ea42ba..b7a6468f9f94 100644 --- a/internal/service/securitylake/aws_log_source_test.go +++ b/internal/service/securitylake/aws_log_source_test.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/securitylake/types" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -28,6 +29,7 @@ func testAccAWSLogSource_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -35,6 +37,54 @@ func testAccAWSLogSource_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccAWSLogSourceConfig_basic(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLogSourceExists(ctx, resourceName, &logSource), + resource.TestCheckResourceAttr(resourceName, "source.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "source.0.accounts.#"), + acctest.CheckResourceAttrAccountID(resourceName, "source.0.accounts.0"), + func(s *terraform.State) error { + return resource.TestCheckTypeSetElemAttr(resourceName, "source.0.accounts.*", acctest.AccountID())(s) + }, + resource.TestCheckResourceAttr(resourceName, "source.0.regions.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "source.0.regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "source.0.source_name", "ROUTE53"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_version", "2.0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSLogSourceConfig_sourceVersion("2.0"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func testAccAWSLogSource_sourceVersion(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_securitylake_aws_log_source.test" + var logSource types.AwsLogSourceConfiguration + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAWSLogSourceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAWSLogSourceConfig_sourceVersion("1.0"), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckAWSLogSourceExists(ctx, resourceName, &logSource), resource.TestCheckResourceAttr(resourceName, "source.#", "1"), @@ -50,6 +100,23 @@ func testAccAWSLogSource_basic(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccAWSLogSourceConfig_sourceVersion("2.0"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLogSourceExists(ctx, resourceName, &logSource), + resource.TestCheckResourceAttr(resourceName, "source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source.0.accounts.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source.0.regions.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "source.0.regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "source.0.source_name", "ROUTE53"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_version", "2.0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -64,9 +131,11 @@ func testAccAWSLogSource_multiRegion(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) + acctest.PreCheckMultipleRegion(t, 2) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), CheckDestroy: testAccCheckAWSLogSourceDestroy(ctx), Steps: []resource.TestStep{ { @@ -78,8 +147,6 @@ func testAccAWSLogSource_multiRegion(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "source.0.regions.#", "2"), resource.TestCheckTypeSetElemAttr(resourceName, "source.0.regions.*", acctest.Region()), resource.TestCheckTypeSetElemAttr(resourceName, "source.0.regions.*", acctest.AlternateRegion()), - resource.TestCheckResourceAttr(resourceName, "source.0.source_name", "ROUTE53"), - resource.TestCheckResourceAttr(resourceName, "source.0.source_version", "1.0"), ), }, { @@ -100,7 +167,7 @@ func testAccAWSLogSource_disappears(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -118,6 +185,46 @@ func testAccAWSLogSource_disappears(t *testing.T) { }) } +func testAccAWSLogSource_multiple(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_securitylake_aws_log_source.test" + resourceName2 := "aws_securitylake_aws_log_source.test2" + var logSource, logSource2 types.AwsLogSourceConfiguration + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAWSLogSourceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAWSLogSourceConfig_multiple(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSLogSourceExists(ctx, resourceName, &logSource), + testAccCheckAWSLogSourceExists(ctx, resourceName2, &logSource2), + + resource.TestCheckResourceAttr(resourceName, "source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_name", "ROUTE53"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_version", "2.0"), + + resource.TestCheckResourceAttr(resourceName2, "source.#", "1"), + resource.TestCheckResourceAttr(resourceName2, "source.0.source_name", "S3_DATA"), + resource.TestCheckResourceAttr(resourceName2, "source.0.source_version", "2.0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckAWSLogSourceDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).SecurityLakeClient(ctx) @@ -166,34 +273,81 @@ func testAccCheckAWSLogSourceExists(ctx context.Context, n string, v *types.AwsL } func testAccAWSLogSourceConfig_basic() string { - return acctest.ConfigCompose(testAccDataLakeConfig_basic(), fmt.Sprintf(` -data "aws_caller_identity" "test" {} + return acctest.ConfigCompose( + testAccDataLakeConfig_basic(), ` +resource "aws_securitylake_aws_log_source" "test" { + source { + accounts = [data.aws_caller_identity.current.account_id] + regions = [data.aws_region.current.name] + source_name = "ROUTE53" + } + depends_on = [aws_securitylake_data_lake.test] +} + +data "aws_region" "current" {} +`) +} +func testAccAWSLogSourceConfig_sourceVersion(version string) string { + return acctest.ConfigCompose( + testAccDataLakeConfig_basic(), fmt.Sprintf(` resource "aws_securitylake_aws_log_source" "test" { source { - accounts = [data.aws_caller_identity.test.account_id] - regions = [%[1]q] + accounts = [data.aws_caller_identity.current.account_id] + regions = [data.aws_region.current.name] source_name = "ROUTE53" - source_version = "1.0" + source_version = %[1]q } depends_on = [aws_securitylake_data_lake.test] } -`, acctest.Region())) + +data "aws_region" "current" {} +`, version)) } func testAccAWSLogSourceConfig_multiRegion(rName string) string { - return acctest.ConfigCompose(testAccDataLakeConfig_replication(rName), fmt.Sprintf(` -data "aws_caller_identity" "test" {} - + return acctest.ConfigCompose( + acctest.ConfigMultipleRegionProvider(2), + testAccDataLakeConfig_replication(rName), ` resource "aws_securitylake_aws_log_source" "test" { source { - accounts = [data.aws_caller_identity.test.account_id] - regions = [%[1]q, %[2]q] - source_name = "ROUTE53" - source_version = "1.0" + accounts = [data.aws_caller_identity.current.account_id] + regions = [data.aws_region.current.name, data.aws_region.alternate.name] + source_name = "ROUTE53" } depends_on = [aws_securitylake_data_lake.test, aws_securitylake_data_lake.region_2] } -`, acctest.Region(), acctest.AlternateRegion())) + +data "aws_region" "current" {} + +data "aws_region" "alternate" { + provider = awsalternate +} +`) +} + +func testAccAWSLogSourceConfig_multiple() string { + return acctest.ConfigCompose( + testAccDataLakeConfig_basic(), ` +resource "aws_securitylake_aws_log_source" "test" { + source { + accounts = [data.aws_caller_identity.current.account_id] + regions = [data.aws_region.current.name] + source_name = "ROUTE53" + } + depends_on = [aws_securitylake_data_lake.test] +} + +resource "aws_securitylake_aws_log_source" "test2" { + source { + accounts = [data.aws_caller_identity.current.account_id] + regions = [data.aws_region.current.name] + source_name = "S3_DATA" + } + depends_on = [aws_securitylake_data_lake.test] +} + +data "aws_region" "current" {} +`) } diff --git a/internal/service/securitylake/custom_log_source.go b/internal/service/securitylake/custom_log_source.go index f02536097fc3..a374ef0615fc 100644 --- a/internal/service/securitylake/custom_log_source.go +++ b/internal/service/securitylake/custom_log_source.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/securitylake" awstypes "github.com/aws/aws-sdk-go-v2/service/securitylake/types" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -88,6 +89,9 @@ func (r *customLogSourceResource) Schema(ctx context.Context, request resource.S }, "source_name": schema.StringAttribute{ Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(20), + }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, @@ -184,7 +188,9 @@ func (r *customLogSourceResource) Create(ctx context.Context, request resource.C return } - output, err := conn.CreateCustomLogSource(ctx, input) + output, err := retryDataLakeConflictWithMutex(ctx, func() (*securitylake.CreateCustomLogSourceOutput, error) { + return conn.CreateCustomLogSource(ctx, input) + }) if err != nil { response.Diagnostics.AddError("creating Security Lake Custom Log Source", err.Error()) @@ -266,7 +272,9 @@ func (r *customLogSourceResource) Delete(ctx context.Context, request resource.D return } - _, err := conn.DeleteCustomLogSource(ctx, input) + _, err := retryDataLakeConflictWithMutex(ctx, func() (*securitylake.DeleteCustomLogSourceOutput, error) { + return conn.DeleteCustomLogSource(ctx, input) + }) if errs.IsA[*awstypes.ResourceNotFoundException](err) { return diff --git a/internal/service/securitylake/custom_log_source_test.go b/internal/service/securitylake/custom_log_source_test.go index 1033236350d6..95bfa7bc0db9 100644 --- a/internal/service/securitylake/custom_log_source_test.go +++ b/internal/service/securitylake/custom_log_source_test.go @@ -6,14 +6,18 @@ package securitylake_test import ( "context" "fmt" + "strings" "testing" + "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/service/securitylake/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "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" tfsecuritylake "github.com/hashicorp/terraform-provider-aws/internal/service/securitylake" + "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -21,30 +25,186 @@ import ( func testAccCustomLogSource_basic(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_custom_log_source.test" + rName := randomCustomLogSourceName() var customLogSource types.CustomLogSourceResource resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckCustomLogSourceDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccCustomLogSourceConfig_basic(), + Config: testAccCustomLogSourceConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + resource.TestCheckResourceAttr(resourceName, "attributes.#", "1"), + acctest.CheckResourceAttrRegionalARN(resourceName, "attributes.0.crawler_arn", "glue", fmt.Sprintf("crawler/%s", rName)), + acctest.CheckResourceAttrRegionalARN(resourceName, "attributes.0.database_arn", "glue", fmt.Sprintf("database/amazon_security_lake_glue_db_%s", strings.Replace(acctest.Region(), "-", "_", -1))), + acctest.CheckResourceAttrRegionalARN(resourceName, "attributes.0.table_arn", "glue", fmt.Sprintf("table/amazon_security_lake_table_%s_ext_%s", strings.Replace(acctest.Region(), "-", "_", -1), strings.Replace(rName, "-", "_", -1))), resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "configuration.0.crawler_configuration.#", "1"), resource.TestCheckResourceAttrPair(resourceName, "configuration.0.crawler_configuration.0.role_arn", "aws_iam_role.test", "arn"), resource.TestCheckResourceAttr(resourceName, "configuration.0.provider_identity.#", "1"), - resource.TestCheckResourceAttr(resourceName, "configuration.0.provider_identity.0.external_id", "windows-sysmon-test"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.provider_identity.0.external_id", fmt.Sprintf("%s-test", rName)), + resource.TestCheckNoResourceAttr(resourceName, "event_classes"), + resource.TestCheckResourceAttr(resourceName, "provider_details.#", "1"), + resource.TestMatchResourceAttr(resourceName, "provider_details.0.location", regexache.MustCompile(fmt.Sprintf(`^s3://aws-security-data-lake-%s-[a-z0-9]{30}/ext/%s/$`, acctest.Region(), rName))), + acctest.CheckResourceAttrGlobalARN(resourceName, "provider_details.0.role_arn", "iam", fmt.Sprintf("role/AmazonSecurityLake-Provider-%s-%s", rName, acctest.Region())), + resource.TestCheckResourceAttr(resourceName, "source_name", rName), + resource.TestCheckNoResourceAttr(resourceName, "source_version"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration"}, + }, + }, + }) +} + +func testAccCustomLogSource_sourceVersion(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_securitylake_custom_log_source.test" + rName := randomCustomLogSourceName() + var customLogSource types.CustomLogSourceResource + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomLogSourceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomLogSourceConfig_sourceVersion(rName, "1.5"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + resource.TestCheckResourceAttr(resourceName, "source_version", "1.5"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration"}, + }, + { + Config: testAccCustomLogSourceConfig_sourceVersion(rName, "2.5"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + resource.TestCheckResourceAttr(resourceName, "source_version", "2.5"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration"}, + }, + }, + }) +} + +func testAccCustomLogSource_multiple(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_securitylake_custom_log_source.test" + resourceName2 := "aws_securitylake_custom_log_source.test2" + rName := randomCustomLogSourceName() + rName2 := randomCustomLogSourceName() + var customLogSource, customLogSource2 types.CustomLogSourceResource + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomLogSourceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomLogSourceConfig_multiple(rName, rName2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + testAccCheckCustomLogSourceExists(ctx, resourceName2, &customLogSource2), + + resource.TestCheckResourceAttr(resourceName, "source_name", rName), + + resource.TestCheckResourceAttr(resourceName2, "source_name", rName2), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration"}, + }, + }, + }) +} + +func testAccCustomLogSource_eventClasses(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_securitylake_custom_log_source.test" + rName := randomCustomLogSourceName() + var customLogSource types.CustomLogSourceResource + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.SecurityLake) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomLogSourceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomLogSourceConfig_eventClasses(rName, "FILE_ACTIVITY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), resource.TestCheckResourceAttr(resourceName, "event_classes.#", "1"), resource.TestCheckTypeSetElemAttr(resourceName, "event_classes.*", "FILE_ACTIVITY"), - resource.TestCheckResourceAttr(resourceName, "source_name", "windows-sysmon"), - resource.TestCheckResourceAttr(resourceName, "source_version", "1.0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration", "event_classes"}, + }, + { + Config: testAccCustomLogSourceConfig_eventClasses(rName, "MEMORY_ACTIVITY", "FILE_ACTIVITY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + resource.TestCheckResourceAttr(resourceName, "event_classes.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "event_classes.*", "MEMORY_ACTIVITY"), + resource.TestCheckTypeSetElemAttr(resourceName, "event_classes.*", "FILE_ACTIVITY"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"configuration", "event_classes"}, + }, + { + Config: testAccCustomLogSourceConfig_eventClasses(rName, "MEMORY_ACTIVITY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), + resource.TestCheckResourceAttr(resourceName, "event_classes.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "event_classes.*", "MEMORY_ACTIVITY"), ), }, { @@ -60,20 +220,21 @@ func testAccCustomLogSource_basic(t *testing.T) { func testAccCustomLogSource_disappears(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_custom_log_source.test" + rName := randomCustomLogSourceName() var customLogSource types.CustomLogSourceResource resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckCustomLogSourceDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccCustomLogSourceConfig_basic(), + Config: testAccCustomLogSourceConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckCustomLogSourceExists(ctx, resourceName, &customLogSource), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfsecuritylake.ResourceCustomLogSource, resourceName), @@ -84,6 +245,10 @@ func testAccCustomLogSource_disappears(t *testing.T) { }) } +func randomCustomLogSourceName() string { + return fmt.Sprintf("%s-%s", acctest.ResourcePrefix, sdkacctest.RandString(20-(len(acctest.ResourcePrefix)+1))) +} + func testAccCheckCustomLogSourceDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).SecurityLakeClient(ctx) @@ -131,10 +296,93 @@ func testAccCheckCustomLogSourceExists(ctx context.Context, n string, v *types.C } } -func testAccCustomLogSourceConfig_basic() string { - return acctest.ConfigCompose(testAccDataLakeConfig_basic(), ` +func testAccCustomLogSourceConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccDataLakeConfig_basic(), fmt.Sprintf(` +resource "aws_securitylake_custom_log_source" "test" { + source_name = %[1]q + + configuration { + crawler_configuration { + role_arn = aws_iam_role.test.arn + } + + provider_identity { + external_id = "%[1]s-test" + principal = data.aws_caller_identity.current.account_id + } + } + + depends_on = [aws_securitylake_data_lake.test, aws_iam_role.test] +} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/service-role/" + + assume_role_policy = < 0) { var awsLogSources []awsLogSubscriberSourceModel diags.Append(item.AwsLogSourceResource.ElementsAs(ctx, &awsLogSources, false)...) - subscriberLogSource := expandSubscriberLogSourceSource(ctx, awsLogSources) + subscriberLogSource := expandSubscriberAwsLogSourceSource(ctx, awsLogSources) sources = append(sources, subscriberLogSource) } if (!item.CustomLogSourceResource.IsNull()) && (len(item.CustomLogSourceResource.Elements()) > 0) { @@ -522,7 +530,7 @@ func expandSubscriptionValueSources(ctx context.Context, subscriberSourcesModels return sources, diags } -func expandSubscriberLogSourceSource(ctx context.Context, awsLogSources []awsLogSubscriberSourceModel) *awstypes.LogSourceResourceMemberAwsLogSource { +func expandSubscriberAwsLogSourceSource(ctx context.Context, awsLogSources []awsLogSubscriberSourceModel) *awstypes.LogSourceResourceMemberAwsLogSource { // nosemgrep:ci.aws-in-func-name if len(awsLogSources) == 0 { return nil } @@ -551,38 +559,63 @@ func expandSubscriberCustomLogSourceSource(ctx context.Context, customLogSources return customLogSourceResource } -func flattenSubscriberSourcesModel(ctx context.Context, apiObject []awstypes.LogSourceResource) (types.List, diag.Diagnostics) { +func flattenSubscriberSources(ctx context.Context, apiObject []awstypes.LogSourceResource) (types.Set, diag.Diagnostics) { var diags diag.Diagnostics elemType := types.ObjectType{AttrTypes: subscriberSourcesModelAttrTypes} + result := types.SetNull(elemType) - obj := map[string]attr.Value{} + var elems []types.Object for _, item := range apiObject { - switch v := item.(type) { - case *awstypes.LogSourceResourceMemberAwsLogSource: - subscriberLogSource, d := flattenSubscriberLogSourceResourceModel(ctx, &v.Value, nil, "aws") - diags.Append(d...) - obj = map[string]attr.Value{ - "aws_log_source_resource": subscriberLogSource, - "custom_log_source_resource": types.ListNull(customLogSubscriberSourceModelAttrTypes), - } - case *awstypes.LogSourceResourceMemberCustomLogSource: - subscriberLogSource, d := flattenSubscriberLogSourceResourceModel(ctx, nil, &v.Value, "custom") - diags.Append(d...) - obj = map[string]attr.Value{ - "aws_log_source_resource": types.ListNull(logSubscriberSourcesModelAttrTypes), - "custom_log_source_resource": subscriberLogSource, - } + elem, d := flattenSubscriberSourcesModel(ctx, item) + diags.Append(d...) + if d.HasError() { + return result, diags } + elems = append(elems, elem) } - objVal, d := types.ObjectValue(subscriberSourcesModelAttrTypes, obj) + setVal, d := types.SetValue(elemType, slices.ApplyToAll(elems, func(o types.Object) attr.Value { + return o + })) diags.Append(d...) - listVal, d := types.ListValue(elemType, []attr.Value{objVal}) + return setVal, diags +} + +func flattenSubscriberSourcesModel(ctx context.Context, apiObject awstypes.LogSourceResource) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + result := types.ObjectUnknown(subscriberSourcesModelAttrTypes) + + obj := map[string]attr.Value{} + + switch v := apiObject.(type) { + case *awstypes.LogSourceResourceMemberAwsLogSource: + subscriberLogSource, d := flattenSubscriberLogSourceResourceModel(ctx, &v.Value, nil, "aws") + diags.Append(d...) + if d.HasError() { + return result, diags + } + obj = map[string]attr.Value{ + "aws_log_source_resource": subscriberLogSource, + "custom_log_source_resource": types.ListNull(customLogSubscriberSourceModelAttrTypes), + } + case *awstypes.LogSourceResourceMemberCustomLogSource: + subscriberLogSource, d := flattenSubscriberLogSourceResourceModel(ctx, nil, &v.Value, "custom") + diags.Append(d...) + if d.HasError() { + return result, diags + } + obj = map[string]attr.Value{ + "aws_log_source_resource": types.ListNull(logSubscriberSourcesModelAttrTypes), + "custom_log_source_resource": subscriberLogSource, + } + } + + result, d := types.ObjectValue(subscriberSourcesModelAttrTypes, obj) diags.Append(d...) - return listVal, diags + return result, diags } func flattenSubscriberLogSourceResourceModel(ctx context.Context, awsLogApiObject *awstypes.AwsLogSourceResource, customLogApiObject *awstypes.CustomLogSourceResource, logSourceType string) (types.List, diag.Diagnostics) { @@ -704,7 +737,7 @@ type subscriberResourceModel struct { AccessTypes types.String `tfsdk:"access_type"` SubscriberArn types.String `tfsdk:"arn"` ID types.String `tfsdk:"id"` - Sources types.List `tfsdk:"source"` + Sources types.Set `tfsdk:"source"` SubscriberDescription types.String `tfsdk:"subscriber_description"` SubscriberIdentity fwtypes.ListNestedObjectValueOf[subscriberIdentityModel] `tfsdk:"subscriber_identity"` SubscriberName types.String `tfsdk:"subscriber_name"` @@ -762,14 +795,14 @@ func (rd *subscriberResourceModel) refreshFromOutput(ctx context.Context, subscr rd.AccessTypes = fwflex.StringValueToFramework(ctx, subscriber.AccessTypes[0]) rd.SubscriberIdentity = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &subscriberIdentity) - sourcesOutput, d := flattenSubscriberSourcesModel(ctx, subscriber.Sources) - diags.Append(d...) rd.ResourceShareArn = fwflex.StringToFrameworkLegacy(ctx, subscriber.ResourceShareArn) rd.ResourceShareName = fwflex.StringToFramework(ctx, subscriber.ResourceShareName) rd.S3BucketArn = fwflex.StringToFramework(ctx, subscriber.S3BucketArn) rd.SubscriberEndpoint = fwflex.StringToFramework(ctx, subscriber.SubscriberEndpoint) rd.SubscriberStatus = fwflex.StringValueToFramework(ctx, subscriber.SubscriberStatus) rd.RoleArn = fwflex.StringToFramework(ctx, subscriber.RoleArn) + sourcesOutput, d := flattenSubscriberSources(ctx, subscriber.Sources) + diags.Append(d...) rd.Sources = sourcesOutput rd.SubscriberName = fwflex.StringToFramework(ctx, subscriber.SubscriberName) rd.SubscriberDescription = fwflex.StringToFramework(ctx, subscriber.SubscriberDescription) diff --git a/internal/service/securitylake/subscriber_notification_test.go b/internal/service/securitylake/subscriber_notification_test.go index db98128e875f..9af420a1e9f8 100644 --- a/internal/service/securitylake/subscriber_notification_test.go +++ b/internal/service/securitylake/subscriber_notification_test.go @@ -8,7 +8,6 @@ import ( "fmt" "testing" - sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -22,13 +21,13 @@ func testAccSubscriberNotification_basic(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_subscriber_notification.test" - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := randomCustomLogSourceName() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -57,13 +56,13 @@ func testAccSubscriberNotification_https(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_subscriber_notification.test" - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := randomCustomLogSourceName() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -94,13 +93,13 @@ func testAccSubscriberNotification_disappears(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_subscriber_notification.test" - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := randomCustomLogSourceName() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -111,8 +110,6 @@ func testAccSubscriberNotification_disappears(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckSubscriberNotificationExists(ctx, resourceName), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfsecuritylake.ResourceSubscriberNotification, resourceName), - resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), - resource.TestCheckResourceAttr(resourceName, "configuration.0.sqs_notification_configuration.#", "1"), ), ExpectNonEmptyPlan: true, }, @@ -123,13 +120,13 @@ func testAccSubscriberNotification_disappears(t *testing.T) { func testAccSubscriberNotification_update(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_securitylake_subscriber_notification.test" - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := randomCustomLogSourceName() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.SecurityLake) - acctest.PreCheckOrganizationsAccount(ctx, t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.SecurityLakeServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -229,50 +226,73 @@ func testAccCheckSubscriberNotificationExists(ctx context.Context, n string) res func testAccSubscriberNotification_config(rName string) string { return acctest.ConfigCompose(testAccDataLakeConfig_basic(), fmt.Sprintf(` +resource "aws_securitylake_subscriber" "test" { + subscriber_name = %[1]q + + source { + custom_log_source_resource { + source_name = aws_securitylake_custom_log_source.test.source_name + source_version = aws_securitylake_custom_log_source.test.source_version + } + } + subscriber_identity { + external_id = "example" + principal = data.aws_caller_identity.current.account_id + } +} +resource "aws_securitylake_custom_log_source" "test" { + source_name = %[1]q -resource "aws_apigatewayv2_api" "test" { - name = %[1]q - protocol_type = "HTTP" + configuration { + crawler_configuration { + role_arn = aws_iam_role.test.arn + } + + provider_identity { + external_id = "%[1]s-test" + principal = data.aws_caller_identity.current.account_id + } + } + + depends_on = [aws_securitylake_data_lake.test, aws_iam_role.test] } resource "aws_iam_role" "test" { - name = "AmazonSecurityLakeCustomDataGlueCrawler-windows-sysmon" + name = %[1]q path = "/service-role/" assume_role_policy = < **NOTE:** A single `aws_securitylake_aws_log_source` should be used to configure a log source across all regions and accounts. + +~> **NOTE:** The underlying `aws_securitylake_data_lake` must be configured before creating the `aws_securitylake_aws_log_source`. Use a `depends_on` statement. + ## Example Usage ### Basic Usage ```terraform -resource "aws_securitylake_aws_log_source" "test" { +resource "aws_securitylake_aws_log_source" "example" { source { - accounts = ["123456789012"] - regions = ["eu-west-1"] - source_name = "ROUTE53" - source_version = "1.0" + accounts = ["123456789012"] + regions = ["eu-west-1"] + source_name = "ROUTE53" } + + depends_on = [aws_securitylake_data_lake.example] } ``` @@ -34,9 +39,12 @@ The following arguments are required: `source` supports the following: * `accounts` - (Optional) Specify the AWS account information where you want to enable Security Lake. + If not specified, uses all accounts included in the Security Lake. * `regions` - (Required) Specify the Regions where you want to enable Security Lake. * `source_name` - (Required) The name for a AWS source. This must be a Regionally unique value. Valid values: `ROUTE53`, `VPC_FLOW`, `SH_FINDINGS`, `CLOUD_TRAIL_MGMT`, `LAMBDA_EXECUTION`, `S3_DATA`. -* `source_version` - (Optional) The version for a AWS source. This must be a Regionally unique value. +* `source_version` - (Optional) The version for a AWS source. + If not specified, the version will be the default. + This must be a Regionally unique value. ## Attribute Reference diff --git a/website/docs/r/securitylake_custom_log_source.html.markdown b/website/docs/r/securitylake_custom_log_source.html.markdown index edf3caf0d809..a62d9a3fdeae 100644 --- a/website/docs/r/securitylake_custom_log_source.html.markdown +++ b/website/docs/r/securitylake_custom_log_source.html.markdown @@ -10,6 +10,8 @@ description: |- Terraform resource for managing an AWS Security Lake Custom Log Source. +~> **NOTE:** The underlying `aws_securitylake_data_lake` must be configured before creating the `aws_securitylake_custom_log_source`. Use a `depends_on` statement. + ## Example Usage ### Basic Usage @@ -30,6 +32,8 @@ resource "aws_securitylake_custom_log_source" "example" { principal = "123456789012" } } + + depends_on = [aws_securitylake_data_lake.example] } ``` @@ -43,8 +47,10 @@ This resource supports the following arguments: * `provider_identity` - (Required) The identity of the log provider for the third-party custom source. * `external_id` - (Required) The external ID used to estalish trust relationship with the AWS identity. * `principal` - (Required) The AWS identity principal. -* `event_classes` - (Required) The Open Cybersecurity Schema Framework (OCSF) event classes which describes the type of data that the custom source will send to Security Lake. -* `source_name` - (Required) Specify the name for a third-party custom source. This must be a Regionally unique value. +* `event_classes` - (Optional) The Open Cybersecurity Schema Framework (OCSF) event classes which describes the type of data that the custom source will send to Security Lake. +* `source_name` - (Required) Specify the name for a third-party custom source. + This must be a Regionally unique value. + Has a maximum length of 20. * `source_version` - (Optional) Specify the source version for the third-party custom source, to limit log collection to a specific version of custom data source. ## Attribute Reference diff --git a/website/docs/r/securitylake_data_lake.html.markdown b/website/docs/r/securitylake_data_lake.html.markdown index e983b587be76..f8234332a3c7 100644 --- a/website/docs/r/securitylake_data_lake.html.markdown +++ b/website/docs/r/securitylake_data_lake.html.markdown @@ -10,6 +10,8 @@ description: |- Terraform resource for managing an AWS Security Lake Data Lake. +~> **NOTE:** The underlying `aws_securitylake_data_lake` must be configured before creating other Security Lake resources. Use a `depends_on` statement. + ## Example Usage ```terraform diff --git a/website/docs/r/securitylake_subscriber.html.markdown b/website/docs/r/securitylake_subscriber.html.markdown index 98268632b977..96ecf03060c7 100644 --- a/website/docs/r/securitylake_subscriber.html.markdown +++ b/website/docs/r/securitylake_subscriber.html.markdown @@ -10,12 +10,13 @@ description: |- Terraform resource for managing an AWS Security Lake Subscriber. +~> **NOTE:** The underlying `aws_securitylake_data_lake` must be configured before creating the `aws_securitylake_subscriber`. Use a `depends_on` statement. + ## Example Usage ```terraform resource "aws_securitylake_subscriber" "example" { subscriber_name = "example-name" - source_version = "1.0" access_type = "S3" source { @@ -28,6 +29,8 @@ resource "aws_securitylake_subscriber" "example" { external_id = "example" principal = "1234567890" } + + depends_on = [aws_securitylake_data_lake.example] } ``` @@ -51,14 +54,14 @@ Sources support the following: * `aws_log_source_resource` - (Optional) Amazon Security Lake supports log and event collection for natively supported AWS services. * `custom_log_source_resource` - (Optional) Amazon Security Lake supports custom source types. -Aws Log Source Resource support the following: +AWS Log Source Resource support the following: -* `source_name` - (Optional) Provides data expiration details of Amazon Security Lake object. +* `source_name` - (Required) Provides data expiration details of Amazon Security Lake object. * `source_version` - (Optional) Provides data storage transition details of Amazon Security Lake object. Custom Log Source Resource support the following: -* `source_name` - (Optional) The name for a third-party custom source. This must be a Regionally unique value. +* `source_name` - (Required) The name for a third-party custom source. This must be a Regionally unique value. * `source_version` - (Optional) The version for a third-party custom source. This must be a Regionally unique value. ## Attribute Reference