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