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 }