diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 59aa69fc87d5..2120a72af986 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -38,6 +38,9 @@ type evaluator struct { parentParser *Parser allowDownloads bool skipCachedModules bool + // stepHooks are functions that are called after each evaluation step. + // They can be used to provide additional semantics to other terraform blocks. + stepHooks []EvaluateStepHook } func newEvaluator( @@ -55,6 +58,7 @@ func newEvaluator( logger *log.Logger, allowDownloads bool, skipCachedModules bool, + stepHooks []EvaluateStepHook, ) *evaluator { // create a context to store variables and make functions available @@ -87,9 +91,12 @@ func newEvaluator( logger: logger, allowDownloads: allowDownloads, skipCachedModules: skipCachedModules, + stepHooks: stepHooks, } } +type EvaluateStepHook func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) + func (e *evaluator) evaluateStep() { e.ctx.Set(e.getValuesByBlockType("variable"), "var") @@ -103,6 +110,10 @@ func (e *evaluator) evaluateStep() { e.ctx.Set(e.getValuesByBlockType("data"), "data") e.ctx.Set(e.getValuesByBlockType("output"), "output") e.ctx.Set(e.getValuesByBlockType("module"), "module") + + for _, hook := range e.stepHooks { + hook(e.ctx, e.blocks, e.inputVars) + } } // exportOutputs is used to export module outputs to the parent module diff --git a/pkg/iac/scanners/terraform/parser/option.go b/pkg/iac/scanners/terraform/parser/option.go index f9ada6545225..eabdd0c23142 100644 --- a/pkg/iac/scanners/terraform/parser/option.go +++ b/pkg/iac/scanners/terraform/parser/option.go @@ -8,6 +8,12 @@ import ( type Option func(p *Parser) +func OptionWithEvalHook(hooks EvaluateStepHook) Option { + return func(p *Parser) { + p.stepHooks = append(p.stepHooks, hooks) + } +} + func OptionWithTFVarsPaths(paths ...string) Option { return func(p *Parser) { p.tfvarsPaths = paths diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go index cc299b1e6b3b..aedb12610b07 100644 --- a/pkg/iac/scanners/terraform/parser/parser.go +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -51,6 +51,7 @@ type Parser struct { fsMap map[string]fs.FS configsFS fs.FS skipPaths []string + stepHooks []EvaluateStepHook } // New creates a new Parser @@ -66,6 +67,7 @@ func New(moduleFS fs.FS, moduleSource string, opts ...Option) *Parser { configsFS: moduleFS, logger: log.WithPrefix("terraform parser").With("module", "root"), tfvars: make(map[string]cty.Value), + stepHooks: make([]EvaluateStepHook, 0), } for _, option := range opts { @@ -304,6 +306,7 @@ func (p *Parser) Load(ctx context.Context) (*evaluator, error) { log.WithPrefix("terraform evaluator"), p.allowDownloads, p.skipCachedModules, + p.stepHooks, ), nil } diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index ff9624734a0f..6cb9c95c00da 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -17,6 +17,7 @@ import ( "github.com/aquasecurity/trivy/internal/testutil" "github.com/aquasecurity/trivy/pkg/iac/terraform" + tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/set" ) @@ -2127,6 +2128,80 @@ func TestTFVarsFileDoesNotExist(t *testing.T) { assert.ErrorContains(t, err, "file does not exist") } +func Test_OptionsWithEvalHook(t *testing.T) { + fs := testutil.CreateFS(t, map[string]string{ + "main.tf": ` +data "your_custom_data" "this" { + default = ["foo", "foh", "fum"] + unaffected = "bar" +} + +// Testing the hook affects some value, which is used in another evaluateStep +// action (expanding blocks) +data "random_thing" "that" { + dynamic "repeated" { + for_each = data.your_custom_data.this.value + content { + value = repeated.value + } + } +} + +locals { + referenced = data.your_custom_data.this.value + static_ref = data.your_custom_data.this.unaffected +} +`}) + + parser := New(fs, "", OptionWithEvalHook( + // A basic example of how to have a 'default' value for a data block. + // To see a more practical example, see how 'evaluateVariable' handles + // the 'default' value of a variable. + func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) { + dataBlocks := blocks.OfType("data") + for _, block := range dataBlocks { + if len(block.Labels()) >= 1 && block.Labels()[0] == "your_custom_data" { + def := block.GetAttribute("default") + ctx.Set(cty.ObjectVal(map[string]cty.Value{ + "value": def.Value(), + }), "data", "your_custom_data", "this") + } + } + + }, + )) + + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, _, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + // Check the default value of the data block + blocks := rootModule.GetDatasByType("your_custom_data") + assert.Len(t, blocks, 1) + expList := cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foh"), cty.StringVal("fum")}) + assert.True(t, expList.Equals(blocks[0].GetAttribute("default").Value()).True(), "default value matched list") + assert.Equal(t, "bar", blocks[0].GetAttribute("unaffected").Value().AsString()) + + // Check the referenced 'data.your_custom_data.this.value' exists in the eval + // context, and it is the default value of the data block. + locals := rootModule.GetBlocks().OfType("locals") + assert.Len(t, locals, 1) + assert.True(t, expList.Equals(locals[0].GetAttribute("referenced").Value()).True(), "referenced value matched list") + assert.Equal(t, "bar", locals[0].GetAttribute("static_ref").Value().AsString()) + + // Check the dynamic block is expanded correctly + dynamicBlocks := rootModule.GetDatasByType("random_thing") + assert.Len(t, dynamicBlocks, 1) + assert.Len(t, dynamicBlocks[0].GetBlocks("repeated"), 3) + for i, repeat := range dynamicBlocks[0].GetBlocks("repeated") { + assert.Equal(t, expList.Index(cty.NumberIntVal(int64(i))), repeat.GetAttribute("value").Value()) + } +} + func Test_OptionsWithTfVars(t *testing.T) { fs := testutil.CreateFS(t, map[string]string{ "main.tf": `resource "test" "this" {