diff --git a/aws/provider.go b/aws/provider.go index a26986ddda1..26e6ac22539 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -465,6 +465,7 @@ func Provider() *schema.Provider { "aws_config_delivery_channel": resourceAwsConfigDeliveryChannel(), "aws_config_organization_custom_rule": resourceAwsConfigOrganizationCustomRule(), "aws_config_organization_managed_rule": resourceAwsConfigOrganizationManagedRule(), + "aws_config_remediation_configuration": resourceAwsConfigRemediationConfiguration(), "aws_cognito_identity_pool": resourceAwsCognitoIdentityPool(), "aws_cognito_identity_pool_roles_attachment": resourceAwsCognitoIdentityPoolRolesAttachment(), "aws_cognito_identity_provider": resourceAwsCognitoIdentityProvider(), diff --git a/aws/resource_aws_config_remediation_configuration.go b/aws/resource_aws_config_remediation_configuration.go new file mode 100644 index 00000000000..ae8b66c7fb2 --- /dev/null +++ b/aws/resource_aws_config_remediation_configuration.go @@ -0,0 +1,240 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/configservice" +) + +func resourceAwsConfigRemediationConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsConfigRemediationConfigurationPut, + Read: resourceAwsConfigRemediationConfigurationRead, + Update: resourceAwsConfigRemediationConfigurationPut, + Delete: resourceAwsConfigRemediationConfigurationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "config_rule_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 64), + }, + "resource_type": { + Type: schema.TypeString, + Optional: true, + }, + "target_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 256), + }, + "target_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(configservice.RemediationTargetType_Values(), false), + }, + "target_version": { + Type: schema.TypeString, + Optional: true, + }, + "parameter": { + Type: schema.TypeSet, + MaxItems: 25, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "resource_value": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 256), + }, + "static_value": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func expandConfigRemediationConfigurationParameters(configured *schema.Set) (map[string]*configservice.RemediationParameterValue, error) { + results := make(map[string]*configservice.RemediationParameterValue) + + for _, item := range configured.List() { + detail := item.(map[string]interface{}) + rpv := configservice.RemediationParameterValue{} + resourceName, ok := detail["name"].(string) + if ok { + results[resourceName] = &rpv + } else { + return nil, fmt.Errorf("Could not extract name from parameter.") + } + if resourceValue, ok := detail["resource_value"].(string); ok && len(resourceValue) > 0 { + rpv.ResourceValue = &configservice.ResourceValue{ + Value: &resourceValue, + } + } else if staticValue, ok := detail["static_value"].(string); ok && len(staticValue) > 0 { + rpv.StaticValue = &configservice.StaticValue{ + Values: []*string{&staticValue}, + } + } else { + return nil, fmt.Errorf("Parameter '%s' needs one of resource_value or static_value", resourceName) + } + } + + return results, nil +} + +func flattenRemediationConfigurationParameters(parameters map[string]*configservice.RemediationParameterValue) []interface{} { + var items []interface{} + + for key, value := range parameters { + item := make(map[string]interface{}) + item["name"] = key + if value.ResourceValue != nil { + item["resource_value"] = *value.ResourceValue.Value + } + if value.StaticValue != nil && len(value.StaticValue.Values) > 0 { + item["static_value"] = *value.StaticValue.Values[0] + } + + items = append(items, item) + } + + return items +} + +func resourceAwsConfigRemediationConfigurationPut(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).configconn + + name := d.Get("config_rule_name").(string) + remediationConfigurationInput := configservice.RemediationConfiguration{ + ConfigRuleName: aws.String(name), + } + + if v, ok := d.GetOk("parameter"); ok { + params, err := expandConfigRemediationConfigurationParameters(v.(*schema.Set)) + if err != nil { + return err + } + remediationConfigurationInput.Parameters = params + } + if v, ok := d.GetOk("resource_type"); ok { + remediationConfigurationInput.ResourceType = aws.String(v.(string)) + } + if v, ok := d.GetOk("target_id"); ok { + remediationConfigurationInput.TargetId = aws.String(v.(string)) + } + if v, ok := d.GetOk("target_type"); ok { + remediationConfigurationInput.TargetType = aws.String(v.(string)) + } + if v, ok := d.GetOk("target_version"); ok { + remediationConfigurationInput.TargetVersion = aws.String(v.(string)) + } + + input := configservice.PutRemediationConfigurationsInput{ + RemediationConfigurations: []*configservice.RemediationConfiguration{&remediationConfigurationInput}, + } + log.Printf("[DEBUG] Creating AWSConfig remediation configuration: %s", input) + _, err := conn.PutRemediationConfigurations(&input) + if err != nil { + return fmt.Errorf("Failed to create AWSConfig remediation configuration: %w", err) + } + + d.SetId(name) + + log.Printf("[DEBUG] AWSConfig config remediation configuration for rule %q created", name) + + return resourceAwsConfigRemediationConfigurationRead(d, meta) +} + +func resourceAwsConfigRemediationConfigurationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).configconn + out, err := conn.DescribeRemediationConfigurations(&configservice.DescribeRemediationConfigurationsInput{ + ConfigRuleNames: []*string{aws.String(d.Id())}, + }) + if err != nil { + if isAWSErr(err, configservice.ErrCodeNoSuchConfigRuleException, "") { + log.Printf("[WARN] Config Rule %q is gone (NoSuchConfigRuleException)", d.Id()) + d.SetId("") + return nil + } + return err + } + + numberOfRemediationConfigurations := len(out.RemediationConfigurations) + if numberOfRemediationConfigurations < 1 { + log.Printf("[WARN] No Remediation Configuration for Config Rule %q (no remediation configuration found)", d.Id()) + d.SetId("") + return nil + } + + log.Printf("[DEBUG] AWS Config remediation configurations received: %s", out) + + remediationConfiguration := out.RemediationConfigurations[0] + d.Set("arn", remediationConfiguration.Arn) + d.Set("config_rule_name", remediationConfiguration.ConfigRuleName) + d.Set("resource_type", remediationConfiguration.ResourceType) + d.Set("target_id", remediationConfiguration.TargetId) + d.Set("target_type", remediationConfiguration.TargetType) + d.Set("target_version", remediationConfiguration.TargetVersion) + d.Set("parameter", flattenRemediationConfigurationParameters(remediationConfiguration.Parameters)) + d.SetId(*remediationConfiguration.ConfigRuleName) + + return nil +} + +func resourceAwsConfigRemediationConfigurationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).configconn + + name := d.Get("config_rule_name").(string) + + deleteRemediationConfigurationInput := configservice.DeleteRemediationConfigurationInput{ + ConfigRuleName: aws.String(name), + } + + if v, ok := d.GetOk("resource_type"); ok { + deleteRemediationConfigurationInput.ResourceType = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Deleting AWS Config remediation configurations for rule %q", name) + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteRemediationConfiguration(&deleteRemediationConfigurationInput) + if err != nil { + if isAWSErr(err, configservice.ErrCodeResourceInUseException, "") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return fmt.Errorf("Deleting Remediation Configurations failed: %s", err) + } + + log.Printf("[DEBUG] AWS Config remediation configurations for rule %q deleted", name) + + return nil +} diff --git a/aws/resource_aws_config_remediation_configuration_test.go b/aws/resource_aws_config_remediation_configuration_test.go new file mode 100644 index 00000000000..f5a1e044af4 --- /dev/null +++ b/aws/resource_aws_config_remediation_configuration_test.go @@ -0,0 +1,293 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/configservice" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func testAccConfigRemediationConfiguration_basic(t *testing.T) { + var rc configservice.RemediationConfiguration + resourceName := "aws_config_remediation_configuration.test" + rInt := acctest.RandInt() + prefix := "Original" + sseAlgorithm := "AES256" + expectedName := fmt.Sprintf("%s-tf-acc-test-%d", prefix, rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckConfigRemediationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfigRemediationConfigurationConfig(prefix, sseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &rc), + resource.TestCheckResourceAttr(resourceName, "config_rule_name", expectedName), + resource.TestCheckResourceAttr(resourceName, "target_id", "AWS-EnableS3BucketEncryption"), + resource.TestCheckResourceAttr(resourceName, "target_type", "SSM_DOCUMENT"), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccConfigRemediationConfiguration_disappears(t *testing.T) { + var rc configservice.RemediationConfiguration + resourceName := "aws_config_remediation_configuration.test" + rInt := acctest.RandInt() + prefix := "original" + sseAlgorithm := "AES256" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckConfigRemediationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfigRemediationConfigurationConfig(prefix, sseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &rc), + testAccCheckResourceDisappears(testAccProvider, resourceAwsConfigRemediationConfiguration(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccConfigRemediationConfiguration_recreates(t *testing.T) { + var original configservice.RemediationConfiguration + var updated configservice.RemediationConfiguration + resourceName := "aws_config_remediation_configuration.test" + rInt := acctest.RandInt() + + originalName := "Original" + updatedName := "Updated" + sseAlgorithm := "AES256" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckConfigRemediationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfigRemediationConfigurationConfig(originalName, sseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &original), + resource.TestCheckResourceAttr(resourceName, "config_rule_name", fmt.Sprintf("%s-tf-acc-test-%d", originalName, rInt)), + ), + }, + { + Config: testAccConfigRemediationConfigurationConfig(updatedName, sseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &updated), + testAccCheckConfigRemediationConfigurationRecreated(t, &original, &updated), + resource.TestCheckResourceAttr(resourceName, "config_rule_name", fmt.Sprintf("%s-tf-acc-test-%d", updatedName, rInt)), + ), + }, + }, + }) +} + +func testAccConfigRemediationConfiguration_updates(t *testing.T) { + var original configservice.RemediationConfiguration + var updated configservice.RemediationConfiguration + resourceName := "aws_config_remediation_configuration.test" + rInt := acctest.RandInt() + + name := "Original" + originalSseAlgorithm := "AES256" + updatedSseAlgorithm := "aws:kms" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckConfigRemediationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfigRemediationConfigurationConfig(name, originalSseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &original), + resource.TestCheckResourceAttr(resourceName, "parameter.2.static_value", originalSseAlgorithm), + ), + }, + { + Config: testAccConfigRemediationConfigurationConfig(name, updatedSseAlgorithm, rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckConfigRemediationConfigurationExists(resourceName, &updated), + testAccCheckConfigRemediationConfigurationNotRecreated(t, &original, &updated), + resource.TestCheckResourceAttr(resourceName, "parameter.2.static_value", updatedSseAlgorithm), + ), + }, + }, + }) +} + +func testAccCheckConfigRemediationConfigurationExists(n string, obj *configservice.RemediationConfiguration) 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 config rule ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).configconn + out, err := conn.DescribeRemediationConfigurations(&configservice.DescribeRemediationConfigurationsInput{ + ConfigRuleNames: []*string{aws.String(rs.Primary.Attributes["config_rule_name"])}, + }) + if err != nil { + return fmt.Errorf("Failed to describe config rule: %s", err) + } + if len(out.RemediationConfigurations) < 1 { + return fmt.Errorf("No config rule found when describing %q", rs.Primary.Attributes["name"]) + } + + rc := out.RemediationConfigurations[0] + *obj = *rc + + return nil + } +} + +func testAccCheckConfigRemediationConfigurationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).configconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_config_remediation_configuration" { + continue + } + + resp, err := conn.DescribeRemediationConfigurations(&configservice.DescribeRemediationConfigurationsInput{ + ConfigRuleNames: []*string{aws.String(rs.Primary.Attributes["config_rule_name"])}, + }) + + if err == nil { + if len(resp.RemediationConfigurations) != 0 && + *resp.RemediationConfigurations[0].ConfigRuleName == rs.Primary.Attributes["name"] { + return fmt.Errorf("remediation configuration(s) still exist for rule: %s", rs.Primary.Attributes["name"]) + } + } + } + + return nil +} + +func testAccConfigRemediationConfigurationConfig(namePrefix, sseAlgorithm string, randInt int) string { + return fmt.Sprintf(` +resource "aws_config_remediation_configuration" "test" { + config_rule_name = aws_config_config_rule.test.name + + resource_type = "AWS::S3::Bucket" + target_id = "AWS-EnableS3BucketEncryption" + target_type = "SSM_DOCUMENT" + target_version = "1" + + parameter { + name = "AutomationAssumeRole" + static_value = aws_iam_role.test.arn + } + parameter { + name = "BucketName" + resource_value = "RESOURCE_ID" + } + parameter { + name = "SSEAlgorithm" + static_value = "%[2]s" + } +} + +resource "aws_sns_topic" "test" { + name = "sns_topic_name" +} + +resource "aws_config_config_rule" "test" { + name = "%[1]s-tf-acc-test-%[3]d" + + source { + owner = "AWS" + source_identifier = "S3_BUCKET_VERSIONING_ENABLED" + } + + depends_on = [aws_config_configuration_recorder.test] +} + +resource "aws_config_configuration_recorder" "test" { + name = "%[1]s-tf-acc-test-%[3]d" + role_arn = aws_iam_role.test.arn +} + +resource "aws_iam_role" "test" { + name = "%[1]s-tf-acc-test-awsconfig-%[3]d" + + assume_role_policy = < **Note:** Config Remediation Configuration requires an existing [Config Rule](/docs/providers/aws/r/config_config_rule.html) to be present. + +## Example Usage + +AWS managed rules can be used by setting the source owner to `AWS` and the source identifier to the name of the managed rule. More information about AWS managed rules can be found in the [AWS Config Developer Guide](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html). + +```hcl +resource "aws_config_config_rule" "this" { + name = "example" + + source { + owner = "AWS" + source_identifier = "S3_BUCKET_VERSIONING_ENABLED" + } +} + +resource "aws_config_remediation_configuration" "this" { + config_rule_name = aws_config_config_rule.this.name + resource_type = "AWS::S3::Bucket" + target_type = "SSM_DOCUMENT" + target_id = "AWS-EnableS3BucketEncryption" + target_version = "1" + + parameter { + name = "AutomationAssumeRole" + static_value = "arn:aws:iam::875924563244:role/security_config" + } + parameter { + name = "BucketName" + resource_value = "RESOURCE_ID" + } + parameter { + name = "SSEAlgorithm" + static_value = "AES256" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `config_rule_name` - (Required) The name of the AWS Config rule +* `resource_type` - (Optional) The type of a resource +* `target_id` - (Required) Target ID is the name of the public document +* `target_type` - (Required) The type of the target. Target executes remediation. For example, SSM document +* `target_version` - (Optional) Version of the target. For example, version of the SSM document +* `parameter` - (Optional) Can be specified multiple times for each + parameter. Each parameter block supports fields documented below. + +The `parameter` block supports: + +The value is either a dynamic (resource) value or a static value. +You must select either a dynamic value or a static value. + +* `name` - (Required) The name of the attribute. +* `resource_value` - (Optional) The value is dynamic and changes at run-time. +* `static_value` - (Optional) The value is static and does not change at run-time. + +## Import + +Remediation Configurations can be imported using the name config_rule_name, e.g. + +``` +$ terraform import aws_config_remediation_configuration.this example +```