| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| |
| "golang.org/x/exp/slices" |
| "golang.org/x/tools/txtar" |
| "golang.org/x/vulndb/cmd/vulnreport/log" |
| "golang.org/x/vulndb/internal/cve5" |
| "golang.org/x/vulndb/internal/genericosv" |
| "golang.org/x/vulndb/internal/ghsa" |
| "golang.org/x/vulndb/internal/idstr" |
| "golang.org/x/vulndb/internal/report" |
| "gopkg.in/yaml.v3" |
| ) |
| |
| type aliasFinder struct { |
| gc ghsaClient |
| } |
| |
| func (af *aliasFinder) setup(ctx context.Context, env environment) error { |
| gc, err := env.GHSAClient(ctx) |
| if err != nil { |
| return err |
| } |
| af.gc = gc |
| return nil |
| } |
| |
| // addMissingAliases uses the existing aliases in a report to find |
| // any missing aliases, and adds them to the report. |
| func (r *yamlReport) addMissingAliases(ctx context.Context, af *aliasFinder) (added int) { |
| all := af.allAliases(ctx, r.Aliases()) |
| // If we have manually marked an identifier as "related", but |
| // not actually an alias, don't override this decision. |
| if len(r.Related) > 0 { |
| all = removeRelated(all, r.Related) |
| } |
| return r.AddAliases(all) |
| } |
| |
| func removeRelated(all, related []string) []string { |
| // This is an uncommon operation, operating on short string slices, |
| // so it doesn't need to be optimized. |
| return slices.DeleteFunc(all, func(s string) bool { |
| return slices.Contains(related, s) |
| }) |
| } |
| |
| // allAliases returns a list of all aliases associated with the given knownAliases, |
| // (including the knownAliases themselves). |
| func (a *aliasFinder) allAliases(ctx context.Context, knownAliases []string) []string { |
| aliasesFor := func(ctx context.Context, alias string) ([]string, error) { |
| switch { |
| case idstr.IsGHSA(alias): |
| return aliasesForGHSA(ctx, alias, a.gc) |
| case idstr.IsCVE(alias): |
| return aliasesForCVE(ctx, alias, a.gc) |
| default: |
| return nil, fmt.Errorf("allAliases(): unsupported alias %s", alias) |
| } |
| } |
| return aliasesBFS(ctx, knownAliases, aliasesFor) |
| } |
| |
| func aliasesBFS(ctx context.Context, knownAliases []string, |
| aliasesFor func(ctx context.Context, alias string) ([]string, error)) (all []string) { |
| var queue []string |
| var seen = make(map[string]bool) |
| queue = append(queue, knownAliases...) |
| |
| for len(queue) > 0 { |
| alias := queue[0] |
| queue = queue[1:] |
| |
| if seen[alias] { |
| continue |
| } |
| |
| seen[alias] = true |
| all = append(all, alias) |
| aliases, err := aliasesFor(ctx, alias) |
| if err != nil { |
| log.Warn(err) |
| continue |
| } |
| queue = append(queue, aliases...) |
| } |
| |
| slices.Sort(all) |
| return slices.Compact(all) |
| } |
| |
| func aliasesForGHSA(ctx context.Context, alias string, gc ghsaClient) (aliases []string, err error) { |
| sa, err := gc.FetchGHSA(ctx, alias) |
| if err != nil { |
| return nil, fmt.Errorf("aliasesForGHSA(%s): could not fetch GHSA record from GraphQL API", alias) |
| } |
| for _, id := range sa.Identifiers { |
| if id.Type == "CVE" || id.Type == "GHSA" { |
| aliases = append(aliases, id.Value) |
| } |
| } |
| return aliases, nil |
| } |
| |
| func aliasesForCVE(ctx context.Context, cve string, gc ghsaClient) (aliases []string, err error) { |
| sas, err := gc.ListForCVE(ctx, cve) |
| if err != nil { |
| return nil, fmt.Errorf("aliasesForCVE(%s): could not find GHSAs from GraphQL API", cve) |
| } |
| for _, sa := range sas { |
| aliases = append(aliases, sa.ID) |
| } |
| return aliases, nil |
| } |
| |
| // sourceFromBestAlias returns a report source fetched from the "best" alias in the list. |
| // By default, it prefers the first GHSA in the list, followed by the first CVE in the list |
| // (if no GHSA is present). |
| // If "preferCVE" is true, it prefers CVEs instead. |
| func (af *aliasFinder) sourceFromBestAlias(ctx context.Context, aliases []string, preferCVE bool) report.Source { |
| firstChoice := idstr.IsGHSA |
| secondChoice := idstr.IsCVE |
| if preferCVE { |
| firstChoice, secondChoice = secondChoice, firstChoice |
| } |
| |
| find := func(f func(string) bool) report.Source { |
| for _, alias := range aliases { |
| if f(alias) { |
| src, err := af.fetch(ctx, alias) |
| if err != nil { |
| log.Warnf("could not fetch record for preferred alias %s: %v", alias, err) |
| continue |
| } |
| return src |
| } |
| } |
| return nil |
| } |
| |
| if src := find(firstChoice); src != nil { |
| return src |
| } |
| return find(secondChoice) |
| } |
| |
| func (a *aliasFinder) fetch(ctx context.Context, alias string) (report.Source, error) { |
| var f report.Fetcher |
| switch { |
| case idstr.IsGHSA(alias): |
| f = genericosv.NewGHSAFetcher(a.gc) |
| case idstr.IsCVE(alias): |
| f = cve5.NewFetcher() |
| default: |
| return nil, fmt.Errorf("alias %s is not supported", alias) |
| } |
| |
| return f.Fetch(ctx, alias) |
| } |
| |
| type ghsaClient interface { |
| FetchGHSA(context.Context, string) (*ghsa.SecurityAdvisory, error) |
| ListForCVE(context.Context, string) ([]*ghsa.SecurityAdvisory, error) |
| } |
| |
| type memGC struct { |
| ghsas map[string]ghsa.SecurityAdvisory |
| byCVE map[string][]*ghsa.SecurityAdvisory |
| } |
| |
| func newMemGC(archive []byte) (*memGC, error) { |
| ar := txtar.Parse(archive) |
| m := &memGC{ |
| ghsas: make(map[string]ghsa.SecurityAdvisory), |
| byCVE: make(map[string][]*ghsa.SecurityAdvisory), |
| } |
| for _, f := range ar.Files { |
| var g ghsa.SecurityAdvisory |
| d := yaml.NewDecoder(bytes.NewBuffer(f.Data)) |
| d.KnownFields(true) |
| if err := d.Decode(&g); err != nil { |
| return nil, err |
| } |
| if f.Name != g.ID { |
| return nil, fmt.Errorf("filename (%s) != ID (%s)", f.Name, g.ID) |
| } |
| m.ghsas[f.Name] = g |
| for _, id := range g.Identifiers { |
| if id.Type == "CVE" { |
| m.byCVE[id.Value] = append(m.byCVE[id.Value], &g) |
| } |
| } |
| } |
| return m, nil |
| } |
| |
| func (m *memGC) FetchGHSA(_ context.Context, id string) (*ghsa.SecurityAdvisory, error) { |
| if sa, ok := m.ghsas[id]; ok { |
| return &sa, nil |
| } |
| return nil, fmt.Errorf("%s not found", id) |
| } |
| |
| func (m *memGC) ListForCVE(_ context.Context, cid string) (result []*ghsa.SecurityAdvisory, _ error) { |
| return m.byCVE[cid], nil |
| } |