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 }