github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/terraform/parser/parser.go (about) 1 package parser 2 3 import ( 4 "context" 5 "io" 6 "io/fs" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/aquasecurity/defsec/pkg/debug" 14 "github.com/aquasecurity/defsec/pkg/scanners/options" 15 "github.com/aquasecurity/defsec/pkg/terraform" 16 tfcontext "github.com/aquasecurity/defsec/pkg/terraform/context" 17 "github.com/hashicorp/hcl/v2" 18 "github.com/hashicorp/hcl/v2/hclparse" 19 "github.com/zclconf/go-cty/cty" 20 21 "github.com/aquasecurity/trivy-iac/pkg/extrafs" 22 ) 23 24 type sourceFile struct { 25 file *hcl.File 26 path string 27 } 28 29 type Metrics struct { 30 Timings struct { 31 DiskIODuration time.Duration 32 ParseDuration time.Duration 33 } 34 Counts struct { 35 Blocks int 36 Modules int 37 ModuleDownloads int 38 Files int 39 } 40 } 41 42 var _ ConfigurableTerraformParser = (*Parser)(nil) 43 44 // Parser is a tool for parsing terraform templates at a given file system location 45 type Parser struct { 46 projectRoot string 47 moduleName string 48 modulePath string 49 moduleSource string 50 moduleFS fs.FS 51 moduleBlock *terraform.Block 52 files []sourceFile 53 tfvarsPaths []string 54 stopOnHCLError bool 55 workspaceName string 56 underlying *hclparse.Parser 57 children []*Parser 58 metrics Metrics 59 options []options.ParserOption 60 debug debug.Logger 61 allowDownloads bool 62 skipCachedModules bool 63 fsMap map[string]fs.FS 64 skipRequired bool 65 configsFS fs.FS 66 } 67 68 func (p *Parser) SetDebugWriter(writer io.Writer) { 69 p.debug = debug.New(writer, "terraform", "parser", "<"+p.moduleName+">") 70 } 71 72 func (p *Parser) SetTFVarsPaths(s ...string) { 73 p.tfvarsPaths = s 74 } 75 76 func (p *Parser) SetStopOnHCLError(b bool) { 77 p.stopOnHCLError = b 78 } 79 80 func (p *Parser) SetWorkspaceName(s string) { 81 p.workspaceName = s 82 } 83 84 func (p *Parser) SetAllowDownloads(b bool) { 85 p.allowDownloads = b 86 } 87 88 func (p *Parser) SetSkipCachedModules(b bool) { 89 p.skipCachedModules = b 90 } 91 92 func (p *Parser) SetSkipRequiredCheck(b bool) { 93 p.skipRequired = b 94 } 95 96 func (p *Parser) SetConfigsFS(fsys fs.FS) { 97 p.configsFS = fsys 98 } 99 100 // New creates a new Parser 101 func New(moduleFS fs.FS, moduleSource string, opts ...options.ParserOption) *Parser { 102 p := &Parser{ 103 workspaceName: "default", 104 underlying: hclparse.NewParser(), 105 options: opts, 106 moduleName: "root", 107 allowDownloads: true, 108 moduleFS: moduleFS, 109 moduleSource: moduleSource, 110 configsFS: moduleFS, 111 } 112 113 for _, option := range opts { 114 option(p) 115 } 116 117 return p 118 } 119 120 func (p *Parser) newModuleParser(moduleFS fs.FS, moduleSource, modulePath, moduleName string, moduleBlock *terraform.Block) *Parser { 121 mp := New(moduleFS, moduleSource) 122 mp.modulePath = modulePath 123 mp.moduleBlock = moduleBlock 124 mp.moduleName = moduleName 125 mp.projectRoot = p.projectRoot 126 p.children = append(p.children, mp) 127 for _, option := range p.options { 128 option(mp) 129 } 130 return mp 131 } 132 133 func (p *Parser) Metrics() Metrics { 134 total := p.metrics 135 for _, child := range p.children { 136 metrics := child.Metrics() 137 total.Counts.Files += metrics.Counts.Files 138 total.Counts.Blocks += metrics.Counts.Blocks 139 total.Timings.ParseDuration += metrics.Timings.ParseDuration 140 total.Timings.DiskIODuration += metrics.Timings.DiskIODuration 141 // NOTE: we don't add module count - this has already propagated to the top level 142 } 143 return total 144 } 145 146 func (p *Parser) ParseFile(_ context.Context, fullPath string) error { 147 diskStart := time.Now() 148 149 isJSON := strings.HasSuffix(fullPath, ".tf.json") 150 isHCL := strings.HasSuffix(fullPath, ".tf") 151 if !isJSON && !isHCL { 152 return nil 153 } 154 155 p.debug.Log("Parsing '%s'...", fullPath) 156 f, err := p.moduleFS.Open(filepath.ToSlash(fullPath)) 157 if err != nil { 158 return err 159 } 160 defer func() { _ = f.Close() }() 161 162 data, err := io.ReadAll(f) 163 if err != nil { 164 return err 165 } 166 p.metrics.Timings.DiskIODuration += time.Since(diskStart) 167 if dir := filepath.Dir(fullPath); p.projectRoot == "" { 168 p.debug.Log("Setting project/module root to '%s'", dir) 169 p.projectRoot = dir 170 p.modulePath = dir 171 } 172 173 start := time.Now() 174 var file *hcl.File 175 var diag hcl.Diagnostics 176 177 if isHCL { 178 file, diag = p.underlying.ParseHCL(data, fullPath) 179 } else { 180 file, diag = p.underlying.ParseJSON(data, fullPath) 181 } 182 if diag != nil && diag.HasErrors() { 183 return diag 184 } 185 p.files = append(p.files, sourceFile{ 186 file: file, 187 path: fullPath, 188 }) 189 p.metrics.Counts.Files++ 190 p.metrics.Timings.ParseDuration += time.Since(start) 191 p.debug.Log("Added file %s.", fullPath) 192 return nil 193 } 194 195 // ParseFS parses a root module, where it exists at the root of the provided filesystem 196 func (p *Parser) ParseFS(ctx context.Context, dir string) error { 197 198 dir = filepath.Clean(dir) 199 200 if p.projectRoot == "" { 201 p.debug.Log("Setting project/module root to '%s'", dir) 202 p.projectRoot = dir 203 p.modulePath = dir 204 } 205 206 slashed := filepath.ToSlash(dir) 207 p.debug.Log("Parsing FS from '%s'", slashed) 208 fileInfos, err := fs.ReadDir(p.moduleFS, slashed) 209 if err != nil { 210 return err 211 } 212 213 var paths []string 214 for _, info := range fileInfos { 215 realPath := filepath.Join(dir, info.Name()) 216 if info.Type()&os.ModeSymlink != 0 { 217 extra, ok := p.moduleFS.(extrafs.FS) 218 if !ok { 219 // we can't handle symlinks in this fs type for now 220 p.debug.Log("Cannot resolve symlink '%s' in '%s' for this fs type", info.Name(), dir) 221 continue 222 } 223 realPath, err = extra.ResolveSymlink(info.Name(), dir) 224 if err != nil { 225 p.debug.Log("Failed to resolve symlink '%s' in '%s': %s", info.Name(), dir, err) 226 continue 227 } 228 info, err := extra.Stat(realPath) 229 if err != nil { 230 p.debug.Log("Failed to stat resolved symlink '%s': %s", realPath, err) 231 continue 232 } 233 if info.IsDir() { 234 continue 235 } 236 p.debug.Log("Resolved symlink '%s' in '%s' to '%s'", info.Name(), dir, realPath) 237 } else if info.IsDir() { 238 continue 239 } 240 paths = append(paths, realPath) 241 } 242 sort.Strings(paths) 243 for _, path := range paths { 244 if err := p.ParseFile(ctx, path); err != nil { 245 if p.stopOnHCLError { 246 return err 247 } 248 p.debug.Log("error parsing '%s': %s", path, err) 249 continue 250 } 251 } 252 253 return nil 254 } 255 256 func (p *Parser) EvaluateAll(ctx context.Context) (terraform.Modules, cty.Value, error) { 257 258 p.debug.Log("Evaluating module...") 259 260 if len(p.files) == 0 { 261 p.debug.Log("No files found, nothing to do.") 262 return nil, cty.NilVal, nil 263 } 264 265 blocks, ignores, err := p.readBlocks(p.files) 266 if err != nil { 267 return nil, cty.NilVal, err 268 } 269 p.debug.Log("Read %d block(s) and %d ignore(s) for module '%s' (%d file[s])...", len(blocks), len(ignores), p.moduleName, len(p.files)) 270 271 p.metrics.Counts.Blocks = len(blocks) 272 273 var inputVars map[string]cty.Value 274 if p.moduleBlock != nil { 275 inputVars = p.moduleBlock.Values().AsValueMap() 276 p.debug.Log("Added %d input variables from module definition.", len(inputVars)) 277 } else { 278 inputVars, err = loadTFVars(p.configsFS, p.tfvarsPaths) 279 if err != nil { 280 return nil, cty.NilVal, err 281 } 282 p.debug.Log("Added %d variables from tfvars.", len(inputVars)) 283 } 284 285 modulesMetadata, metadataPath, err := loadModuleMetadata(p.moduleFS, p.projectRoot) 286 if err != nil { 287 p.debug.Log("Error loading module metadata: %s.", err) 288 } else { 289 p.debug.Log("Loaded module metadata for %d module(s) from '%s'.", len(modulesMetadata.Modules), metadataPath) 290 } 291 292 workingDir, err := os.Getwd() 293 if err != nil { 294 return nil, cty.NilVal, err 295 } 296 p.debug.Log("Working directory for module evaluation is '%s'", workingDir) 297 evaluator := newEvaluator( 298 p.moduleFS, 299 p, 300 p.projectRoot, 301 p.modulePath, 302 workingDir, 303 p.moduleName, 304 blocks, 305 inputVars, 306 modulesMetadata, 307 p.workspaceName, 308 ignores, 309 p.debug.Extend("evaluator"), 310 p.allowDownloads, 311 p.skipCachedModules, 312 ) 313 modules, fsMap, parseDuration := evaluator.EvaluateAll(ctx) 314 p.metrics.Counts.Modules = len(modules) 315 p.metrics.Timings.ParseDuration = parseDuration 316 p.debug.Log("Finished parsing module '%s'.", p.moduleName) 317 p.fsMap = fsMap 318 return modules, evaluator.exportOutputs(), nil 319 } 320 321 func (p *Parser) GetFilesystemMap() map[string]fs.FS { 322 if p.fsMap == nil { 323 return make(map[string]fs.FS) 324 } 325 return p.fsMap 326 } 327 328 func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, terraform.Ignores, error) { 329 var blocks terraform.Blocks 330 var ignores terraform.Ignores 331 moduleCtx := tfcontext.NewContext(&hcl.EvalContext{}, nil) 332 for _, file := range files { 333 fileBlocks, fileIgnores, err := loadBlocksFromFile(file, p.moduleSource) 334 if err != nil { 335 if p.stopOnHCLError { 336 return nil, nil, err 337 } 338 p.debug.Log("Encountered HCL parse error: %s", err) 339 continue 340 } 341 for _, fileBlock := range fileBlocks { 342 blocks = append(blocks, terraform.NewBlock(fileBlock, moduleCtx, p.moduleBlock, nil, p.moduleSource, p.moduleFS)) 343 } 344 ignores = append(ignores, fileIgnores...) 345 } 346 347 sortBlocksByHierarchy(blocks) 348 return blocks, ignores, nil 349 }