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  }