Skip to content

New Attribute for r/wafv2_rule_group: rule_json attribute #43397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/433245.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_wafv2_rule_group: Add `rules_json` argument
```
82 changes: 82 additions & 0 deletions internal/service/wafv2/flex.go
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,41 @@ func expandWebACLRulesJSON(rawRules string) ([]awstypes.Rule, error) {
return rules, nil
}

func expandRuleGroupRulesJSON(rawRules string) ([]awstypes.Rule, error) {
// Backwards compatibility.
if rawRules == "" {
return nil, errors.New("decoding JSON: unexpected end of JSON input")
}

var temp []any
err := tfjson.DecodeFromBytes([]byte(rawRules), &temp)
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}

for _, v := range temp {
walkRulesGroupJSON(reflect.ValueOf(v))
}

out, err := tfjson.EncodeToBytes(temp)
if err != nil {
return nil, err
}

var rules []awstypes.Rule
err = tfjson.DecodeFromBytes(out, &rules)
if err != nil {
return nil, err
}

for i, r := range rules {
if reflect.ValueOf(r).IsZero() {
return nil, fmt.Errorf("invalid Rule Group Rule supplied at index (%d)", i)
}
}
return rules, nil
}

func walkWebACLJSON(v reflect.Value) {
m := map[string][]struct {
key string
Expand Down Expand Up @@ -1125,6 +1160,53 @@ func walkWebACLJSON(v reflect.Value) {
}
}

func walkRulesGroupJSON(v reflect.Value) {
m := map[string][]struct {
key string
outputType any
}{
"ByteMatchStatement": {
{key: "SearchString", outputType: []byte{}},
},
}

for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
v = v.Elem()
}

switch v.Kind() {
case reflect.Map:
for _, k := range v.MapKeys() {
if val, ok := m[k.String()]; ok {
st := v.MapIndex(k).Interface().(map[string]any)
for _, va := range val {
if st[va.key] == nil {
continue
}
str := st[va.key]
switch reflect.ValueOf(va.outputType).Kind() {
case reflect.Slice, reflect.Array:
switch reflect.ValueOf(va.outputType).Type().Elem().Kind() {
case reflect.Uint8:
base64String := itypes.Base64Encode([]byte(str.(string)))
st[va.key] = base64String
default:
}
default:
}
}
} else {
walkRulesGroupJSON(v.MapIndex(k))
}
}
case reflect.Array, reflect.Slice:
for i := range v.Len() {
walkRulesGroupJSON(v.Index(i))
}
default:
}
}

func expandWebACLRules(l []any) []awstypes.Rule {
if len(l) == 0 || l[0] == nil {
return nil
Expand Down
53 changes: 47 additions & 6 deletions internal/service/wafv2/rule_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/create"
Expand All @@ -26,6 +27,7 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)

Expand Down Expand Up @@ -98,9 +100,21 @@ func resourceRuleGroup() *schema.Resource {
validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z_-]+$`), "must contain only alphanumeric hyphen and underscore characters"),
),
},
"rules_json": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{names.AttrRule},
ValidateFunc: validation.StringIsJSON,
DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs,
StateFunc: func(v any) string {
json, _ := structure.NormalizeJsonString(v)
return json
},
},
names.AttrRule: {
Type: schema.TypeSet,
Optional: true,
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"rules_json"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrAction: {
Expand Down Expand Up @@ -155,12 +169,23 @@ func resourceRuleGroupCreate(ctx context.Context, d *schema.ResourceData, meta a
input := &wafv2.CreateRuleGroupInput{
Capacity: aws.Int64(int64(d.Get("capacity").(int))),
Name: aws.String(name),
Rules: expandRules(d.Get(names.AttrRule).(*schema.Set).List()),
Scope: awstypes.Scope(d.Get(names.AttrScope).(string)),
Tags: getTagsIn(ctx),
VisibilityConfig: expandVisibilityConfig(d.Get("visibility_config").([]any)),
}

if v, ok := d.GetOk(names.AttrRule); ok {
input.Rules = expandRules(v.(*schema.Set).List())
}

if v, ok := d.GetOk("rules_json"); ok {
rules, err := expandRuleGroupRulesJSON(v.(string))
if err != nil {
return sdkdiag.AppendErrorf(diags, "setting rule: %s", err)
}
input.Rules = rules
}

if v, ok := d.GetOk("custom_response_body"); ok && v.(*schema.Set).Len() > 0 {
input.CustomResponseBodies = expandCustomResponseBodies(v.(*schema.Set).List())
}
Expand Down Expand Up @@ -212,8 +237,13 @@ func resourceRuleGroupRead(ctx context.Context, d *schema.ResourceData, meta any
d.Set("lock_token", output.LockToken)
d.Set(names.AttrName, ruleGroup.Name)
d.Set(names.AttrNamePrefix, create.NamePrefixFromName(aws.ToString(ruleGroup.Name)))
if err := d.Set(names.AttrRule, flattenRules(ruleGroup.Rules)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting rule: %s", err)
if _, ok := d.GetOk("rules_json"); !ok {
if err := d.Set(names.AttrRule, flattenRules(ruleGroup.Rules)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting rule: %s", err)
}
} else {
d.Set("rules_json", d.Get("rules_json"))
d.Set(names.AttrRule, nil)
}
if err := d.Set("visibility_config", flattenVisibilityConfig(ruleGroup.VisibilityConfig)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting visibility_config: %s", err)
Expand All @@ -231,11 +261,22 @@ func resourceRuleGroupUpdate(ctx context.Context, d *schema.ResourceData, meta a
Id: aws.String(d.Id()),
LockToken: aws.String(d.Get("lock_token").(string)),
Name: aws.String(d.Get(names.AttrName).(string)),
Rules: expandRules(d.Get(names.AttrRule).(*schema.Set).List()),
Scope: awstypes.Scope(d.Get(names.AttrScope).(string)),
VisibilityConfig: expandVisibilityConfig(d.Get("visibility_config").([]any)),
}

if v, ok := d.GetOk(names.AttrRule); ok {
input.Rules = expandRules(v.(*schema.Set).List())
}

if v, ok := d.GetOk("rules_json"); ok {
rules, err := expandRuleGroupRulesJSON(v.(string))
if err != nil {
return sdkdiag.AppendErrorf(diags, "expanding WAFv2 RuleGroup JSON rule (%s): %s", d.Id(), err)
}
input.Rules = rules
}

if v, ok := d.GetOk("custom_response_body"); ok && v.(*schema.Set).Len() > 0 {
input.CustomResponseBodies = expandCustomResponseBodies(v.(*schema.Set).List())
}
Expand Down
72 changes: 72 additions & 0 deletions internal/service/wafv2/rule_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5701,3 +5701,75 @@ resource "aws_wafv2_rule_group" "test" {
}
`, rName)
}
func TestAccWAFV2RuleGroup_rulesJSON(t *testing.T) {
ctx := acctest.Context(t)
var v awstypes.RuleGroup
ruleGroupName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_wafv2_rule_group.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckScopeRegional(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckRuleGroupDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccRuleGroupConfig_rulesJSON(ruleGroupName),
Check: resource.ComposeTestCheckFunc(
testAccCheckRuleGroupExists(ctx, resourceName, &v),
acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "wafv2", regexache.MustCompile(`regional/rulegroup/.+$`)),
resource.TestCheckResourceAttrSet(resourceName, "rules_json"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"rules_json", names.AttrRule},
ImportStateIdFunc: testAccRuleGroupImportStateIdFunc(resourceName),
},
},
})
}

func testAccRuleGroupConfig_rulesJSON(rName string) string {
return fmt.Sprintf(`
resource "aws_wafv2_rule_group" "test" {
capacity = 100
name = %[1]q
scope = "REGIONAL"

rules_json = jsonencode([{
Name = "rule-1"
Priority = 1
Action = {
Count = {}
}
Statement = {
ByteMatchStatement = {
SearchString = "badbot"
FieldToMatch = {
UriPath = {}
}
TextTransformations = [{
Priority = 1
Type = "NONE"
}]
PositionalConstraint = "CONTAINS"
}
}
VisibilityConfig = {
CloudwatchMetricsEnabled = false
MetricName = "friendly-rule-metric-name"
SampledRequestsEnabled = false
}
}])

visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "friendly-metric-name"
sampled_requests_enabled = false
}
}
`, rName)
}
43 changes: 43 additions & 0 deletions website/docs/r/wafv2_rule_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,48 @@ resource "aws_wafv2_rule_group" "example" {
}
```

### Using rule_json

```terraform
resource "aws_wafv2_rule_group" "example" {
name = "example-rule-group"
scope = "REGIONAL"
capacity = 100

rule_json = jsonencode([{
Name = "rule-1"
Priority = 1
Action = {
Count = {}
}
Statement = {
ByteMatchStatement = {
SearchString = "badbot"
FieldToMatch = {
UriPath = {}
}
TextTransformations = [{
Priority = 1
Type = "NONE"
}]
PositionalConstraint = "CONTAINS"
}
}
VisibilityConfig = {
CloudwatchMetricsEnabled = false
MetricName = "friendly-rule-metric-name"
SampledRequestsEnabled = false
}
}])

visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "friendly-metric-name"
sampled_requests_enabled = false
}
}
```

## Argument Reference

This resource supports the following arguments:
Expand All @@ -323,6 +365,7 @@ This resource supports the following arguments:
* `name` - (Required, Forces new resource) A friendly name of the rule group.
* `name_prefix` - (Optional) Creates a unique name beginning with the specified prefix. Conflicts with `name`.
* `rule` - (Optional) The rule blocks used to identify the web requests that you want to `allow`, `block`, or `count`. See [Rules](#rules) below for details.
* `rule_json` - (Optional) Raw JSON string to allow more than three nested statements. Conflicts with `rule` attribute. This is for advanced use cases where more than 3 levels of nested statements are required. **There is no drift detection at this time**. If you use this attribute instead of `rule`, you will be foregoing drift detection. Additionally, importing an existing rule group into a configuration with `rule_json` set will result in a one time in-place update as the remote rule configuration is initially written to the `rule` attribute. See the AWS [documentation](https://docs.aws.amazon.com/waf/latest/APIReference/API_CreateRuleGroup.html) for the JSON structure.
* `scope` - (Required, Forces new resource) Specifies whether this is for an AWS CloudFront distribution or for a regional application. Valid values are `CLOUDFRONT` or `REGIONAL`. To work with CloudFront, you must also specify the region `us-east-1` (N. Virginia) on the AWS provider.
* `tags` - (Optional) An array of key:value pairs to associate with the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
* `visibility_config` - (Required) Defines and enables Amazon CloudWatch metrics and web request sample collection. See [Visibility Configuration](#visibility-configuration) below for details.
Expand Down
Loading