diff --git a/.changelog/44426.txt b/.changelog/44426.txt new file mode 100644 index 000000000000..e426b2f1a6be --- /dev/null +++ b/.changelog/44426.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl_rule_group_association: Added support for `managed_rule_group_configs` within `managed_rule_group` and root-level `visibility_config` block for CloudWatch metrics configuration +``` diff --git a/internal/service/wafv2/flex.go b/internal/service/wafv2/flex.go index c0809dbf0af3..3d0acdeb2cf9 100644 --- a/internal/service/wafv2/flex.go +++ b/internal/service/wafv2/flex.go @@ -4,6 +4,7 @@ package wafv2 import ( + "context" "errors" "fmt" "reflect" @@ -11,8 +12,14 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" awstypes "github.com/aws/aws-sdk-go-v2/service/wafv2/types" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/flex" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" tfjson "github.com/hashicorp/terraform-provider-aws/internal/json" itypes "github.com/hashicorp/terraform-provider-aws/internal/types" "github.com/hashicorp/terraform-provider-aws/names" @@ -3635,3 +3642,151 @@ func flattenFieldToProtect(apiObject *awstypes.FieldToProtect) any { return []any{tfMap} } + +func expandACFPManagedRuleConfig( + ctx context.Context, + tfACFPRuleConfigModel awsManagedRulesACFPRuleSetModel, +) (awstypes.AWSManagedRulesACFPRuleSet, diag.Diagnostics) { + var diags diag.Diagnostics + var acfpRuleSetConfig awstypes.AWSManagedRulesACFPRuleSet + + if tfACFPRuleConfigModel.RequestInspection.IsNull() && tfACFPRuleConfigModel.RequestInspection.IsUnknown() { + // auto flex works, no need to manually expand + diags.Append(fwflex.Expand(ctx, tfACFPRuleConfigModel, &acfpRuleSetConfig)...) + return acfpRuleSetConfig, diags + } + + acfpRuleSetConfig = awstypes.AWSManagedRulesACFPRuleSet{ + CreationPath: tfACFPRuleConfigModel.CreationPath.ValueStringPointer(), + RegistrationPagePath: tfACFPRuleConfigModel.RegistrationPagePath.ValueStringPointer(), + EnableRegexInPath: tfACFPRuleConfigModel.EnableRegexInPath.ValueBool(), + } + + var tfReqInspection requestInspectionACFPModel + diags.Append(tfACFPRuleConfigModel.RequestInspection.Elements()[0].(fwtypes.ObjectValueOf[requestInspectionACFPModel]).As(ctx, &tfReqInspection, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + + addressFields := make([]awstypes.AddressField, 0) + + for _, af := range tfReqInspection.AddressFields.Elements() { + var tfAddressField addressFieldModel + diags.Append(af.(fwtypes.ObjectValueOf[addressFieldModel]).As(ctx, &tfAddressField, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + for _, id := range tfAddressField.Identifiers.Elements() { + addressFields = append(addressFields, awstypes.AddressField{ + Identifier: id.(types.String).ValueStringPointer(), + }) + } + } + + phoneNumberFields := make([]awstypes.PhoneNumberField, 0) + for _, pf := range tfReqInspection.PhoneNumberFields.Elements() { + var tfPhoneNumberField phoneNumberFieldModel + diags.Append(pf.(fwtypes.ObjectValueOf[phoneNumberFieldModel]).As(ctx, &tfPhoneNumberField, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + for _, id := range tfPhoneNumberField.Identifiers.Elements() { + phoneNumberFields = append(phoneNumberFields, awstypes.PhoneNumberField{ + Identifier: id.(types.String).ValueStringPointer(), + }) + } + } + + acfpRuleSetConfig.RequestInspection = &awstypes.RequestInspectionACFP{ + AddressFields: addressFields, + PhoneNumberFields: phoneNumberFields, + PayloadType: awstypes.PayloadType(tfReqInspection.PayloadType.ValueString()), + } + + diags.Append(fwflex.Expand(ctx, tfReqInspection.EmailField, &acfpRuleSetConfig.RequestInspection.EmailField)...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + + diags.Append(fwflex.Expand(ctx, tfReqInspection.UsernameField, &acfpRuleSetConfig.RequestInspection.UsernameField)...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + + diags.Append(fwflex.Expand(ctx, tfReqInspection.PasswordField, &acfpRuleSetConfig.RequestInspection.PasswordField)...) + if diags.HasError() { + return acfpRuleSetConfig, diags + } + + return acfpRuleSetConfig, diags +} + +func flattenACFPRuleConfig( + ctx context.Context, + acfp *awstypes.AWSManagedRulesACFPRuleSet, + managedRuleGroupConfigs []awstypes.ManagedRuleGroupConfig, +) (fwtypes.ListNestedObjectValueOf[managedRuleGroupConfigModel], diag.Diagnostics) { + var diags diag.Diagnostics + + if acfp.RequestInspection == nil { + // autoflex is possible + var tfManagedRuleGroupConfigModel managedRuleGroupConfigModel + diags.Append(fwflex.Flatten(ctx, managedRuleGroupConfigs, &tfManagedRuleGroupConfigModel)...) + return fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*managedRuleGroupConfigModel{&tfManagedRuleGroupConfigModel}), diags + } + + requestInspection := acfp.RequestInspection + var emailField emailFieldModel + var usernameField usernameFieldModel + var passwordField passwordFieldModel + + fwflex.Flatten(ctx, requestInspection.EmailField, &emailField) + fwflex.Flatten(ctx, requestInspection.UsernameField, &usernameField) + fwflex.Flatten(ctx, requestInspection.PasswordField, &passwordField) + + var addrList []attr.Value + for _, af := range requestInspection.AddressFields { + addrList = append(addrList, types.StringValue(aws.ToString(af.Identifier))) + } + ids := fwtypes.NewListValueOfMust[types.String](ctx, addrList) + afModel := &addressFieldModel{ + Identifiers: ids, + } + + var phoneList []attr.Value + for _, pf := range requestInspection.PhoneNumberFields { + phoneList = append(phoneList, types.StringValue(aws.ToString(pf.Identifier))) + } + pfModel := &phoneNumberFieldModel{ + Identifiers: fwtypes.NewListValueOfMust[types.String](ctx, phoneList), + } + + tfRequestInspection := &requestInspectionACFPModel{ + PayloadType: types.StringValue(string(requestInspection.PayloadType)), + EmailField: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*emailFieldModel{&emailField}), + UsernameField: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*usernameFieldModel{&usernameField}), + PasswordField: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*passwordFieldModel{&passwordField}), + AddressFields: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*addressFieldModel{afModel}), + PhoneNumberFields: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*phoneNumberFieldModel{pfModel}), + } + + tfACFPModel := &awsManagedRulesACFPRuleSetModel{ + CreationPath: types.StringValue(aws.ToString(acfp.CreationPath)), + RegistrationPagePath: types.StringValue(aws.ToString(acfp.RegistrationPagePath)), + EnableRegexInPath: types.BoolValue(acfp.EnableRegexInPath), + ResponseInspection: fwtypes.NewListNestedObjectValueOfNull[responseInspectionModel](ctx), + RequestInspection: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*requestInspectionACFPModel{tfRequestInspection}), + } + + patchedRuleGroupConfig := make([]*managedRuleGroupConfigModel, 0, 1) + + tfManagedRuleGroupConfigModel := &managedRuleGroupConfigModel{ + AWSManagedRulesACFPRuleSet: fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*awsManagedRulesACFPRuleSetModel{tfACFPModel}), + AWSManagedRulesATPRuleSet: fwtypes.NewListNestedObjectValueOfNull[awsManagedRulesATPRuleSetModel](ctx), + AWSManagedRulesBotControlRuleSet: fwtypes.NewListNestedObjectValueOfNull[awsManagedRulesBotControlRuleSetModel](ctx), + AWSManagedRulesAntiDDoSRuleSet: fwtypes.NewListNestedObjectValueOfNull[awsManagedRulesAntiDDoSRuleSetModel](ctx), + } + patchedRuleGroupConfig = append(patchedRuleGroupConfig, tfManagedRuleGroupConfigModel) + + return fwtypes.NewListNestedObjectValueOfSliceMust(ctx, patchedRuleGroupConfig), diags +} diff --git a/internal/service/wafv2/web_acl_rule_group_association.go b/internal/service/wafv2/web_acl_rule_group_association.go index a21361204a9c..0671b39758eb 100644 --- a/internal/service/wafv2/web_acl_rule_group_association.go +++ b/internal/service/wafv2/web_acl_rule_group_association.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/service/wafv2" @@ -17,10 +18,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -64,6 +68,431 @@ type resourceWebACLRuleGroupAssociation struct { } func (r *resourceWebACLRuleGroupAssociation) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + usernameFieldLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[usernameFieldModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrIdentifier: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 512), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + Description: "Identifier of the username field", + }, + }, + }, + } + + passwordFieldLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[passwordFieldModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrIdentifier: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 512), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + Description: "Identifier of the password field", + }, + }, + }, + } + + managedRulegroupConfigACFPRequestInspectionLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[requestInspectionACFPModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "payload_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.PayloadType](), + Required: true, + Description: "Payload type for inspection, either JSON or FORM_ENCODED.", + }, + }, + Blocks: map[string]schema.Block{ + "username_field": usernameFieldLNB, + "address_fields": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[addressFieldModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(0), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "identifiers": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + Description: "Identifiers of the address fields", + }, + }, + }, + }, + "email_field": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[emailFieldModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(0), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrIdentifier: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 512), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + Description: "Identifier of the email field", + }, + }, + }, + }, + "password_field": passwordFieldLNB, + "phone_number_fields": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[phoneNumberFieldModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(0), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "identifiers": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + Description: "Identifiers of the phone number fields", + }, + }, + }, + }, + }, + }, + } + managedRulegroupConfigATPRequestInspectionLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[requestInspectionModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "payload_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.PayloadType](), + Required: true, + Description: "Payload type for inspection, either JSON or FORM_ENCODED.", + }, + }, + Blocks: map[string]schema.Block{ + "password_field": passwordFieldLNB, + "username_field": usernameFieldLNB, + }, + }, + } + managedRulegroupConfigResponseInspectionLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[responseInspectionModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "body_contains": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[responseInspectionBodyContainsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "failure_strings": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a failed login or account creation attempt", + }, + "success_strings": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a successful login or account creation attempt", + }, + }, + }, + }, + names.AttrHeader: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[responseInspectionHeaderModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 256), + }, + Description: "Name of the HTTP header to inspect", + }, + "failure_values": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a failed login or account creation attempt", + }, + "success_values": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a successful login or account creation attempt", + }, + }, + }, + }, + names.AttrJSON: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[responseInspectionJsonModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrIdentifier: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 256), + }, + Description: "Identifier of the JSON field to inspect", + }, + "failure_values": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a failed login or account creation attempt", + }, + "success_values": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Description: "Strings that indicate a successful login or account creation attempt", + }, + }, + }, + }, + names.AttrStatusCode: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[responseInspectionStatusCodeModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "failure_codes": schema.SetAttribute{ + Required: true, + ElementType: types.Int32Type, + Description: "Status codes that indicate a failed login or account creation attempt", + }, + "success_codes": schema.SetAttribute{ + Required: true, + ElementType: types.Int32Type, + Description: "Status codes that indicate a successful login or account creation attempt", + }, + }, + }, + }, + }, + }, + } + managedRulegroupConfigLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[managedRuleGroupConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "aws_managed_rules_acfp_rule_set": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[awsManagedRulesACFPRuleSetModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "creation_path": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 256), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + Description: "Path to the account creation endpoint on the protected website", + }, + "enable_regex_in_path": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "registration_page_path": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 256), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + }, + }, + Blocks: map[string]schema.Block{ + "request_inspection": managedRulegroupConfigACFPRequestInspectionLNB, + "response_inspection": managedRulegroupConfigResponseInspectionLNB, + }, + }, + }, + "aws_managed_rules_anti_ddos_rule_set": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[awsManagedRulesAntiDDoSRuleSetModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "sensitivity_to_block": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.SensitivityToAct](), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(awstypes.SensitivityToActLow)), + }, + }, + Blocks: map[string]schema.Block{ + "client_side_action_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[clientSideActionConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "challenge": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[clientSideActionModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "exempt_uri_regular_expression": schema.ListNestedBlock{ + //CustomType: fwtypes.NewListNestedObjectTypeOf[exemptUriRegularExpressionModel](ctx), + CustomType: fwtypes.NewListNestedObjectTypeOf[regexModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(5), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "regex_string": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 512), + }, + }, + }, + }, + }, + }, + Attributes: map[string]schema.Attribute{ + "sensitivity": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.SensitivityToAct](), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(awstypes.SensitivityToActHigh)), + }, + "usage_of_action": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.UsageOfAction](), + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "aws_managed_rules_atp_rule_set": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[awsManagedRulesATPRuleSetModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "enable_regex_in_path": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "login_path": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 256), + stringvalidator.RegexMatches( + regexache.MustCompile(`\S`), + "can not be empty string", + ), + ), + }, + }, + }, + Blocks: map[string]schema.Block{ + "request_inspection": managedRulegroupConfigATPRequestInspectionLNB, + "response_inspection": managedRulegroupConfigResponseInspectionLNB, + }, + }, + }, + "aws_managed_rules_bot_control_rule_set": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[awsManagedRulesBotControlRuleSetModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "enable_machine_learning": schema.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + "inspection_level": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.InspectionLevel](), + Required: true, + }, + }, + }, + }, + }, + }, + } ruleActionOverrideLNB := schema.ListNestedBlock{ CustomType: fwtypes.NewListNestedObjectTypeOf[ruleActionOverrideModel](ctx), Validators: []validator.List{ @@ -426,11 +855,44 @@ func (r *resourceWebACLRuleGroupAssociation) Schema(ctx context.Context, req res }, }, Blocks: map[string]schema.Block{ - "rule_action_override": ruleActionOverrideLNB, + "rule_action_override": ruleActionOverrideLNB, + "managed_rule_group_configs": managedRulegroupConfigLNB, }, }, Description: "Managed rule group configuration.", }, + "visibility_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[visibilityConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "sampled_requests_enabled": schema.BoolAttribute{ + Required: true, + Description: "Indicates whether to store a sampling of the web requests that match the rule.", + }, + "cloudwatch_metrics_enabled": schema.BoolAttribute{ + Required: true, + Description: "Indicates whether the rule is available for use in the metrics for the web ACL.", + }, + names.AttrMetricName: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(1, 128), + stringvalidator.RegexMatches( + regexache.MustCompile(`^[0-9A-Za-z_-]+$`), + "can only contain alphanumeric characters, hyphens, underscores, and colons", + ), + ), + }, + Description: "A name for the metrics for this rule.", + }, + }, + }, + Description: "Visibility configuration for the rule.", + }, names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ Create: true, Update: true, @@ -551,6 +1013,44 @@ func (r *resourceWebACLRuleGroupAssociation) Create(ctx context.Context, req res managedRuleGroupStatement.Version = aws.String(ruleGroupVersion) } + // Add managed rule group configurations if specified + if !managedRuleGroupRef.ManagedRuleGroupConfig.IsNull() && !managedRuleGroupRef.ManagedRuleGroupConfig.IsUnknown() { + var tfManagedRuleGroupConfigModel managedRuleGroupConfigModel + var managedRuleGroupConfigs []awstypes.ManagedRuleGroupConfig + + resp.Diagnostics.Append(managedRuleGroupRef.ManagedRuleGroupConfig.Elements()[0].(fwtypes.ObjectValueOf[managedRuleGroupConfigModel]).As(ctx, &tfManagedRuleGroupConfigModel, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + + if !tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.IsNull() && !tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.IsUnknown() { + // custom expand for ACFP Managed Rule Group Config + var tfACFPConfig awsManagedRulesACFPRuleSetModel + resp.Diagnostics.Append(tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.Elements()[0].(fwtypes.ObjectValueOf[awsManagedRulesACFPRuleSetModel]).As(ctx, &tfACFPConfig, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + + acfpConfig, diags := expandACFPManagedRuleConfig(ctx, tfACFPConfig) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + managedRuleGroupConfigs = append(managedRuleGroupConfigs, awstypes.ManagedRuleGroupConfig{ + AWSManagedRulesACFPRuleSet: &acfpConfig, + }) + } else { + // let auto flex handle expand + resp.Diagnostics.Append(fwflex.Expand(ctx, managedRuleGroupRef.ManagedRuleGroupConfig, &managedRuleGroupConfigs)...) + if resp.Diagnostics.HasError() { + return + } + } + + managedRuleGroupStatement.ManagedRuleGroupConfigs = managedRuleGroupConfigs + } + // Add rule action overrides if specified if !managedRuleGroupRef.RuleActionOverride.IsNull() && !managedRuleGroupRef.RuleActionOverride.IsUnknown() { resp.Diagnostics.Append(fwflex.Expand(ctx, managedRuleGroupRef.RuleActionOverride, &ruleActionOverrides)...) @@ -574,16 +1074,29 @@ func (r *resourceWebACLRuleGroupAssociation) Create(ctx context.Context, req res return } - // Create new rule with the appropriate statement type - newRule := awstypes.Rule{ - Name: plan.RuleName.ValueStringPointer(), - Priority: plan.Priority.ValueInt32(), - Statement: ruleStatement, - VisibilityConfig: &awstypes.VisibilityConfig{ + // Build visibility config for the new rule + var visibilityConfig awstypes.VisibilityConfig + if !plan.VisibilityConfig.IsNull() && !plan.VisibilityConfig.IsUnknown() { + // Expand user-provided visibility_config into AWS SDK type + resp.Diagnostics.Append(fwflex.Expand(ctx, plan.VisibilityConfig, &visibilityConfig)...) + if resp.Diagnostics.HasError() { + return + } + } else { + // Use defaults when not provided in plan, for backward compatibility of this provider + visibilityConfig = awstypes.VisibilityConfig{ SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: plan.RuleName.ValueStringPointer(), - }, + } + } + + // Create new rule with the appropriate statement type + newRule := awstypes.Rule{ + Name: plan.RuleName.ValueStringPointer(), + Priority: plan.Priority.ValueInt32(), + Statement: ruleStatement, + VisibilityConfig: &visibilityConfig, } // Set override action @@ -758,8 +1271,39 @@ func (r *resourceWebACLRuleGroupAssociation) Read(ctx context.Context, req resou ruleActionOverrides = fwtypes.NewListNestedObjectValueOfNull[ruleActionOverrideModel](ctx) } + var managedRuleGroupConfigs fwtypes.ListNestedObjectValueOf[managedRuleGroupConfigModel] + if len(managedStmt.ManagedRuleGroupConfigs) > 0 { + sdkManagedRuleGroupConfigs := managedStmt.ManagedRuleGroupConfigs[0] + + if acfp := sdkManagedRuleGroupConfigs.AWSManagedRulesACFPRuleSet; acfp != nil { + var diags diag.Diagnostics + managedRuleGroupConfigs, diags = flattenACFPRuleConfig(ctx, acfp, managedStmt.ManagedRuleGroupConfigs) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } else { + resp.Diagnostics.Append(fwflex.Flatten(ctx, managedStmt.ManagedRuleGroupConfigs, &managedRuleGroupConfigs)...) + if resp.Diagnostics.HasError() { + return + } + + if botControl := sdkManagedRuleGroupConfigs.AWSManagedRulesBotControlRuleSet; botControl != nil { + var tfManagedRuleGroupConfigModel managedRuleGroupConfigModel + resp.Diagnostics.Append(managedRuleGroupConfigs.Elements()[0].(fwtypes.ObjectValueOf[managedRuleGroupConfigModel]).As(ctx, &tfManagedRuleGroupConfigModel, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + tfManagedRuleGroupConfigModel = patchBotControlFlatten(ctx, botControl, tfManagedRuleGroupConfigModel) + managedRuleGroupConfigs = fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*managedRuleGroupConfigModel{&tfManagedRuleGroupConfigModel}) + } + } + } else { + managedRuleGroupConfigs = fwtypes.NewListNestedObjectValueOfNull[managedRuleGroupConfigModel](ctx) + } // Update the managed rule group nested structure managedRuleGroupRef.RuleActionOverride = ruleActionOverrides + managedRuleGroupRef.ManagedRuleGroupConfig = managedRuleGroupConfigs listValue, diags := fwtypes.NewListNestedObjectValueOfSlice(ctx, []*managedRuleGroupModel{&managedRuleGroupRef}, nil) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -776,6 +1320,22 @@ func (r *resourceWebACLRuleGroupAssociation) Read(ctx context.Context, req resou found = true state.Priority = types.Int32Value(rule.Priority) + var visibilityConfig fwtypes.ListNestedObjectValueOf[visibilityConfigModel] + // for backward compatibility, visibility_config to be read only if it's not set to default + if rule.VisibilityConfig != nil { + isDefault := rule.VisibilityConfig.SampledRequestsEnabled && + rule.VisibilityConfig.CloudWatchMetricsEnabled && + aws.ToString(rule.VisibilityConfig.MetricName) == aws.ToString(rule.Name) + + if !isDefault { + resp.Diagnostics.Append(fwflex.Flatten(ctx, rule.VisibilityConfig, &visibilityConfig)...) + if resp.Diagnostics.HasError() { + return + } + state.VisibilityConfig = visibilityConfig + } + } + // Determine override action overrideAction := overrideActionNone if rule.OverrideAction != nil { @@ -862,8 +1422,26 @@ func (r *resourceWebACLRuleGroupAssociation) Update(ctx context.Context, req res } } + // update visibility config if changed + var visibilityConfig awstypes.VisibilityConfig + if !plan.VisibilityConfig.IsNull() && !plan.VisibilityConfig.IsUnknown() { + resp.Diagnostics.Append(fwflex.Expand(ctx, plan.VisibilityConfig, &visibilityConfig)...) + if resp.Diagnostics.HasError() { + return + } + webACL.WebACL.Rules[i].VisibilityConfig = &visibilityConfig + } else { + visibilityConfig = awstypes.VisibilityConfig{ + SampledRequestsEnabled: true, + CloudWatchMetricsEnabled: true, + MetricName: plan.RuleName.ValueStringPointer(), + } + } + // Update rule action overrides from nested structure (both custom and managed) var overrides []awstypes.RuleActionOverride + var managedRuleGroupConfigs []awstypes.ManagedRuleGroupConfig + if !plan.RuleGroupReference.IsNull() && !plan.RuleGroupReference.IsUnknown() { ruleGroupRefs := plan.RuleGroupReference.Elements() if len(ruleGroupRefs) > 0 { @@ -895,6 +1473,40 @@ func (r *resourceWebACLRuleGroupAssociation) Update(ctx context.Context, req res return } } + + if !managedRuleGroupRef.ManagedRuleGroupConfig.IsNull() && !managedRuleGroupRef.ManagedRuleGroupConfig.IsUnknown() { + var tfManagedRuleGroupConfigModel managedRuleGroupConfigModel + + resp.Diagnostics.Append(managedRuleGroupRef.ManagedRuleGroupConfig.Elements()[0].(fwtypes.ObjectValueOf[managedRuleGroupConfigModel]).As(ctx, &tfManagedRuleGroupConfigModel, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + + if !tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.IsNull() && !tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.IsUnknown() { + // custom expand for ACFP Managed Rule Group Config + var tfACFPConfig awsManagedRulesACFPRuleSetModel + resp.Diagnostics.Append(tfManagedRuleGroupConfigModel.AWSManagedRulesACFPRuleSet.Elements()[0].(fwtypes.ObjectValueOf[awsManagedRulesACFPRuleSetModel]).As(ctx, &tfACFPConfig, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + + acfpConfig, diags := expandACFPManagedRuleConfig(ctx, tfACFPConfig) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + managedRuleGroupConfigs = append(managedRuleGroupConfigs, awstypes.ManagedRuleGroupConfig{ + AWSManagedRulesACFPRuleSet: &acfpConfig, + }) + } else { + // let auto flex handle expand + resp.Diagnostics.Append(fwflex.Expand(ctx, managedRuleGroupRef.ManagedRuleGroupConfig, &managedRuleGroupConfigs)...) + if resp.Diagnostics.HasError() { + return + } + } + } } } @@ -904,6 +1516,7 @@ func (r *resourceWebACLRuleGroupAssociation) Update(ctx context.Context, req res webACL.WebACL.Rules[i].Statement.RuleGroupReferenceStatement.RuleActionOverrides = overrides } else if webACL.WebACL.Rules[i].Statement.ManagedRuleGroupStatement != nil { webACL.WebACL.Rules[i].Statement.ManagedRuleGroupStatement.RuleActionOverrides = overrides + webACL.WebACL.Rules[i].Statement.ManagedRuleGroupStatement.ManagedRuleGroupConfigs = managedRuleGroupConfigs } } @@ -1131,6 +1744,8 @@ func (r *resourceWebACLRuleGroupAssociation) ImportState(ctx context.Context, re Name: types.StringValue(ruleGroupName), VendorName: types.StringValue(vendorName), RuleActionOverride: fwtypes.NewListNestedObjectValueOfNull[ruleActionOverrideModel](ctx), + + ManagedRuleGroupConfig: fwtypes.NewListNestedObjectValueOfNull[managedRuleGroupConfigModel](ctx), } if version != "" { managedRuleGroupRef.Version = types.StringValue(version) @@ -1186,6 +1801,22 @@ func parseWebACLARN(arn string) (id, name, scope string, err error) { return resourceParts[idIndex], resourceParts[nameIndex], scopeValue, nil } +func patchBotControlFlatten( + ctx context.Context, + botControl *awstypes.AWSManagedRulesBotControlRuleSet, + tfManagedRuleGroupConfigModel managedRuleGroupConfigModel, +) managedRuleGroupConfigModel { + if botControl.InspectionLevel == awstypes.InspectionLevelCommon { + tfBotControlModelConfig := awsManagedRulesBotControlRuleSetModel{ + InspectionLevel: types.StringValue(string(awstypes.InspectionLevelCommon)), + EnableMachineLearning: types.BoolValue(false), + } + tfManagedRuleGroupConfigModel.AWSManagedRulesBotControlRuleSet = fwtypes.NewListNestedObjectValueOfSliceMust(ctx, []*awsManagedRulesBotControlRuleSetModel{&tfBotControlModelConfig}) + } + + return tfManagedRuleGroupConfigModel +} + type resourceWebACLRuleGroupAssociationModel struct { framework.WithRegionModel RuleName types.String `tfsdk:"rule_name"` @@ -1194,9 +1825,16 @@ type resourceWebACLRuleGroupAssociationModel struct { ManagedRuleGroup fwtypes.ListNestedObjectValueOf[managedRuleGroupModel] `tfsdk:"managed_rule_group"` WebACLARN types.String `tfsdk:"web_acl_arn"` OverrideAction types.String `tfsdk:"override_action"` + VisibilityConfig fwtypes.ListNestedObjectValueOf[visibilityConfigModel] `tfsdk:"visibility_config"` Timeouts timeouts.Value `tfsdk:"timeouts"` } +type visibilityConfigModel struct { + CloudWatchMetricsEnabled types.Bool `tfsdk:"cloudwatch_metrics_enabled"` + MetricName types.String `tfsdk:"metric_name"` + SampledRequestsEnabled types.Bool `tfsdk:"sampled_requests_enabled"` +} + type ruleGroupReferenceModel struct { ARN types.String `tfsdk:"arn"` RuleActionOverride fwtypes.ListNestedObjectValueOf[ruleActionOverrideModel] `tfsdk:"rule_action_override"` @@ -1207,6 +1845,118 @@ type managedRuleGroupModel struct { VendorName types.String `tfsdk:"vendor_name"` Version types.String `tfsdk:"version"` RuleActionOverride fwtypes.ListNestedObjectValueOf[ruleActionOverrideModel] `tfsdk:"rule_action_override"` + + ManagedRuleGroupConfig fwtypes.ListNestedObjectValueOf[managedRuleGroupConfigModel] `tfsdk:"managed_rule_group_configs"` +} + +type managedRuleGroupConfigModel struct { + AWSManagedRulesACFPRuleSet fwtypes.ListNestedObjectValueOf[awsManagedRulesACFPRuleSetModel] `tfsdk:"aws_managed_rules_acfp_rule_set"` + AWSManagedRulesATPRuleSet fwtypes.ListNestedObjectValueOf[awsManagedRulesATPRuleSetModel] `tfsdk:"aws_managed_rules_atp_rule_set"` + AWSManagedRulesAntiDDoSRuleSet fwtypes.ListNestedObjectValueOf[awsManagedRulesAntiDDoSRuleSetModel] `tfsdk:"aws_managed_rules_anti_ddos_rule_set"` + AWSManagedRulesBotControlRuleSet fwtypes.ListNestedObjectValueOf[awsManagedRulesBotControlRuleSetModel] `tfsdk:"aws_managed_rules_bot_control_rule_set"` +} + +type awsManagedRulesBotControlRuleSetModel struct { + InspectionLevel types.String `tfsdk:"inspection_level"` + EnableMachineLearning types.Bool `tfsdk:"enable_machine_learning"` +} + +type awsManagedRulesAntiDDoSRuleSetModel struct { + ClientSideActionConfig fwtypes.ListNestedObjectValueOf[clientSideActionConfigModel] `tfsdk:"client_side_action_config"` + SensitivityToBlock types.String `tfsdk:"sensitivity_to_block"` +} + +type clientSideActionConfigModel struct { + Challenge fwtypes.ListNestedObjectValueOf[clientSideActionModel] `tfsdk:"challenge"` +} + +type clientSideActionModel struct { + UsageOfAction types.String `tfsdk:"usage_of_action"` + ExemptUriRegularExpressions fwtypes.ListNestedObjectValueOf[regexModel] `tfsdk:"exempt_uri_regular_expression"` + Sensitivity types.String `tfsdk:"sensitivity"` +} + +type regexModel struct { + RegexString types.String `tfsdk:"regex_string"` +} + +type awsManagedRulesATPRuleSetModel struct { + LoginPath types.String `tfsdk:"login_path"` + EnableRegexInPath types.Bool `tfsdk:"enable_regex_in_path"` + RequestInspection fwtypes.ListNestedObjectValueOf[requestInspectionModel] `tfsdk:"request_inspection"` + ResponseInspection fwtypes.ListNestedObjectValueOf[responseInspectionModel] `tfsdk:"response_inspection"` +} + +type requestInspectionModel struct { + PasswordField fwtypes.ListNestedObjectValueOf[passwordFieldModel] `tfsdk:"password_field"` + PayloadType types.String `tfsdk:"payload_type"` + UsernameField fwtypes.ListNestedObjectValueOf[usernameFieldModel] `tfsdk:"username_field"` +} + +type usernameFieldModel struct { + Identifier types.String `tfsdk:"identifier"` +} + +type passwordFieldModel struct { + Identifier types.String `tfsdk:"identifier"` +} + +type awsManagedRulesACFPRuleSetModel struct { + CreationPath types.String `tfsdk:"creation_path"` + RegistrationPagePath types.String `tfsdk:"registration_page_path"` + RequestInspection fwtypes.ListNestedObjectValueOf[requestInspectionACFPModel] `tfsdk:"request_inspection"` + EnableRegexInPath types.Bool `tfsdk:"enable_regex_in_path"` + ResponseInspection fwtypes.ListNestedObjectValueOf[responseInspectionModel] `tfsdk:"response_inspection"` +} + +type requestInspectionACFPModel struct { + PayloadType types.String `tfsdk:"payload_type"` + AddressFields fwtypes.ListNestedObjectValueOf[addressFieldModel] `tfsdk:"address_fields"` + EmailField fwtypes.ListNestedObjectValueOf[emailFieldModel] `tfsdk:"email_field"` + PasswordField fwtypes.ListNestedObjectValueOf[passwordFieldModel] `tfsdk:"password_field"` + UsernameField fwtypes.ListNestedObjectValueOf[usernameFieldModel] `tfsdk:"username_field"` + PhoneNumberFields fwtypes.ListNestedObjectValueOf[phoneNumberFieldModel] `tfsdk:"phone_number_fields"` +} + +type phoneNumberFieldModel struct { + Identifiers fwtypes.ListValueOf[types.String] `tfsdk:"identifiers"` +} + +type emailFieldModel struct { + Identifier types.String `tfsdk:"identifier"` +} + +type addressFieldModel struct { + Identifiers fwtypes.ListValueOf[types.String] `tfsdk:"identifiers"` +} + +type responseInspectionModel struct { + BodyContains fwtypes.ListNestedObjectValueOf[responseInspectionBodyContainsModel] `tfsdk:"body_contains"` + Header fwtypes.ListNestedObjectValueOf[responseInspectionHeaderModel] `tfsdk:"header"` + Json fwtypes.ListNestedObjectValueOf[responseInspectionJsonModel] `tfsdk:"json"` + StatusCode fwtypes.ListNestedObjectValueOf[responseInspectionStatusCodeModel] `tfsdk:"status_code"` +} + +type responseInspectionBodyContainsModel struct { + FailureStrings fwtypes.SetValueOf[types.String] `tfsdk:"failure_strings"` + SuccessStrings fwtypes.SetValueOf[types.String] `tfsdk:"success_strings"` +} + +type responseInspectionHeaderModel struct { + FailureValues fwtypes.SetValueOf[types.String] `tfsdk:"failure_values"` + Name types.String `tfsdk:"name"` + SuccessValues fwtypes.SetValueOf[types.String] `tfsdk:"success_values"` +} + +type responseInspectionJsonModel struct { + FailureValues fwtypes.SetValueOf[types.String] `tfsdk:"failure_values"` + Identifier types.String `tfsdk:"identifier"` + SuccessValues fwtypes.SetValueOf[types.String] `tfsdk:"success_values"` +} + +type responseInspectionStatusCodeModel struct { + FailureCodes fwtypes.SetValueOf[types.Int32] `tfsdk:"failure_codes"` + SuccessCodes fwtypes.SetValueOf[types.Int32] `tfsdk:"success_codes"` } type ruleActionOverrideModel struct { diff --git a/internal/service/wafv2/web_acl_rule_group_association_test.go b/internal/service/wafv2/web_acl_rule_group_association_test.go index b87471a9da8e..6a7fa9f16898 100644 --- a/internal/service/wafv2/web_acl_rule_group_association_test.go +++ b/internal/service/wafv2/web_acl_rule_group_association_test.go @@ -171,6 +171,46 @@ func TestAccWAFV2WebACLRuleGroupAssociation_basic(t *testing.T) { }) } +func TestAccWAFV2WebACLRuleGroupAssociation_withVisibilityConfig(t *testing.T) { + ctx := acctest.Context(t) + var v wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + webACLResourceName := "aws_wafv2_web_acl.test" + ruleGroupResourceName := "aws_wafv2_rule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_withVisibilityConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "rule_name", fmt.Sprintf("%s-association", rName)), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "10"), + resource.TestCheckResourceAttr(resourceName, "override_action", "none"), + resource.TestCheckResourceAttrPair(resourceName, "web_acl_arn", webACLResourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(resourceName, "rule_group_reference.0.arn", ruleGroupResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "visibility_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "visibility_config.0.cloudwatch_metrics_enabled", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "visibility_config.0.metric_name", "friendly-metric-name"), + resource.TestCheckResourceAttr(resourceName, "visibility_config.0.sampled_requests_enabled", acctest.CtFalse), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + func TestAccWAFV2WebACLRuleGroupAssociation_disappears(t *testing.T) { ctx := acctest.Context(t) var v wafv2.GetWebACLOutput @@ -562,6 +602,289 @@ func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ruleActionOverride( }) } +func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ManagedRuleGroupConfig_basic(t *testing.T) { + ctx := acctest.Context(t) + var webACL wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.managed_rule_group_configs.0.aws_managed_rules_bot_control_rule_set.0.inspection_level", "COMMON"), + ), + }, + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfigUpdate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.managed_rule_group_configs.0.aws_managed_rules_bot_control_rule_set.0.inspection_level", "TARGETED"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationManagedRuleGroupImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + +func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ManagedRuleGroupConfig_ACFPRuleSet(t *testing.T) { + ctx := acctest.Context(t) + var webACL wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_acfpRuleSet(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesACFPRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_acfp_rule_set.0.creation_path": "/creation", + "aws_managed_rules_acfp_rule_set.0.registration_page_path": "/registration", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.email_field.0.identifier": "/email", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.password_field.0.identifier": "/password", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.phone_number_fields.0.identifiers.0": "/phone1", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.phone_number_fields.0.identifiers.1": "/phone2", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.address_fields.0.identifiers.0": "home", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.address_fields.0.identifiers.1": "work", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.payload_type": "JSON", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.username_field.0.identifier": "/username", + }), + ), + }, + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_acfpRuleSetUpdate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesACFPRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_acfp_rule_set.0.enable_regex_in_path": acctest.CtTrue, + "aws_managed_rules_acfp_rule_set.0.creation_path": "/creation", + "aws_managed_rules_acfp_rule_set.0.registration_page_path": "/registration", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.email_field.0.identifier": "/email", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.password_field.0.identifier": "/pass", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.phone_number_fields.0.identifiers.0": "/phone3", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.address_fields.0.identifiers.0": "mobile", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.payload_type": "JSON", + "aws_managed_rules_acfp_rule_set.0.request_inspection.0.username_field.0.identifier": "/user", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationManagedRuleGroupImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + +func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ManagedRuleGroupConfig_AntiDDoSRuleSet(t *testing.T) { + ctx := acctest.Context(t) + var webACL wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_antiDDoSRuleSet(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesAntiDDoSRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.usage_of_action": "ENABLED", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.#": "2", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.0.regex_string": "\\/api\\/", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.1.regex_string": "jpg", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.sensitivity": "MEDIUM", + "aws_managed_rules_anti_ddos_rule_set.0.sensitivity_to_block": "HIGH", + }), + ), + }, + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_antiDDoSRuleSetWithDefaultSensitivity(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesAntiDDoSRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.usage_of_action": "ENABLED", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.#": "2", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.0.regex_string": "\\/api\\/", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.exempt_uri_regular_expression.1.regex_string": "jpg", + "aws_managed_rules_anti_ddos_rule_set.0.client_side_action_config.0.challenge.0.sensitivity": "HIGH", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationManagedRuleGroupImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + +func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ManagedRuleGroupConfig_ATPRuleSet(t *testing.T) { + ctx := acctest.Context(t) + var webACL wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_atpRuleSet(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesATPRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_atp_rule_set.0.login_path": "/api/1/signin", + "aws_managed_rules_atp_rule_set.0.request_inspection.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.password_field.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.password_field.0.identifier": "/password", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.username_field.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.username_field.0.identifier": "/username", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.payload_type": "JSON", + "aws_managed_rules_atp_rule_set.0.response_inspection.#": "0", + }), + ), + }, + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_atpRuleSetUpdate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesATPRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_atp_rule_set.0.enable_regex_in_path": acctest.CtTrue, + "aws_managed_rules_atp_rule_set.0.login_path": "/api/2/signin", + "aws_managed_rules_atp_rule_set.0.request_inspection.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.password_field.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.password_field.0.identifier": "/pass", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.username_field.#": "1", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.username_field.0.identifier": "/user", + "aws_managed_rules_atp_rule_set.0.request_inspection.0.payload_type": "JSON", + "aws_managed_rules_atp_rule_set.0.response_inspection.#": "0", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationManagedRuleGroupImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + +func TestAccWAFV2WebACLRuleGroupAssociation_ManagedRuleGroup_ManagedRuleGroupConfig_BotControl(t *testing.T) { + ctx := acctest.Context(t) + var webACL wafv2.GetWebACLOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_wafv2_web_acl_rule_group_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebACLRuleGroupAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_botControl(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebACLRuleGroupAssociationExists(ctx, resourceName, &webACL), + // Check top-level attributes + resource.TestCheckResourceAttr(resourceName, "rule_name", "test-rule"), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "1"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.name", "AWSManagedRulesBotControlRuleSet"), + resource.TestCheckResourceAttr(resourceName, "managed_rule_group.0.vendor_name", "AWS"), + + // Verify Managed Rule Group Config + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "managed_rule_group.0.managed_rule_group_configs.*", map[string]string{ + "aws_managed_rules_bot_control_rule_set.0.inspection_level": "TARGETED", + "aws_managed_rules_bot_control_rule_set.0.enable_machine_learning": acctest.CtTrue, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccWebACLRuleGroupAssociationManagedRuleGroupImportStateIDFunc(resourceName), + ImportStateVerifyIdentifierAttribute: "web_acl_arn", + }, + }, + }) +} + func testAccCheckWebACLRuleGroupAssociationDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).WAFV2Client(ctx) @@ -839,7 +1162,7 @@ resource "aws_wafv2_web_acl_rule_group_association" "test" { `, rName) } -func testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_overrideAction(rName, overrideAction string) string { +func testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_withVisibilityConfig(rName string) string { return fmt.Sprintf(` resource "aws_wafv2_rule_group" "test" { name = %[1]q @@ -851,7 +1174,7 @@ resource "aws_wafv2_rule_group" "test" { priority = 1 action { - block {} + count {} } statement { @@ -894,19 +1217,24 @@ resource "aws_wafv2_web_acl" "test" { } resource "aws_wafv2_web_acl_rule_group_association" "test" { - rule_name = "%[1]s-association" - priority = 10 - web_acl_arn = aws_wafv2_web_acl.test.arn - override_action = %[2]q + rule_name = "%[1]s-association" + priority = 10 + web_acl_arn = aws_wafv2_web_acl.test.arn rule_group_reference { arn = aws_wafv2_rule_group.test.arn } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } } -`, rName, overrideAction) +`, rName) } -func testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_ruleActionOverride(rName string) string { +func testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_overrideAction(rName, overrideAction string) string { return fmt.Sprintf(` resource "aws_wafv2_rule_group" "test" { name = %[1]q @@ -934,27 +1262,6 @@ resource "aws_wafv2_rule_group" "test" { } } - rule { - name = "rule-2" - priority = 2 - - action { - allow {} - } - - statement { - ip_set_reference_statement { - arn = aws_wafv2_ip_set.test.arn - } - } - - visibility_config { - cloudwatch_metrics_enabled = false - metric_name = "rule-2" - sampled_requests_enabled = false - } - } - visibility_config { cloudwatch_metrics_enabled = false metric_name = %[1]q @@ -962,14 +1269,102 @@ resource "aws_wafv2_rule_group" "test" { } } -resource "aws_wafv2_ip_set" "test" { +resource "aws_wafv2_web_acl" "test" { name = %[1]q scope = "REGIONAL" - ip_address_version = "IPV4" - addresses = ["192.0.2.0/24"] -} - + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "%[1]s-association" + priority = 10 + web_acl_arn = aws_wafv2_web_acl.test.arn + override_action = %[2]q + + rule_group_reference { + arn = aws_wafv2_rule_group.test.arn + } +} +`, rName, overrideAction) +} + +func testAccWebACLRuleGroupAssociationConfig_RuleGroupReference_ruleActionOverride(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_rule_group" "test" { + name = %[1]q + scope = "REGIONAL" + capacity = 10 + + rule { + name = "rule-1" + priority = 1 + + action { + block {} + } + + statement { + geo_match_statement { + country_codes = ["US", "CA"] + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "rule-1" + sampled_requests_enabled = false + } + } + + rule { + name = "rule-2" + priority = 2 + + action { + allow {} + } + + statement { + ip_set_reference_statement { + arn = aws_wafv2_ip_set.test.arn + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "rule-2" + sampled_requests_enabled = false + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } +} + +resource "aws_wafv2_ip_set" "test" { + name = %[1]q + scope = "REGIONAL" + + ip_address_version = "IPV4" + addresses = ["192.0.2.0/24"] +} + resource "aws_wafv2_web_acl" "test" { name = %[1]q scope = "REGIONAL" @@ -1502,3 +1897,466 @@ resource "aws_wafv2_web_acl_rule_group_association" "test" { } `, rName) } + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesBotControlRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_bot_control_rule_set { + inspection_level = "COMMON" + } + } + } + + override_action = "none" +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfigUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesBotControlRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_bot_control_rule_set { + inspection_level = "TARGETED" + } + } + } + + override_action = "none" +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_acfpRuleSet(rName string) string { + return fmt.Sprintf(` + +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + + name = "AWSManagedRulesACFPRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_acfp_rule_set { + creation_path = "/creation" + registration_page_path = "/registration" + request_inspection { + email_field { + identifier = "/email" + } + password_field { + identifier = "/password" + } + phone_number_fields { + identifiers = ["/phone1", "/phone2"] + } + address_fields { + identifiers = ["home", "work"] + } + payload_type = "JSON" + username_field { + identifier = "/username" + } + } + } + } + } +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_acfpRuleSetUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesACFPRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_acfp_rule_set { + enable_regex_in_path = true + creation_path = "/creation" + registration_page_path = "/registration" + + request_inspection { + email_field { + identifier = "/email" + } + password_field { + identifier = "/pass" + } + phone_number_fields { + identifiers = ["/phone3"] + } + address_fields { + identifiers = ["mobile"] + } + payload_type = "JSON" + username_field { + identifier = "/user" + } + } + } + } + } + + override_action = "none" +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_antiDDoSRuleSet(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + + name = "AWSManagedRulesAntiDDoSRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_anti_ddos_rule_set { + client_side_action_config { + challenge { + usage_of_action = "ENABLED" + exempt_uri_regular_expression { + regex_string = "\\/api\\/" + } + exempt_uri_regular_expression { + regex_string = "jpg" + } + sensitivity = "MEDIUM" + } + } + sensitivity_to_block = "HIGH" + } + } + + } + +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_antiDDoSRuleSetWithDefaultSensitivity(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesAntiDDoSRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_anti_ddos_rule_set { + client_side_action_config { + challenge { + usage_of_action = "ENABLED" + exempt_uri_regular_expression { + regex_string = "\\/api\\/" + } + exempt_uri_regular_expression { + regex_string = "jpg" + } + } + } + } + } + } +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_atpRuleSet(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesATPRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_atp_rule_set { + login_path = "/api/1/signin" + + request_inspection { + password_field { + identifier = "/password" + } + + payload_type = "JSON" + + username_field { + identifier = "/username" + } + } + } + } + } + + override_action = "none" +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_atpRuleSetUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesATPRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_atp_rule_set { + enable_regex_in_path = true + login_path = "/api/2/signin" + + request_inspection { + password_field { + identifier = "/pass" + } + payload_type = "JSON" + username_field { + identifier = "/user" + } + } + } + } + } + + override_action = "none" +} +`, rName) +} + +func testAccWebACLRuleGroupAssociationConfig_ManagedRuleGroupConfig_botControl(rName string) string { + return fmt.Sprintf(` +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "test" { + rule_name = "test-rule" + priority = 1 + web_acl_arn = aws_wafv2_web_acl.test.arn + + managed_rule_group { + name = "AWSManagedRulesBotControlRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_bot_control_rule_set { + inspection_level = "TARGETED" + enable_machine_learning = true + } + } + } + + override_action = "none" +} +`, rName) +} diff --git a/website/docs/r/wafv2_web_acl_rule_group_association.html.markdown b/website/docs/r/wafv2_web_acl_rule_group_association.html.markdown index 81e2a614ede9..267cba813416 100644 --- a/website/docs/r/wafv2_web_acl_rule_group_association.html.markdown +++ b/website/docs/r/wafv2_web_acl_rule_group_association.html.markdown @@ -176,6 +176,71 @@ resource "aws_wafv2_web_acl_rule_group_association" "managed_with_overrides" { } ``` +### Managed Rule Group - With Managed Rule Group Configs + +```terraform +resource "aws_wafv2_web_acl" "example" { + name = "example" + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "example-regional-waf" + sampled_requests_enabled = true + } + + lifecycle { + ignore_changes = [rule] + } +} + +resource "aws_wafv2_web_acl_rule_group_association" "managed_rule_group_configs" { + rule_name = "acfp-ruleset-with-rule-config" + priority = 70 + web_acl_arn = aws_wafv2_web_acl.example.arn + + managed_rule_group { + name = "AWSManagedRulesACFPRuleSet" + vendor_name = "AWS" + + managed_rule_group_configs { + aws_managed_rules_acfp_rule_set { + creation_path = "/creation" + registration_page_path = "/registration" + request_inspection { + email_field { + identifier = "/email" + } + password_field { + identifier = "/password" + } + phone_number_fields { + identifiers = ["/phone1", "/phone2"] + } + address_fields { + identifiers = ["home", "work"] + } + payload_type = "JSON" + username_field { + identifier = "/username" + } + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "friendly-metric-name" + sampled_requests_enabled = true + } +} +``` + ### Custom Rule Group - With Override Action ```terraform @@ -389,6 +454,15 @@ The following arguments are optional: * `override_action` - (Optional) Override action for the rule group. Valid values are `none` and `count`. Defaults to `none`. When set to `count`, the actions defined in the rule group rules are overridden to count matches instead of blocking or allowing requests. * `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). * `rule_group_reference` - (Optional) Custom Rule Group reference configuration. One of `rule_group_reference` or `managed_rule_group` is required. Conflicts with `managed_rule_group`. [See below](#rule_group_reference). +* `visibility_config` - (Optional) Defines and enables Amazon CloudWatch metrics and web request sample collection. See [`visibility_config`](#visibility_config-block) below for details. + +### `visibility_config` Block + +The `visibility_config` block supports the following arguments: + +* `cloudwatch_metrics_enabled` - (Required) Whether the associated resource sends metrics to CloudWatch. For the list of available metrics, see [AWS WAF Metrics](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html#waf-metrics). +* `metric_name` - (Required) A friendly name of the CloudWatch metric. The name can contain only alphanumeric characters (A-Z, a-z, 0-9) hyphen(-) and underscore (\_), with length from one to 128 characters. It can't contain whitespace or metric names reserved for AWS WAF, for example `All` and `Default_Action`. +* `sampled_requests_enabled` - (Required) Whether AWS WAF should store a sampling of the web requests that match the rules. You can view the sampled requests through the AWS WAF console. ### rule_group_reference @@ -401,6 +475,7 @@ The following arguments are optional: * `vendor_name` - (Required) Name of the managed rule group vendor. For AWS managed rule groups, this is `AWS`. * `version` - (Optional) Version of the managed rule group. If not specified, the default version is used. * `rule_action_override` - (Optional) Override actions for specific rules within the rule group. [See below](#rule_action_override). +* `managed_rule_group_configs`- (Optional) Additional information that's used by a managed rule group. Only one rule attribute is allowed in each config. See [`managed_rule_group_configs`](#managed_rule_group_configs-block) for more details ### rule_action_override @@ -457,6 +532,112 @@ Exactly one of the following action blocks must be specified: * `name` - (Required) Name of the response header. * `value` - (Required) Value of the response header. +### `managed_rule_group_configs` Block + +The `managed_rule_group_configs` block support the following arguments: + +* `aws_managed_rules_bot_control_rule_set` - (Optional) Additional configuration for using the Bot Control managed rule group. Use this to specify the inspection level that you want to use. See [`aws_managed_rules_bot_control_rule_set`](#aws_managed_rules_bot_control_rule_set-block) for more details +* `aws_managed_rules_acfp_rule_set` - (Optional) Additional configuration for using the Account Creation Fraud Prevention managed rule group. Use this to specify information such as the registration page of your application and the type of content to accept or reject from the client. +* `aws_managed_rules_anti_ddos_rule_set` - (Optional) Configuration for using the anti-DDoS managed rule group. See [`aws_managed_rules_anti_ddos_rule_set`](#aws_managed_rules_anti_ddos_rule_set-block) for more details. +* `aws_managed_rules_atp_rule_set` - (Optional) Additional configuration for using the Account Takeover Protection managed rule group. Use this to specify information such as the sign-in page of your application and the type of content to accept or reject from the client. + +### `aws_managed_rules_bot_control_rule_set` Block + +* `enable_machine_learning` - (Optional) Applies only to the targeted inspection level. Determines whether to use machine learning (ML) to analyze your web traffic for bot-related activity. Defaults to `false`. +* `inspection_level` - (Optional) The inspection level to use for the Bot Control rule group. + +### `aws_managed_rules_acfp_rule_set` Block + +* `creation_path` - (Required) The path of the account creation endpoint for your application. This is the page on your website that accepts the completed registration form for a new user. This page must accept POST requests. +* `enable_regex_in_path` - (Optional) Whether or not to allow the use of regular expressions in the login page path. +* `registration_page_path` - (Required) The path of the account registration endpoint for your application. This is the page on your website that presents the registration form to new users. This page must accept GET text/html requests. +* `request_inspection` - (Optional) The criteria for inspecting login requests, used by the ATP rule group to validate credentials usage. See [`request_inspection`](#request_inspection-block-acfp) for more details. +* `response_inspection` - (Optional) The criteria for inspecting responses to login requests, used by the ATP rule group to track login failure rates. Note that Response Inspection is available only on web ACLs that protect CloudFront distributions. See [`response_inspection`](#response_inspection-block) for more details. + +### `request_inspection` Block (ACFP) + +* `addressFields` (Optional) The names of the fields in the request payload that contain your customer's primary physical address. See [`addressFields`](#address_fields-block) for more details. +* `emailField` (Optional) The name of the field in the request payload that contains your customer's email. See [`emailField`](#email_field-block) for more details. +* `passwordField` (Optional) Details about your login page password field. See [`passwordField`](#password_field-block) for more details. +* `payloadType` (Required) The payload type for your login endpoint, either JSON or form encoded. +* `phoneNumberFields` (Optional) The names of the fields in the request payload that contain your customer's primary phone number. See [`phoneNumberFields`](#phone_number_fields-block) for more details. +* `usernameField` (Optional) Details about your login page username field. See [`usernameField`](#username_field-block) for more details. + +### `aws_managed_rules_anti_ddos_rule_set` Block + +* `client_side_action_config` - (Required) Configuration for the request handling that's applied by the managed rule group rules `ChallengeAllDuringEvent` and `ChallengeDDoSRequests` during a distributed denial of service (DDoS) attack. See [`client_side_action_config`](#client_side_action_config-block) for more details. +* `sensitivity_to_block` - (Optional) Sensitivity that the rule group rule DDoSRequests uses when matching against the DDoS suspicion labeling on a request. Valid values are `LOW` (Default), `MEDIUM`, and `HIGH`. + +### `client_side_action_config` Block + +* `challenge` - (Required) Configuration for the use of the `AWSManagedRulesAntiDDoSRuleSet` rules `ChallengeAllDuringEvent` and `ChallengeDDoSRequests`. + * `exempt_uri_regular_expression` - (Optional) Block for the list of the regular expressions to match against the web request URI, used to identify requests that can't handle a silent browser challenge. + * `regex_string` - (Optional) Regular expression string. + * `sensitivity` - (Optional) Sensitivity that the rule group rule ChallengeDDoSRequests uses when matching against the DDoS suspicion labeling on a request. Valid values are `LOW`, `MEDIUM` and `HIGH` (Default). + * `usage_of_action` - (Required) Configuration whether to use the `AWSManagedRulesAntiDDoSRuleSet` rules `ChallengeAllDuringEvent` and `ChallengeDDoSRequests` in the rule group evaluation. Valid values are `ENABLED` and `DISABLED`. + +### `aws_managed_rules_atp_rule_set` Block + +* `enable_regex_in_path` - (Optional) Whether or not to allow the use of regular expressions in the login page path. +* `login_path` - (Required) The path of the login endpoint for your application. +* `request_inspection` - (Optional) The criteria for inspecting login requests, used by the ATP rule group to validate credentials usage. See [`request_inspection`](#request_inspection-block) for more details. +* `response_inspection` - (Optional) The criteria for inspecting responses to login requests, used by the ATP rule group to track login failure rates. Note that Response Inspection is available only on web ACLs that protect CloudFront distributions. See [`response_inspection`](#response_inspection-block) for more details. + +### `request_inspection` Block + +* `password_field` (Optional) Details about your login page password field. See [`password_field`](#password_field-block) for more details. +* `payload_type` (Required) The payload type for your login endpoint, either JSON or form encoded. +* `username_field` (Optional) Details about your login page username field. See [`username_field`](#username_field-block) for more details. + +### `address_fields` Block + +* `identifiers` - (Required) The names of the address fields. + +### `email_field` Block + +* `identifier` - (Required) The name of the field in the request payload that contains your customer's email. + +### `password_field` Block + +* `identifier` - (Required) The name of the password field. + +### `phone_number_fields` Block + +* `identifiers` - (Required) The names of the phone number fields. + +### `username_field` Block + +* `identifier` - (Required) The name of the username field. + +### `response_inspection` Block + +* `body_contains` (Optional) Configures inspection of the response body. See [`body_contains`](#body_contains-block) for more details. +* `header` (Optional) Configures inspection of the response header.See [`header`](#header-block) for more details. +* `json` (Optional) Configures inspection of the response JSON. See [`json`](#json-block) for more details. +* `status_code` (Optional) Configures inspection of the response status code.See [`status_code`](#status_code-block) for more details. + +### `body_contains` Block + +* `success_strings` (Required) Strings in the body of the response that indicate a successful login attempt. +* `failure_strings` (Required) Strings in the body of the response that indicate a failed login attempt. + +### `header` Block + +* `name` (Required) The name of the header to match against. The name must be an exact match, including case. +* `success_values` (Required) Values in the response header with the specified name that indicate a successful login attempt. +* `failure_values` (Required) Values in the response header with the specified name that indicate a failed login attempt. + +### `json` Block + +* `identifier` (Required) The identifier for the value to match against in the JSON. +* `success_strings` (Required) Strings in the body of the response that indicate a successful login attempt. +* `failure_strings` (Required) Strings in the body of the response that indicate a failed login attempt. + +### `status_code` Block + +* `success_codes` (Required) Status codes in the response that indicate a successful login attempt. +* `failure_codes` (Required) Status codes in the response that indicate a failed login attempt. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: