Skip to content

Commit f5d3b52

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

8 files changed

+231
-2
lines changed

docs/rules/aws_ephemeral_resources.md

+44
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+
```
+91
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package ephemeral
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
8+
)
9+
10+
func Test_AwsEphemeralResources(t *testing.T) {
11+
cases := []struct {
12+
Name string
13+
Content string
14+
Expected helper.Issues
15+
Fixed string
16+
}{
17+
{
18+
Name: "basic aws_eks_cluster_auth",
19+
Content: `
20+
data "aws_eks_cluster_auth" "test" {
21+
}
22+
`,
23+
Expected: helper.Issues{
24+
{
25+
Rule: NewAwsEphemeralResourcesRule(),
26+
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.`,
27+
Range: hcl.Range{
28+
Filename: "resource.tf",
29+
Start: hcl.Pos{Line: 2, Column: 1},
30+
End: hcl.Pos{Line: 2, Column: 5},
31+
},
32+
},
33+
},
34+
Fixed: `
35+
ephemeral "aws_eks_cluster_auth" "test" {
36+
}
37+
`,
38+
},
39+
}
40+
41+
rule := NewAwsEphemeralResourcesRule()
42+
43+
for _, tc := range cases {
44+
filename := "resource.tf"
45+
runner := helper.TestRunner(t, map[string]string{filename: tc.Content})
46+
47+
if err := rule.Check(runner); err != nil {
48+
t.Fatalf("Unexpected error occurred: %s", err)
49+
}
50+
helper.AssertIssues(t, tc.Expected, runner.Issues)
51+
52+
want := map[string]string{}
53+
if tc.Fixed != "" {
54+
want[filename] = tc.Fixed
55+
}
56+
helper.AssertChanges(t, want, runner.Changes())
57+
}
58+
}
+12
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+
}
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

+15-1
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

+1
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
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.2
1+
1.11.2

0 commit comments

Comments
 (0)