diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 1fe9a72fdcac..0185d3fb62a8 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -54,6 +54,7 @@ func newEvaluator( logger debug.Logger, allowDownloads bool, skipCachedModules bool, + moduleVariables cty.Value, ) *evaluator { // create a context to store variables and make functions available @@ -61,6 +62,8 @@ func newEvaluator( Functions: Functions(target, modulePath), }, nil) + ctx.Set(moduleVariables, "module") + // these variables are made available by terraform to each module ctx.SetByDot(cty.StringVal(workspace), "terraform.workspace") ctx.SetByDot(cty.StringVal(projectRootPath), "path.root") @@ -156,8 +159,15 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str parseDuration += time.Since(start) e.debug.Log("Starting submodule evaluation...") + + moduleDefinitions, err := sortModuleDefinitions(e.loadModules(ctx)) + if err != nil { + e.debug.Log("Modules have a cyclic dependency, so some `count`, `for_each` and `dynamic` expressions may not be evaluated.") + } + var modules terraform.Modules - for _, definition := range e.loadModules(ctx) { + for _, definition := range moduleDefinitions { + definition.Parser.moduleVariables = e.ctx.Get("module") submodules, outputs, err := definition.Parser.EvaluateAll(ctx) if err != nil { e.debug.Log("Failed to evaluate submodule '%s': %s.", definition.Name, err) diff --git a/pkg/iac/scanners/terraform/parser/load_module.go b/pkg/iac/scanners/terraform/parser/load_module.go index 461d7a7a1a56..f4d2101e47f1 100644 --- a/pkg/iac/scanners/terraform/parser/load_module.go +++ b/pkg/iac/scanners/terraform/parser/load_module.go @@ -2,11 +2,13 @@ package parser import ( "context" + "errors" "fmt" "io/fs" "path" "strings" + "github.com/samber/lo" "github.com/zclconf/go-cty/cty" "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/resolvers" @@ -153,3 +155,104 @@ func (e *evaluator) loadExternalModule(ctx context.Context, b *terraform.Block, External: true, }, nil } + +func sortModuleDefinitions(modules []*ModuleDefinition) ([]*ModuleDefinition, error) { + graph := buildModuleDefinitionsGraph(modules) + + if graph.hasCycle() { + return modules, errors.New("graph is cyclical") + } + + sortedDefNames := graph.sort() + + defsMap := lo.SliceToMap(modules, func(definition *ModuleDefinition) (string, *ModuleDefinition) { + return definition.Name, definition + }) + + return lo.FilterMap(sortedDefNames, func(defName string, _ int) (*ModuleDefinition, bool) { + val, exists := defsMap[defName] + return val, exists + }), nil +} + +type moduleDefinitionsGraph map[string][]string + +func buildModuleDefinitionsGraph(modules []*ModuleDefinition) moduleDefinitionsGraph { + graph := lo.SliceToMap(modules, func(definition *ModuleDefinition) (string, []string) { + return definition.Name, nil + }) + + for _, def := range modules { + for _, ref := range def.Definition.AllReferences() { + if ref.BlockType() != terraform.TypeModule { + continue + } + + referencedModule := ref.TypeLabel() + if referencedModule != "" { + graph[def.Name] = append(graph[def.Name], referencedModule) + } + } + } + + return graph +} + +func (g moduleDefinitionsGraph) hasCycle() bool { + visited := make(map[string]bool) + recStack := make(map[string]bool) + + var dfs func(string) bool + dfs = func(node string) bool { + visited[node] = true + recStack[node] = true + + for _, neighbor := range g[node] { + if visited[neighbor] && recStack[neighbor] { + return true + } + if !visited[neighbor] && dfs(neighbor) { + return true + } + } + + recStack[node] = false + return false + } + + for node := range g { + if dfs(node) { + return true + } + } + + return false +} + +func (g moduleDefinitionsGraph) sort() []string { + var ( + visited = make(map[string]bool) + stack = make([]string, 0, len(g)) + dfs func(n string) + ) + + dfs = func(n string) { + if visited[n] { + return + } + + visited[n] = true + + for _, neighbor := range g[n] { + dfs(neighbor) + } + + stack = append(stack, n) + } + + for node := range g { + dfs(node) + } + + return stack +} diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go index e09e9e621ef4..95a5245e2eea 100644 --- a/pkg/iac/scanners/terraform/parser/parser.go +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -64,6 +64,7 @@ type Parser struct { fsMap map[string]fs.FS skipRequired bool configsFS fs.FS + moduleVariables cty.Value } func (p *Parser) SetDebugWriter(writer io.Writer) { @@ -310,6 +311,7 @@ func (p *Parser) EvaluateAll(ctx context.Context) (terraform.Modules, cty.Value, p.debug.Extend("evaluator"), p.allowDownloads, p.skipCachedModules, + p.moduleVariables, ) modules, fsMap, parseDuration := evaluator.EvaluateAll(ctx) p.metrics.Counts.Modules = len(modules) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 12594841251b..bce91d21522f 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1497,6 +1497,76 @@ resource "test_block" "this" { }) } +func TestModuleRefersToOutputOfAnotherModule(t *testing.T) { + files := map[string]string{ + "main.tf": ` +module "module2" { + source = "./modules/foo" +} + +module "module1" { + source = "./modules/bar" + test_var = module.module2.test_out +} +`, + "modules/foo/main.tf": ` +output "test_out" { + value = "test_value" +} +`, + "modules/bar/main.tf": ` +variable "test_var" {} + +resource "test_resource" "this" { + dynamic "dynamic_block" { + for_each = [var.test_var] + content { + some_attr = dynamic_block.value + } + } +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 3) + + resources := modules.GetResourcesByType("test_resource") + require.Len(t, resources, 1) + + attr, _ := resources[0].GetNestedAttribute("dynamic_block.some_attr") + require.NotNil(t, attr) + + assert.Equal(t, "test_value", attr.GetRawValue()) +} + +func TestModuleRefersToModuleThatDoesNotExist(t *testing.T) { + files := map[string]string{ + "main.tf": ` +module "module2" { + source = "./modules/foo" +} + +module "module1" { + source = "./modules/bar" + test_var = module.module22.test_out +} +`, + "modules/foo/main.tf": ` +output "test_out" { + value = "test_value" +} +`, + "modules/bar/main.tf": ` +variable "test_var" {} + +resource "test_resource" "this" {} +`, + } + + parse(t, files) +} + func parse(t *testing.T, files map[string]string) terraform.Modules { fs := testutil.CreateFS(t, files) parser := New(fs, "", OptionStopOnHCLError(true)) diff --git a/pkg/iac/terraform/block.go b/pkg/iac/terraform/block.go index 6807fddd0f7d..8c1c43842fb2 100644 --- a/pkg/iac/terraform/block.go +++ b/pkg/iac/terraform/block.go @@ -31,6 +31,15 @@ type Block struct { reference Reference } +func (b Block) AllReferences() []*Reference { + var references []*Reference + for _, attr := range b.attributes { + references = append(references, attr.AllReferences()...) + } + + return references +} + func NewBlock(hclBlock *hcl.Block, ctx *context.Context, moduleBlock *Block, parentBlock *Block, moduleSource string, moduleFS fs.FS, index ...cty.Value) *Block { if ctx == nil {