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  }