Skip to content

Commit 003e350

Browse files
committed
feat: add suggestions to replace data sources with ephemeral alternatives
1 parent 4f21b6d commit 003e350

File tree

10 files changed

+537
-18
lines changed

10 files changed

+537
-18
lines changed
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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
9+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
10+
"github.com/terraform-linters/tflint-ruleset-aws/project"
11+
)
12+
13+
// TODO: Write the rule's description here
14+
// AwsEphemeralResourcesRule checks ...
15+
type AwsEphemeralResourcesRule struct {
16+
tflint.DefaultRule
17+
18+
replacingEphemeralResources []string
19+
}
20+
21+
// NewAwsEphemeralResourcesRule returns new rule with default attributes
22+
func NewAwsEphemeralResourcesRule() *AwsEphemeralResourcesRule {
23+
return &AwsEphemeralResourcesRule{
24+
replacingEphemeralResources: []string{
25+
"aws_eks_cluster_auth",
26+
"aws_kms_secrets",
27+
"aws_lambda_invocation",
28+
"aws_secretsmanager_random_password",
29+
"aws_secretsmanager_secret_version",
30+
"aws_ssm_parameter",
31+
},
32+
}
33+
}
34+
35+
// Name returns the rule name
36+
func (r *AwsEphemeralResourcesRule) Name() string {
37+
return "aws_ephemeral_resources"
38+
}
39+
40+
// Enabled returns whether the rule is enabled by default
41+
func (r *AwsEphemeralResourcesRule) Enabled() bool {
42+
return false
43+
}
44+
45+
// Severity returns the rule severity
46+
func (r *AwsEphemeralResourcesRule) Severity() tflint.Severity {
47+
return tflint.WARNING
48+
}
49+
50+
// Link returns the rule reference link
51+
func (r *AwsEphemeralResourcesRule) Link() string {
52+
return project.ReferenceLink(r.Name())
53+
}
54+
55+
// Check checks if there is an ephemeral resource which can replace an data source
56+
func (r *AwsEphemeralResourcesRule) Check(runner tflint.Runner) error {
57+
for _, resourceType := range r.replacingEphemeralResources {
58+
resources, err := GetDataSourceContent(runner, resourceType, &hclext.BodySchema{}, nil)
59+
if err != nil {
60+
return err
61+
}
62+
63+
for _, resource := range resources.Blocks {
64+
if err := runner.EmitIssueWithFix(
65+
r,
66+
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),
67+
resource.TypeRange,
68+
func(f tflint.Fixer) error {
69+
return f.ReplaceText(resource.TypeRange, "ephemeral")
70+
},
71+
); err != nil {
72+
return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err)
73+
}
74+
}
75+
}
76+
77+
return nil
78+
}
79+
80+
func GetDataSourceContent(r tflint.Runner, name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
81+
body, err := r.GetModuleContent(&hclext.BodySchema{
82+
Blocks: []hclext.BlockSchema{
83+
{Type: "data", LabelNames: []string{"type", "name"}, Body: schema},
84+
},
85+
}, opts)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
91+
for _, resource := range body.Blocks {
92+
if resource.Labels[0] != name {
93+
continue
94+
}
95+
96+
content.Blocks = append(content.Blocks, resource)
97+
}
98+
99+
return content, nil
100+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
9+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
10+
"github.com/terraform-linters/tflint-ruleset-aws/project"
11+
)
12+
13+
// TODO: Write the rule's description here
14+
// AwsEphemeralResourcesRule checks ...
15+
type AwsEphemeralResourcesRule struct {
16+
tflint.DefaultRule
17+
18+
replacingEphemeralResources []string
19+
}
20+
21+
// NewAwsEphemeralResourcesRule returns new rule with default attributes
22+
func NewAwsEphemeralResourcesRule() *AwsEphemeralResourcesRule {
23+
return &AwsEphemeralResourcesRule{
24+
replacingEphemeralResources: []string{
25+
{{- range $value := . }}
26+
"{{ $value}}",
27+
{{- end }}
28+
},
29+
}
30+
}
31+
32+
// Name returns the rule name
33+
func (r *AwsEphemeralResourcesRule) Name() string {
34+
return "aws_ephemeral_resources"
35+
}
36+
37+
// Enabled returns whether the rule is enabled by default
38+
func (r *AwsEphemeralResourcesRule) Enabled() bool {
39+
return false
40+
}
41+
42+
// Severity returns the rule severity
43+
func (r *AwsEphemeralResourcesRule) Severity() tflint.Severity {
44+
return tflint.WARNING
45+
}
46+
47+
// Link returns the rule reference link
48+
func (r *AwsEphemeralResourcesRule) Link() string {
49+
return project.ReferenceLink(r.Name())
50+
}
51+
52+
// Check checks if there is an ephemeral resource which can replace an data source
53+
func (r *AwsEphemeralResourcesRule) Check(runner tflint.Runner) error {
54+
for _, resourceType := range r.replacingEphemeralResources {
55+
resources, err := GetDataSourceContent(runner, resourceType, &hclext.BodySchema{}, nil)
56+
if err != nil {
57+
return err
58+
}
59+
60+
for _, resource := range resources.Blocks {
61+
if err := runner.EmitIssueWithFix(
62+
r,
63+
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),
64+
resource.TypeRange,
65+
func(f tflint.Fixer) error {
66+
return f.ReplaceText(resource.TypeRange, "ephemeral")
67+
},
68+
); err != nil {
69+
return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err)
70+
}
71+
}
72+
}
73+
74+
return nil
75+
}
76+
77+
func GetDataSourceContent(r tflint.Runner, name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
78+
body, err := r.GetModuleContent(&hclext.BodySchema{
79+
Blocks: []hclext.BlockSchema{
80+
{Type: "data", LabelNames: []string{"type", "name"}, Body: schema},
81+
},
82+
}, opts)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
88+
for _, resource := range body.Blocks {
89+
if resource.Labels[0] != name {
90+
continue
91+
}
92+
93+
content.Blocks = append(content.Blocks, resource)
94+
}
95+
96+
return content, nil
97+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
import (
6+
"testing"
7+
8+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
9+
)
10+
11+
func Test_AwsEphemeralResources(t *testing.T) {
12+
cases := []struct {
13+
Name string
14+
Content string
15+
Expected helper.Issues
16+
Fixed string
17+
}{
18+
19+
{{- range $value := . }}
20+
{
21+
Name: "basic {{ $value }}",
22+
Content: `
23+
data "{{ $value }}" "test" {
24+
}
25+
`,
26+
Expected: helper.Issues{
27+
{
28+
Rule: NewAwsEphemeralResourcesRule(),
29+
Message: `"{{ $value }}" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource "{{ $value }}" instead.`,
30+
},
31+
},
32+
Fixed: `
33+
ephemeral "{{ $value }}" "test" {
34+
}
35+
`,
36+
},
37+
{{- end }}
38+
}
39+
40+
rule := NewAwsEphemeralResourcesRule()
41+
42+
for _, tc := range cases {
43+
filename := "resource.tf"
44+
runner := helper.TestRunner(t, map[string]string{filename: tc.Content})
45+
46+
if err := rule.Check(runner); err != nil {
47+
t.Fatalf("Unexpected error occurred: %s", err)
48+
}
49+
helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues)
50+
51+
want := map[string]string{}
52+
if tc.Fixed != "" {
53+
want[filename] = tc.Fixed
54+
}
55+
helper.AssertChanges(t, want, runner.Changes())
56+
}
57+
}

0 commit comments

Comments
 (0)