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 }