github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/terraform/parser/load_module.go (about) 1 package parser 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/fs" 8 "path/filepath" 9 "strings" 10 11 "github.com/khulnasoft-lab/defsec/pkg/scanners/terraform/parser/resolvers" 12 "github.com/khulnasoft-lab/defsec/pkg/terraform" 13 14 "github.com/zclconf/go-cty/cty" 15 ) 16 17 type moduleLoadError struct { 18 source string 19 err error 20 } 21 22 func (m *moduleLoadError) Error() string { 23 return fmt.Sprintf("failed to load module '%s': %s", m.source, m.err) 24 } 25 26 type ModuleDefinition struct { 27 Name string 28 Path string 29 FileSystem fs.FS 30 Definition *terraform.Block 31 Parser *Parser 32 External bool 33 } 34 35 // LoadModules reads all module blocks and loads the underlying modules, adding blocks to e.moduleBlocks 36 func (e *evaluator) loadModules(ctx context.Context) []*ModuleDefinition { 37 38 blocks := e.blocks 39 40 var moduleDefinitions []*ModuleDefinition 41 42 expanded := e.expandBlocks(blocks.OfType("module")) 43 44 var loadErrors []*moduleLoadError 45 46 for _, moduleBlock := range expanded { 47 if moduleBlock.Label() == "" { 48 continue 49 } 50 moduleDefinition, err := e.loadModule(ctx, moduleBlock) 51 if err != nil { 52 var loadErr *moduleLoadError 53 if errors.As(err, &loadErr) { 54 var found bool 55 for _, fm := range loadErrors { 56 if fm.source == loadErr.source { 57 found = true 58 break 59 } 60 } 61 if !found { 62 loadErrors = append(loadErrors, loadErr) 63 } 64 continue 65 } 66 e.debug.Log("Failed to load module '%s'. Maybe try 'terraform init'?", err) 67 continue 68 } 69 e.debug.Log("Loaded module '%s' from '%s'.", moduleDefinition.Name, moduleDefinition.Path) 70 moduleDefinitions = append(moduleDefinitions, moduleDefinition) 71 } 72 73 return moduleDefinitions 74 } 75 76 // takes in a module "x" {} block and loads resources etc. into e.moduleBlocks - additionally returns variables to add to ["module.x.*"] variables 77 func (e *evaluator) loadModule(ctx context.Context, b *terraform.Block) (*ModuleDefinition, error) { 78 79 metadata := b.GetMetadata() 80 81 if b.Label() == "" { 82 return nil, fmt.Errorf("module without label at %s", metadata.Range()) 83 } 84 85 var source string 86 attrs := b.Attributes() 87 for _, attr := range attrs { 88 if attr.Name() == "source" { 89 sourceVal := attr.Value() 90 if sourceVal.Type() == cty.String { 91 source = sourceVal.AsString() 92 } 93 } 94 } 95 if source == "" { 96 return nil, fmt.Errorf("could not read module source attribute at %s", metadata.Range().String()) 97 } 98 99 if def, err := e.loadModuleFromTerraformCache(ctx, b, source); err == nil { 100 e.debug.Log("found module '%s' in .terraform/modules", source) 101 return def, nil 102 } 103 104 // we don't have the module installed via 'terraform init' so we need to grab it... 105 return e.loadExternalModule(ctx, b, source) 106 } 107 108 func (e *evaluator) loadModuleFromTerraformCache(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) { 109 var modulePath string 110 if e.moduleMetadata != nil { 111 // if we have module metadata we can parse all the modules as they'll be cached locally! 112 name := b.ModuleName() 113 for _, module := range e.moduleMetadata.Modules { 114 if module.Key == name { 115 modulePath = filepath.Clean(filepath.Join(e.projectRootPath, module.Dir)) 116 break 117 } 118 } 119 } 120 if modulePath == "" { 121 return nil, fmt.Errorf("failed to load module from .terraform/modules") 122 } 123 if strings.HasPrefix(source, ".") { 124 source = "" 125 } 126 127 if prefix, relativeDir, ok := strings.Cut(source, "//"); ok && !strings.HasSuffix(prefix, ":") && strings.Count(prefix, "/") == 2 { 128 if !strings.HasSuffix(modulePath, relativeDir) { 129 modulePath = fmt.Sprintf("%s/%s", modulePath, relativeDir) 130 } 131 } 132 133 e.debug.Log("Module '%s' resolved to path '%s' in filesystem '%s' using modules.json", b.FullName(), modulePath, e.filesystem) 134 moduleParser := e.parentParser.newModuleParser(e.filesystem, source, modulePath, b.Label(), b) 135 if err := moduleParser.ParseFS(ctx, modulePath); err != nil { 136 return nil, err 137 } 138 return &ModuleDefinition{ 139 Name: b.Label(), 140 Path: modulePath, 141 Definition: b, 142 Parser: moduleParser, 143 FileSystem: e.filesystem, 144 }, nil 145 } 146 147 func (e *evaluator) loadExternalModule(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) { 148 149 e.debug.Log("locating non-initialised module '%s'...", source) 150 151 version := b.GetAttribute("version").AsStringValueOrDefault("", b).Value() 152 opt := resolvers.Options{ 153 Source: source, 154 OriginalSource: source, 155 Version: version, 156 OriginalVersion: version, 157 WorkingDir: e.projectRootPath, 158 Name: b.FullName(), 159 ModulePath: e.modulePath, 160 DebugLogger: e.debug.Extend("resolver"), 161 AllowDownloads: e.allowDownloads, 162 AllowCache: e.allowDownloads, 163 } 164 filesystem, prefix, path, err := resolveModule(ctx, e.filesystem, opt) 165 if err != nil { 166 return nil, err 167 } 168 prefix = filepath.Join(e.parentParser.moduleSource, prefix) 169 e.debug.Log("Module '%s' resolved to path '%s' in filesystem '%s' with prefix '%s'", b.FullName(), path, filesystem, prefix) 170 moduleParser := e.parentParser.newModuleParser(filesystem, prefix, path, b.Label(), b) 171 if err := moduleParser.ParseFS(ctx, path); err != nil { 172 return nil, err 173 } 174 return &ModuleDefinition{ 175 Name: b.Label(), 176 Path: path, 177 Definition: b, 178 Parser: moduleParser, 179 FileSystem: filesystem, 180 External: true, 181 }, nil 182 }