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 }