github.com/quay/claircore@v1.5.28/internal/matcher/match.go (about)

     1  package matcher
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"runtime"
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	"github.com/quay/zlog"
    13  	"golang.org/x/sync/errgroup"
    14  
    15  	"github.com/quay/claircore"
    16  	"github.com/quay/claircore/datastore"
    17  	"github.com/quay/claircore/libvuln/driver"
    18  )
    19  
    20  // BUG(hank) Match and EnrichedMatch have different semantics for errors
    21  // returned by their inferior processes: Match runs all of them to completion or
    22  // the incoming Context is canceled. EnrichedMatch eagerly cancels all Matchers
    23  // upon the first error, ala "errgroup" semantics but purposefully does not
    24  // return errors on any Enricher errors. If I recall correctly, this was on
    25  // purpose. The Match function is probably obsolete now; we should work on
    26  // removing it from the relevant APIs.
    27  
    28  // Match receives an IndexReport and creates a VulnerabilityReport containing matched vulnerabilities
    29  func Match(ctx context.Context, ir *claircore.IndexReport, matchers []driver.Matcher, store datastore.Vulnerability) (*claircore.VulnerabilityReport, error) {
    30  	// the vulnerability report we are creating
    31  	vr := &claircore.VulnerabilityReport{
    32  		Hash:                   ir.Hash,
    33  		Packages:               ir.Packages,
    34  		Environments:           ir.Environments,
    35  		Distributions:          ir.Distributions,
    36  		Repositories:           ir.Repositories,
    37  		Vulnerabilities:        map[string]*claircore.Vulnerability{},
    38  		PackageVulnerabilities: map[string][]string{},
    39  	}
    40  	lim := runtime.GOMAXPROCS(0)
    41  
    42  	// extract IndexRecords from the IndexReport
    43  	records := ir.IndexRecords()
    44  	// a channel where concurrent controllers will deliver vulnerabilities affecting a package.
    45  	// maps a package id to a list of vulnerabilities.
    46  	ctrlC := make(chan map[string][]*claircore.Vulnerability, lim)
    47  	var errMu sync.Mutex
    48  	errs := make([]error, 0, lim)
    49  	// fan out all controllers, write their output to ctrlC, close ctrlC once all writers finish
    50  	go func() {
    51  		defer close(ctrlC)
    52  		var wg sync.WaitGroup
    53  		wg.Add(len(matchers))
    54  		for i := range matchers {
    55  			m := matchers[i]
    56  			go func() {
    57  				defer wg.Done()
    58  				mc := NewController(m, store)
    59  				vulns, err := mc.Match(ctx, records)
    60  				if err != nil {
    61  					errMu.Lock()
    62  					errs = append(errs, err)
    63  					errMu.Unlock()
    64  					return
    65  				}
    66  				// in event of slow reader go routines will block
    67  				ctrlC <- vulns
    68  			}()
    69  		}
    70  		wg.Wait()
    71  	}()
    72  	// loop ranges until ctrlC is closed and fully drained, ctrlC is guaranteed to close
    73  	for vulnsByPackage := range ctrlC {
    74  		for pkgID, vulns := range vulnsByPackage {
    75  			for _, vuln := range vulns {
    76  				vr.Vulnerabilities[vuln.ID] = vuln
    77  				vr.PackageVulnerabilities[pkgID] = append(vr.PackageVulnerabilities[pkgID], vuln.ID)
    78  			}
    79  		}
    80  	}
    81  	return vr, errors.Join(errs...)
    82  }
    83  
    84  // Store is the interface that can retrieve Enrichments and Vulnerabilities.
    85  type Store interface {
    86  	datastore.Vulnerability
    87  	datastore.Enrichment
    88  }
    89  
    90  // EnrichedMatch receives an IndexReport and creates a VulnerabilityReport
    91  // containing matched vulnerabilities and any relevant enrichments.
    92  func EnrichedMatch(ctx context.Context, ir *claircore.IndexReport, ms []driver.Matcher, es []driver.Enricher, s Store) (*claircore.VulnerabilityReport, error) {
    93  	// the vulnerability report we are creating
    94  	vr := &claircore.VulnerabilityReport{
    95  		Hash:                   ir.Hash,
    96  		Packages:               ir.Packages,
    97  		Environments:           ir.Environments,
    98  		Distributions:          ir.Distributions,
    99  		Repositories:           ir.Repositories,
   100  		Vulnerabilities:        map[string]*claircore.Vulnerability{},
   101  		PackageVulnerabilities: map[string][]string{},
   102  		// The Enrichments member isn't constructed here because it's
   103  		// constructed separately and then added.
   104  	}
   105  	// extract IndexRecords from the IndexReport
   106  	records := ir.IndexRecords()
   107  	lim := runtime.GOMAXPROCS(0)
   108  
   109  	// Set up a pool to run matchers
   110  	mCh := make(chan driver.Matcher)
   111  	vCh := make(chan map[string][]*claircore.Vulnerability, lim)
   112  	mg, mctx := errgroup.WithContext(ctx) // match group, match context
   113  	for i := 0; i < lim; i++ {
   114  		mg.Go(func() error { // Worker
   115  			var m driver.Matcher
   116  			for m = range mCh {
   117  				select {
   118  				case <-mctx.Done():
   119  					return mctx.Err()
   120  				default:
   121  				}
   122  				vs, err := NewController(m, s).Match(mctx, records)
   123  				if err != nil {
   124  					return fmt.Errorf("matcher error: %w", err)
   125  				}
   126  				vCh <- vs
   127  			}
   128  			return nil
   129  		})
   130  	}
   131  	// Set up a pool to watch the matchers and attach results to the report.
   132  	var vg errgroup.Group // Used for easy grouping. Does cancellation on a previous Context.
   133  	vg.Go(func() error {  // Pipeline watcher
   134  	Send:
   135  		for _, m := range ms {
   136  			select {
   137  			case <-mctx.Done():
   138  				break Send
   139  			case mCh <- m:
   140  			}
   141  		}
   142  		close(mCh)
   143  		defer close(vCh)
   144  		if err := mg.Wait(); err != nil {
   145  			return err
   146  		}
   147  		return nil
   148  	})
   149  	vg.Go(func() error { // Collector
   150  		for pkgVuln := range vCh {
   151  			for pkg, vs := range pkgVuln {
   152  				for _, v := range vs {
   153  					vr.Vulnerabilities[v.ID] = v
   154  					vr.PackageVulnerabilities[pkg] = append(vr.PackageVulnerabilities[pkg], v.ID)
   155  				}
   156  			}
   157  		}
   158  		return nil
   159  	})
   160  	if err := vg.Wait(); err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	// Set up a pool to run the enrichers and attach results to the report.
   165  	eCh := make(chan driver.Enricher)
   166  	type entry struct {
   167  		kind string
   168  		msg  []json.RawMessage
   169  	}
   170  	rCh := make(chan *entry, lim)
   171  	eg, ectx := errgroup.WithContext(ctx)
   172  	eg.Go(func() error { // Sender
   173  	Send:
   174  		for _, e := range es {
   175  			select {
   176  			case eCh <- e:
   177  			case <-ectx.Done():
   178  				break Send
   179  			}
   180  		}
   181  		close(eCh)
   182  		return nil
   183  	})
   184  	eg.Go(func() error { // Collector
   185  		em := make(map[string][]json.RawMessage)
   186  		for e := range rCh {
   187  			em[e.kind] = append(em[e.kind], e.msg...)
   188  		}
   189  		vr.Enrichments = em
   190  		return nil
   191  	})
   192  	// Use an atomic to track closing the results channel.
   193  	ct := uint32(lim)
   194  	for i := 0; i < lim; i++ {
   195  		eg.Go(func() error { // Worker
   196  			defer func() {
   197  				if atomic.AddUint32(&ct, ^uint32(0)) == 0 {
   198  					close(rCh)
   199  				}
   200  			}()
   201  			var e driver.Enricher
   202  			for e = range eCh {
   203  				kind, msg, err := e.Enrich(ectx, getter(s, e.Name()), vr)
   204  				if err != nil {
   205  					zlog.Error(ctx).
   206  						Err(err).
   207  						Msg("enrichment error")
   208  					continue
   209  				}
   210  				if len(msg) == 0 {
   211  					zlog.Debug(ctx).
   212  						Str("name", e.Name()).
   213  						Msg("enricher reported nothing, skipping")
   214  					continue
   215  				}
   216  				res := entry{
   217  					msg:  msg,
   218  					kind: kind,
   219  				}
   220  				select {
   221  				case rCh <- &res:
   222  				case <-ectx.Done():
   223  					return ectx.Err()
   224  				}
   225  			}
   226  			return nil
   227  		})
   228  	}
   229  	if err := eg.Wait(); err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	return vr, nil
   234  }
   235  
   236  // Getter returns a type implementing driver.EnrichmentGetter.
   237  func getter(s datastore.Enrichment, name string) *enrichmentGetter {
   238  	return &enrichmentGetter{s: s, name: name}
   239  }
   240  
   241  type enrichmentGetter struct {
   242  	s    datastore.Enrichment
   243  	name string
   244  }
   245  
   246  var _ driver.EnrichmentGetter = (*enrichmentGetter)(nil)
   247  
   248  func (e *enrichmentGetter) GetEnrichment(ctx context.Context, tags []string) ([]driver.EnrichmentRecord, error) {
   249  	return e.s.GetEnrichment(ctx, e.name, tags)
   250  }