package parser import ( "context" "path" "sort" "strings" "github.com/samber/lo" "github.com/zclconf/go-cty/cty" "github.com/aquasecurity/trivy/pkg/iac/terraform" ) // FindRootModules takes a list of module paths and identifies the root local modules. // It builds a graph based on the module dependencies and determines the modules that have no incoming dependencies, // considering them as root modules. func (p *Parser) FindRootModules(ctx context.Context, dirs []string) ([]string, error) { // skip cached terraform modules as they cannot be root modules dirs = lo.Filter(dirs, func(dir string, _ int) bool { return !strings.Contains(dir, ".terraform/modules/") }) for _, dir := range dirs { if err := p.ParseFS(ctx, dir); err != nil { return nil, err } } blocks, _, err := p.readBlocks(p.files) if err != nil { return nil, err } g := buildGraph(blocks, dirs) rootModules := g.rootModules() sort.Strings(rootModules) return rootModules, nil } type modulesGraph map[string][]string func buildGraph(blocks terraform.Blocks, paths []string) modulesGraph { moduleBlocks := blocks.OfType("module") graph := lo.SliceToMap(paths, func(p string) (string, []string) { return p, nil }) for _, block := range moduleBlocks { sourceVal := block.GetAttribute("source").Value() if sourceVal.Type() != cty.String { continue } source := sourceVal.AsString() if strings.HasPrefix(source, ".") { filename := block.GetMetadata().Range().GetFilename() dir := path.Dir(filename) graph[dir] = append(graph[dir], path.Join(dir, source)) } } return graph } func (g modulesGraph) rootModules() []string { incomingEdges := make(map[string]int) for _, neighbors := range g { for _, neighbor := range neighbors { incomingEdges[neighbor]++ } } var roots []string for module := range g { if incomingEdges[module] == 0 { roots = append(roots, module) } } return roots }