github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/terraform/scanner.go (about)

     1  package terraform
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/fs"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/aquasecurity/defsec/pkg/debug"
    14  	"github.com/aquasecurity/defsec/pkg/framework"
    15  	"github.com/aquasecurity/defsec/pkg/scan"
    16  	"github.com/aquasecurity/defsec/pkg/scanners/options"
    17  	"github.com/aquasecurity/defsec/pkg/terraform"
    18  	"github.com/aquasecurity/defsec/pkg/types"
    19  	"golang.org/x/exp/slices"
    20  
    21  	"github.com/aquasecurity/defsec/pkg/rego"
    22  	"github.com/aquasecurity/trivy-iac/pkg/extrafs"
    23  	"github.com/aquasecurity/trivy-iac/pkg/scanners"
    24  	"github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/executor"
    25  	"github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser"
    26  	"github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser/resolvers"
    27  )
    28  
    29  var _ scanners.FSScanner = (*Scanner)(nil)
    30  var _ options.ConfigurableScanner = (*Scanner)(nil)
    31  var _ ConfigurableTerraformScanner = (*Scanner)(nil)
    32  
    33  type Scanner struct {
    34  	sync.Mutex
    35  	options               []options.ScannerOption
    36  	parserOpt             []options.ParserOption
    37  	executorOpt           []executor.Option
    38  	dirs                  map[string]struct{}
    39  	forceAllDirs          bool
    40  	policyDirs            []string
    41  	policyReaders         []io.Reader
    42  	regoScanner           *rego.Scanner
    43  	execLock              sync.RWMutex
    44  	debug                 debug.Logger
    45  	frameworks            []framework.Framework
    46  	spec                  string
    47  	loadEmbeddedLibraries bool
    48  	loadEmbeddedPolicies  bool
    49  }
    50  
    51  func (s *Scanner) SetSpec(spec string) {
    52  	s.spec = spec
    53  }
    54  
    55  func (s *Scanner) SetRegoOnly(regoOnly bool) {
    56  	s.executorOpt = append(s.executorOpt, executor.OptionWithRegoOnly(regoOnly))
    57  }
    58  
    59  func (s *Scanner) SetFrameworks(frameworks []framework.Framework) {
    60  	s.frameworks = frameworks
    61  }
    62  
    63  func (s *Scanner) SetUseEmbeddedPolicies(b bool) {
    64  	s.loadEmbeddedPolicies = b
    65  }
    66  
    67  func (s *Scanner) SetUseEmbeddedLibraries(b bool) {
    68  	s.loadEmbeddedLibraries = b
    69  }
    70  
    71  func (s *Scanner) Name() string {
    72  	return "Terraform"
    73  }
    74  
    75  func (s *Scanner) SetForceAllDirs(b bool) {
    76  	s.forceAllDirs = b
    77  }
    78  
    79  func (s *Scanner) AddParserOptions(options ...options.ParserOption) {
    80  	s.parserOpt = append(s.parserOpt, options...)
    81  }
    82  
    83  func (s *Scanner) AddExecutorOptions(options ...executor.Option) {
    84  	s.executorOpt = append(s.executorOpt, options...)
    85  }
    86  
    87  func (s *Scanner) SetPolicyReaders(readers []io.Reader) {
    88  	s.policyReaders = readers
    89  }
    90  
    91  func (s *Scanner) SetSkipRequiredCheck(skip bool) {
    92  	s.parserOpt = append(s.parserOpt, options.ParserWithSkipRequiredCheck(skip))
    93  }
    94  
    95  func (s *Scanner) SetDebugWriter(writer io.Writer) {
    96  	s.parserOpt = append(s.parserOpt, options.ParserWithDebug(writer))
    97  	s.executorOpt = append(s.executorOpt, executor.OptionWithDebugWriter(writer))
    98  	s.debug = debug.New(writer, "terraform", "scanner")
    99  }
   100  
   101  func (s *Scanner) SetTraceWriter(_ io.Writer) {
   102  }
   103  
   104  func (s *Scanner) SetPerResultTracingEnabled(_ bool) {
   105  }
   106  
   107  func (s *Scanner) SetPolicyDirs(dirs ...string) {
   108  	s.policyDirs = dirs
   109  }
   110  
   111  func (s *Scanner) SetDataDirs(_ ...string)         {}
   112  func (s *Scanner) SetPolicyNamespaces(_ ...string) {}
   113  
   114  func (s *Scanner) SetPolicyFilesystem(_ fs.FS) {
   115  	// handled by rego when option is passed on
   116  }
   117  
   118  func (s *Scanner) SetDataFilesystem(_ fs.FS) {
   119  	// handled by rego when option is passed on
   120  }
   121  func (s *Scanner) SetRegoErrorLimit(_ int) {}
   122  
   123  type Metrics struct {
   124  	Parser   parser.Metrics
   125  	Executor executor.Metrics
   126  	Timings  struct {
   127  		Total time.Duration
   128  	}
   129  }
   130  
   131  func New(options ...options.ScannerOption) *Scanner {
   132  	s := &Scanner{
   133  		dirs:    make(map[string]struct{}),
   134  		options: options,
   135  	}
   136  	for _, opt := range options {
   137  		opt(s)
   138  	}
   139  	return s
   140  }
   141  
   142  func (s *Scanner) ScanFS(ctx context.Context, target fs.FS, dir string) (scan.Results, error) {
   143  	results, _, err := s.ScanFSWithMetrics(ctx, target, dir)
   144  	return results, err
   145  }
   146  
   147  func (s *Scanner) initRegoScanner(srcFS fs.FS) (*rego.Scanner, error) {
   148  	s.Lock()
   149  	defer s.Unlock()
   150  	if s.regoScanner != nil {
   151  		return s.regoScanner, nil
   152  	}
   153  	regoScanner := rego.NewScanner(types.SourceCloud, s.options...)
   154  	regoScanner.SetParentDebugLogger(s.debug)
   155  
   156  	if err := regoScanner.LoadPolicies(s.loadEmbeddedLibraries, s.loadEmbeddedPolicies, srcFS, s.policyDirs, s.policyReaders); err != nil {
   157  		return nil, err
   158  	}
   159  	s.regoScanner = regoScanner
   160  	return regoScanner, nil
   161  }
   162  
   163  // terraformRootModule represents the module to be used as the root module for Terraform deployment.
   164  type terraformRootModule struct {
   165  	rootPath string
   166  	childs   terraform.Modules
   167  	fsMap    map[string]fs.FS
   168  }
   169  
   170  func excludeNonRootModules(modules []terraformRootModule) []terraformRootModule {
   171  	var result []terraformRootModule
   172  	var childPaths []string
   173  
   174  	for _, module := range modules {
   175  		childPaths = append(childPaths, module.childs.ChildModulesPaths()...)
   176  	}
   177  
   178  	for _, module := range modules {
   179  		// if the path of the root module matches the path of the child module,
   180  		// then we should not scan it
   181  		if !slices.Contains(childPaths, module.rootPath) {
   182  			result = append(result, module)
   183  		}
   184  	}
   185  	return result
   186  }
   187  
   188  func (s *Scanner) ScanFSWithMetrics(ctx context.Context, target fs.FS, dir string) (scan.Results, Metrics, error) {
   189  
   190  	var metrics Metrics
   191  
   192  	s.debug.Log("Scanning [%s] at '%s'...", target, dir)
   193  
   194  	// find directories which directly contain tf files (and have no parent containing tf files)
   195  	rootDirs := s.findRootModules(target, dir, dir)
   196  	sort.Strings(rootDirs)
   197  
   198  	if len(rootDirs) == 0 {
   199  		s.debug.Log("no root modules found")
   200  		return nil, metrics, nil
   201  	}
   202  
   203  	regoScanner, err := s.initRegoScanner(target)
   204  	if err != nil {
   205  		return nil, metrics, err
   206  	}
   207  
   208  	s.execLock.Lock()
   209  	s.executorOpt = append(s.executorOpt, executor.OptionWithRegoScanner(regoScanner), executor.OptionWithFrameworks(s.frameworks...))
   210  	s.execLock.Unlock()
   211  
   212  	var allResults scan.Results
   213  
   214  	// parse all root module directories
   215  	var rootModules []terraformRootModule
   216  	for _, dir := range rootDirs {
   217  
   218  		s.debug.Log("Scanning root module '%s'...", dir)
   219  
   220  		p := parser.New(target, "", s.parserOpt...)
   221  
   222  		if err := p.ParseFS(ctx, dir); err != nil {
   223  			return nil, metrics, err
   224  		}
   225  
   226  		modules, _, err := p.EvaluateAll(ctx)
   227  		if err != nil {
   228  			return nil, metrics, err
   229  		}
   230  
   231  		parserMetrics := p.Metrics()
   232  		metrics.Parser.Counts.Blocks += parserMetrics.Counts.Blocks
   233  		metrics.Parser.Counts.Modules += parserMetrics.Counts.Modules
   234  		metrics.Parser.Counts.Files += parserMetrics.Counts.Files
   235  		metrics.Parser.Timings.DiskIODuration += parserMetrics.Timings.DiskIODuration
   236  		metrics.Parser.Timings.ParseDuration += parserMetrics.Timings.ParseDuration
   237  
   238  		rootModules = append(rootModules, terraformRootModule{
   239  			rootPath: dir,
   240  			childs:   modules,
   241  			fsMap:    p.GetFilesystemMap(),
   242  		})
   243  	}
   244  
   245  	rootModules = excludeNonRootModules(rootModules)
   246  	for _, module := range rootModules {
   247  		s.execLock.RLock()
   248  		e := executor.New(s.executorOpt...)
   249  		s.execLock.RUnlock()
   250  		results, execMetrics, err := e.Execute(module.childs)
   251  		if err != nil {
   252  			return nil, metrics, err
   253  		}
   254  
   255  		for i, result := range results {
   256  			if result.Metadata().Range().GetFS() != nil {
   257  				continue
   258  			}
   259  			key := result.Metadata().Range().GetFSKey()
   260  			if key == "" {
   261  				continue
   262  			}
   263  			if filesystem, ok := module.fsMap[key]; ok {
   264  				override := scan.Results{
   265  					result,
   266  				}
   267  				override.SetSourceAndFilesystem(result.Range().GetSourcePrefix(), filesystem, false)
   268  				results[i] = override[0]
   269  			}
   270  		}
   271  
   272  		metrics.Executor.Counts.Passed += execMetrics.Counts.Passed
   273  		metrics.Executor.Counts.Failed += execMetrics.Counts.Failed
   274  		metrics.Executor.Counts.Ignored += execMetrics.Counts.Ignored
   275  		metrics.Executor.Counts.Critical += execMetrics.Counts.Critical
   276  		metrics.Executor.Counts.High += execMetrics.Counts.High
   277  		metrics.Executor.Counts.Medium += execMetrics.Counts.Medium
   278  		metrics.Executor.Counts.Low += execMetrics.Counts.Low
   279  		metrics.Executor.Timings.Adaptation += execMetrics.Timings.Adaptation
   280  		metrics.Executor.Timings.RunningChecks += execMetrics.Timings.RunningChecks
   281  
   282  		allResults = append(allResults, results...)
   283  	}
   284  
   285  	metrics.Parser.Counts.ModuleDownloads = resolvers.Remote.GetDownloadCount()
   286  
   287  	metrics.Timings.Total += metrics.Parser.Timings.DiskIODuration
   288  	metrics.Timings.Total += metrics.Parser.Timings.ParseDuration
   289  	metrics.Timings.Total += metrics.Executor.Timings.Adaptation
   290  	metrics.Timings.Total += metrics.Executor.Timings.RunningChecks
   291  
   292  	return allResults, metrics, nil
   293  }
   294  
   295  func (s *Scanner) removeNestedDirs(dirs []string) []string {
   296  	if s.forceAllDirs {
   297  		return dirs
   298  	}
   299  	var clean []string
   300  	for _, dirA := range dirs {
   301  		dirOK := true
   302  		for _, dirB := range dirs {
   303  			if dirA == dirB {
   304  				continue
   305  			}
   306  			if str, err := filepath.Rel(dirB, dirA); err == nil && !strings.HasPrefix(str, "..") {
   307  				dirOK = false
   308  				break
   309  			}
   310  		}
   311  		if dirOK {
   312  			clean = append(clean, dirA)
   313  		}
   314  	}
   315  	return clean
   316  }
   317  
   318  func (s *Scanner) findRootModules(target fs.FS, scanDir string, dirs ...string) []string {
   319  
   320  	var roots []string
   321  	var others []string
   322  
   323  	for _, dir := range dirs {
   324  		if s.isRootModule(target, dir) {
   325  			roots = append(roots, dir)
   326  			if !s.forceAllDirs {
   327  				continue
   328  			}
   329  		}
   330  
   331  		// if this isn't a root module, look at directories inside it
   332  		files, err := fs.ReadDir(target, filepath.ToSlash(dir))
   333  		if err != nil {
   334  			continue
   335  		}
   336  		for _, file := range files {
   337  			realPath := filepath.Join(dir, file.Name())
   338  			if symFS, ok := target.(extrafs.ReadLinkFS); ok {
   339  				realPath, err = symFS.ResolveSymlink(realPath, scanDir)
   340  				if err != nil {
   341  					s.debug.Log("failed to resolve symlink '%s': %s", file.Name(), err)
   342  					continue
   343  				}
   344  			}
   345  			if file.IsDir() {
   346  				others = append(others, realPath)
   347  			} else if statFS, ok := target.(fs.StatFS); ok {
   348  				info, err := statFS.Stat(filepath.ToSlash(realPath))
   349  				if err != nil {
   350  					continue
   351  				}
   352  				if info.IsDir() {
   353  					others = append(others, realPath)
   354  				}
   355  			}
   356  		}
   357  	}
   358  
   359  	if (len(roots) == 0 || s.forceAllDirs) && len(others) > 0 {
   360  		roots = append(roots, s.findRootModules(target, scanDir, others...)...)
   361  	}
   362  
   363  	return s.removeNestedDirs(roots)
   364  }
   365  
   366  func (s *Scanner) isRootModule(target fs.FS, dir string) bool {
   367  	files, err := fs.ReadDir(target, filepath.ToSlash(dir))
   368  	if err != nil {
   369  		s.debug.Log("failed to read dir '%s' from filesystem [%s]: %s", dir, target, err)
   370  		return false
   371  	}
   372  	for _, file := range files {
   373  		if strings.HasSuffix(file.Name(), ".tf") || strings.HasSuffix(file.Name(), ".tf.json") {
   374  			return true
   375  		}
   376  	}
   377  	return false
   378  }