github.com/quay/claircore@v1.5.28/libindex/libindex.go (about)

     1  package libindex
     2  
     3  import (
     4  	"context"
     5  	"crypto/md5"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"sort"
    12  	"time"
    13  
    14  	"github.com/quay/zlog"
    15  	"golang.org/x/sync/errgroup"
    16  
    17  	"github.com/quay/claircore"
    18  	"github.com/quay/claircore/alpine"
    19  	"github.com/quay/claircore/dpkg"
    20  	"github.com/quay/claircore/gobin"
    21  	"github.com/quay/claircore/indexer"
    22  	"github.com/quay/claircore/indexer/controller"
    23  	"github.com/quay/claircore/java"
    24  	"github.com/quay/claircore/pkg/omnimatcher"
    25  	"github.com/quay/claircore/python"
    26  	"github.com/quay/claircore/rhel"
    27  	"github.com/quay/claircore/rhel/rhcc"
    28  	"github.com/quay/claircore/rpm"
    29  	"github.com/quay/claircore/ruby"
    30  	"github.com/quay/claircore/whiteout"
    31  )
    32  
    33  const versionMagic = "libindex number: 2\n"
    34  
    35  // LockSource abstracts over how locks are implemented.
    36  //
    37  // An online system needs distributed locks, offline use cases can use
    38  // process-local locks.
    39  type LockSource interface {
    40  	TryLock(context.Context, string) (context.Context, context.CancelFunc)
    41  	Lock(context.Context, string) (context.Context, context.CancelFunc)
    42  	Close(context.Context) error
    43  }
    44  
    45  // Libindex implements the method set for scanning and indexing a Manifest.
    46  type Libindex struct {
    47  	// holds dependencies for creating a libindex instance
    48  	*Options
    49  	// a store implementation which will be shared between scanner instances
    50  	store indexer.Store
    51  	// a shareable http client
    52  	client *http.Client
    53  	// Locker provides system-wide locks.
    54  	locker LockSource
    55  	// an opaque and unique string representing the configured
    56  	// state of the indexer. see setState for more information.
    57  	state string
    58  	// FetchArena is an arena to fetch layers into. It ensures layers are
    59  	// fetched once and not removed while in use.
    60  	fa indexer.FetchArena
    61  	// vscnrs is a convenience object for holding a list of versioned scanners
    62  	vscnrs indexer.VersionedScanners
    63  	// indexerOptions hold construction context for the layerScanner and the
    64  	// controller factory.
    65  	indexerOptions *indexer.Options
    66  }
    67  
    68  // New creates a new instance of libindex.
    69  //
    70  // The passed http.Client will be used for fetching layers and any HTTP requests
    71  // made by scanners.
    72  func New(ctx context.Context, opts *Options, cl *http.Client) (*Libindex, error) {
    73  	ctx = zlog.ContextWithValues(ctx, "component", "libindex/New")
    74  	// required
    75  	if opts.Locker == nil {
    76  		return nil, fmt.Errorf("field Locker cannot be nil")
    77  	}
    78  	if opts.Store == nil {
    79  		return nil, fmt.Errorf("field Store cannot be nil")
    80  	}
    81  	if opts.FetchArena == nil {
    82  		return nil, fmt.Errorf("field FetchArena cannot be nil")
    83  	}
    84  
    85  	// optional
    86  	if (opts.ScanLockRetry == 0) || (opts.ScanLockRetry < time.Second) {
    87  		opts.ScanLockRetry = DefaultScanLockRetry
    88  	}
    89  	if opts.LayerScanConcurrency == 0 {
    90  		opts.LayerScanConcurrency = DefaultLayerScanConcurrency
    91  	}
    92  	if opts.ControllerFactory == nil {
    93  		opts.ControllerFactory = controller.New
    94  	}
    95  	if opts.Ecosystems == nil {
    96  		opts.Ecosystems = []*indexer.Ecosystem{
    97  			dpkg.NewEcosystem(ctx),
    98  			alpine.NewEcosystem(ctx),
    99  			rhel.NewEcosystem(ctx),
   100  			rpm.NewEcosystem(ctx),
   101  			python.NewEcosystem(ctx),
   102  			java.NewEcosystem(ctx),
   103  			rhcc.NewEcosystem(ctx),
   104  			gobin.NewEcosystem(ctx),
   105  			ruby.NewEcosystem(ctx),
   106  		}
   107  	}
   108  	// Add whiteout objects
   109  	// Always add the whiteout ecosystem
   110  	opts.Ecosystems = append(opts.Ecosystems, whiteout.NewEcosystem(ctx))
   111  	opts.Resolvers = []indexer.Resolver{
   112  		&whiteout.Resolver{},
   113  	}
   114  
   115  	if cl == nil {
   116  		return nil, errors.New("invalid *http.Client")
   117  	}
   118  
   119  	l := &Libindex{
   120  		Options: opts,
   121  		client:  cl,
   122  		store:   opts.Store,
   123  		locker:  opts.Locker,
   124  		fa:      opts.FetchArena,
   125  	}
   126  
   127  	// register any new scanners.
   128  	pscnrs, dscnrs, rscnrs, fscnrs, err := indexer.EcosystemsToScanners(ctx, opts.Ecosystems)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	vscnrs := indexer.MergeVS(pscnrs, dscnrs, rscnrs, fscnrs)
   133  
   134  	err = l.store.RegisterScanners(ctx, vscnrs)
   135  	if err != nil {
   136  		return nil, fmt.Errorf("failed to register configured scanners: %v", err)
   137  	}
   138  
   139  	// set the indexer's state
   140  	err = l.setState(ctx, vscnrs)
   141  	if err != nil {
   142  		return nil, fmt.Errorf("failed to set the indexer state: %v", err)
   143  	}
   144  
   145  	zlog.Info(ctx).Msg("registered configured scanners")
   146  	l.vscnrs = vscnrs
   147  
   148  	// create indexer.Options
   149  	l.indexerOptions = &indexer.Options{
   150  		Store:         l.store,
   151  		FetchArena:    l.fa,
   152  		Ecosystems:    opts.Ecosystems,
   153  		Vscnrs:        l.vscnrs,
   154  		Client:        l.client,
   155  		ScannerConfig: opts.ScannerConfig,
   156  		Resolvers:     opts.Resolvers,
   157  	}
   158  	l.indexerOptions.LayerScanner, err = indexer.NewLayerScanner(ctx, opts.LayerScanConcurrency, l.indexerOptions)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	return l, nil
   164  }
   165  
   166  // Close releases held resources.
   167  func (l *Libindex) Close(ctx context.Context) error {
   168  	l.locker.Close(ctx)
   169  	l.store.Close(ctx)
   170  	l.fa.Close(ctx)
   171  	return nil
   172  }
   173  
   174  // Index performs a scan and index of each layer within the provided Manifest.
   175  //
   176  // If the index operation cannot start an error will be returned.
   177  // If an error occurs during scan the error will be propagated inside the IndexReport.
   178  func (l *Libindex) Index(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error) {
   179  	ctx = zlog.ContextWithValues(ctx,
   180  		"component", "libindex/Libindex.Index",
   181  		"manifest", manifest.Hash.String())
   182  	zlog.Info(ctx).Msg("index request start")
   183  	defer zlog.Info(ctx).Msg("index request done")
   184  
   185  	zlog.Debug(ctx).Msg("locking attempt")
   186  	lc, done := l.locker.Lock(ctx, manifest.Hash.String())
   187  	defer done()
   188  	// The process may have waited on the lock, so check that the context is
   189  	// still active.
   190  	if err := lc.Err(); !errors.Is(err, nil) {
   191  		return nil, err
   192  	}
   193  	zlog.Debug(ctx).Msg("locking OK")
   194  	c := l.ControllerFactory(l.indexerOptions)
   195  	return c.Index(lc, manifest)
   196  }
   197  
   198  // State returns an opaque identifier identifying how the struct is currently
   199  // configured.
   200  //
   201  // If the identifier has changed, clients should arrange for layers to be
   202  // re-indexed.
   203  func (l *Libindex) State(ctx context.Context) (string, error) {
   204  	return l.state, nil
   205  }
   206  
   207  // setState creates a unique and opaque identifier representing the indexer's
   208  // configuration state.
   209  //
   210  // Indexers running different scanner versions will produce different state strings.
   211  // Thus this state value can be used as a cue for clients to re-index their manifests
   212  // and obtain a new IndexReport.
   213  func (l *Libindex) setState(ctx context.Context, vscnrs indexer.VersionedScanners) error {
   214  	h := md5.New()
   215  	var ns []string
   216  	m := make(map[string][]byte)
   217  	for _, s := range vscnrs {
   218  		n := s.Name()
   219  		m[n] = []byte(n + s.Version() + s.Kind() + "\n")
   220  		// TODO(hank) Should this take into account configuration? E.g. If a
   221  		// scanner implements the configurable interface, should we expect that
   222  		// we can serialize the scanner's concrete type?
   223  		ns = append(ns, n)
   224  	}
   225  	if _, err := io.WriteString(h, versionMagic); err != nil {
   226  		return err
   227  	}
   228  	sort.Strings(ns)
   229  	for _, n := range ns {
   230  		if _, err := h.Write(m[n]); err != nil {
   231  			return err
   232  		}
   233  	}
   234  	l.state = hex.EncodeToString(h.Sum(nil))
   235  	return nil
   236  }
   237  
   238  // IndexReport retrieves an IndexReport for a particular manifest hash, if it exists.
   239  func (l *Libindex) IndexReport(ctx context.Context, hash claircore.Digest) (*claircore.IndexReport, bool, error) {
   240  	return l.store.IndexReport(ctx, hash)
   241  }
   242  
   243  // AffectedManifests retrieves a list of affected manifests when provided a list of vulnerabilities.
   244  func (l *Libindex) AffectedManifests(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {
   245  	ctx = zlog.ContextWithValues(ctx, "component", "libindex/Libindex.AffectedManifests")
   246  	om := omnimatcher.New(nil)
   247  
   248  	affected := claircore.NewAffectedManifests()
   249  	g, ctx := errgroup.WithContext(ctx)
   250  	// TODO(hank) Look in the git history and see if there's any hint where this
   251  	// number comes from. I suspect it's a WAG constant.
   252  	g.SetLimit(20)
   253  	do := func(i int) func() error {
   254  		return func() error {
   255  			select {
   256  			case <-ctx.Done():
   257  				return context.Cause(ctx)
   258  			default:
   259  			}
   260  			hashes, err := l.store.AffectedManifests(ctx, vulns[i], om.Vulnerable)
   261  			if err != nil {
   262  				return err
   263  			}
   264  			affected.Add(&vulns[i], hashes...)
   265  			return nil
   266  		}
   267  	}
   268  V:
   269  	for i := 0; i < len(vulns); i++ {
   270  		g.Go(do(i))
   271  		select {
   272  		case <-ctx.Done():
   273  			break V
   274  		default:
   275  		}
   276  	}
   277  	if err := g.Wait(); err != nil {
   278  		return nil, fmt.Errorf("received error retrieving affected manifests: %w", err)
   279  	}
   280  	affected.Sort()
   281  	return &affected, nil
   282  }
   283  
   284  // DeleteManifests removes manifests specified by the provided digests.
   285  //
   286  // Providing an unknown digest is not an error.
   287  func (l *Libindex) DeleteManifests(ctx context.Context, d ...claircore.Digest) ([]claircore.Digest, error) {
   288  	ctx = zlog.ContextWithValues(ctx, "component", "libindex/Libindex.DeleteManifests")
   289  	return l.store.DeleteManifests(ctx, d...)
   290  }