Skip to content

Commit 951a5cb

Browse files
committed
feat: add suggestions to replace data sources with ephemeral alternatives
1 parent 5ca13ab commit 951a5cb

File tree

8 files changed

+225
-2
lines changed

8 files changed

+225
-2
lines changed

docs/rules/aws_ephemeral_resources.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# aws_ephemeral_resources
2+
3+
Recommends using available [ephemeral resources](https://developer.hashicorp.com/terraform/language/resources/ephemeral/reference) instead of the original data source. This is only valid for Terraform v1.10+.
4+
5+
## Example
6+
7+
This example uses `aws_secretsmanager_random_password`, but the rule applies to all data sources with an ephemeral equivalent:
8+
9+
```hcl
10+
data "aws_secretsmanager_random_password" "test" {
11+
password_length = 50
12+
exclude_numbers = true
13+
}
14+
```
15+
16+
```
17+
$ tflint
18+
1 issue(s) found:
19+
20+
Warning: [Fixable] "aws_secretsmanager_random_password" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource "aws_secretsmanager_random_password" instead. (aws_ephemeral_resources)
21+
22+
on test.tf line 2:
23+
2: data "aws_secretsmanager_random_password" "test"
24+
25+
```
26+
27+
## Why
28+
29+
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 ephemeral resources, which are not persisted to state. Other resources can refer to their values, but executing of the lookup is defered until the apply stage.
30+
31+
Using ephemeral resources mitigates the risk of a malicious actor obtaining privileged credentials by accessing Terraform state files directly. Prefer using them over the original data sources for sensitive data.
32+
33+
## How To Fix
34+
35+
Replace the data source with its ephemeral resource equivalent. Use resources with write-only arguments or in provider configuration to ensure that the sensitive value is not persisted to state.
36+
37+
In case of the previously shown `aws_secretsmanager_random_password` data source, replace `data` by `ephemeral`:
38+
39+
```hcl
40+
ephemeral "aws_secretsmanager_random_password" "test" {
41+
password_length = 50
42+
exclude_numbers = true
43+
}
44+
```
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package ephemeral
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
7+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
8+
"github.com/terraform-linters/tflint-ruleset-aws/project"
9+
)
10+
11+
// TODO: Write the rule's description here
12+
// AwsEphemeralResourcesRule checks ...
13+
type AwsEphemeralResourcesRule struct {
14+
tflint.DefaultRule
15+
16+
replacingEphemeralResources []string
17+
}
18+
19+
// NewAwsEphemeralResourcesRule returns new rule with default attributes
20+
func NewAwsEphemeralResourcesRule() *AwsEphemeralResourcesRule {
21+
return &AwsEphemeralResourcesRule{
22+
replacingEphemeralResources: replacingEphemeralResources
23+
}
24+
}
25+
26+
// Name returns the rule name
27+
func (r *AwsEphemeralResourcesRule) Name() string {
28+
return "aws_ephemeral_resources"
29+
}
30+
31+
// Enabled returns whether the rule is enabled by default
32+
func (r *AwsEphemeralResourcesRule) Enabled() bool {
33+
return false
34+
}
35+
36+
// Severity returns the rule severity
37+
func (r *AwsEphemeralResourcesRule) Severity() tflint.Severity {
38+
return tflint.WARNING
39+
}
40+
41+
// Link returns the rule reference link
42+
func (r *AwsEphemeralResourcesRule) Link() string {
43+
return project.ReferenceLink(r.Name())
44+
}
45+
46+
// Check checks if there is an ephemeral resource which can replace an data source
47+
func (r *AwsEphemeralResourcesRule) Check(runner tflint.Runner) error {
48+
for _, resourceType := range r.replacingEphemeralResources {
49+
resources, err := GetDataSourceContent(runner, resourceType, &hclext.BodySchema{}, nil)
50+
if err != nil {
51+
return err
52+
}
53+
54+
for _, resource := range resources.Blocks {
55+
if err := runner.EmitIssueWithFix(
56+
r,
57+
fmt.Sprintf("\"%s\" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource \"%s\" instead.", resourceType, resourceType),
58+
resource.TypeRange,
59+
func(f tflint.Fixer) error {
60+
return f.ReplaceText(resource.TypeRange, "ephemeral")
61+
},
62+
); err != nil {
63+
return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err)
64+
}
65+
}
66+
}
67+
68+
return nil
69+
}
70+
71+
func GetDataSourceContent(r tflint.Runner, name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
72+
body, err := r.GetModuleContent(&hclext.BodySchema{
73+
Blocks: []hclext.BlockSchema{
74+
{Type: "data", LabelNames: []string{"type", "name"}, Body: schema},
75+
},
76+
}, opts)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
82+
for _, resource := range body.Blocks {
83+
if resource.Labels[0] != name {
84+
continue
85+
}
86+
87+
content.Blocks = append(content.Blocks, resource)
88+
}
89+
90+
return content, nil
91+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package ephemeral
2+
3+
import (
4+
"testing"
5+
6+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
7+
)
8+
9+
func Test_AwsEphemeralResources(t *testing.T) {
10+
cases := []struct {
11+
Name string
12+
Content string
13+
Expected helper.Issues
14+
Fixed string
15+
}{
16+
{
17+
Name: "basic aws_eks_cluster_auth",
18+
Content: `
19+
data "aws_eks_cluster_auth" "test" {
20+
}
21+
`,
22+
Expected: helper.Issues{
23+
{
24+
Rule: NewAwsEphemeralResourcesRule(),
25+
Message: `"aws_eks_cluster_auth" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource "aws_eks_cluster_auth" instead.`,
26+
},
27+
},
28+
Fixed: `
29+
ephemeral "aws_eks_cluster_auth" "test" {
30+
}
31+
`,
32+
},
33+
}
34+
35+
rule := NewAwsEphemeralResourcesRule()
36+
37+
for _, tc := range cases {
38+
filename := "resource.tf"
39+
runner := helper.TestRunner(t, map[string]string{filename: tc.Content})
40+
41+
if err := rule.Check(runner); err != nil {
42+
t.Fatalf("Unexpected error occurred: %s", err)
43+
}
44+
helper.AssertIssues(t, tc.Expected, runner.Issues)
45+
46+
want := map[string]string{}
47+
if tc.Fixed != "" {
48+
want[filename] = tc.Fixed
49+
}
50+
helper.AssertChanges(t, want, runner.Changes())
51+
}
52+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
var replacingEphemeralResources = []string{
6+
"aws_eks_cluster_auth",
7+
"aws_kms_secrets",
8+
"aws_lambda_invocation",
9+
"aws_secretsmanager_random_password",
10+
"aws_secretsmanager_secret_version",
11+
"aws_ssm_parameter",
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
var replacingEphemeralResources = []string{
6+
{{- range $value := . }}
7+
"{{ $value}}",
8+
{{- end }}
9+
}

rules/ephemeral/generator/main.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"slices"
45
"strings"
56

67
tfjson "github.com/hashicorp/terraform-json"
@@ -25,8 +26,21 @@ func main() {
2526
}
2627
}
2728

28-
// Generate the write-only arguments variable to file
29+
// Generate the write-only arguments variable
2930
utils.GenerateFile("../../rules/ephemeral/write_only_arguments_gen.go", "../../rules/ephemeral/write_only_arguments_gen.go.tmpl", resourcesWithWriteOnly)
31+
32+
ephemeralResourcesAsDataAlternative := []string{}
33+
// Iterate over all ephemeral resources in the AWS provider schema
34+
for resourceName, _ := range awsProvider.EphemeralResourceSchemas {
35+
if awsProvider.DataSourceSchemas[resourceName] != nil {
36+
ephemeralResourcesAsDataAlternative = append(ephemeralResourcesAsDataAlternative, resourceName)
37+
}
38+
}
39+
40+
slices.Sort(ephemeralResourcesAsDataAlternative)
41+
42+
// Generate the ephemeral resources variable
43+
utils.GenerateFile("../../rules/ephemeral/ephemeral_resources_gen.go", "../../rules/ephemeral/ephemeral_resources_gen.go.tmpl", ephemeralResourcesAsDataAlternative)
3044
}
3145

3246
func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument {

rules/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var manualRules = []tflint.Rule{
4646
NewAwsSecurityGroupRuleDeprecatedRule(),
4747
NewAwsIAMRoleDeprecatedPolicyAttributesRule(),
4848
ephemeral.NewAwsWriteOnlyArgumentsRule(),
49+
ephemeral.NewAwsEphemeralResourcesRule(),
4950
}
5051

5152
// Rules is a list of all rules
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.2
1+
1.11.2

0 commit comments

Comments
 (0)