github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/terraform/parser/evaluator.go (about)

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