github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/terraform/parser/evaluator.go (about)

     1  package parser
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"reflect"
     8  	"time"
     9  
    10  	"github.com/khulnasoft-lab/defsec/pkg/types"
    11  
    12  	"github.com/khulnasoft-lab/defsec/pkg/debug"
    13  
    14  	tfcontext "github.com/khulnasoft-lab/defsec/pkg/scanners/terraform/context"
    15  	"github.com/khulnasoft-lab/defsec/pkg/terraform"
    16  
    17  	"github.com/hashicorp/hcl/v2"
    18  	"github.com/zclconf/go-cty/cty"
    19  	"github.com/zclconf/go-cty/cty/gocty"
    20  )
    21  
    22  const (
    23  	maxContextIterations = 32
    24  )
    25  
    26  type evaluator struct {
    27  	filesystem      fs.FS
    28  	ctx             *tfcontext.Context
    29  	blocks          terraform.Blocks
    30  	inputVars       map[string]cty.Value
    31  	moduleMetadata  *modulesMetadata
    32  	projectRootPath string // root of the current scan
    33  	modulePath      string
    34  	moduleName      string
    35  	ignores         terraform.Ignores
    36  	parentParser    *Parser
    37  	debug           debug.Logger
    38  	allowDownloads  bool
    39  }
    40  
    41  func newEvaluator(
    42  	target fs.FS,
    43  	parentParser *Parser,
    44  	projectRootPath string,
    45  	modulePath string,
    46  	workingDir string,
    47  	moduleName string,
    48  	blocks terraform.Blocks,
    49  	inputVars map[string]cty.Value,
    50  	moduleMetadata *modulesMetadata,
    51  	workspace string,
    52  	ignores []terraform.Ignore,
    53  	logger debug.Logger,
    54  	allowDownloads bool,
    55  ) *evaluator {
    56  
    57  	// create a context to store variables and make functions available
    58  	ctx := tfcontext.NewContext(&hcl.EvalContext{
    59  		Functions: Functions(target, modulePath),
    60  	}, nil)
    61  
    62  	// these variables are made available by terraform to each module
    63  	ctx.SetByDot(cty.StringVal(workspace), "terraform.workspace")
    64  	ctx.SetByDot(cty.StringVal(projectRootPath), "path.root")
    65  	ctx.SetByDot(cty.StringVal(modulePath), "path.module")
    66  	ctx.SetByDot(cty.StringVal(workingDir), "path.cwd")
    67  
    68  	// each block gets its own scope to define variables in
    69  	for _, b := range blocks {
    70  		b.OverrideContext(ctx.NewChild())
    71  	}
    72  
    73  	return &evaluator{
    74  		filesystem:      target,
    75  		parentParser:    parentParser,
    76  		modulePath:      modulePath,
    77  		moduleName:      moduleName,
    78  		projectRootPath: projectRootPath,
    79  		ctx:             ctx,
    80  		blocks:          blocks,
    81  		inputVars:       inputVars,
    82  		moduleMetadata:  moduleMetadata,
    83  		ignores:         ignores,
    84  		debug:           logger,
    85  		allowDownloads:  allowDownloads,
    86  	}
    87  }
    88  
    89  func (e *evaluator) evaluateStep() {
    90  
    91  	e.ctx.Set(e.getValuesByBlockType("variable"), "var")
    92  	e.ctx.Set(e.getValuesByBlockType("locals"), "local")
    93  	e.ctx.Set(e.getValuesByBlockType("provider"), "provider")
    94  
    95  	resources := e.getValuesByBlockType("resource")
    96  	for key, resource := range resources.AsValueMap() {
    97  		e.ctx.Set(resource, key)
    98  	}
    99  
   100  	e.ctx.Set(e.getValuesByBlockType("data"), "data")
   101  	e.ctx.Set(e.getValuesByBlockType("output"), "output")
   102  }
   103  
   104  // exportOutputs is used to export module outputs to the parent module
   105  func (e *evaluator) exportOutputs() cty.Value {
   106  	data := make(map[string]cty.Value)
   107  	for _, block := range e.blocks.OfType("output") {
   108  		attr := block.GetAttribute("value")
   109  		if attr.IsNil() {
   110  			continue
   111  		}
   112  		data[block.Label()] = attr.Value()
   113  		e.debug.Log("Added module output %s=%s.", block.Label(), attr.Value().GoString())
   114  	}
   115  	return cty.ObjectVal(data)
   116  }
   117  
   118  func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[string]fs.FS, time.Duration) {
   119  
   120  	fsKey := types.CreateFSKey(e.filesystem)
   121  	e.debug.Log("Filesystem key is '%s'", fsKey)
   122  
   123  	fsMap := make(map[string]fs.FS)
   124  	fsMap[fsKey] = e.filesystem
   125  
   126  	var parseDuration time.Duration
   127  
   128  	var lastContext hcl.EvalContext
   129  	start := time.Now()
   130  	e.debug.Log("Starting module evaluation...")
   131  	for i := 0; i < maxContextIterations; i++ {
   132  
   133  		e.evaluateStep()
   134  
   135  		// if ctx matches the last evaluation, we can bail, nothing left to resolve
   136  		if i > 0 && reflect.DeepEqual(lastContext.Variables, e.ctx.Inner().Variables) {
   137  			break
   138  		}
   139  
   140  		if len(e.ctx.Inner().Variables) != len(lastContext.Variables) {
   141  			lastContext.Variables = make(map[string]cty.Value, len(e.ctx.Inner().Variables))
   142  		}
   143  		for k, v := range e.ctx.Inner().Variables {
   144  			lastContext.Variables[k] = v
   145  		}
   146  	}
   147  
   148  	// expand out resources and modules via count (not a typo, we do this twice so every order is processed)
   149  	e.blocks = e.expandBlocks(e.blocks)
   150  	e.blocks = e.expandBlocks(e.blocks)
   151  
   152  	parseDuration += time.Since(start)
   153  
   154  	e.debug.Log("Starting submodule evaluation...")
   155  	var modules []*terraform.Module
   156  	for _, definition := range e.loadModules(ctx) {
   157  		submodules, outputs, err := definition.Parser.EvaluateAll(ctx)
   158  		if err != nil {
   159  			e.debug.Log("Failed to evaluate submodule '%s': %s.", definition.Name, err)
   160  			continue
   161  		}
   162  		// export module outputs
   163  		e.ctx.Set(outputs, "module", definition.Name)
   164  		modules = append(modules, submodules...)
   165  		for key, val := range definition.Parser.GetFilesystemMap() {
   166  			fsMap[key] = val
   167  		}
   168  	}
   169  	e.debug.Log("Finished processing %d submodule(s).", len(modules))
   170  
   171  	e.debug.Log("Starting post-submodule evaluation...")
   172  	for i := 0; i < maxContextIterations; i++ {
   173  
   174  		e.evaluateStep()
   175  
   176  		// if ctx matches the last evaluation, we can bail, nothing left to resolve
   177  		if i > 0 && reflect.DeepEqual(lastContext.Variables, e.ctx.Inner().Variables) {
   178  			break
   179  		}
   180  
   181  		if len(e.ctx.Inner().Variables) != len(lastContext.Variables) {
   182  			lastContext.Variables = make(map[string]cty.Value, len(e.ctx.Inner().Variables))
   183  		}
   184  		for k, v := range e.ctx.Inner().Variables {
   185  			lastContext.Variables[k] = v
   186  		}
   187  	}
   188  
   189  	e.debug.Log("Module evaluation complete.")
   190  	parseDuration += time.Since(start)
   191  	return append([]*terraform.Module{terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores)}, modules...), fsMap, parseDuration
   192  }
   193  
   194  func (e *evaluator) expandBlocks(blocks terraform.Blocks) terraform.Blocks {
   195  	return e.expandDynamicBlocks(e.expandBlockForEaches(e.expandBlockCounts(blocks))...)
   196  }
   197  
   198  func (e *evaluator) expandDynamicBlocks(blocks ...*terraform.Block) terraform.Blocks {
   199  	for _, b := range blocks {
   200  		e.expandDynamicBlock(b)
   201  	}
   202  	return blocks
   203  }
   204  
   205  func (e *evaluator) expandDynamicBlock(b *terraform.Block) {
   206  	for _, sub := range b.AllBlocks() {
   207  		e.expandDynamicBlock(sub)
   208  	}
   209  	for _, sub := range b.AllBlocks().OfType("dynamic") {
   210  		blockName := sub.TypeLabel()
   211  		expanded := e.expandBlockForEaches(terraform.Blocks{sub})
   212  		for _, ex := range expanded {
   213  			if content := ex.GetBlock("content"); content.IsNotNil() {
   214  				_ = e.expandDynamicBlocks(content)
   215  				b.InjectBlock(content, blockName)
   216  			}
   217  		}
   218  	}
   219  }
   220  
   221  func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Blocks {
   222  	var forEachFiltered terraform.Blocks
   223  
   224  	for _, block := range blocks {
   225  
   226  		forEachAttr := block.GetAttribute("for_each")
   227  
   228  		if forEachAttr.IsNil() || block.IsCountExpanded() || (block.Type() != "resource" && block.Type() != "module" && block.Type() != "dynamic") {
   229  			forEachFiltered = append(forEachFiltered, block)
   230  			continue
   231  		}
   232  		if !forEachAttr.Value().IsNull() && forEachAttr.Value().IsKnown() && forEachAttr.IsIterable() {
   233  			var clones []cty.Value
   234  			_ = forEachAttr.Each(func(key cty.Value, val cty.Value) {
   235  
   236  				index := key
   237  
   238  				switch val.Type() {
   239  				case cty.String, cty.Number:
   240  					index = val
   241  				}
   242  
   243  				clone := block.Clone(index)
   244  
   245  				ctx := clone.Context()
   246  
   247  				e.copyVariables(block, clone)
   248  
   249  				ctx.SetByDot(key, "each.key")
   250  				ctx.SetByDot(val, "each.value")
   251  
   252  				ctx.Set(key, block.TypeLabel(), "key")
   253  				ctx.Set(val, block.TypeLabel(), "value")
   254  
   255  				forEachFiltered = append(forEachFiltered, clone)
   256  
   257  				clones = append(clones, clone.Values())
   258  				metadata := clone.GetMetadata()
   259  				e.ctx.SetByDot(clone.Values(), metadata.Reference())
   260  			})
   261  			metadata := block.GetMetadata()
   262  			if len(clones) == 0 {
   263  				e.ctx.SetByDot(cty.EmptyTupleVal, metadata.Reference())
   264  			} else {
   265  				e.ctx.SetByDot(cty.TupleVal(clones), metadata.Reference())
   266  			}
   267  			e.debug.Log("Expanded block '%s' into %d clones via 'for_each' attribute.", block.LocalName(), len(clones))
   268  		}
   269  	}
   270  
   271  	return forEachFiltered
   272  }
   273  
   274  func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks {
   275  	var countFiltered terraform.Blocks
   276  	for _, block := range blocks {
   277  		countAttr := block.GetAttribute("count")
   278  		if countAttr.IsNil() || block.IsCountExpanded() || (block.Type() != "resource" && block.Type() != "module") {
   279  			countFiltered = append(countFiltered, block)
   280  			continue
   281  		}
   282  		count := 1
   283  		if !countAttr.Value().IsNull() && countAttr.Value().IsKnown() {
   284  			if countAttr.Value().Type() == cty.Number {
   285  				f, _ := countAttr.Value().AsBigFloat().Float64()
   286  				count = int(f)
   287  			}
   288  		}
   289  
   290  		var clones []cty.Value
   291  		for i := 0; i < count; i++ {
   292  			c, _ := gocty.ToCtyValue(i, cty.Number)
   293  			clone := block.Clone(c)
   294  			clones = append(clones, clone.Values())
   295  			block.TypeLabel()
   296  			countFiltered = append(countFiltered, clone)
   297  			metadata := clone.GetMetadata()
   298  			e.ctx.SetByDot(clone.Values(), metadata.Reference())
   299  		}
   300  		metadata := block.GetMetadata()
   301  		if len(clones) == 0 {
   302  			e.ctx.SetByDot(cty.EmptyTupleVal, metadata.Reference())
   303  		} else {
   304  			e.ctx.SetByDot(cty.TupleVal(clones), metadata.Reference())
   305  		}
   306  		e.debug.Log("Expanded block '%s' into %d clones via 'count' attribute.", block.LocalName(), len(clones))
   307  	}
   308  
   309  	return countFiltered
   310  }
   311  
   312  func (e *evaluator) copyVariables(from, to *terraform.Block) {
   313  
   314  	var fromBase string
   315  	var fromRel string
   316  	var toRel string
   317  
   318  	switch from.Type() {
   319  	case "resource":
   320  		fromBase = from.TypeLabel()
   321  		fromRel = from.NameLabel()
   322  		toRel = to.NameLabel()
   323  	case "module":
   324  		fromBase = from.Type()
   325  		fromRel = from.TypeLabel()
   326  		toRel = to.TypeLabel()
   327  	default:
   328  		return
   329  	}
   330  
   331  	srcValue := e.ctx.Root().Get(fromBase, fromRel)
   332  	if srcValue == cty.NilVal {
   333  		return
   334  	}
   335  	e.ctx.Root().Set(srcValue, fromBase, toRel)
   336  }
   337  
   338  func (e *evaluator) evaluateVariable(b *terraform.Block) (cty.Value, error) {
   339  	if b.Label() == "" {
   340  		return cty.NilVal, fmt.Errorf("empty label - cannot resolve")
   341  	}
   342  	if override, exists := e.inputVars[b.Label()]; exists {
   343  		return override, nil
   344  	}
   345  	attributes := b.Attributes()
   346  	if attributes == nil {
   347  		return cty.NilVal, fmt.Errorf("cannot resolve variable with no attributes")
   348  	}
   349  	if def, exists := attributes["default"]; exists {
   350  		return def.NullableValue(), nil
   351  	}
   352  	return cty.NilVal, fmt.Errorf("no value found")
   353  }
   354  
   355  func (e *evaluator) evaluateOutput(b *terraform.Block) (cty.Value, error) {
   356  	if b.Label() == "" {
   357  		return cty.NilVal, fmt.Errorf("empty label - cannot resolve")
   358  	}
   359  
   360  	attribute := b.GetAttribute("value")
   361  	if attribute.IsNil() {
   362  		return cty.NilVal, fmt.Errorf("cannot resolve variable with no attributes")
   363  	}
   364  	return attribute.Value(), nil
   365  }
   366  
   367  // returns true if all evaluations were successful
   368  func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
   369  
   370  	blocksOfType := e.blocks.OfType(blockType)
   371  	values := make(map[string]cty.Value)
   372  
   373  	for _, b := range blocksOfType {
   374  
   375  		switch b.Type() {
   376  		case "variable": // variables are special in that their value comes from the "default" attribute
   377  			val, err := e.evaluateVariable(b)
   378  			if err != nil {
   379  				continue
   380  			}
   381  			values[b.Label()] = val
   382  		case "output":
   383  			val, err := e.evaluateOutput(b)
   384  			if err != nil {
   385  				continue
   386  			}
   387  			values[b.Label()] = val
   388  		case "locals", "moved", "import":
   389  			for key, val := range b.Values().AsValueMap() {
   390  				values[key] = val
   391  			}
   392  		case "provider", "module", "check":
   393  			if b.Label() == "" {
   394  				continue
   395  			}
   396  			values[b.Label()] = b.Values()
   397  		case "resource", "data":
   398  			if len(b.Labels()) < 2 {
   399  				continue
   400  			}
   401  
   402  			blockMap, ok := values[b.Labels()[0]]
   403  			if !ok {
   404  				values[b.Labels()[0]] = cty.ObjectVal(make(map[string]cty.Value))
   405  				blockMap = values[b.Labels()[0]]
   406  			}
   407  
   408  			valueMap := blockMap.AsValueMap()
   409  			if valueMap == nil {
   410  				valueMap = make(map[string]cty.Value)
   411  			}
   412  
   413  			valueMap[b.Labels()[1]] = b.Values()
   414  			values[b.Labels()[0]] = cty.ObjectVal(valueMap)
   415  		}
   416  	}
   417  
   418  	return cty.ObjectVal(values)
   419  }