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

     1  package executor
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	runtimeDebug "runtime/debug"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/aquasecurity/defsec/pkg/rego"
    13  	"github.com/aquasecurity/defsec/pkg/scan"
    14  	"github.com/aquasecurity/defsec/pkg/state"
    15  	"github.com/aquasecurity/defsec/pkg/terraform"
    16  	types "github.com/aquasecurity/defsec/pkg/types/rules"
    17  )
    18  
    19  type Pool struct {
    20  	size         int
    21  	modules      terraform.Modules
    22  	state        *state.State
    23  	rules        []types.RegisteredRule
    24  	ignoreErrors bool
    25  	rs           *rego.Scanner
    26  	regoOnly     bool
    27  }
    28  
    29  func NewPool(size int, rules []types.RegisteredRule, modules terraform.Modules, state *state.State, ignoreErrors bool, regoScanner *rego.Scanner, regoOnly bool) *Pool {
    30  	return &Pool{
    31  		size:         size,
    32  		rules:        rules,
    33  		state:        state,
    34  		modules:      modules,
    35  		ignoreErrors: ignoreErrors,
    36  		rs:           regoScanner,
    37  		regoOnly:     regoOnly,
    38  	}
    39  }
    40  
    41  // Run runs the job in the pool - this will only return an error if a job panics
    42  func (p *Pool) Run() (scan.Results, error) {
    43  
    44  	outgoing := make(chan Job, p.size*2)
    45  
    46  	var workers []*Worker
    47  	for i := 0; i < p.size; i++ {
    48  		worker := NewWorker(outgoing)
    49  		go worker.Start()
    50  		workers = append(workers, worker)
    51  	}
    52  
    53  	if p.rs != nil {
    54  		var basePath string
    55  		if len(p.modules) > 0 {
    56  			basePath = p.modules[0].RootPath()
    57  		}
    58  		outgoing <- &regoJob{
    59  			state:    p.state,
    60  			scanner:  p.rs,
    61  			basePath: basePath,
    62  		}
    63  	}
    64  
    65  	if !p.regoOnly {
    66  		for _, r := range p.rules {
    67  			if r.GetRule().CustomChecks.Terraform != nil && r.GetRule().CustomChecks.Terraform.Check != nil {
    68  				// run local hcl rule
    69  				for _, module := range p.modules {
    70  					mod := *module
    71  					outgoing <- &hclModuleRuleJob{
    72  						module:       &mod,
    73  						rule:         r,
    74  						ignoreErrors: p.ignoreErrors,
    75  					}
    76  				}
    77  			} else {
    78  				// run defsec rule
    79  				outgoing <- &infraRuleJob{
    80  					state:        p.state,
    81  					rule:         r,
    82  					ignoreErrors: p.ignoreErrors,
    83  				}
    84  			}
    85  		}
    86  	}
    87  
    88  	close(outgoing)
    89  
    90  	var results scan.Results
    91  	for _, worker := range workers {
    92  		results = append(results, worker.Wait()...)
    93  		if err := worker.Error(); err != nil {
    94  			return nil, err
    95  		}
    96  	}
    97  
    98  	return results, nil
    99  }
   100  
   101  type Job interface {
   102  	Run() (scan.Results, error)
   103  }
   104  
   105  type infraRuleJob struct {
   106  	state *state.State
   107  	rule  types.RegisteredRule
   108  
   109  	ignoreErrors bool
   110  }
   111  
   112  type hclModuleRuleJob struct {
   113  	module       *terraform.Module
   114  	rule         types.RegisteredRule
   115  	ignoreErrors bool
   116  }
   117  
   118  type regoJob struct {
   119  	state    *state.State
   120  	scanner  *rego.Scanner
   121  	basePath string
   122  }
   123  
   124  func (h *infraRuleJob) Run() (_ scan.Results, err error) {
   125  	if h.ignoreErrors {
   126  		defer func() {
   127  			if panicErr := recover(); panicErr != nil {
   128  				err = fmt.Errorf("%s\n%s", panicErr, string(runtimeDebug.Stack()))
   129  			}
   130  		}()
   131  	}
   132  	return h.rule.Evaluate(h.state), err
   133  }
   134  
   135  func (h *hclModuleRuleJob) Run() (results scan.Results, err error) {
   136  	if h.ignoreErrors {
   137  		defer func() {
   138  			if panicErr := recover(); panicErr != nil {
   139  				err = fmt.Errorf("%s\n%s", panicErr, string(runtimeDebug.Stack()))
   140  			}
   141  		}()
   142  	}
   143  	customCheck := h.rule.GetRule().CustomChecks.Terraform
   144  	for _, block := range h.module.GetBlocks() {
   145  		if !isCustomCheckRequiredForBlock(customCheck, block) {
   146  			continue
   147  		}
   148  		results = append(results, customCheck.Check(block, h.module)...)
   149  	}
   150  	results.SetRule(h.rule.GetRule())
   151  	return
   152  }
   153  
   154  func (h *regoJob) Run() (results scan.Results, err error) {
   155  	regoResults, err := h.scanner.ScanInput(context.TODO(), rego.Input{
   156  		Contents: h.state.ToRego(),
   157  		Path:     h.basePath,
   158  	})
   159  	if err != nil {
   160  		return nil, fmt.Errorf("rego scan error: %w", err)
   161  	}
   162  	return regoResults, nil
   163  }
   164  
   165  // nolint
   166  func isCustomCheckRequiredForBlock(custom *scan.TerraformCustomCheck, b *terraform.Block) bool {
   167  
   168  	var found bool
   169  	for _, requiredType := range custom.RequiredTypes {
   170  		if b.Type() == requiredType {
   171  			found = true
   172  			break
   173  		}
   174  	}
   175  	if !found && len(custom.RequiredTypes) > 0 {
   176  		return false
   177  	}
   178  
   179  	found = false
   180  	for _, requiredLabel := range custom.RequiredLabels {
   181  		if requiredLabel == "*" || (len(b.Labels()) > 0 && wildcardMatch(requiredLabel, b.TypeLabel())) {
   182  			found = true
   183  			break
   184  		}
   185  	}
   186  	if !found && len(custom.RequiredLabels) > 0 {
   187  		return false
   188  	}
   189  
   190  	found = false
   191  	if len(custom.RequiredSources) > 0 && b.Type() == terraform.TypeModule.Name() {
   192  		if sourceAttr := b.GetAttribute("source"); sourceAttr.IsNotNil() {
   193  			values := sourceAttr.AsStringValues().AsStrings()
   194  			if len(values) == 0 {
   195  				return false
   196  			}
   197  			sourcePath := values[0]
   198  
   199  			// resolve module source path to path relative to cwd
   200  			if strings.HasPrefix(sourcePath, ".") {
   201  				sourcePath = cleanPathRelativeToWorkingDir(filepath.Dir(b.GetMetadata().Range().GetFilename()), sourcePath)
   202  			}
   203  
   204  			for _, requiredSource := range custom.RequiredSources {
   205  				if requiredSource == "*" || wildcardMatch(requiredSource, sourcePath) {
   206  					found = true
   207  					break
   208  				}
   209  			}
   210  		}
   211  		return found
   212  	}
   213  
   214  	return true
   215  }
   216  
   217  func cleanPathRelativeToWorkingDir(dir, path string) string {
   218  	absPath := filepath.Clean(filepath.Join(dir, path))
   219  	wDir, err := os.Getwd()
   220  	if err != nil {
   221  		return absPath
   222  	}
   223  	relPath, err := filepath.Rel(wDir, absPath)
   224  	if err != nil {
   225  		return absPath
   226  	}
   227  	return relPath
   228  }
   229  
   230  func wildcardMatch(pattern string, subject string) bool {
   231  	if pattern == "" {
   232  		return false
   233  	}
   234  	parts := strings.Split(pattern, "*")
   235  	var lastIndex int
   236  	for i, part := range parts {
   237  		if part == "" {
   238  			continue
   239  		}
   240  		if i == 0 {
   241  			if !strings.HasPrefix(subject, part) {
   242  				return false
   243  			}
   244  		}
   245  		if i == len(parts)-1 {
   246  			if !strings.HasSuffix(subject, part) {
   247  				return false
   248  			}
   249  		}
   250  		newIndex := strings.Index(subject, part)
   251  		if newIndex < lastIndex {
   252  			return false
   253  		}
   254  		lastIndex = newIndex
   255  	}
   256  	return true
   257  }
   258  
   259  type Worker struct {
   260  	incoming <-chan Job
   261  	mu       sync.Mutex
   262  	results  scan.Results
   263  	panic    interface{}
   264  }
   265  
   266  func NewWorker(incoming <-chan Job) *Worker {
   267  	w := &Worker{
   268  		incoming: incoming,
   269  	}
   270  	w.mu.Lock()
   271  	return w
   272  }
   273  
   274  func (w *Worker) Start() {
   275  	defer w.mu.Unlock()
   276  	w.results = nil
   277  	for job := range w.incoming {
   278  		func() {
   279  			results, err := job.Run()
   280  			if err != nil {
   281  				w.panic = err
   282  			}
   283  			w.results = append(w.results, results...)
   284  		}()
   285  	}
   286  }
   287  
   288  func (w *Worker) Wait() scan.Results {
   289  	w.mu.Lock()
   290  	defer w.mu.Unlock()
   291  	return w.results
   292  }
   293  
   294  func (w *Worker) Error() error {
   295  	if w.panic == nil {
   296  		return nil
   297  	}
   298  	return fmt.Errorf("job failed: %s", w.panic)
   299  }