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