From f017f28cb2b7e1eaea5682c27ec76996e0afa1ec Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:49:34 +0300 Subject: [PATCH 01/15] Validate against secret attributes --- ...etsmanager_secret_version_secret_string.md | 55 ++++++++++++ ...etsmanager_secret_version_secret_string.go | 85 +++++++++++++++++++ ...nager_secret_version_secret_string_test.go | 57 +++++++++++++ rules/provider.go | 1 + 4 files changed, 198 insertions(+) create mode 100644 docs/rules/aws_secretsmanager_secret_version_secret_string.md create mode 100644 rules/aws_secretsmanager_secret_version_secret_string.go create mode 100644 rules/aws_secretsmanager_secret_version_secret_string_test.go diff --git a/docs/rules/aws_secretsmanager_secret_version_secret_string.md b/docs/rules/aws_secretsmanager_secret_version_secret_string.md new file mode 100644 index 00000000..90fa6246 --- /dev/null +++ b/docs/rules/aws_secretsmanager_secret_version_secret_string.md @@ -0,0 +1,55 @@ +# aws_secretsmanager_secret_version_secret_string + +Disallows using the `secret_string` attribute, in favor of the `secret_string_wo` attribute. + +## Example + +```hcl +resource "aws_secretsmanager_secret_version" "test" { + secret_string = var.secret +} +``` + +``` +$ tflint +1 issue(s) found: + +Warning: "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_secretsmanager_secret_version_secret_string) + + on test.tf line 3: + 3: secret_string = var.secret + +``` + +## Why + +Saving secrets to state or plan files is a bad practice. It can cause serious security issues. Keeping secrets from these files is possible in most of the cases by using write-only attributes. + +## How To Fix + +Replace the `secret_string` with `secret_string_wo` and `secret_string_wo_version`, either via using an ephemeral resource as source or using an ephemeral variable: + +```hcl +ephemeral "random_password" "test" { + length = 32 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = ephemeral.random_password.test.value + secret_string_wo_version = 1 +} +``` + +```hcl +variable "test" { + type = string + ephemeral = true # Optional, non-ephemeral values can also be used for write-only attributes + description = "Input variable for a secret" +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = var.test + secret_string_wo_version = 1 +} +``` \ No newline at end of file diff --git a/rules/aws_secretsmanager_secret_version_secret_string.go b/rules/aws_secretsmanager_secret_version_secret_string.go new file mode 100644 index 00000000..c810d2d4 --- /dev/null +++ b/rules/aws_secretsmanager_secret_version_secret_string.go @@ -0,0 +1,85 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/zclconf/go-cty/cty" +) + +// AwsSecretsmanagerSecretVersionSecretStringRule checks if the secret_string attribute is used in secretsmanager_secret_version +// It emits a warning if the attribute is used, as it is a non-ephemeral attribute and the secret is stored in state +// It suggests using secret_string_wo instead +type AwsSecretsmanagerSecretVersionSecretStringRule struct { + tflint.DefaultRule + + resourceType string + attributeName string + writeOnlyAttributeName string +} + +// NewAwsSecretsmanagerSecretVersionSecretStringRule returns new rule with default attributes +func NewAwsSecretsmanagerSecretVersionSecretStringRule() *AwsSecretsmanagerSecretVersionSecretStringRule { + return &AwsSecretsmanagerSecretVersionSecretStringRule{ + resourceType: "aws_secretsmanager_secret_version", + attributeName: "secret_string", + writeOnlyAttributeName: "secret_string_wo", + } +} + +// Name returns the rule name +func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Name() string { + return "aws_secretsmanager_secret_version_secret_string" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks whether the secret string attribute exists +func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Check(runner tflint.Runner) error { + resources, err := runner.GetResourceContent(r.resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: r.attributeName}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes[r.attributeName] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + if !val.IsNull() { + runner.EmitIssue( + r, + fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", r.attributeName, r.writeOnlyAttributeName), + attribute.Expr.Range(), + ) + } + return nil + }, nil) + if err != nil { + return err + } + } + + return nil +} diff --git a/rules/aws_secretsmanager_secret_version_secret_string_test.go b/rules/aws_secretsmanager_secret_version_secret_string_test.go new file mode 100644 index 00000000..28ffb463 --- /dev/null +++ b/rules/aws_secretsmanager_secret_version_secret_string_test.go @@ -0,0 +1,57 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsSecretsmanagerSecretVersionSecretString(t *testing.T) { + cases := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "basic", + Content: ` +resource "aws_secretsmanager_secret_version" "test" { + secret_string = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsSecretsmanagerSecretVersionSecretStringRule(), + Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "secret_string_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 19}, + End: hcl.Pos{Line: 3, Column: 25}, + }, + }, + }, + }, + { + Name: "everything is fine", + Content: ` +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + } + + rule := NewAwsSecretsmanagerSecretVersionSecretStringRule() + + for _, tc := range cases { + runner := helper.TestRunner(t, map[string]string{"resource.tf": tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssues(t, tc.Expected, runner.Issues) + } +} diff --git a/rules/provider.go b/rules/provider.go index 654407b8..d10068f3 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -44,6 +44,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), NewAwsIAMRoleDeprecatedPolicyAttributesRule(), + NewAwsSecretsmanagerSecretVersionSecretStringRule(), } // Rules is a list of all rules From 1a5f21597eb66a3ca4b3e669f9159a696f740aef Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:53:10 +0300 Subject: [PATCH 02/15] fix: turn it into one rule for all write-only attributes --- ...string.md => aws_write_only_attributes.md} | 10 +- ...etsmanager_secret_version_secret_string.go | 85 ------- ...nager_secret_version_secret_string_test.go | 57 ----- rules/aws_write_only_attributes.go | 118 +++++++++ rules/aws_write_only_attributes_test.go | 236 ++++++++++++++++++ rules/provider.go | 2 +- 6 files changed, 361 insertions(+), 147 deletions(-) rename docs/rules/{aws_secretsmanager_secret_version_secret_string.md => aws_write_only_attributes.md} (62%) delete mode 100644 rules/aws_secretsmanager_secret_version_secret_string.go delete mode 100644 rules/aws_secretsmanager_secret_version_secret_string_test.go create mode 100644 rules/aws_write_only_attributes.go create mode 100644 rules/aws_write_only_attributes_test.go diff --git a/docs/rules/aws_secretsmanager_secret_version_secret_string.md b/docs/rules/aws_write_only_attributes.md similarity index 62% rename from docs/rules/aws_secretsmanager_secret_version_secret_string.md rename to docs/rules/aws_write_only_attributes.md index 90fa6246..0254b493 100644 --- a/docs/rules/aws_secretsmanager_secret_version_secret_string.md +++ b/docs/rules/aws_write_only_attributes.md @@ -1,9 +1,11 @@ -# aws_secretsmanager_secret_version_secret_string +# aws_write_only_attributes -Disallows using the `secret_string` attribute, in favor of the `secret_string_wo` attribute. +Disallows using the normal attribute, in favor of an available write-only attribute. This is only valid for Terraform version 1.11.0 and later. ## Example +In this example `aws_secretsmanager_secret_version` is used, but you can replace that with any resource with write-only attributes: + ```hcl resource "aws_secretsmanager_secret_version" "test" { secret_string = var.secret @@ -14,7 +16,7 @@ resource "aws_secretsmanager_secret_version" "test" { $ tflint 1 issue(s) found: -Warning: "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_secretsmanager_secret_version_secret_string) +Warning: [Fixable] "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_write_only_attributes) on test.tf line 3: 3: secret_string = var.secret @@ -27,7 +29,7 @@ Saving secrets to state or plan files is a bad practice. It can cause serious se ## How To Fix -Replace the `secret_string` with `secret_string_wo` and `secret_string_wo_version`, either via using an ephemeral resource as source or using an ephemeral variable: +Replace the `secret_string` with `secret_string_wo`, preferable via using an ephemeral resource as source or using an ephemeral variable: ```hcl ephemeral "random_password" "test" { diff --git a/rules/aws_secretsmanager_secret_version_secret_string.go b/rules/aws_secretsmanager_secret_version_secret_string.go deleted file mode 100644 index c810d2d4..00000000 --- a/rules/aws_secretsmanager_secret_version_secret_string.go +++ /dev/null @@ -1,85 +0,0 @@ -package rules - -import ( - "fmt" - - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/zclconf/go-cty/cty" -) - -// AwsSecretsmanagerSecretVersionSecretStringRule checks if the secret_string attribute is used in secretsmanager_secret_version -// It emits a warning if the attribute is used, as it is a non-ephemeral attribute and the secret is stored in state -// It suggests using secret_string_wo instead -type AwsSecretsmanagerSecretVersionSecretStringRule struct { - tflint.DefaultRule - - resourceType string - attributeName string - writeOnlyAttributeName string -} - -// NewAwsSecretsmanagerSecretVersionSecretStringRule returns new rule with default attributes -func NewAwsSecretsmanagerSecretVersionSecretStringRule() *AwsSecretsmanagerSecretVersionSecretStringRule { - return &AwsSecretsmanagerSecretVersionSecretStringRule{ - resourceType: "aws_secretsmanager_secret_version", - attributeName: "secret_string", - writeOnlyAttributeName: "secret_string_wo", - } -} - -// Name returns the rule name -func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Name() string { - return "aws_secretsmanager_secret_version_secret_string" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Enabled() bool { - return true -} - -// Severity returns the rule severity -func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Severity() tflint.Severity { - return tflint.WARNING -} - -// Link returns the rule reference link -func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -// Check checks whether the secret string attribute exists -func (r *AwsSecretsmanagerSecretVersionSecretStringRule) Check(runner tflint.Runner) error { - resources, err := runner.GetResourceContent(r.resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: r.attributeName}, - }, - }, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - attribute, exists := resource.Body.Attributes[r.attributeName] - if !exists { - continue - } - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - if !val.IsNull() { - runner.EmitIssue( - r, - fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", r.attributeName, r.writeOnlyAttributeName), - attribute.Expr.Range(), - ) - } - return nil - }, nil) - if err != nil { - return err - } - } - - return nil -} diff --git a/rules/aws_secretsmanager_secret_version_secret_string_test.go b/rules/aws_secretsmanager_secret_version_secret_string_test.go deleted file mode 100644 index 28ffb463..00000000 --- a/rules/aws_secretsmanager_secret_version_secret_string_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package rules - -import ( - "testing" - - hcl "github.com/hashicorp/hcl/v2" - "github.com/terraform-linters/tflint-plugin-sdk/helper" -) - -func Test_AwsSecretsmanagerSecretVersionSecretString(t *testing.T) { - cases := []struct { - Name string - Content string - Expected helper.Issues - }{ - { - Name: "basic", - Content: ` -resource "aws_secretsmanager_secret_version" "test" { - secret_string = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsSecretsmanagerSecretVersionSecretStringRule(), - Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "secret_string_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 19}, - End: hcl.Pos{Line: 3, Column: 25}, - }, - }, - }, - }, - { - Name: "everything is fine", - Content: ` -resource "aws_secretsmanager_secret_version" "test" { - secret_string_wo = "test" -} -`, - Expected: helper.Issues{}, - }, - } - - rule := NewAwsSecretsmanagerSecretVersionSecretStringRule() - - for _, tc := range cases { - runner := helper.TestRunner(t, map[string]string{"resource.tf": tc.Content}) - - if err := rule.Check(runner); err != nil { - t.Fatalf("Unexpected error occurred: %s", err) - } - - helper.AssertIssues(t, tc.Expected, runner.Issues) - } -} diff --git a/rules/aws_write_only_attributes.go b/rules/aws_write_only_attributes.go new file mode 100644 index 00000000..c6800a54 --- /dev/null +++ b/rules/aws_write_only_attributes.go @@ -0,0 +1,118 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/zclconf/go-cty/cty" +) + +// AwsWriteOnlyAttributesRule checks if a write-only attribute is available for sensitive input attributes +// It emits a warning if the normal attribute is used, as that is a non-ephemeral attribute and the secret value is stored in state +type AwsWriteOnlyAttributesRule struct { + tflint.DefaultRule + + writeOnlyAttributes map[string]writeOnlyAttribute +} + +type writeOnlyAttribute struct { + original string + alternative string +} + +// NewAwsWriteOnlyAttributesRule returns new rule with default attributes +func NewAwsWriteOnlyAttributesRule() *AwsWriteOnlyAttributesRule { + writeOnlyAttributes := map[string]writeOnlyAttribute{ + "aws_secretsmanager_secret_version": { + original: "secret_string", + alternative: "secret_string_wo", + }, + "aws_rds_cluster": { + original: "master_password", + alternative: "master_password_wo", + }, + "aws_redshift_cluster": { + original: "master_password", + alternative: "master_password_wo", + }, + "aws_docdb_cluster": { + original: "master_password", + alternative: "master_password_wo", + }, + "aws_redshiftserverless_namespace": { + original: "admin_password", + alternative: "admin_password_wo", + }, + "aws_ssm_parameter": { + original: "value", + alternative: "value_wo", + }, + } + return &AwsWriteOnlyAttributesRule{ + writeOnlyAttributes: writeOnlyAttributes, + } +} + +// Name returns the rule name +func (r *AwsWriteOnlyAttributesRule) Name() string { + return "aws_write_only_attributes" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsWriteOnlyAttributesRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AwsWriteOnlyAttributesRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsWriteOnlyAttributesRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks whether the secret string attribute exists +func (r *AwsWriteOnlyAttributesRule) Check(runner tflint.Runner) error { + for resourceType, attributes := range r.writeOnlyAttributes { + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: attributes.original}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes[attributes.original] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + if !val.IsNull() { + if err := runner.EmitIssueWithFix( + r, + fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", attributes.original, attributes.alternative), + attribute.Expr.Range(), + func(f tflint.Fixer) error { + return f.ReplaceText(attribute.NameRange, attributes.alternative) + }, + ); err != nil { + return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) + } + } + return nil + }, nil) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/rules/aws_write_only_attributes_test.go b/rules/aws_write_only_attributes_test.go new file mode 100644 index 00000000..bf8c78fe --- /dev/null +++ b/rules/aws_write_only_attributes_test.go @@ -0,0 +1,236 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsWriteOnlyAttribute(t *testing.T) { + cases := []struct { + Name string + Content string + Expected helper.Issues + Fixed string + }{ + { + Name: "basic aws_secretsmanager_secret_version", + Content: ` +resource "aws_secretsmanager_secret_version" "test" { + secret_string = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "secret_string_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 19}, + End: hcl.Pos{Line: 3, Column: 25}, + }, + }, + }, + Fixed: ` +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_secretsmanager_secret_version", + Content: ` +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + { + Name: "basic aws_rds_cluster", + Content: ` +resource "aws_rds_cluster" "test" { + master_password = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 21}, + End: hcl.Pos{Line: 3, Column: 27}, + }, + }, + }, + Fixed: ` +resource "aws_rds_cluster" "test" { + master_password_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_rds_cluster", + Content: ` +resource "aws_rds_cluster" "test" { + master_password_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + { + Name: "basic aws_redshift_cluster", + Content: ` +resource "aws_redshift_cluster" "test" { + master_password = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 21}, + End: hcl.Pos{Line: 3, Column: 27}, + }, + }, + }, + Fixed: ` +resource "aws_redshift_cluster" "test" { + master_password_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_redshift_cluster", + Content: ` +resource "aws_redshift_cluster" "test" { + master_password_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + { + Name: "basic aws_docdb_cluster", + Content: ` +resource "aws_docdb_cluster" "test" { + master_password = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 21}, + End: hcl.Pos{Line: 3, Column: 27}, + }, + }, + }, + Fixed: ` +resource "aws_docdb_cluster" "test" { + master_password_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_docdb_cluster", + Content: ` +resource "aws_docdb_cluster" "test" { + master_password_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + + { + Name: "basic aws_redshiftserverless_namespace", + Content: ` +resource "aws_redshiftserverless_namespace" "test" { + admin_password = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "admin_password_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 20}, + End: hcl.Pos{Line: 3, Column: 26}, + }, + }, + }, + Fixed: ` +resource "aws_redshiftserverless_namespace" "test" { + admin_password_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_redshiftserverless_namespace", + Content: ` +resource "aws_redshiftserverless_namespace" "test" { + admin_password_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + { + Name: "basic aws_ssm_parameter", + Content: ` +resource "aws_ssm_parameter" "test" { + value = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyAttributesRule(), + Message: `"value" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "value_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 11}, + End: hcl.Pos{Line: 3, Column: 17}, + }, + }, + }, + + Fixed: ` +resource "aws_ssm_parameter" "test" { + value_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_ssm_parameter", + Content: ` +resource "aws_ssm_parameter" "test" { + value_wo = "test" +} +`, + Expected: helper.Issues{}, + }, + } + + rule := NewAwsWriteOnlyAttributesRule() + + for _, tc := range cases { + filename := "resource.tf" + runner := helper.TestRunner(t, map[string]string{filename: tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + helper.AssertIssues(t, tc.Expected, runner.Issues) + + want := map[string]string{} + if tc.Fixed != "" { + want[filename] = tc.Fixed + } + helper.AssertChanges(t, want, runner.Changes()) + } +} diff --git a/rules/provider.go b/rules/provider.go index d10068f3..9772589c 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -44,7 +44,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), NewAwsIAMRoleDeprecatedPolicyAttributesRule(), - NewAwsSecretsmanagerSecretVersionSecretStringRule(), + NewAwsWriteOnlyAttributesRule(), } // Rules is a list of all rules From 97cfaa9bf12e0b089b3d19b9ef547dd7e23ef505 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:06:47 +0300 Subject: [PATCH 03/15] feat: also suggest the managed password solutions available --- rules/aws_write_only_attributes.go | 41 +++++++++++++++---------- rules/aws_write_only_attributes_test.go | 6 ++-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/rules/aws_write_only_attributes.go b/rules/aws_write_only_attributes.go index c6800a54..edee5055 100644 --- a/rules/aws_write_only_attributes.go +++ b/rules/aws_write_only_attributes.go @@ -18,36 +18,40 @@ type AwsWriteOnlyAttributesRule struct { } type writeOnlyAttribute struct { - original string - alternative string + original string + writeOnlyAlternative string + otherAlternative string } // NewAwsWriteOnlyAttributesRule returns new rule with default attributes func NewAwsWriteOnlyAttributesRule() *AwsWriteOnlyAttributesRule { writeOnlyAttributes := map[string]writeOnlyAttribute{ "aws_secretsmanager_secret_version": { - original: "secret_string", - alternative: "secret_string_wo", + original: "secret_string", + writeOnlyAlternative: "secret_string_wo", }, "aws_rds_cluster": { - original: "master_password", - alternative: "master_password_wo", + original: "master_password", + writeOnlyAlternative: "master_password_wo", + otherAlternative: "manage_master_user_password", }, "aws_redshift_cluster": { - original: "master_password", - alternative: "master_password_wo", + original: "master_password", + writeOnlyAlternative: "master_password_wo", + otherAlternative: "manage_master_password", }, "aws_docdb_cluster": { - original: "master_password", - alternative: "master_password_wo", + original: "master_password", + writeOnlyAlternative: "master_password_wo", }, "aws_redshiftserverless_namespace": { - original: "admin_password", - alternative: "admin_password_wo", + original: "admin_password", + writeOnlyAlternative: "admin_password_wo", + otherAlternative: "manage_admin_password", }, "aws_ssm_parameter": { - original: "value", - alternative: "value_wo", + original: "value", + writeOnlyAlternative: "value_wo", }, } return &AwsWriteOnlyAttributesRule{ @@ -94,13 +98,18 @@ func (r *AwsWriteOnlyAttributesRule) Check(runner tflint.Runner) error { } err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + mitigation := fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", attributes.original, attributes.writeOnlyAlternative) + if attributes.otherAlternative != "" { + mitigation += fmt.Sprintf(" Alternatively, you can use \"%s\" to manage the secret in an different way.", attributes.otherAlternative) + } + if !val.IsNull() { if err := runner.EmitIssueWithFix( r, - fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", attributes.original, attributes.alternative), + mitigation, attribute.Expr.Range(), func(f tflint.Fixer) error { - return f.ReplaceText(attribute.NameRange, attributes.alternative) + return f.ReplaceText(attribute.NameRange, attributes.writeOnlyAlternative) }, ); err != nil { return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) diff --git a/rules/aws_write_only_attributes_test.go b/rules/aws_write_only_attributes_test.go index bf8c78fe..ba94392f 100644 --- a/rules/aws_write_only_attributes_test.go +++ b/rules/aws_write_only_attributes_test.go @@ -57,7 +57,7 @@ resource "aws_rds_cluster" "test" { Expected: helper.Issues{ { Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo". Alternatively, you can use "manage_master_user_password" to manage the secret in an different way.`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 21}, @@ -90,7 +90,7 @@ resource "aws_redshift_cluster" "test" { Expected: helper.Issues{ { Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo". Alternatively, you can use "manage_master_password" to manage the secret in an different way.`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 21}, @@ -157,7 +157,7 @@ resource "aws_redshiftserverless_namespace" "test" { Expected: helper.Issues{ { Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "admin_password_wo".`, + Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "admin_password_wo". Alternatively, you can use "manage_admin_password" to manage the secret in an different way.`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 20}, From 092ef595c0982f8f50ca325a4651b3f85a28f27f Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:19:10 +0300 Subject: [PATCH 04/15] docs: update docs based on suggested alternatives --- docs/rules/aws_write_only_attributes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index 0254b493..44ff5f6e 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -1,6 +1,6 @@ # aws_write_only_attributes -Disallows using the normal attribute, in favor of an available write-only attribute. This is only valid for Terraform version 1.11.0 and later. +Disallows using the normal attribute containing sensitive information, in favor of an available write-only attribute. This is only valid for Terraform version 1.11.0 and later. If there alternative options available to prevent this information ending up in state, these are suggested as well. ## Example From 6852f203a0ffc84fec2509a983fde87efd92bd8c Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:09:18 +0300 Subject: [PATCH 05/15] fix: suggestion 1 Co-authored-by: Ben Drucker --- docs/rules/aws_write_only_attributes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index 44ff5f6e..ebb71901 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -25,7 +25,9 @@ Warning: [Fixable] "secret_string" is a non-ephemeral attribute, which means thi ## Why -Saving secrets to state or plan files is a bad practice. It can cause serious security issues. Keeping secrets from these files is possible in most of the cases by using write-only attributes. +By default, sensitive attributes are still stored in state, just hidden from view in plan output. Other resources are able to refer to these attributes. Current versions of Terraform also include support for write-only arguments, which are not persisted to state. Other resources cannot refer to their values. + +Using write-only arguments mitigates the risk of a malicious actor obtaining privileged credentials by accessing Terraform state files directly. Prefer using them over the original sensitive attribute unless you need to refer to it in other blocks, such as a [root `output`](https://developer.hashicorp.com/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files), that cannot be ephemeral. ## How To Fix From b0b5312c97720cf8877e5c68dd1d1ffc84a68e17 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:09:41 +0300 Subject: [PATCH 06/15] fix: suggestion 2 Co-authored-by: Ben Drucker --- docs/rules/aws_write_only_attributes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index ebb71901..abddeb7d 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -4,7 +4,7 @@ Disallows using the normal attribute containing sensitive information, in favor ## Example -In this example `aws_secretsmanager_secret_version` is used, but you can replace that with any resource with write-only attributes: +This example uses `aws_secretsmanager_secret_version`, but the rule applies to all resources with write-only attributes: ```hcl resource "aws_secretsmanager_secret_version" "test" { From 23ef504c9722a26b06d1c941c38c43b2130d28ac Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:09:57 +0300 Subject: [PATCH 07/15] fix: suggestion 3 Co-authored-by: Ben Drucker --- docs/rules/aws_write_only_attributes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index abddeb7d..f680e4f7 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -1,6 +1,6 @@ # aws_write_only_attributes -Disallows using the normal attribute containing sensitive information, in favor of an available write-only attribute. This is only valid for Terraform version 1.11.0 and later. If there alternative options available to prevent this information ending up in state, these are suggested as well. +Recommends using available [write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral/write-only) instead of the original sensitive attribute. This is only valid for Terraform v1.11+. ## Example From 962d77b0d0914ffc488ee0f07f0c9d34d4243dab Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:10:16 +0300 Subject: [PATCH 08/15] fix: suggestion 4 Co-authored-by: Ben Drucker --- docs/rules/aws_write_only_attributes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index f680e4f7..d40e825f 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -31,7 +31,7 @@ Using write-only arguments mitigates the risk of a malicious actor obtaining pri ## How To Fix -Replace the `secret_string` with `secret_string_wo`, preferable via using an ephemeral resource as source or using an ephemeral variable: +Replace the attribute with its write-only argument equivalent. Reference an ephemeral resource or ephemeral variable to ensure that the sensitive value is not persisted to state. ```hcl ephemeral "random_password" "test" { From a5c5b1c74935c367cab721c8720e76827fcdb258 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:42:40 +0300 Subject: [PATCH 09/15] fix: disabled by default, DB Instance added, wo attribute -> wo argument rewrite --- docs/rules/aws_write_only_attributes.md | 8 +- rules/aws_write_only_argurments.go | 121 +++++++++++++++++ ...t.go => aws_write_only_argurments_test.go} | 60 +++++++-- rules/aws_write_only_attributes.go | 127 ------------------ rules/provider.go | 2 +- 5 files changed, 173 insertions(+), 145 deletions(-) create mode 100644 rules/aws_write_only_argurments.go rename rules/{aws_write_only_attributes_test.go => aws_write_only_argurments_test.go} (79%) delete mode 100644 rules/aws_write_only_attributes.go diff --git a/docs/rules/aws_write_only_attributes.md b/docs/rules/aws_write_only_attributes.md index d40e825f..0cf716b7 100644 --- a/docs/rules/aws_write_only_attributes.md +++ b/docs/rules/aws_write_only_attributes.md @@ -1,10 +1,10 @@ -# aws_write_only_attributes +# aws_write_only_arguments Recommends using available [write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral/write-only) instead of the original sensitive attribute. This is only valid for Terraform v1.11+. ## Example -This example uses `aws_secretsmanager_secret_version`, but the rule applies to all resources with write-only attributes: +This example uses `aws_secretsmanager_secret_version`, but the rule applies to all resources with write-only arguments: ```hcl resource "aws_secretsmanager_secret_version" "test" { @@ -16,7 +16,7 @@ resource "aws_secretsmanager_secret_version" "test" { $ tflint 1 issue(s) found: -Warning: [Fixable] "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_write_only_attributes) +Warning: [Fixable] "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_write_only_arguments) on test.tf line 3: 3: secret_string = var.secret @@ -48,7 +48,7 @@ resource "aws_secretsmanager_secret_version" "test" { ```hcl variable "test" { type = string - ephemeral = true # Optional, non-ephemeral values can also be used for write-only attributes + ephemeral = true # Optional, non-ephemeral values can also be used for write-only arguments description = "Input variable for a secret" } diff --git a/rules/aws_write_only_argurments.go b/rules/aws_write_only_argurments.go new file mode 100644 index 00000000..fe5445fb --- /dev/null +++ b/rules/aws_write_only_argurments.go @@ -0,0 +1,121 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/zclconf/go-cty/cty" +) + +// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes +type AwsWriteOnlyArgumentsRule struct { + tflint.DefaultRule + + writeOnlyArguments map[string]writeOnlyArgument +} + +type writeOnlyArgument struct { + originalAttribute string + writeOnlyAlternative string +} + +// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes +func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { + writeOnlyArguments := map[string]writeOnlyArgument{ + "aws_secretsmanager_secret_version": { + originalAttribute: "secret_string", + writeOnlyAlternative: "secret_string_wo", + }, + "aws_rds_cluster": { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + "aws_db_instance": { + originalAttribute: "password", + writeOnlyAlternative: "password_wo", + }, + "aws_redshift_cluster": { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + "aws_docdb_cluster": { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + "aws_redshiftserverless_namespace": { + originalAttribute: "admin_password", + writeOnlyAlternative: "admin_password_wo", + }, + "aws_ssm_parameter": { + originalAttribute: "value", + writeOnlyAlternative: "value_wo", + }, + } + return &AwsWriteOnlyArgumentsRule{ + writeOnlyArguments: writeOnlyArguments, + } +} + +// Name returns the rule name +func (r *AwsWriteOnlyArgumentsRule) Name() string { + return "aws_write_only_arguments" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsWriteOnlyArgumentsRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsWriteOnlyArgumentsRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks whether the sensitive attribute exists +func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { + for resourceType, attributes := range r.writeOnlyArguments { + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: attributes.originalAttribute}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes[attributes.originalAttribute] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + if !val.IsNull() { + if err := runner.EmitIssueWithFix( + r, + fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", attributes.originalAttribute, attributes.writeOnlyAlternative), + attribute.Expr.Range(), + func(f tflint.Fixer) error { + return f.ReplaceText(attribute.NameRange, attributes.writeOnlyAlternative) + }, + ); err != nil { + return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) + } + } + return nil + }, nil) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/rules/aws_write_only_attributes_test.go b/rules/aws_write_only_argurments_test.go similarity index 79% rename from rules/aws_write_only_attributes_test.go rename to rules/aws_write_only_argurments_test.go index ba94392f..7b7d5e99 100644 --- a/rules/aws_write_only_attributes_test.go +++ b/rules/aws_write_only_argurments_test.go @@ -23,8 +23,8 @@ resource "aws_secretsmanager_secret_version" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "secret_string_wo".`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "secret_string_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 19}, @@ -56,8 +56,8 @@ resource "aws_rds_cluster" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo". Alternatively, you can use "manage_master_user_password" to manage the secret in an different way.`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 21}, @@ -77,6 +77,40 @@ resource "aws_rds_cluster" "test" { resource "aws_rds_cluster" "test" { master_password_wo = "test" } +`, + Expected: helper.Issues{}, + }, + + { + Name: "basic aws_db_instance", + Content: ` +resource "aws_db_instance" "test" { + password = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "password_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 14}, + End: hcl.Pos{Line: 3, Column: 20}, + }, + }, + }, + Fixed: ` +resource "aws_db_instance" "test" { + password_wo = "test" +} +`, + }, + { + Name: "everything is fine aws_db_instance", + Content: ` +resource "aws_db_instance" "test" { + password_wo = "test" +} `, Expected: helper.Issues{}, }, @@ -89,8 +123,8 @@ resource "aws_redshift_cluster" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo". Alternatively, you can use "manage_master_password" to manage the secret in an different way.`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 21}, @@ -122,8 +156,8 @@ resource "aws_docdb_cluster" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "master_password_wo".`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 21}, @@ -156,8 +190,8 @@ resource "aws_redshiftserverless_namespace" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "admin_password_wo". Alternatively, you can use "manage_admin_password" to manage the secret in an different way.`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "admin_password_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 20}, @@ -189,8 +223,8 @@ resource "aws_ssm_parameter" "test" { `, Expected: helper.Issues{ { - Rule: NewAwsWriteOnlyAttributesRule(), - Message: `"value" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute "value_wo".`, + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"value" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "value_wo".`, Range: hcl.Range{ Filename: "resource.tf", Start: hcl.Pos{Line: 3, Column: 11}, @@ -216,7 +250,7 @@ resource "aws_ssm_parameter" "test" { }, } - rule := NewAwsWriteOnlyAttributesRule() + rule := NewAwsWriteOnlyArgumentsRule() for _, tc := range cases { filename := "resource.tf" diff --git a/rules/aws_write_only_attributes.go b/rules/aws_write_only_attributes.go deleted file mode 100644 index edee5055..00000000 --- a/rules/aws_write_only_attributes.go +++ /dev/null @@ -1,127 +0,0 @@ -package rules - -import ( - "fmt" - - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/zclconf/go-cty/cty" -) - -// AwsWriteOnlyAttributesRule checks if a write-only attribute is available for sensitive input attributes -// It emits a warning if the normal attribute is used, as that is a non-ephemeral attribute and the secret value is stored in state -type AwsWriteOnlyAttributesRule struct { - tflint.DefaultRule - - writeOnlyAttributes map[string]writeOnlyAttribute -} - -type writeOnlyAttribute struct { - original string - writeOnlyAlternative string - otherAlternative string -} - -// NewAwsWriteOnlyAttributesRule returns new rule with default attributes -func NewAwsWriteOnlyAttributesRule() *AwsWriteOnlyAttributesRule { - writeOnlyAttributes := map[string]writeOnlyAttribute{ - "aws_secretsmanager_secret_version": { - original: "secret_string", - writeOnlyAlternative: "secret_string_wo", - }, - "aws_rds_cluster": { - original: "master_password", - writeOnlyAlternative: "master_password_wo", - otherAlternative: "manage_master_user_password", - }, - "aws_redshift_cluster": { - original: "master_password", - writeOnlyAlternative: "master_password_wo", - otherAlternative: "manage_master_password", - }, - "aws_docdb_cluster": { - original: "master_password", - writeOnlyAlternative: "master_password_wo", - }, - "aws_redshiftserverless_namespace": { - original: "admin_password", - writeOnlyAlternative: "admin_password_wo", - otherAlternative: "manage_admin_password", - }, - "aws_ssm_parameter": { - original: "value", - writeOnlyAlternative: "value_wo", - }, - } - return &AwsWriteOnlyAttributesRule{ - writeOnlyAttributes: writeOnlyAttributes, - } -} - -// Name returns the rule name -func (r *AwsWriteOnlyAttributesRule) Name() string { - return "aws_write_only_attributes" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsWriteOnlyAttributesRule) Enabled() bool { - return true -} - -// Severity returns the rule severity -func (r *AwsWriteOnlyAttributesRule) Severity() tflint.Severity { - return tflint.WARNING -} - -// Link returns the rule reference link -func (r *AwsWriteOnlyAttributesRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -// Check checks whether the secret string attribute exists -func (r *AwsWriteOnlyAttributesRule) Check(runner tflint.Runner) error { - for resourceType, attributes := range r.writeOnlyAttributes { - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: attributes.original}, - }, - }, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - attribute, exists := resource.Body.Attributes[attributes.original] - if !exists { - continue - } - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - mitigation := fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only attribute \"%s\".", attributes.original, attributes.writeOnlyAlternative) - if attributes.otherAlternative != "" { - mitigation += fmt.Sprintf(" Alternatively, you can use \"%s\" to manage the secret in an different way.", attributes.otherAlternative) - } - - if !val.IsNull() { - if err := runner.EmitIssueWithFix( - r, - mitigation, - attribute.Expr.Range(), - func(f tflint.Fixer) error { - return f.ReplaceText(attribute.NameRange, attributes.writeOnlyAlternative) - }, - ); err != nil { - return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) - } - } - return nil - }, nil) - if err != nil { - return err - } - } - } - - return nil -} diff --git a/rules/provider.go b/rules/provider.go index 9772589c..5a2a149f 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -44,7 +44,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), NewAwsIAMRoleDeprecatedPolicyAttributesRule(), - NewAwsWriteOnlyAttributesRule(), + NewAwsWriteOnlyArgumentsRule(), } // Rules is a list of all rules From 2ea69ddd314f3dd4706468e9c3a4823b08f0bab3 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Sat, 12 Apr 2025 01:05:48 +0300 Subject: [PATCH 10/15] feat: update for auto gen --- rules/aws_write_only_arguments.go | 137 ++++++++++++++++++ ...st.go => aws_write_only_arguments_test.go} | 123 ++++++---------- rules/aws_write_only_argurments.go | 121 ---------------- rules/write_only/generator/main.go | 67 +++++++++ rules/write_only/rule.go.tmpl | 103 +++++++++++++ rules/write_only/rule_test.go.tmpl | 66 +++++++++ rules/write_only/write_only.go | 3 + 7 files changed, 418 insertions(+), 202 deletions(-) create mode 100644 rules/aws_write_only_arguments.go rename rules/{aws_write_only_argurments_test.go => aws_write_only_arguments_test.go} (77%) delete mode 100644 rules/aws_write_only_argurments.go create mode 100644 rules/write_only/generator/main.go create mode 100644 rules/write_only/rule.go.tmpl create mode 100644 rules/write_only/rule_test.go.tmpl create mode 100644 rules/write_only/write_only.go diff --git a/rules/aws_write_only_arguments.go b/rules/aws_write_only_arguments.go new file mode 100644 index 00000000..f40d0058 --- /dev/null +++ b/rules/aws_write_only_arguments.go @@ -0,0 +1,137 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/zclconf/go-cty/cty" +) + +// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes +type AwsWriteOnlyArgumentsRule struct { + tflint.DefaultRule + + writeOnlyArguments map[string][]writeOnlyArgument +} + +type writeOnlyArgument struct { + originalAttribute string + writeOnlyAlternative string +} + +// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes +func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { + writeOnlyArguments := map[string][]writeOnlyArgument{ + "aws_db_instance": { + { + originalAttribute: "password", + writeOnlyAlternative: "password_wo", + }, + }, + "aws_docdb_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + }, + "aws_rds_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + }, + "aws_redshift_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + }, + }, + "aws_redshiftserverless_namespace": { + { + originalAttribute: "admin_user_password", + writeOnlyAlternative: "admin_user_password_wo", + }, + }, + "aws_secretsmanager_secret_version": { + { + originalAttribute: "secret_string", + writeOnlyAlternative: "secret_string_wo", + }, + }, + "aws_ssm_parameter": { + { + originalAttribute: "value", + writeOnlyAlternative: "value_wo", + }, + }, + } + return &AwsWriteOnlyArgumentsRule{ + writeOnlyArguments: writeOnlyArguments, + } +} + +// Name returns the rule name +func (r *AwsWriteOnlyArgumentsRule) Name() string { + return "aws_write_only_arguments" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsWriteOnlyArgumentsRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsWriteOnlyArgumentsRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks whether the sensitive attribute exists +func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { + for resourceType, attributes := range r.writeOnlyArguments { + for _, resourceAttribute := range attributes { + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: resourceAttribute.originalAttribute}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes[resourceAttribute.originalAttribute] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + if !val.IsNull() { + if err := runner.EmitIssueWithFix( + r, + fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative), + attribute.Expr.Range(), + func(f tflint.Fixer) error { + return f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + }, + ); err != nil { + return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) + } + } + return nil + }, nil) + if err != nil { + return err + } + } + } + } + + return nil +} diff --git a/rules/aws_write_only_argurments_test.go b/rules/aws_write_only_arguments_test.go similarity index 77% rename from rules/aws_write_only_argurments_test.go rename to rules/aws_write_only_arguments_test.go index 7b7d5e99..e151e435 100644 --- a/rules/aws_write_only_argurments_test.go +++ b/rules/aws_write_only_arguments_test.go @@ -3,7 +3,6 @@ package rules import ( "testing" - hcl "github.com/hashicorp/hcl/v2" "github.com/terraform-linters/tflint-plugin-sdk/helper" ) @@ -15,42 +14,37 @@ func Test_AwsWriteOnlyAttribute(t *testing.T) { Fixed string }{ { - Name: "basic aws_secretsmanager_secret_version", + Name: "basic aws_db_instance", Content: ` -resource "aws_secretsmanager_secret_version" "test" { - secret_string = "test" +resource "aws_db_instance" "test" { + password = "test" } `, Expected: helper.Issues{ { Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "secret_string_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 19}, - End: hcl.Pos{Line: 3, Column: 25}, - }, + Message: `"password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "password_wo".`, }, }, Fixed: ` -resource "aws_secretsmanager_secret_version" "test" { - secret_string_wo = "test" +resource "aws_db_instance" "test" { + password_wo = "test" } `, }, { - Name: "everything is fine aws_secretsmanager_secret_version", + Name: "everything is fine aws_db_instance", Content: ` -resource "aws_secretsmanager_secret_version" "test" { - secret_string_wo = "test" +resource "aws_db_instance" "test" { + password_wo = "test" } `, Expected: helper.Issues{}, }, { - Name: "basic aws_rds_cluster", + Name: "basic aws_docdb_cluster", Content: ` -resource "aws_rds_cluster" "test" { +resource "aws_docdb_cluster" "test" { master_password = "test" } `, @@ -58,58 +52,47 @@ resource "aws_rds_cluster" "test" { { Rule: NewAwsWriteOnlyArgumentsRule(), Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 21}, - End: hcl.Pos{Line: 3, Column: 27}, - }, }, }, Fixed: ` -resource "aws_rds_cluster" "test" { +resource "aws_docdb_cluster" "test" { master_password_wo = "test" } `, }, { - Name: "everything is fine aws_rds_cluster", + Name: "everything is fine aws_docdb_cluster", Content: ` -resource "aws_rds_cluster" "test" { +resource "aws_docdb_cluster" "test" { master_password_wo = "test" } `, Expected: helper.Issues{}, }, - { - Name: "basic aws_db_instance", + Name: "basic aws_rds_cluster", Content: ` -resource "aws_db_instance" "test" { - password = "test" +resource "aws_rds_cluster" "test" { + master_password = "test" } `, Expected: helper.Issues{ { Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "password_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 14}, - End: hcl.Pos{Line: 3, Column: 20}, - }, + Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, }, }, Fixed: ` -resource "aws_db_instance" "test" { - password_wo = "test" +resource "aws_rds_cluster" "test" { + master_password_wo = "test" } `, }, { - Name: "everything is fine aws_db_instance", + Name: "everything is fine aws_rds_cluster", Content: ` -resource "aws_db_instance" "test" { - password_wo = "test" +resource "aws_rds_cluster" "test" { + master_password_wo = "test" } `, Expected: helper.Issues{}, @@ -125,11 +108,6 @@ resource "aws_redshift_cluster" "test" { { Rule: NewAwsWriteOnlyArgumentsRule(), Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 21}, - End: hcl.Pos{Line: 3, Column: 27}, - }, }, }, Fixed: ` @@ -148,68 +126,57 @@ resource "aws_redshift_cluster" "test" { Expected: helper.Issues{}, }, { - Name: "basic aws_docdb_cluster", + Name: "basic aws_redshiftserverless_namespace", Content: ` -resource "aws_docdb_cluster" "test" { - master_password = "test" +resource "aws_redshiftserverless_namespace" "test" { + admin_user_password = "test" } `, Expected: helper.Issues{ { Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 21}, - End: hcl.Pos{Line: 3, Column: 27}, - }, + Message: `"admin_user_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "admin_user_password_wo".`, }, }, Fixed: ` -resource "aws_docdb_cluster" "test" { - master_password_wo = "test" +resource "aws_redshiftserverless_namespace" "test" { + admin_user_password_wo = "test" } `, }, { - Name: "everything is fine aws_docdb_cluster", + Name: "everything is fine aws_redshiftserverless_namespace", Content: ` -resource "aws_docdb_cluster" "test" { - master_password_wo = "test" +resource "aws_redshiftserverless_namespace" "test" { + admin_user_password_wo = "test" } `, Expected: helper.Issues{}, }, - { - Name: "basic aws_redshiftserverless_namespace", + Name: "basic aws_secretsmanager_secret_version", Content: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_password = "test" +resource "aws_secretsmanager_secret_version" "test" { + secret_string = "test" } `, Expected: helper.Issues{ { Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"admin_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "admin_password_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 20}, - End: hcl.Pos{Line: 3, Column: 26}, - }, + Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "secret_string_wo".`, }, }, Fixed: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_password_wo = "test" +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = "test" } `, }, { - Name: "everything is fine aws_redshiftserverless_namespace", + Name: "everything is fine aws_secretsmanager_secret_version", Content: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_password_wo = "test" +resource "aws_secretsmanager_secret_version" "test" { + secret_string_wo = "test" } `, Expected: helper.Issues{}, @@ -225,14 +192,8 @@ resource "aws_ssm_parameter" "test" { { Rule: NewAwsWriteOnlyArgumentsRule(), Message: `"value" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "value_wo".`, - Range: hcl.Range{ - Filename: "resource.tf", - Start: hcl.Pos{Line: 3, Column: 11}, - End: hcl.Pos{Line: 3, Column: 17}, - }, }, }, - Fixed: ` resource "aws_ssm_parameter" "test" { value_wo = "test" @@ -259,7 +220,7 @@ resource "aws_ssm_parameter" "test" { if err := rule.Check(runner); err != nil { t.Fatalf("Unexpected error occurred: %s", err) } - helper.AssertIssues(t, tc.Expected, runner.Issues) + helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) want := map[string]string{} if tc.Fixed != "" { diff --git a/rules/aws_write_only_argurments.go b/rules/aws_write_only_argurments.go deleted file mode 100644 index fe5445fb..00000000 --- a/rules/aws_write_only_argurments.go +++ /dev/null @@ -1,121 +0,0 @@ -package rules - -import ( - "fmt" - - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/zclconf/go-cty/cty" -) - -// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes -type AwsWriteOnlyArgumentsRule struct { - tflint.DefaultRule - - writeOnlyArguments map[string]writeOnlyArgument -} - -type writeOnlyArgument struct { - originalAttribute string - writeOnlyAlternative string -} - -// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes -func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { - writeOnlyArguments := map[string]writeOnlyArgument{ - "aws_secretsmanager_secret_version": { - originalAttribute: "secret_string", - writeOnlyAlternative: "secret_string_wo", - }, - "aws_rds_cluster": { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - }, - "aws_db_instance": { - originalAttribute: "password", - writeOnlyAlternative: "password_wo", - }, - "aws_redshift_cluster": { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - }, - "aws_docdb_cluster": { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - }, - "aws_redshiftserverless_namespace": { - originalAttribute: "admin_password", - writeOnlyAlternative: "admin_password_wo", - }, - "aws_ssm_parameter": { - originalAttribute: "value", - writeOnlyAlternative: "value_wo", - }, - } - return &AwsWriteOnlyArgumentsRule{ - writeOnlyArguments: writeOnlyArguments, - } -} - -// Name returns the rule name -func (r *AwsWriteOnlyArgumentsRule) Name() string { - return "aws_write_only_arguments" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsWriteOnlyArgumentsRule) Enabled() bool { - return false -} - -// Severity returns the rule severity -func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity { - return tflint.WARNING -} - -// Link returns the rule reference link -func (r *AwsWriteOnlyArgumentsRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -// Check checks whether the sensitive attribute exists -func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { - for resourceType, attributes := range r.writeOnlyArguments { - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: attributes.originalAttribute}, - }, - }, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - attribute, exists := resource.Body.Attributes[attributes.originalAttribute] - if !exists { - continue - } - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - if !val.IsNull() { - if err := runner.EmitIssueWithFix( - r, - fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", attributes.originalAttribute, attributes.writeOnlyAlternative), - attribute.Expr.Range(), - func(f tflint.Fixer) error { - return f.ReplaceText(attribute.NameRange, attributes.writeOnlyAlternative) - }, - ); err != nil { - return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) - } - } - return nil - }, nil) - if err != nil { - return err - } - } - } - - return nil -} diff --git a/rules/write_only/generator/main.go b/rules/write_only/generator/main.go new file mode 100644 index 00000000..37b59f9b --- /dev/null +++ b/rules/write_only/generator/main.go @@ -0,0 +1,67 @@ +//go:build generators + +package main + +import ( + "strings" + + tfjson "github.com/hashicorp/terraform-json" + utils "github.com/terraform-linters/tflint-ruleset-aws/rules/generator-utils" +) + +type writeOnlyArgument struct { + OriginalAttribute string + WriteOnlyAlternative string +} + +func main() { + awsProvider := utils.LoadProviderSchema("../../tools/provider-schema/schema.json") + + resourcesWithWriteOnly := map[string][]writeOnlyArgument{} + // Iterate over all resources in the AWS provider schema + for resourceName, resource := range awsProvider.ResourceSchemas { + if arguments := writeOnlyArguments(resource); len(arguments) > 0 { + // gather sensitive attributes with write only argument alternatives + resourcesWithWriteOnly[resourceName] = findReplaceableAttribute(arguments, resource) + } + } + + // Generate the rule file + utils.GenerateFile("../../rules/aws_write_only_arguments.go", "../../rules/write_only/rule.go.tmpl", resourcesWithWriteOnly) + + // Generate the test file + utils.GenerateFile("../../rules/aws_write_only_arguments_test.go", "../../rules/write_only/rule_test.go.tmpl", resourcesWithWriteOnly) +} + +func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument { + writeOnlyArguments := []writeOnlyArgument{} + + for _, argument := range arguments { + // Check if the argument ends with "_wo" and if the original attribute without "_wo" suffix exists in the resource schema + if attribute := strings.TrimSuffix(argument, "_wo"); strings.HasSuffix(argument, "_wo") && resource.Block.Attributes[attribute] != nil { + writeOnlyArguments = append(writeOnlyArguments, writeOnlyArgument{ + OriginalAttribute: attribute, + WriteOnlyAlternative: argument, + }) + } + } + + return writeOnlyArguments +} + +func writeOnlyArguments(resource *tfjson.Schema) []string { + if resource == nil || resource.Block == nil { + return []string{} + } + + writeOnlyArguments := []string{} + + // Check if the resource has any write-only attributes + for name, attribute := range resource.Block.Attributes { + if attribute.WriteOnly { + writeOnlyArguments = append(writeOnlyArguments, name) + } + } + + return writeOnlyArguments +} diff --git a/rules/write_only/rule.go.tmpl b/rules/write_only/rule.go.tmpl new file mode 100644 index 00000000..4a6c7f94 --- /dev/null +++ b/rules/write_only/rule.go.tmpl @@ -0,0 +1,103 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/zclconf/go-cty/cty" +) + +// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes +type AwsWriteOnlyArgumentsRule struct { + tflint.DefaultRule + + writeOnlyArguments map[string][]writeOnlyArgument +} + +type writeOnlyArgument struct { + originalAttribute string + writeOnlyAlternative string +} + +// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes +func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { + writeOnlyArguments := map[string][]writeOnlyArgument{ + {{- range $name, $value := . }} + "{{ $name }}": { {{- range $kk, $writeOnly := $value }} + { + originalAttribute: "{{ $writeOnly.OriginalAttribute }}", + writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}", + }, + }, + {{- end -}}{{- end }} + } + return &AwsWriteOnlyArgumentsRule{ + writeOnlyArguments: writeOnlyArguments, + } +} + +// Name returns the rule name +func (r *AwsWriteOnlyArgumentsRule) Name() string { + return "aws_write_only_arguments" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsWriteOnlyArgumentsRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsWriteOnlyArgumentsRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks whether the sensitive attribute exists +func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { + for resourceType, attributes := range r.writeOnlyArguments { + for _, resourceAttribute := range attributes { + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: resourceAttribute.originalAttribute}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes[resourceAttribute.originalAttribute] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + if !val.IsNull() { + if err := runner.EmitIssueWithFix( + r, + fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative), + attribute.Expr.Range(), + func(f tflint.Fixer) error { + return f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + }, + ); err != nil { + return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) + } + } + return nil + }, nil) + if err != nil { + return err + } + } + } + } + + return nil +} diff --git a/rules/write_only/rule_test.go.tmpl b/rules/write_only/rule_test.go.tmpl new file mode 100644 index 00000000..6e5345d8 --- /dev/null +++ b/rules/write_only/rule_test.go.tmpl @@ -0,0 +1,66 @@ +package rules + +import ( + "testing" + + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsWriteOnlyAttribute(t *testing.T) { + cases := []struct { + Name string + Content string + Expected helper.Issues + Fixed string + }{ + {{- range $name, $value := . }} + {{- range $kk, $writeOnly := $value }} + { + Name: "basic {{ $name }}", + Content: ` +resource "{{ $name }}" "test" { + {{ $writeOnly.OriginalAttribute }} = "test" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsWriteOnlyArgumentsRule(), + Message: `"{{ $writeOnly.OriginalAttribute }}" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "{{ $writeOnly.WriteOnlyAlternative }}".`, + }, + }, + Fixed: ` +resource "{{ $name }}" "test" { + {{ $writeOnly.WriteOnlyAlternative }} = "test" +} +`, + }, + { + Name: "everything is fine {{ $name }}", + Content: ` +resource "{{ $name }}" "test" { + {{ $writeOnly.WriteOnlyAlternative }} = "test" +} +`, + Expected: helper.Issues{}, + }, + {{- end -}}{{- end }} + } + + rule := NewAwsWriteOnlyArgumentsRule() + + for _, tc := range cases { + filename := "resource.tf" + runner := helper.TestRunner(t, map[string]string{filename: tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) + + want := map[string]string{} + if tc.Fixed != "" { + want[filename] = tc.Fixed + } + helper.AssertChanges(t, want, runner.Changes()) + } +} diff --git a/rules/write_only/write_only.go b/rules/write_only/write_only.go new file mode 100644 index 00000000..fe8d41bc --- /dev/null +++ b/rules/write_only/write_only.go @@ -0,0 +1,3 @@ +//go:generate go run -tags generators ./generator/main.go + +package write_only From e051181b1b8c131c810121590555e9906d65d9a4 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Sat, 12 Apr 2025 01:29:45 +0300 Subject: [PATCH 11/15] refactor: moving files around + DO NOT EDIT comment --- rules/provider.go | 3 ++- rules/{ => write_only}/aws_write_only_arguments.go | 4 +++- rules/{ => write_only}/aws_write_only_arguments_test.go | 4 +++- rules/write_only/generator/main.go | 4 ++-- rules/write_only/rule.go.tmpl | 4 +++- rules/write_only/rule_test.go.tmpl | 4 +++- 6 files changed, 16 insertions(+), 7 deletions(-) rename rules/{ => write_only}/aws_write_only_arguments.go (97%) rename rules/{ => write_only}/aws_write_only_arguments_test.go (98%) diff --git a/rules/provider.go b/rules/provider.go index 5a2a149f..75a6799a 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -4,6 +4,7 @@ import ( "github.com/terraform-linters/tflint-plugin-sdk/tflint" "github.com/terraform-linters/tflint-ruleset-aws/rules/api" "github.com/terraform-linters/tflint-ruleset-aws/rules/models" + "github.com/terraform-linters/tflint-ruleset-aws/rules/write_only" ) var manualRules = []tflint.Rule{ @@ -44,7 +45,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), NewAwsIAMRoleDeprecatedPolicyAttributesRule(), - NewAwsWriteOnlyArgumentsRule(), + write_only.NewAwsWriteOnlyArgumentsRule(), } // Rules is a list of all rules diff --git a/rules/aws_write_only_arguments.go b/rules/write_only/aws_write_only_arguments.go similarity index 97% rename from rules/aws_write_only_arguments.go rename to rules/write_only/aws_write_only_arguments.go index f40d0058..4cea1236 100644 --- a/rules/aws_write_only_arguments.go +++ b/rules/write_only/aws_write_only_arguments.go @@ -1,4 +1,6 @@ -package rules +// This file generated by `generator/main.go`. DO NOT EDIT + +package write_only import ( "fmt" diff --git a/rules/aws_write_only_arguments_test.go b/rules/write_only/aws_write_only_arguments_test.go similarity index 98% rename from rules/aws_write_only_arguments_test.go rename to rules/write_only/aws_write_only_arguments_test.go index e151e435..86ea27d7 100644 --- a/rules/aws_write_only_arguments_test.go +++ b/rules/write_only/aws_write_only_arguments_test.go @@ -1,4 +1,6 @@ -package rules +// This file generated by `generator/main.go`. DO NOT EDIT + +package write_only import ( "testing" diff --git a/rules/write_only/generator/main.go b/rules/write_only/generator/main.go index 37b59f9b..5c9969ca 100644 --- a/rules/write_only/generator/main.go +++ b/rules/write_only/generator/main.go @@ -27,10 +27,10 @@ func main() { } // Generate the rule file - utils.GenerateFile("../../rules/aws_write_only_arguments.go", "../../rules/write_only/rule.go.tmpl", resourcesWithWriteOnly) + utils.GenerateFile("../../rules/write_only/aws_write_only_arguments.go", "../../rules/write_only/rule.go.tmpl", resourcesWithWriteOnly) // Generate the test file - utils.GenerateFile("../../rules/aws_write_only_arguments_test.go", "../../rules/write_only/rule_test.go.tmpl", resourcesWithWriteOnly) + utils.GenerateFile("../../rules/write_only/aws_write_only_arguments_test.go", "../../rules/write_only/rule_test.go.tmpl", resourcesWithWriteOnly) } func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument { diff --git a/rules/write_only/rule.go.tmpl b/rules/write_only/rule.go.tmpl index 4a6c7f94..ca0cd4f5 100644 --- a/rules/write_only/rule.go.tmpl +++ b/rules/write_only/rule.go.tmpl @@ -1,4 +1,6 @@ -package rules +// This file generated by `generator/main.go`. DO NOT EDIT + +package write_only import ( "fmt" diff --git a/rules/write_only/rule_test.go.tmpl b/rules/write_only/rule_test.go.tmpl index 6e5345d8..8dffb8fe 100644 --- a/rules/write_only/rule_test.go.tmpl +++ b/rules/write_only/rule_test.go.tmpl @@ -1,4 +1,6 @@ -package rules +// This file generated by `generator/main.go`. DO NOT EDIT + +package write_only import ( "testing" From 52316d52229c67cfe4fdf9e87773d29cd026fb6d Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Sat, 12 Apr 2025 15:17:01 +0300 Subject: [PATCH 12/15] refactor: rename package to `ephemeral` --- .../{write_only => ephemeral}/aws_write_only_arguments.go | 2 +- .../aws_write_only_arguments_rule.go.tmpl} | 2 +- .../aws_write_only_arguments_rule_test.go.tmpl} | 2 +- .../aws_write_only_arguments_test.go | 2 +- .../{write_only/write_only.go => ephemeral/ephemeral.go} | 2 +- rules/{write_only => ephemeral}/generator/main.go | 8 ++++---- rules/provider.go | 5 ++--- 7 files changed, 11 insertions(+), 12 deletions(-) rename rules/{write_only => ephemeral}/aws_write_only_arguments.go (99%) rename rules/{write_only/rule.go.tmpl => ephemeral/aws_write_only_arguments_rule.go.tmpl} (99%) rename rules/{write_only/rule_test.go.tmpl => ephemeral/aws_write_only_arguments_rule_test.go.tmpl} (98%) rename rules/{write_only => ephemeral}/aws_write_only_arguments_test.go (99%) rename rules/{write_only/write_only.go => ephemeral/ephemeral.go} (75%) rename rules/{write_only => ephemeral}/generator/main.go (81%) diff --git a/rules/write_only/aws_write_only_arguments.go b/rules/ephemeral/aws_write_only_arguments.go similarity index 99% rename from rules/write_only/aws_write_only_arguments.go rename to rules/ephemeral/aws_write_only_arguments.go index 4cea1236..89fda9dc 100644 --- a/rules/write_only/aws_write_only_arguments.go +++ b/rules/ephemeral/aws_write_only_arguments.go @@ -1,6 +1,6 @@ // This file generated by `generator/main.go`. DO NOT EDIT -package write_only +package ephemeral import ( "fmt" diff --git a/rules/write_only/rule.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl similarity index 99% rename from rules/write_only/rule.go.tmpl rename to rules/ephemeral/aws_write_only_arguments_rule.go.tmpl index ca0cd4f5..afe44f5b 100644 --- a/rules/write_only/rule.go.tmpl +++ b/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl @@ -1,6 +1,6 @@ // This file generated by `generator/main.go`. DO NOT EDIT -package write_only +package ephemeral import ( "fmt" diff --git a/rules/write_only/rule_test.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl similarity index 98% rename from rules/write_only/rule_test.go.tmpl rename to rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl index 8dffb8fe..044ff858 100644 --- a/rules/write_only/rule_test.go.tmpl +++ b/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl @@ -1,6 +1,6 @@ // This file generated by `generator/main.go`. DO NOT EDIT -package write_only +package ephemeral import ( "testing" diff --git a/rules/write_only/aws_write_only_arguments_test.go b/rules/ephemeral/aws_write_only_arguments_test.go similarity index 99% rename from rules/write_only/aws_write_only_arguments_test.go rename to rules/ephemeral/aws_write_only_arguments_test.go index 86ea27d7..c1bd6904 100644 --- a/rules/write_only/aws_write_only_arguments_test.go +++ b/rules/ephemeral/aws_write_only_arguments_test.go @@ -1,6 +1,6 @@ // This file generated by `generator/main.go`. DO NOT EDIT -package write_only +package ephemeral import ( "testing" diff --git a/rules/write_only/write_only.go b/rules/ephemeral/ephemeral.go similarity index 75% rename from rules/write_only/write_only.go rename to rules/ephemeral/ephemeral.go index fe8d41bc..e7c9be5a 100644 --- a/rules/write_only/write_only.go +++ b/rules/ephemeral/ephemeral.go @@ -1,3 +1,3 @@ //go:generate go run -tags generators ./generator/main.go -package write_only +package ephemeral diff --git a/rules/write_only/generator/main.go b/rules/ephemeral/generator/main.go similarity index 81% rename from rules/write_only/generator/main.go rename to rules/ephemeral/generator/main.go index 5c9969ca..88efb6e3 100644 --- a/rules/write_only/generator/main.go +++ b/rules/ephemeral/generator/main.go @@ -26,11 +26,11 @@ func main() { } } - // Generate the rule file - utils.GenerateFile("../../rules/write_only/aws_write_only_arguments.go", "../../rules/write_only/rule.go.tmpl", resourcesWithWriteOnly) + // Generate the write-only rule file + utils.GenerateFile("../../rules/ephemeral/aws_write_only_arguments.go", "../../rules/ephemeral/aws_write_only_arguments_rule.go.tmpl", resourcesWithWriteOnly) - // Generate the test file - utils.GenerateFile("../../rules/write_only/aws_write_only_arguments_test.go", "../../rules/write_only/rule_test.go.tmpl", resourcesWithWriteOnly) + // Generate the write-only test file + utils.GenerateFile("../../rules/ephemeral/aws_write_only_arguments_test.go", "../../rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl", resourcesWithWriteOnly) } func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument { diff --git a/rules/provider.go b/rules/provider.go index 75a6799a..d0ff6e90 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -3,8 +3,8 @@ package rules import ( "github.com/terraform-linters/tflint-plugin-sdk/tflint" "github.com/terraform-linters/tflint-ruleset-aws/rules/api" + "github.com/terraform-linters/tflint-ruleset-aws/rules/ephemeral" "github.com/terraform-linters/tflint-ruleset-aws/rules/models" - "github.com/terraform-linters/tflint-ruleset-aws/rules/write_only" ) var manualRules = []tflint.Rule{ @@ -44,8 +44,7 @@ var manualRules = []tflint.Rule{ NewAwsProviderMissingDefaultTagsRule(), NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), - NewAwsIAMRoleDeprecatedPolicyAttributesRule(), - write_only.NewAwsWriteOnlyArgumentsRule(), + ephemeral.NewAwsWriteOnlyArgumentsRule(), } // Rules is a list of all rules From 6780682296b9700e439880a5bc1c0468425ea770 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:48:18 +0300 Subject: [PATCH 13/15] fix: add *_wo_version attribute via autofix as well --- rules/ephemeral/aws_write_only_arguments.go | 46 ++++++++++++------- .../aws_write_only_arguments_rule.go.tmpl | 16 +++++-- ...aws_write_only_arguments_rule_test.go.tmpl | 6 ++- .../aws_write_only_arguments_test.go | 42 +++++++++++------ rules/ephemeral/generator/main.go | 14 ++++-- rules/provider.go | 1 + 6 files changed, 82 insertions(+), 43 deletions(-) diff --git a/rules/ephemeral/aws_write_only_arguments.go b/rules/ephemeral/aws_write_only_arguments.go index 89fda9dc..a1ca8807 100644 --- a/rules/ephemeral/aws_write_only_arguments.go +++ b/rules/ephemeral/aws_write_only_arguments.go @@ -19,8 +19,9 @@ type AwsWriteOnlyArgumentsRule struct { } type writeOnlyArgument struct { - originalAttribute string - writeOnlyAlternative string + originalAttribute string + writeOnlyAlternative string + writeOnlyVersionAttribute string } // NewAwsWriteOnlyArgumentsRule returns new rule with default attributes @@ -28,44 +29,51 @@ func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { writeOnlyArguments := map[string][]writeOnlyArgument{ "aws_db_instance": { { - originalAttribute: "password", - writeOnlyAlternative: "password_wo", + originalAttribute: "password", + writeOnlyAlternative: "password_wo", + writeOnlyVersionAttribute: "password_wo_version", }, }, "aws_docdb_cluster": { { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", }, }, "aws_rds_cluster": { { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", }, }, "aws_redshift_cluster": { { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", }, }, "aws_redshiftserverless_namespace": { { - originalAttribute: "admin_user_password", - writeOnlyAlternative: "admin_user_password_wo", + originalAttribute: "admin_user_password", + writeOnlyAlternative: "admin_user_password_wo", + writeOnlyVersionAttribute: "admin_user_password_wo_version", }, }, "aws_secretsmanager_secret_version": { { - originalAttribute: "secret_string", - writeOnlyAlternative: "secret_string_wo", + originalAttribute: "secret_string", + writeOnlyAlternative: "secret_string_wo", + writeOnlyVersionAttribute: "secret_string_wo_version", }, }, "aws_ssm_parameter": { { - originalAttribute: "value", - writeOnlyAlternative: "value_wo", + originalAttribute: "value", + writeOnlyAlternative: "value_wo", + writeOnlyVersionAttribute: "value_wo_version", }, }, } @@ -120,7 +128,11 @@ func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative), attribute.Expr.Range(), func(f tflint.Fixer) error { - return f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + err := f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + if err != nil { + return err + } + return f.InsertTextAfter(attribute.Range, fmt.Sprintf("\n %s = 1", resourceAttribute.writeOnlyVersionAttribute)) }, ); err != nil { return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) diff --git a/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl index afe44f5b..7d2adeac 100644 --- a/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl +++ b/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl @@ -19,8 +19,9 @@ type AwsWriteOnlyArgumentsRule struct { } type writeOnlyArgument struct { - originalAttribute string - writeOnlyAlternative string + originalAttribute string + writeOnlyAlternative string + writeOnlyVersionAttribute string } // NewAwsWriteOnlyArgumentsRule returns new rule with default attributes @@ -29,8 +30,9 @@ func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { {{- range $name, $value := . }} "{{ $name }}": { {{- range $kk, $writeOnly := $value }} { - originalAttribute: "{{ $writeOnly.OriginalAttribute }}", - writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}", + originalAttribute: "{{ $writeOnly.OriginalAttribute }}", + writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}", + writeOnlyVersionAttribute: "{{ $writeOnly.WriteOnlyVersionAttribute }}", }, }, {{- end -}}{{- end }} @@ -86,7 +88,11 @@ func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative), attribute.Expr.Range(), func(f tflint.Fixer) error { - return f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + err := f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) + if err != nil { + return err + } + return f.InsertTextAfter(attribute.Range, fmt.Sprintf("\n %s = 1", resourceAttribute.writeOnlyVersionAttribute)) }, ); err != nil { return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) diff --git a/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl index 044ff858..47e5b9ac 100644 --- a/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl +++ b/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl @@ -32,7 +32,8 @@ resource "{{ $name }}" "test" { }, Fixed: ` resource "{{ $name }}" "test" { - {{ $writeOnly.WriteOnlyAlternative }} = "test" + {{ $writeOnly.WriteOnlyAlternative }} = "test" + {{ $writeOnly.WriteOnlyVersionAttribute }} = 1 } `, }, @@ -40,7 +41,8 @@ resource "{{ $name }}" "test" { Name: "everything is fine {{ $name }}", Content: ` resource "{{ $name }}" "test" { - {{ $writeOnly.WriteOnlyAlternative }} = "test" + {{ $writeOnly.WriteOnlyAlternative }} = "test" + {{ $writeOnly.WriteOnlyVersionAttribute }} = 1 } `, Expected: helper.Issues{}, diff --git a/rules/ephemeral/aws_write_only_arguments_test.go b/rules/ephemeral/aws_write_only_arguments_test.go index c1bd6904..c83f7bfc 100644 --- a/rules/ephemeral/aws_write_only_arguments_test.go +++ b/rules/ephemeral/aws_write_only_arguments_test.go @@ -30,7 +30,8 @@ resource "aws_db_instance" "test" { }, Fixed: ` resource "aws_db_instance" "test" { - password_wo = "test" + password_wo = "test" + password_wo_version = 1 } `, }, @@ -38,7 +39,8 @@ resource "aws_db_instance" "test" { Name: "everything is fine aws_db_instance", Content: ` resource "aws_db_instance" "test" { - password_wo = "test" + password_wo = "test" + password_wo_version = 1 } `, Expected: helper.Issues{}, @@ -58,7 +60,8 @@ resource "aws_docdb_cluster" "test" { }, Fixed: ` resource "aws_docdb_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, }, @@ -66,7 +69,8 @@ resource "aws_docdb_cluster" "test" { Name: "everything is fine aws_docdb_cluster", Content: ` resource "aws_docdb_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, Expected: helper.Issues{}, @@ -86,7 +90,8 @@ resource "aws_rds_cluster" "test" { }, Fixed: ` resource "aws_rds_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, }, @@ -94,7 +99,8 @@ resource "aws_rds_cluster" "test" { Name: "everything is fine aws_rds_cluster", Content: ` resource "aws_rds_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, Expected: helper.Issues{}, @@ -114,7 +120,8 @@ resource "aws_redshift_cluster" "test" { }, Fixed: ` resource "aws_redshift_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, }, @@ -122,7 +129,8 @@ resource "aws_redshift_cluster" "test" { Name: "everything is fine aws_redshift_cluster", Content: ` resource "aws_redshift_cluster" "test" { - master_password_wo = "test" + master_password_wo = "test" + master_password_wo_version = 1 } `, Expected: helper.Issues{}, @@ -142,7 +150,8 @@ resource "aws_redshiftserverless_namespace" "test" { }, Fixed: ` resource "aws_redshiftserverless_namespace" "test" { - admin_user_password_wo = "test" + admin_user_password_wo = "test" + admin_user_password_wo_version = 1 } `, }, @@ -150,7 +159,8 @@ resource "aws_redshiftserverless_namespace" "test" { Name: "everything is fine aws_redshiftserverless_namespace", Content: ` resource "aws_redshiftserverless_namespace" "test" { - admin_user_password_wo = "test" + admin_user_password_wo = "test" + admin_user_password_wo_version = 1 } `, Expected: helper.Issues{}, @@ -170,7 +180,8 @@ resource "aws_secretsmanager_secret_version" "test" { }, Fixed: ` resource "aws_secretsmanager_secret_version" "test" { - secret_string_wo = "test" + secret_string_wo = "test" + secret_string_wo_version = 1 } `, }, @@ -178,7 +189,8 @@ resource "aws_secretsmanager_secret_version" "test" { Name: "everything is fine aws_secretsmanager_secret_version", Content: ` resource "aws_secretsmanager_secret_version" "test" { - secret_string_wo = "test" + secret_string_wo = "test" + secret_string_wo_version = 1 } `, Expected: helper.Issues{}, @@ -198,7 +210,8 @@ resource "aws_ssm_parameter" "test" { }, Fixed: ` resource "aws_ssm_parameter" "test" { - value_wo = "test" + value_wo = "test" + value_wo_version = 1 } `, }, @@ -206,7 +219,8 @@ resource "aws_ssm_parameter" "test" { Name: "everything is fine aws_ssm_parameter", Content: ` resource "aws_ssm_parameter" "test" { - value_wo = "test" + value_wo = "test" + value_wo_version = 1 } `, Expected: helper.Issues{}, diff --git a/rules/ephemeral/generator/main.go b/rules/ephemeral/generator/main.go index 88efb6e3..7795fb54 100644 --- a/rules/ephemeral/generator/main.go +++ b/rules/ephemeral/generator/main.go @@ -10,8 +10,9 @@ import ( ) type writeOnlyArgument struct { - OriginalAttribute string - WriteOnlyAlternative string + OriginalAttribute string + WriteOnlyAlternative string + WriteOnlyVersionAttribute string } func main() { @@ -38,10 +39,13 @@ func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []wri for _, argument := range arguments { // Check if the argument ends with "_wo" and if the original attribute without "_wo" suffix exists in the resource schema - if attribute := strings.TrimSuffix(argument, "_wo"); strings.HasSuffix(argument, "_wo") && resource.Block.Attributes[attribute] != nil { + attribute := strings.TrimSuffix(argument, "_wo") + versionAttribute := attribute + "_wo_version" + if strings.HasSuffix(argument, "_wo") && resource.Block.Attributes[attribute] != nil && resource.Block.Attributes[versionAttribute] != nil { writeOnlyArguments = append(writeOnlyArguments, writeOnlyArgument{ - OriginalAttribute: attribute, - WriteOnlyAlternative: argument, + OriginalAttribute: attribute, + WriteOnlyAlternative: argument, + WriteOnlyVersionAttribute: versionAttribute, }) } } diff --git a/rules/provider.go b/rules/provider.go index d0ff6e90..4c937956 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -44,6 +44,7 @@ var manualRules = []tflint.Rule{ NewAwsProviderMissingDefaultTagsRule(), NewAwsSecurityGroupInlineRulesRule(), NewAwsSecurityGroupRuleDeprecatedRule(), + NewAwsIAMRoleDeprecatedPolicyAttributesRule(), ephemeral.NewAwsWriteOnlyArgumentsRule(), } From 2b61e581da93920c3f013f579602708977135310 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:12:52 +0300 Subject: [PATCH 14/15] refactor: only generate write only arguments map, reduce tests --- rules/ephemeral/aws_write_only_arguments.go | 53 ----- .../aws_write_only_arguments_rule.go.tmpl | 111 ----------- ...aws_write_only_arguments_rule_test.go.tmpl | 70 ------- .../aws_write_only_arguments_test.go | 186 +----------------- rules/ephemeral/generator/main.go | 7 +- rules/ephemeral/write_only_arguments_gen.go | 55 ++++++ .../write_only_arguments_gen.go.tmpl | 15 ++ 7 files changed, 74 insertions(+), 423 deletions(-) delete mode 100644 rules/ephemeral/aws_write_only_arguments_rule.go.tmpl delete mode 100644 rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl create mode 100644 rules/ephemeral/write_only_arguments_gen.go create mode 100644 rules/ephemeral/write_only_arguments_gen.go.tmpl diff --git a/rules/ephemeral/aws_write_only_arguments.go b/rules/ephemeral/aws_write_only_arguments.go index a1ca8807..6c04ac0a 100644 --- a/rules/ephemeral/aws_write_only_arguments.go +++ b/rules/ephemeral/aws_write_only_arguments.go @@ -1,5 +1,3 @@ -// This file generated by `generator/main.go`. DO NOT EDIT - package ephemeral import ( @@ -26,57 +24,6 @@ type writeOnlyArgument struct { // NewAwsWriteOnlyArgumentsRule returns new rule with default attributes func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { - writeOnlyArguments := map[string][]writeOnlyArgument{ - "aws_db_instance": { - { - originalAttribute: "password", - writeOnlyAlternative: "password_wo", - writeOnlyVersionAttribute: "password_wo_version", - }, - }, - "aws_docdb_cluster": { - { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - writeOnlyVersionAttribute: "master_password_wo_version", - }, - }, - "aws_rds_cluster": { - { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - writeOnlyVersionAttribute: "master_password_wo_version", - }, - }, - "aws_redshift_cluster": { - { - originalAttribute: "master_password", - writeOnlyAlternative: "master_password_wo", - writeOnlyVersionAttribute: "master_password_wo_version", - }, - }, - "aws_redshiftserverless_namespace": { - { - originalAttribute: "admin_user_password", - writeOnlyAlternative: "admin_user_password_wo", - writeOnlyVersionAttribute: "admin_user_password_wo_version", - }, - }, - "aws_secretsmanager_secret_version": { - { - originalAttribute: "secret_string", - writeOnlyAlternative: "secret_string_wo", - writeOnlyVersionAttribute: "secret_string_wo_version", - }, - }, - "aws_ssm_parameter": { - { - originalAttribute: "value", - writeOnlyAlternative: "value_wo", - writeOnlyVersionAttribute: "value_wo_version", - }, - }, - } return &AwsWriteOnlyArgumentsRule{ writeOnlyArguments: writeOnlyArguments, } diff --git a/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl deleted file mode 100644 index 7d2adeac..00000000 --- a/rules/ephemeral/aws_write_only_arguments_rule.go.tmpl +++ /dev/null @@ -1,111 +0,0 @@ -// This file generated by `generator/main.go`. DO NOT EDIT - -package ephemeral - -import ( - "fmt" - - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/zclconf/go-cty/cty" -) - -// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes -type AwsWriteOnlyArgumentsRule struct { - tflint.DefaultRule - - writeOnlyArguments map[string][]writeOnlyArgument -} - -type writeOnlyArgument struct { - originalAttribute string - writeOnlyAlternative string - writeOnlyVersionAttribute string -} - -// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes -func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule { - writeOnlyArguments := map[string][]writeOnlyArgument{ - {{- range $name, $value := . }} - "{{ $name }}": { {{- range $kk, $writeOnly := $value }} - { - originalAttribute: "{{ $writeOnly.OriginalAttribute }}", - writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}", - writeOnlyVersionAttribute: "{{ $writeOnly.WriteOnlyVersionAttribute }}", - }, - }, - {{- end -}}{{- end }} - } - return &AwsWriteOnlyArgumentsRule{ - writeOnlyArguments: writeOnlyArguments, - } -} - -// Name returns the rule name -func (r *AwsWriteOnlyArgumentsRule) Name() string { - return "aws_write_only_arguments" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsWriteOnlyArgumentsRule) Enabled() bool { - return false -} - -// Severity returns the rule severity -func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity { - return tflint.WARNING -} - -// Link returns the rule reference link -func (r *AwsWriteOnlyArgumentsRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -// Check checks whether the sensitive attribute exists -func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error { - for resourceType, attributes := range r.writeOnlyArguments { - for _, resourceAttribute := range attributes { - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: resourceAttribute.originalAttribute}, - }, - }, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - attribute, exists := resource.Body.Attributes[resourceAttribute.originalAttribute] - if !exists { - continue - } - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - if !val.IsNull() { - if err := runner.EmitIssueWithFix( - r, - fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative), - attribute.Expr.Range(), - func(f tflint.Fixer) error { - err := f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative) - if err != nil { - return err - } - return f.InsertTextAfter(attribute.Range, fmt.Sprintf("\n %s = 1", resourceAttribute.writeOnlyVersionAttribute)) - }, - ); err != nil { - return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err) - } - } - return nil - }, nil) - if err != nil { - return err - } - } - } - } - - return nil -} diff --git a/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl b/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl deleted file mode 100644 index 47e5b9ac..00000000 --- a/rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl +++ /dev/null @@ -1,70 +0,0 @@ -// This file generated by `generator/main.go`. DO NOT EDIT - -package ephemeral - -import ( - "testing" - - "github.com/terraform-linters/tflint-plugin-sdk/helper" -) - -func Test_AwsWriteOnlyAttribute(t *testing.T) { - cases := []struct { - Name string - Content string - Expected helper.Issues - Fixed string - }{ - {{- range $name, $value := . }} - {{- range $kk, $writeOnly := $value }} - { - Name: "basic {{ $name }}", - Content: ` -resource "{{ $name }}" "test" { - {{ $writeOnly.OriginalAttribute }} = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"{{ $writeOnly.OriginalAttribute }}" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "{{ $writeOnly.WriteOnlyAlternative }}".`, - }, - }, - Fixed: ` -resource "{{ $name }}" "test" { - {{ $writeOnly.WriteOnlyAlternative }} = "test" - {{ $writeOnly.WriteOnlyVersionAttribute }} = 1 -} -`, - }, - { - Name: "everything is fine {{ $name }}", - Content: ` -resource "{{ $name }}" "test" { - {{ $writeOnly.WriteOnlyAlternative }} = "test" - {{ $writeOnly.WriteOnlyVersionAttribute }} = 1 -} -`, - Expected: helper.Issues{}, - }, - {{- end -}}{{- end }} - } - - rule := NewAwsWriteOnlyArgumentsRule() - - for _, tc := range cases { - filename := "resource.tf" - runner := helper.TestRunner(t, map[string]string{filename: tc.Content}) - - if err := rule.Check(runner); err != nil { - t.Fatalf("Unexpected error occurred: %s", err) - } - helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) - - want := map[string]string{} - if tc.Fixed != "" { - want[filename] = tc.Fixed - } - helper.AssertChanges(t, want, runner.Changes()) - } -} diff --git a/rules/ephemeral/aws_write_only_arguments_test.go b/rules/ephemeral/aws_write_only_arguments_test.go index c83f7bfc..f54d0963 100644 --- a/rules/ephemeral/aws_write_only_arguments_test.go +++ b/rules/ephemeral/aws_write_only_arguments_test.go @@ -1,5 +1,3 @@ -// This file generated by `generator/main.go`. DO NOT EDIT - package ephemeral import ( @@ -16,157 +14,7 @@ func Test_AwsWriteOnlyAttribute(t *testing.T) { Fixed string }{ { - Name: "basic aws_db_instance", - Content: ` -resource "aws_db_instance" "test" { - password = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "password_wo".`, - }, - }, - Fixed: ` -resource "aws_db_instance" "test" { - password_wo = "test" - password_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_db_instance", - Content: ` -resource "aws_db_instance" "test" { - password_wo = "test" - password_wo_version = 1 -} -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_docdb_cluster", - Content: ` -resource "aws_docdb_cluster" "test" { - master_password = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - }, - }, - Fixed: ` -resource "aws_docdb_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_docdb_cluster", - Content: ` -resource "aws_docdb_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_rds_cluster", - Content: ` -resource "aws_rds_cluster" "test" { - master_password = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - }, - }, - Fixed: ` -resource "aws_rds_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_rds_cluster", - Content: ` -resource "aws_rds_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_redshift_cluster", - Content: ` -resource "aws_redshift_cluster" "test" { - master_password = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"master_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "master_password_wo".`, - }, - }, - Fixed: ` -resource "aws_redshift_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_redshift_cluster", - Content: ` -resource "aws_redshift_cluster" "test" { - master_password_wo = "test" - master_password_wo_version = 1 -} -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_redshiftserverless_namespace", - Content: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_user_password = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"admin_user_password" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "admin_user_password_wo".`, - }, - }, - Fixed: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_user_password_wo = "test" - admin_user_password_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_redshiftserverless_namespace", - Content: ` -resource "aws_redshiftserverless_namespace" "test" { - admin_user_password_wo = "test" - admin_user_password_wo_version = 1 -} -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_secretsmanager_secret_version", + Name: "basic", Content: ` resource "aws_secretsmanager_secret_version" "test" { secret_string = "test" @@ -186,42 +34,12 @@ resource "aws_secretsmanager_secret_version" "test" { `, }, { - Name: "everything is fine aws_secretsmanager_secret_version", + Name: "everything is fine", Content: ` resource "aws_secretsmanager_secret_version" "test" { secret_string_wo = "test" secret_string_wo_version = 1 } -`, - Expected: helper.Issues{}, - }, - { - Name: "basic aws_ssm_parameter", - Content: ` -resource "aws_ssm_parameter" "test" { - value = "test" -} -`, - Expected: helper.Issues{ - { - Rule: NewAwsWriteOnlyArgumentsRule(), - Message: `"value" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "value_wo".`, - }, - }, - Fixed: ` -resource "aws_ssm_parameter" "test" { - value_wo = "test" - value_wo_version = 1 -} -`, - }, - { - Name: "everything is fine aws_ssm_parameter", - Content: ` -resource "aws_ssm_parameter" "test" { - value_wo = "test" - value_wo_version = 1 -} `, Expected: helper.Issues{}, }, diff --git a/rules/ephemeral/generator/main.go b/rules/ephemeral/generator/main.go index 7795fb54..6abd7be1 100644 --- a/rules/ephemeral/generator/main.go +++ b/rules/ephemeral/generator/main.go @@ -27,11 +27,8 @@ func main() { } } - // Generate the write-only rule file - utils.GenerateFile("../../rules/ephemeral/aws_write_only_arguments.go", "../../rules/ephemeral/aws_write_only_arguments_rule.go.tmpl", resourcesWithWriteOnly) - - // Generate the write-only test file - utils.GenerateFile("../../rules/ephemeral/aws_write_only_arguments_test.go", "../../rules/ephemeral/aws_write_only_arguments_rule_test.go.tmpl", resourcesWithWriteOnly) + // Generate the write-only arguments variable to file + utils.GenerateFile("../../rules/ephemeral/write_only_arguments_gen.go", "../../rules/ephemeral/write_only_arguments_gen.go.tmpl", resourcesWithWriteOnly) } func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument { diff --git a/rules/ephemeral/write_only_arguments_gen.go b/rules/ephemeral/write_only_arguments_gen.go new file mode 100644 index 00000000..47e58cea --- /dev/null +++ b/rules/ephemeral/write_only_arguments_gen.go @@ -0,0 +1,55 @@ +// This file generated by `generator/main.go`. DO NOT EDIT + +package ephemeral + +var writeOnlyArguments = map[string][]writeOnlyArgument{ + "aws_db_instance": { + { + originalAttribute: "password", + writeOnlyAlternative: "password_wo", + writeOnlyVersionAttribute: "password_wo_version", + }, + }, + "aws_docdb_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", + }, + }, + "aws_rds_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", + }, + }, + "aws_redshift_cluster": { + { + originalAttribute: "master_password", + writeOnlyAlternative: "master_password_wo", + writeOnlyVersionAttribute: "master_password_wo_version", + }, + }, + "aws_redshiftserverless_namespace": { + { + originalAttribute: "admin_user_password", + writeOnlyAlternative: "admin_user_password_wo", + writeOnlyVersionAttribute: "admin_user_password_wo_version", + }, + }, + "aws_secretsmanager_secret_version": { + { + originalAttribute: "secret_string", + writeOnlyAlternative: "secret_string_wo", + writeOnlyVersionAttribute: "secret_string_wo_version", + }, + }, + "aws_ssm_parameter": { + { + originalAttribute: "value", + writeOnlyAlternative: "value_wo", + writeOnlyVersionAttribute: "value_wo_version", + }, + }, +} diff --git a/rules/ephemeral/write_only_arguments_gen.go.tmpl b/rules/ephemeral/write_only_arguments_gen.go.tmpl new file mode 100644 index 00000000..769a3371 --- /dev/null +++ b/rules/ephemeral/write_only_arguments_gen.go.tmpl @@ -0,0 +1,15 @@ +// This file generated by `generator/main.go`. DO NOT EDIT + +package ephemeral + +var writeOnlyArguments = map[string][]writeOnlyArgument{ + {{- range $name, $value := . }} + "{{ $name }}": { {{- range $kk, $writeOnly := $value }} + { + originalAttribute: "{{ $writeOnly.OriginalAttribute }}", + writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}", + writeOnlyVersionAttribute: "{{ $writeOnly.WriteOnlyVersionAttribute }}", + }, + }, + {{- end -}}{{- end }} +} From 00574055ddee6775225152ae5ce81cb2332b7b40 Mon Sep 17 00:00:00 2001 From: aristosvo <8375124+aristosvo@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:59:33 +0300 Subject: [PATCH 15/15] fix: adjust for comments --- rules/ephemeral/aws_write_only_arguments_test.go | 8 +++++++- rules/ephemeral/generator/main.go | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rules/ephemeral/aws_write_only_arguments_test.go b/rules/ephemeral/aws_write_only_arguments_test.go index f54d0963..d3ccf390 100644 --- a/rules/ephemeral/aws_write_only_arguments_test.go +++ b/rules/ephemeral/aws_write_only_arguments_test.go @@ -3,6 +3,7 @@ package ephemeral import ( "testing" + "github.com/hashicorp/hcl/v2" "github.com/terraform-linters/tflint-plugin-sdk/helper" ) @@ -24,6 +25,11 @@ resource "aws_secretsmanager_secret_version" "test" { { Rule: NewAwsWriteOnlyArgumentsRule(), Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "secret_string_wo".`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 19}, + End: hcl.Pos{Line: 3, Column: 25}, + }, }, }, Fixed: ` @@ -54,7 +60,7 @@ resource "aws_secretsmanager_secret_version" "test" { if err := rule.Check(runner); err != nil { t.Fatalf("Unexpected error occurred: %s", err) } - helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) + helper.AssertIssues(t, tc.Expected, runner.Issues) want := map[string]string{} if tc.Fixed != "" { diff --git a/rules/ephemeral/generator/main.go b/rules/ephemeral/generator/main.go index 6abd7be1..02fecab5 100644 --- a/rules/ephemeral/generator/main.go +++ b/rules/ephemeral/generator/main.go @@ -1,5 +1,3 @@ -//go:build generators - package main import (