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  }