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

     1  // Package osv is an updater for OSV-formatted advisories.
     2  package osv
     3  
     4  import (
     5  	"archive/zip"
     6  	"bufio"
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"net/http"
    15  	"net/http/httputil"
    16  	"net/url"
    17  	"path"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/Masterminds/semver"
    22  	"github.com/quay/zlog"
    23  
    24  	"github.com/quay/claircore"
    25  	"github.com/quay/claircore/libvuln/driver"
    26  	"github.com/quay/claircore/pkg/tmp"
    27  )
    28  
    29  var (
    30  	_ driver.Updater           = (*updater)(nil)
    31  	_ driver.Configurable      = (*updater)(nil)
    32  	_ driver.UpdaterSetFactory = (*Factory)(nil)
    33  	_ driver.Configurable      = (*Factory)(nil)
    34  )
    35  
    36  // DefaultURL is the S3 bucket provided by the OSV project.
    37  //
    38  //doc:url updater
    39  const DefaultURL = `https://osv-vulnerabilities.storage.googleapis.com/`
    40  
    41  // Factory is the UpdaterSetFactory exposed by this package.
    42  //
    43  // [Configure] must be called before [UpdaterSet]. See the [FactoryConfig] type.
    44  type Factory struct {
    45  	root *url.URL
    46  	c    *http.Client
    47  	// Allow is a bool-and-map-of-bool.
    48  	//
    49  	// If populated, only extant entries are allowed. If not populated,
    50  	// everything is allowed. It uses a bool to make a conditional simpler later.
    51  	allow map[string]bool
    52  	etag  string
    53  }
    54  
    55  // FactoryConfig is the configuration that this updater accepts.
    56  //
    57  // By convention, it's at a key called "osv".
    58  type FactoryConfig struct {
    59  	// The URL serving data in the same layout as the OSV project's public S3
    60  	// bucket.
    61  	URL string `json:"url" yaml:"url"`
    62  	// Allowlist is a list of ecosystems to allow. When this is unset, all are
    63  	// allowed.
    64  	//
    65  	// Extant ecosystems are discovered at runtime, see the OSV Schema
    66  	// (https://ossf.github.io/osv-schema/) or the "ecosystems.txt" file in the
    67  	// OSV data for the current list.
    68  	Allowlist []string `json:"allowlist" yaml:"allowlist"`
    69  }
    70  
    71  // Configure implements driver.Configurable.
    72  func (u *Factory) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error {
    73  	ctx = zlog.ContextWithValues(ctx, "component", "updater/osv/factory.Configure")
    74  	var err error
    75  
    76  	u.c = c
    77  	u.root, err = url.Parse(DefaultURL)
    78  	if err != nil {
    79  		panic(fmt.Sprintf("programmer error: %v", err))
    80  	}
    81  
    82  	var cfg FactoryConfig
    83  	if err := f(&cfg); err != nil {
    84  		return err
    85  	}
    86  	if cfg.URL != "" {
    87  		u.root, err = url.Parse(cfg.URL)
    88  		if err != nil {
    89  			return err
    90  		}
    91  	}
    92  	if l := len(cfg.Allowlist); l != 0 {
    93  		u.allow = make(map[string]bool, l)
    94  		for _, a := range cfg.Allowlist {
    95  			u.allow[strings.ToLower(a)] = true
    96  		}
    97  	}
    98  
    99  	zlog.Debug(ctx).Msg("loaded incoming config")
   100  	return nil
   101  }
   102  
   103  func (f *Factory) UpdaterSet(ctx context.Context) (s driver.UpdaterSet, err error) {
   104  	ctx = zlog.ContextWithValues(ctx, "component", "updater/osv/factory.UpdaterSet")
   105  	s = driver.NewUpdaterSet()
   106  	if f.root == nil || f.c == nil {
   107  		return s, errors.New("osv: factory not configured") // Purposely return an unhandleable error.
   108  	}
   109  	var stats struct {
   110  		ecosystems []string
   111  		skipped    []string
   112  	}
   113  	defer func() {
   114  		// This is an info print so operators can compare their allow list,
   115  		// if need be.
   116  		zlog.Info(ctx).
   117  			Strs("ecosystems", stats.ecosystems).
   118  			Strs("skipped", stats.skipped).
   119  			Msg("ecosystems stats")
   120  	}()
   121  
   122  	uri := *f.root
   123  	uri.Path = "/ecosystems.txt"
   124  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
   125  	if err != nil {
   126  		return s, fmt.Errorf("osv: martian request: %w", err)
   127  	}
   128  	req.Header.Set(`accept`, `text/plain`)
   129  	if f.etag != "" {
   130  		req.Header.Set(`if-none-match`, f.etag)
   131  	}
   132  	res, err := f.c.Do(req)
   133  	if err != nil {
   134  		return s, err
   135  	}
   136  	seen := make(map[string]struct{})
   137  	// This is straight-line through the switch to make sure the Body is closed
   138  	// there.
   139  	switch res.StatusCode {
   140  	case http.StatusOK:
   141  		scr := bufio.NewScanner(res.Body)
   142  		for scr.Scan() {
   143  			k := scr.Text()
   144  			e := strings.ToLower(k)
   145  			// Currently, there's some versioned ecosystems. This branch removes the versioning.
   146  			if idx := strings.Index(e, ":"); idx != -1 {
   147  				e = e[:idx]
   148  			}
   149  			// Check for duplicates, removing the version will create some.
   150  			if _, ok := seen[e]; ok {
   151  				continue
   152  			}
   153  			seen[e] = struct{}{}
   154  			if _, ok := ignore[e]; ok {
   155  				zlog.Debug(ctx).
   156  					Str("ecosystem", e).
   157  					Msg("ignoring ecosystem")
   158  				continue
   159  			}
   160  			stats.ecosystems = append(stats.ecosystems, e)
   161  			if f.allow != nil && !f.allow[e] {
   162  				stats.skipped = append(stats.skipped, e)
   163  				continue
   164  			}
   165  			name := "osv/" + e
   166  			uri := (*f.root).JoinPath(k, "all.zip")
   167  			up := &updater{name: name, ecosystem: e, c: f.c, uri: uri}
   168  			if err = s.Add(up); err != nil {
   169  				zlog.Error(ctx).
   170  					Str("ecosystem", e).
   171  					Err(err).
   172  					Msg("Failed to add updater to updaterset")
   173  				continue
   174  			}
   175  		}
   176  		err = scr.Err()
   177  		f.etag = res.Header.Get("etag")
   178  	case http.StatusNotModified:
   179  		return s, nil
   180  	default:
   181  		var buf bytes.Buffer
   182  		buf.ReadFrom(io.LimitReader(res.Body, 256))
   183  		b, _ := httputil.DumpRequest(req, false)
   184  		err = fmt.Errorf("osv: unexpected response from %q: %v (request: %q) (body: %q)", res.Request.URL, res.Status, b, buf)
   185  	}
   186  	if err := res.Body.Close(); err != nil {
   187  		zlog.Info(ctx).
   188  			Err(err).
   189  			Msg("error closing ecosystems.txt response body")
   190  	}
   191  	if err != nil {
   192  		return s, err
   193  	}
   194  
   195  	return s, nil
   196  }
   197  
   198  // Ignore is a set of incoming ecosystems that we can throw out immediately.
   199  var ignore = map[string]struct{}{
   200  	"alpine":         {}, // Have a dedicated alpine updater.
   201  	"android":        {}, // AFAIK, there's no Android container runtime.
   202  	"debian":         {}, // Have a dedicated debian updater.
   203  	"github actions": {}, // Shouldn't be in containers?
   204  	"linux":          {}, // Containers have no say in the kernel.
   205  	"oss-fuzz":       {}, // Seems to only record git revisions.
   206  }
   207  
   208  type updater struct {
   209  	name      string
   210  	ecosystem string
   211  	c         *http.Client
   212  	uri       *url.URL
   213  }
   214  
   215  func (u *updater) Name() string { return u.name }
   216  
   217  type UpdaterConfig struct {
   218  	// The URL serving data dumps behind an S3 API.
   219  	//
   220  	// Authentication is unconfigurable, the ListObjectsV2 API must be publicly
   221  	// accessible.
   222  	URL string `json:"url" yaml:"url"`
   223  }
   224  
   225  // Configure implements driver.Configurable.
   226  func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error {
   227  	ctx = zlog.ContextWithValues(ctx, "component", "updater/osv/updater.Configure")
   228  	var err error
   229  
   230  	u.c = c
   231  	var cfg UpdaterConfig
   232  	if err := f(&cfg); err != nil {
   233  		return err
   234  	}
   235  	if cfg.URL != "" {
   236  		u.uri, err = url.Parse(cfg.URL)
   237  		if err != nil {
   238  			return err
   239  		}
   240  	}
   241  	zlog.Debug(ctx).Msg("loaded incoming config")
   242  	return nil
   243  }
   244  
   245  // Fetcher implements driver.Updater.
   246  func (u *updater) Fetch(ctx context.Context, fp driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) {
   247  	ctx = zlog.ContextWithValues(ctx, "component", "updater/osv/updater.Fetch")
   248  
   249  	out, err := tmp.NewFile("", "osv.fetch.*")
   250  	if err != nil {
   251  		return nil, fp, err
   252  	}
   253  	defer func() {
   254  		if _, err := out.Seek(0, io.SeekStart); err != nil {
   255  			zlog.Warn(ctx).
   256  				Err(err).
   257  				Msg("unable to seek file back to start")
   258  		}
   259  	}()
   260  	zlog.Debug(ctx).
   261  		Str("filename", out.Name()).
   262  		Msg("opened temporary file for output")
   263  	w := zip.NewWriter(out)
   264  	defer w.Close()
   265  	var ct int
   266  	// Copy the root URI, then append the ecosystem key and file name.
   267  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.uri.String(), nil)
   268  	if err != nil {
   269  		return nil, fp, fmt.Errorf("osv: martian request: %w", err)
   270  	}
   271  	req.Header.Set(`accept`, `application/zip`)
   272  	if fp != "" {
   273  		zlog.Debug(ctx).
   274  			Str("hint", string(fp)).
   275  			Msg("using hint")
   276  		req.Header.Set("if-none-match", string(fp))
   277  	}
   278  
   279  	res, err := u.c.Do(req)
   280  	if err != nil {
   281  		return nil, fp, err
   282  	}
   283  	// This switch is straight-line code to ensure that the response body is always closed.
   284  	switch res.StatusCode {
   285  	case http.StatusOK:
   286  		n := u.ecosystem + ".zip"
   287  		var dst io.Writer
   288  		dst, err = w.CreateHeader(&zip.FileHeader{Name: n, Method: zip.Store})
   289  		if err == nil {
   290  			_, err = io.Copy(dst, res.Body)
   291  		}
   292  		if err != nil {
   293  			break
   294  		}
   295  		zlog.Debug(ctx).
   296  			Str("name", n).
   297  			Msg("wrote zip")
   298  		ct++
   299  	case http.StatusNotModified:
   300  	default:
   301  		err = fmt.Errorf("osv: unexpected response from %q: %v", res.Request.URL.String(), res.Status)
   302  	}
   303  	if err := res.Body.Close(); err != nil {
   304  		zlog.Info(ctx).
   305  			Err(err).
   306  			Msg("error closing advisory zip response body")
   307  	}
   308  	if err != nil {
   309  		return nil, fp, err
   310  	}
   311  	newEtag := res.Header.Get(`etag`)
   312  	zlog.Info(ctx).
   313  		Int("count", ct).
   314  		Msg("found updates")
   315  	if ct == 0 {
   316  		return nil, fp, driver.Unchanged
   317  	}
   318  
   319  	return out, driver.Fingerprint(newEtag), nil
   320  }
   321  
   322  // Fetcher implements driver.Updater.
   323  func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) {
   324  	ctx = zlog.ContextWithValues(ctx, "component", "updater/osv/updater.Parse")
   325  	ra, ok := r.(io.ReaderAt)
   326  	if !ok {
   327  		zlog.Info(ctx).
   328  			Msg("spooling to disk")
   329  		tf, err := tmp.NewFile("", `osv.parse.spool.*`)
   330  		if err != nil {
   331  			return nil, err
   332  		}
   333  		defer tf.Close()
   334  		if _, err := io.Copy(tf, r); err != nil {
   335  			return nil, err
   336  		}
   337  		ra = tf
   338  	}
   339  
   340  	var sz int64 = -1
   341  	switch v := ra.(type) {
   342  	case sizer:
   343  		sz = v.Size()
   344  	case fileStat:
   345  		fi, err := v.Stat()
   346  		if err != nil {
   347  			return nil, err
   348  		}
   349  		sz = fi.Size()
   350  	case io.Seeker:
   351  		cur, err := v.Seek(0, io.SeekCurrent)
   352  		if err != nil {
   353  			return nil, err
   354  		}
   355  		sz, err = v.Seek(0, io.SeekEnd)
   356  		if err != nil {
   357  			return nil, err
   358  		}
   359  		if _, err := v.Seek(cur, io.SeekStart); err != nil {
   360  			return nil, err
   361  		}
   362  	default:
   363  		return nil, errors.New("osv: unable to determine size of zip file")
   364  	}
   365  
   366  	z, err := zip.NewReader(ra, sz)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	tf, err := tmp.NewFile("", `osv.parse.*`)
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  	defer tf.Close()
   376  	now := time.Now()
   377  	ecs := newECS(u.Name())
   378  	for _, zf := range z.File {
   379  		ctx := zlog.ContextWithValues(ctx, "dumpfile", zf.Name)
   380  		zlog.Debug(ctx).
   381  			Msg("found file")
   382  		r, err := zf.Open()
   383  		if err != nil {
   384  			return nil, err
   385  		}
   386  		if _, err := tf.Seek(0, io.SeekStart); err != nil {
   387  			return nil, err
   388  		}
   389  		sz, err := io.Copy(tf, r)
   390  		if err != nil {
   391  			return nil, err
   392  		}
   393  		z, err := zip.NewReader(tf, sz)
   394  		if err != nil {
   395  			return nil, err
   396  		}
   397  		name := strings.TrimSuffix(path.Base(zf.Name), ".zip")
   398  
   399  		var skipped stats
   400  		var ct int
   401  		for _, zf := range z.File {
   402  			ctx := zlog.ContextWithValues(ctx, "advisory", strings.TrimSuffix(path.Base(zf.Name), ".json"))
   403  			ct++
   404  			var a advisory
   405  			rc, err := zf.Open()
   406  			if err != nil {
   407  				return nil, err
   408  			}
   409  			err = json.NewDecoder(rc).Decode(&a)
   410  			rc.Close()
   411  			if err != nil {
   412  				return nil, err
   413  			}
   414  
   415  			switch {
   416  			case !a.Withdrawn.IsZero() && now.After(a.Withdrawn):
   417  				skipped.Withdrawn = append(skipped.Withdrawn, a.ID)
   418  				continue
   419  			case len(a.Affected) == 0:
   420  				skipped.Unaffected = append(skipped.Unaffected, a.ID)
   421  				continue
   422  			default:
   423  			}
   424  
   425  			if err := ecs.Insert(ctx, &skipped, name, &a); err != nil {
   426  				return nil, err
   427  			}
   428  		}
   429  		zlog.Debug(ctx).
   430  			Int("count", ct).
   431  			Msg("processed advisories")
   432  		zlog.Debug(ctx).
   433  			Strs("withdrawn", skipped.Withdrawn).
   434  			Strs("unaffected", skipped.Unaffected).
   435  			Strs("ignored", skipped.Ignored).
   436  			Msg("skipped advisories")
   437  	}
   438  	zlog.Info(ctx).
   439  		Int("count", ecs.Len()).
   440  		Msg("found vulnerabilities")
   441  
   442  	return ecs.Finalize(), nil
   443  }
   444  
   445  type (
   446  	fileStat interface{ Stat() (fs.FileInfo, error) }
   447  	sizer    interface{ Size() int64 }
   448  
   449  	stats struct {
   450  		Withdrawn  []string
   451  		Unaffected []string
   452  		Ignored    []string
   453  	}
   454  )
   455  
   456  // Ecs is an entity-component system for vulnerabilities.
   457  //
   458  // This is organized this way to help consolidate allocations.
   459  type ecs struct {
   460  	Updater string
   461  
   462  	pkgindex  map[string]int
   463  	repoindex map[string]int
   464  
   465  	Vulnerability []claircore.Vulnerability
   466  	Package       []claircore.Package
   467  	Distribution  []claircore.Distribution
   468  	Repository    []claircore.Repository
   469  }
   470  
   471  const (
   472  	ecosystemGo       = `Go`
   473  	ecosystemMaven    = `Maven`
   474  	ecosystemNPM      = `npm`
   475  	ecosystemPyPI     = `PyPI`
   476  	ecosystemRubyGems = `RubyGems`
   477  )
   478  
   479  func newECS(u string) ecs {
   480  	return ecs{
   481  		Updater:   u,
   482  		pkgindex:  make(map[string]int),
   483  		repoindex: make(map[string]int),
   484  	}
   485  }
   486  
   487  func (e *ecs) Insert(ctx context.Context, skipped *stats, name string, a *advisory) (err error) {
   488  	if a.GitOnly() {
   489  		return nil
   490  	}
   491  	var b strings.Builder
   492  	var proto claircore.Vulnerability
   493  	proto.Name = a.ID
   494  	proto.Description = a.Summary
   495  	proto.Issued = a.Published
   496  	proto.Updater = e.Updater
   497  	proto.NormalizedSeverity = claircore.Unknown
   498  	for _, s := range a.Severity {
   499  		var err error
   500  		switch s.Type {
   501  		case `CVSS_V3`:
   502  			proto.Severity = s.Score
   503  			proto.NormalizedSeverity, err = fromCVSS3(ctx, s.Score)
   504  		case `CVSS_V2`:
   505  			proto.Severity = s.Score
   506  			proto.NormalizedSeverity, err = fromCVSS2(s.Score)
   507  		default:
   508  			// We didn't get a severity from the CVSS scores
   509  			continue
   510  		}
   511  		if err != nil {
   512  			zlog.Info(ctx).
   513  				Err(err).
   514  				Msg("odd cvss mangling result")
   515  		}
   516  	}
   517  
   518  	if proto.Severity == "" {
   519  		// Try and extract a severity from the database_specific object
   520  		var databaseJSON map[string]json.RawMessage
   521  		if err := json.Unmarshal([]byte(a.Database), &databaseJSON); err == nil {
   522  			var severityString string
   523  			if err := json.Unmarshal(databaseJSON["severity"], &severityString); err == nil {
   524  				proto.Severity = severityString
   525  				proto.NormalizedSeverity = severityFromDBString(severityString)
   526  			}
   527  		}
   528  	}
   529  
   530  	for i, ref := range a.References {
   531  		if i != 0 {
   532  			b.WriteByte(' ')
   533  		}
   534  		b.WriteString(ref.URL)
   535  	}
   536  	proto.Links = b.String()
   537  	for i := range a.Affected {
   538  		af := &a.Affected[i]
   539  		v := e.NewVulnerability()
   540  		(*v) = proto
   541  		for _, r := range af.Ranges {
   542  			switch r.Type {
   543  			case `SEMVER`:
   544  				v.Range = &claircore.Range{}
   545  			case `ECOSYSTEM`:
   546  				b.Reset()
   547  			case `GIT`:
   548  				// ignore, not going to fetch source.
   549  				continue
   550  			default:
   551  				zlog.Debug(ctx).
   552  					Str("type", r.Type).
   553  					Msg("odd range type")
   554  			}
   555  			// This does some heavy assumptions about valid inputs.
   556  			ranges := make(url.Values)
   557  			for _, ev := range r.Events {
   558  				var err error
   559  				switch r.Type {
   560  				case `SEMVER`:
   561  					var ver *semver.Version
   562  					switch {
   563  					case ev.Introduced == "0": // -Inf
   564  						v.Range.Lower.Kind = `semver`
   565  					case ev.Introduced != "":
   566  						ver, err = semver.NewVersion(ev.Introduced)
   567  						if err == nil {
   568  							v.Range.Lower = claircore.FromSemver(ver)
   569  						}
   570  					case ev.Fixed != "": // less than
   571  						ver, err = semver.NewVersion(ev.Fixed)
   572  						if err == nil {
   573  							v.Range.Upper = claircore.FromSemver(ver)
   574  							v.FixedInVersion = ver.Original()
   575  						}
   576  					case ev.LastAffected != "" && len(af.Versions) != 0: // less than equal to
   577  						// TODO(hank) Should be able to convert this to a "less than."
   578  						zlog.Info(ctx).
   579  							Str("which", "last_affected").
   580  							Str("event", ev.LastAffected).
   581  							Strs("versions", af.Versions).
   582  							Msg("unsure how to interpret event")
   583  					case ev.LastAffected != "": // less than equal to
   584  						// This is semver, so we should be able to calculate the
   585  						// "next" version:
   586  						ver, err = semver.NewVersion(ev.LastAffected)
   587  						if err == nil {
   588  							nv := ver.IncPatch()
   589  							v.Range.Upper = claircore.FromSemver(&nv)
   590  						}
   591  					case ev.Limit == "*": // +Inf
   592  						v.Range.Upper.Kind = `semver`
   593  						v.Range.Upper.V[0] = 65535
   594  					case ev.Limit != "": // Something arbitrary
   595  						zlog.Info(ctx).
   596  							Str("which", "limit").
   597  							Str("event", ev.Limit).
   598  							Msg("unsure how to interpret event")
   599  					}
   600  				case `ECOSYSTEM`:
   601  					switch af.Package.Ecosystem {
   602  					case ecosystemMaven, ecosystemPyPI, ecosystemRubyGems:
   603  						switch {
   604  						case ev.Introduced == "0":
   605  						case ev.Introduced != "":
   606  							ranges.Add("introduced", ev.Introduced)
   607  						case ev.Fixed != "":
   608  							ranges.Add("fixed", ev.Fixed)
   609  						case ev.LastAffected != "":
   610  							ranges.Add("lastAffected", ev.LastAffected)
   611  						}
   612  					case ecosystemGo, ecosystemNPM:
   613  						return fmt.Errorf(`unexpected "ECOSYSTEM" entry for %q ecosystem: %s`, af.Package.Ecosystem, a.ID)
   614  					default:
   615  						switch {
   616  						case ev.Introduced == "0": // -Inf
   617  						case ev.Introduced != "":
   618  						case ev.Fixed != "":
   619  							v.FixedInVersion = ev.Fixed
   620  						case ev.LastAffected != "":
   621  						case ev.Limit == "*": // +Inf
   622  						case ev.Limit != "":
   623  						}
   624  					}
   625  				}
   626  				if err != nil {
   627  					zlog.Warn(ctx).Err(err).Msg("event version error")
   628  				}
   629  			}
   630  			if len(ranges) > 0 {
   631  				switch af.Package.Ecosystem {
   632  				case ecosystemMaven, ecosystemPyPI, ecosystemRubyGems:
   633  					v.FixedInVersion = ranges.Encode()
   634  				}
   635  			}
   636  
   637  			if r := v.Range; r != nil {
   638  				// We have an implicit +Inf range if there's a single event,
   639  				// this should catch it?
   640  				if r.Upper.Kind == "" {
   641  					r.Upper.Kind = r.Lower.Kind
   642  					r.Upper.V[0] = 65535
   643  				}
   644  				if r.Lower.Compare(&r.Upper) == 1 {
   645  					e.RemoveVulnerability(v)
   646  					skipped.Ignored = append(skipped.Ignored, fmt.Sprintf("%s(%s,%s)", a.ID, r.Lower.String(), r.Upper.String()))
   647  					continue
   648  				}
   649  			}
   650  			var vs string
   651  			switch r.Type {
   652  			case `ECOSYSTEM`:
   653  				vs = b.String()
   654  			}
   655  			pkgName := af.Package.PURL
   656  			switch af.Package.Ecosystem {
   657  			case ecosystemGo, ecosystemMaven, ecosystemNPM, ecosystemPyPI, ecosystemRubyGems:
   658  				pkgName = af.Package.Name
   659  			}
   660  			pkg, novel := e.LookupPackage(pkgName, vs)
   661  			v.Package = pkg
   662  			switch af.Package.Ecosystem {
   663  			case ecosystemGo, ecosystemMaven, ecosystemNPM, ecosystemPyPI, ecosystemRubyGems:
   664  				v.Package.Kind = claircore.BINARY
   665  			}
   666  			if novel {
   667  				pkg.RepositoryHint = af.Package.Ecosystem
   668  			}
   669  			if repo := e.LookupRepository(name); repo != nil {
   670  				v.Repo = repo
   671  			}
   672  		}
   673  	}
   674  	return nil
   675  }
   676  
   677  // severityFromDBString takes a severity string defined in the
   678  // database_specific object and parses it into a claircore.Severity.
   679  // Known severities in the OSV data are (in varying cases):
   680  //   - CRITICAL
   681  //   - HIGH
   682  //   - LOW
   683  //   - MEDIUM
   684  //   - MODERATE
   685  //   - UNKNOWN
   686  func severityFromDBString(s string) (sev claircore.Severity) {
   687  	sev = claircore.Unknown
   688  	switch {
   689  	case strings.EqualFold(s, "unknown"):
   690  		sev = claircore.Unknown
   691  	case strings.EqualFold(s, "negligible"):
   692  		sev = claircore.Negligible
   693  	case strings.EqualFold(s, "low"):
   694  		sev = claircore.Low
   695  	case strings.EqualFold(s, "moderate"), strings.EqualFold(s, "medium"):
   696  		sev = claircore.Medium
   697  	case strings.EqualFold(s, "high"):
   698  		sev = claircore.High
   699  	case strings.EqualFold(s, "critical"):
   700  		sev = claircore.Critical
   701  	}
   702  	return sev
   703  }
   704  
   705  // All the methods follow the same pattern: just reslice the slice if
   706  // there's space, or use append to do an alloc+copy.
   707  
   708  func (e *ecs) NewVulnerability() *claircore.Vulnerability {
   709  	i := len(e.Vulnerability)
   710  	if cap(e.Vulnerability) > i {
   711  		e.Vulnerability = e.Vulnerability[:i+1]
   712  	} else {
   713  		e.Vulnerability = append(e.Vulnerability, claircore.Vulnerability{})
   714  	}
   715  	return &e.Vulnerability[i]
   716  }
   717  
   718  // RemoveVulnerability does what it says on the tin.
   719  //
   720  // Will cause copying if the vulnerability is not the most recent returned from
   721  // NewVulnerability.
   722  func (e *ecs) RemoveVulnerability(v *claircore.Vulnerability) {
   723  	// NOTE(hank) This could use a bitset to track occupancy, but I don't know
   724  	// if that's worth the hassle.
   725  
   726  	// This is a weird construction, but it's testing for pointer equality
   727  	// backwards through the slice. It's allow to go to a negative index to
   728  	// trigger a panic if the element isn't found. That shouldn't happen.
   729  	//
   730  	// If there's some reason that should be allowed to happen, a defer with a
   731  	// recover can be added here.
   732  	i := len(e.Vulnerability) - 1
   733  	for ; i >= -1 && v != &e.Vulnerability[i]; i-- {
   734  	}
   735  	if i != len(e.Vulnerability)-1 {
   736  		// If this isn't the last element, copy all elements after the
   737  		// discovered position to the memory starting at the discovered
   738  		// position.
   739  		copy(e.Vulnerability[i:], e.Vulnerability[i+1:])
   740  	}
   741  	// Reset the now unused element at the end. Not doing this can leak memory.
   742  	e.Vulnerability[len(e.Vulnerability)-1] = claircore.Vulnerability{}
   743  	e.Vulnerability = e.Vulnerability[:len(e.Vulnerability)-1]
   744  }
   745  
   746  func (e *ecs) LookupPackage(name string, ver string) (*claircore.Package, bool) {
   747  	key := fmt.Sprintf("%s\x00%s", name, ver)
   748  	i, ok := e.pkgindex[key]
   749  	if !ok {
   750  		i = len(e.Package)
   751  		if cap(e.Package) > i {
   752  			e.Package = e.Package[:i+1]
   753  		} else {
   754  			e.Package = append(e.Package, claircore.Package{})
   755  		}
   756  		e.Package[i].Name = name
   757  		e.Package[i].Version = ver
   758  		e.pkgindex[key] = i
   759  	}
   760  	return &e.Package[i], ok
   761  }
   762  
   763  func (e *ecs) LookupRepository(name string) (r *claircore.Repository) {
   764  	key := name
   765  	i, ok := e.repoindex[key]
   766  	if !ok {
   767  		i = len(e.Repository)
   768  		if cap(e.Repository) > i {
   769  			e.Repository = e.Repository[:i+1]
   770  		} else {
   771  			e.Repository = append(e.Repository, claircore.Repository{})
   772  		}
   773  		e.Repository[i].Name = name
   774  		switch name {
   775  		case "crates.io":
   776  			e.Repository[i].URI = `https://crates.io/`
   777  		case "go":
   778  			e.Repository[i].URI = `https://pkg.go.dev/`
   779  		case "npm":
   780  			e.Repository[i].URI = `https://www.npmjs.com/`
   781  		case "nuget":
   782  			e.Repository[i].URI = `https://www.nuget.org/packages/`
   783  		case "oss-fuzz":
   784  			e.Repository[i].URI = `https://google.github.io/oss-fuzz/`
   785  		case "packagist":
   786  			e.Repository[i].URI = `https://packagist.org/`
   787  		case "pypi":
   788  			e.Repository[i].URI = `https://pypi.org/`
   789  		case "rubygems":
   790  			e.Repository[i].URI = `https://rubygems.org/gems/`
   791  		case "maven":
   792  			e.Repository[i].URI = `https://repo1.maven.apache.org/maven2`
   793  		}
   794  		e.repoindex[key] = i
   795  	}
   796  	return &e.Repository[i]
   797  }
   798  
   799  func (e *ecs) Len() int {
   800  	return len(e.Vulnerability)
   801  }
   802  
   803  func (e *ecs) Finalize() []*claircore.Vulnerability {
   804  	r := make([]*claircore.Vulnerability, len(e.Vulnerability))
   805  	for i := range e.Vulnerability {
   806  		r[i] = &e.Vulnerability[i]
   807  	}
   808  	return r
   809  }