diff --git a/.changelog/21560.txt b/.changelog/21560.txt new file mode 100644 index 000000000000..d4d4d07906d6 --- /dev/null +++ b/.changelog/21560.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_finding_aggregator +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7471764fb8fd..921b8f9f75ef 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1519,6 +1519,7 @@ func Provider() *schema.Provider { "aws_securityhub_product_subscription": securityhub.ResourceProductSubscription(), "aws_securityhub_standards_control": securityhub.ResourceStandardsControl(), "aws_securityhub_standards_subscription": securityhub.ResourceStandardsSubscription(), + "aws_securityhub_finding_aggregator": securityhub.ResourceFindingAggregator(), "aws_serverlessapplicationrepository_cloudformation_stack": serverlessapprepo.ResourceCloudFormationStack(), diff --git a/internal/service/securityhub/finding_aggregator.go b/internal/service/securityhub/finding_aggregator.go new file mode 100644 index 000000000000..a8e93810f9b3 --- /dev/null +++ b/internal/service/securityhub/finding_aggregator.go @@ -0,0 +1,176 @@ +package securityhub + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/flex" +) + +const ( + allRegions = "ALL_REGIONS" + allRegionsExceptSpecified = "ALL_REGIONS_EXCEPT_SPECIFIED" + specifiedRegions = "SPECIFIED_REGIONS" +) + +func ResourceFindingAggregator() *schema.Resource { + return &schema.Resource{ + Create: resourceFindingAggregatorCreate, + Read: resourceFindingAggregatorRead, + Update: resourceFindingAggregatorUpdate, + Delete: resourceFindingAggregatorDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "linking_mode": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + allRegions, + allRegionsExceptSpecified, + specifiedRegions, + }, false), + }, + "specified_regions": { + Type: schema.TypeSet, + MinItems: 1, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceFindingAggregatorCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SecurityHubConn + + linkingMode := d.Get("linking_mode").(string) + + req := &securityhub.CreateFindingAggregatorInput{ + RegionLinkingMode: &linkingMode, + } + + if v, ok := d.GetOk("specified_regions"); ok && (linkingMode == allRegionsExceptSpecified || linkingMode == specifiedRegions) { + req.Regions = flex.ExpandStringSet(v.(*schema.Set)) + } + + log.Printf("[DEBUG] Creating Security Hub finding aggregator") + + resp, err := conn.CreateFindingAggregator(req) + + if err != nil { + return fmt.Errorf("Error creating finding aggregator for Security Hub: %s", err) + } + + d.SetId(aws.StringValue(resp.FindingAggregatorArn)) + + return resourceFindingAggregatorRead(d, meta) +} + +func resourceFindingAggregatorRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SecurityHubConn + + aggregatorArn := d.Id() + + log.Printf("[DEBUG] Reading Security Hub finding aggregator to find %s", aggregatorArn) + + aggregator, err := FindingAggregatorCheckExists(conn, aggregatorArn) + + if err != nil { + return fmt.Errorf("Error reading Security Hub finding aggregator to find %s: %s", aggregatorArn, err) + } + + if aggregator == nil { + log.Printf("[WARN] Security Hub finding aggregator (%s) not found, removing from state", aggregatorArn) + d.SetId("") + return nil + } + + d.Set("linking_mode", aggregator.RegionLinkingMode) + + if len(aggregator.Regions) > 0 { + d.Set("specified_regions", flex.FlattenStringList(aggregator.Regions)) + } + + return nil +} + +func FindingAggregatorCheckExists(conn *securityhub.SecurityHub, findingAggregatorArn string) (*securityhub.GetFindingAggregatorOutput, error) { + input := &securityhub.ListFindingAggregatorsInput{} + + var found *securityhub.GetFindingAggregatorOutput + var err error = nil + + err = conn.ListFindingAggregatorsPages(input, func(page *securityhub.ListFindingAggregatorsOutput, lastPage bool) bool { + for _, aggregator := range page.FindingAggregators { + if aws.StringValue(aggregator.FindingAggregatorArn) == findingAggregatorArn { + getInput := &securityhub.GetFindingAggregatorInput{ + FindingAggregatorArn: &findingAggregatorArn, + } + found, err = conn.GetFindingAggregator(getInput) + return false + } + } + return !lastPage + }) + + if err != nil { + return nil, err + } + + return found, nil +} + +func resourceFindingAggregatorUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SecurityHubConn + + aggregatorArn := d.Id() + + linkingMode := d.Get("linking_mode").(string) + + req := &securityhub.UpdateFindingAggregatorInput{ + FindingAggregatorArn: &aggregatorArn, + RegionLinkingMode: &linkingMode, + } + + if v, ok := d.GetOk("specified_regions"); ok && (linkingMode == allRegionsExceptSpecified || linkingMode == specifiedRegions) { + req.Regions = flex.ExpandStringSet(v.(*schema.Set)) + } + + resp, err := conn.UpdateFindingAggregator(req) + + if err != nil { + return fmt.Errorf("Error updating Security Hub finding aggregator (%s): %w", aggregatorArn, err) + } + + d.SetId(aws.StringValue(resp.FindingAggregatorArn)) + + return resourceFindingAggregatorRead(d, meta) +} + +func resourceFindingAggregatorDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).SecurityHubConn + + aggregatorArn := d.Id() + + log.Printf("[DEBUG] Disabling Security Hub finding aggregator %s", aggregatorArn) + + _, err := conn.DeleteFindingAggregator(&securityhub.DeleteFindingAggregatorInput{ + FindingAggregatorArn: &aggregatorArn, + }) + + if err != nil { + return fmt.Errorf("Error disabling Security Hub finding aggregator %s: %s", aggregatorArn, err) + } + + return nil +} diff --git a/internal/service/securityhub/finding_aggregator_test.go b/internal/service/securityhub/finding_aggregator_test.go new file mode 100644 index 000000000000..15eefa62d34e --- /dev/null +++ b/internal/service/securityhub/finding_aggregator_test.go @@ -0,0 +1,167 @@ +package securityhub_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "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" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfsecurityhub "github.com/hashicorp/terraform-provider-aws/internal/service/securityhub" +) + +func testAccFindingAggregator_basic(t *testing.T) { + resourceName := "aws_securityhub_finding_aggregator.test_aggregator" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, securityhub.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckFindingAggregatorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccFindingAggregatorAllRegionsConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckFindingAggregatorExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "linking_mode", "ALL_REGIONS"), + resource.TestCheckNoResourceAttr(resourceName, "specified_regions"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFindingAggregatorSpecifiedRegionsConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckFindingAggregatorExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "linking_mode", "SPECIFIED_REGIONS"), + resource.TestCheckResourceAttr(resourceName, "specified_regions.#", "3"), + ), + }, + { + Config: testAccFindingAggregatorAllRegionsExceptSpecifiedConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckFindingAggregatorExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "linking_mode", "ALL_REGIONS_EXCEPT_SPECIFIED"), + resource.TestCheckResourceAttr(resourceName, "specified_regions.#", "2"), + ), + }, + }, + }) +} + +func testAccFindingAggregator_disappears(t *testing.T) { + resourceName := "aws_securityhub_finding_aggregator.test_aggregator" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, securityhub.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckFindingAggregatorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccFindingAggregatorAllRegionsConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckFindingAggregatorExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfsecurityhub.ResourceFindingAggregator(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckFindingAggregatorExists(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) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Security Hub finding aggregator ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).SecurityHubConn + + _, err := conn.GetFindingAggregator(&securityhub.GetFindingAggregatorInput{ + FindingAggregatorArn: &rs.Primary.ID, + }) + + if err != nil { + return fmt.Errorf("Failed to get finding aggregator: %s", err) + } + + return nil + } +} + +func testAccCheckFindingAggregatorDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SecurityHubConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_finding_aggregator" { + continue + } + + _, err := conn.GetFindingAggregator(&securityhub.GetFindingAggregatorInput{ + FindingAggregatorArn: &rs.Primary.ID, + }) + + if tfawserr.ErrMessageContains(err, securityhub.ErrCodeInvalidAccessException, "not subscribed to AWS Security Hub") { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Security Hub Finding Aggregator %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccFindingAggregatorAllRegionsConfig() string { + return ` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "test_aggregator" { + linking_mode = "ALL_REGIONS" + + depends_on = [aws_securityhub_account.example] +} +` +} + +func testAccFindingAggregatorSpecifiedRegionsConfig() string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "test_aggregator" { + linking_mode = "SPECIFIED_REGIONS" + specified_regions = ["%s", "%s", "%s"] + + depends_on = [aws_securityhub_account.example] +} +`, endpoints.EuWest1RegionID, endpoints.EuWest2RegionID, endpoints.UsEast1RegionID) +} + +func testAccFindingAggregatorAllRegionsExceptSpecifiedConfig() string { + return fmt.Sprintf(` +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "test_aggregator" { + linking_mode = "ALL_REGIONS_EXCEPT_SPECIFIED" + specified_regions = ["%s", "%s"] + + depends_on = [aws_securityhub_account.example] +} +`, endpoints.EuWest1RegionID, endpoints.EuWest2RegionID) +} diff --git a/internal/service/securityhub/securityhub_test.go b/internal/service/securityhub/securityhub_test.go index 969997b56e9b..b6b7aacaeb0b 100644 --- a/internal/service/securityhub/securityhub_test.go +++ b/internal/service/securityhub/securityhub_test.go @@ -55,6 +55,10 @@ func TestAccSecurityHub_serial(t *testing.T) { "basic": testAccStandardsSubscription_basic, "disappears": testAccStandardsSubscription_disappears, }, + "FindingAggregator": { + "basic": testAccFindingAggregator_basic, + "disappears": testAccFindingAggregator_disappears, + }, } for group, m := range testCases { diff --git a/website/docs/r/securityhub_finding_aggregator.markdown b/website/docs/r/securityhub_finding_aggregator.markdown new file mode 100644 index 000000000000..51c53ded0af9 --- /dev/null +++ b/website/docs/r/securityhub_finding_aggregator.markdown @@ -0,0 +1,78 @@ +--- +subcategory: "Security Hub" +layout: "aws" +page_title: "AWS: aws_securityhub_finding_aggregator" +description: |- + Manages a Security Hub finding aggregator +--- + +# Resource: aws_securityhub_finding_aggregator + +Manages a Security Hub finding aggregator. Security Hub needs to be enabled in a region in order for the aggregator to pull through findings. + +## Example Usage + +### All Regions Usage + +The following example will enable the aggregator for every region. + +```terraform +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "example" { + linking_mode = "ALL_REGIONS" + + depends_on = [aws_securityhub_account.example] +} +``` + +### All Regions Except Specified Regions Usage + +The following example will enable the aggregator for every region except those specified in `specified_regions`. + +```terraform +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "example" { + linking_mode = "ALL_REGIONS_EXCEPT_SPECIFIED" + specified_regions = ["eu-west-1", "eu-west-2"] + + depends_on = [aws_securityhub_account.example] +} +``` + +### Specified Regions Usage + +The following example will enable the aggregator for every region specified in `specified_regions`. + +```terraform +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_finding_aggregator" "example" { + linking_mode = "SPECIFIED_REGIONS" + specified_regions = ["eu-west-1", "eu-west-2"] + + depends_on = [aws_securityhub_account.example] +} +``` + +## Argument Reference + +The following arguments are supported: + +- `linking_mode` - (Required) Indicates whether to aggregate findings from all of the available Regions or from a specified list. The options are `ALL_REGIONS`, `ALL_REGIONS_EXCEPT_SPECIFIED` or `SPECIFIED_REGIONS`. When `ALL_REGIONS` or `ALL_REGIONS_EXCEPT_SPECIFIED` are used, Security Hub will automatically aggregate findings from new Regions as Security Hub supports them and you opt into them. +- `specified_regions` - (Optional) List of regions to include or exclude (required if `linking_mode` is set to `ALL_REGIONS_EXCEPT_SPECIFIED` or `SPECIFIED_REGIONS`) + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `arn` - Amazon Resource Name (ARN) of the Security Hub finding aggregator. + +## Import + +An existing Security Hub finding aggregator can be imported using the `arn`, e.g., + +``` +$ terraform import aws_securityhub_finding_aggregator.example arn:aws:securityhub:eu-west-1:123456789098:finding-aggregator/abcd1234-abcd-1234-1234-abcdef123456 +```