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