blob: 31986a040db1929665f56560eeb988df808405ef [file] [log] [blame]
// Copyright 2024 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 report
import (
"context"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/vulndb/internal/gitrepo"
"gopkg.in/yaml.v3"
)
var (
// YAMLDir is the name of the directory in the vulndb repo that
// contains reports.
YAMLDir = filepath.Join(dataFolder, reportsFolder)
// ExcludedDir is the name of the directory in the vulndb repo that
// contains excluded reports.
ExcludedDir = filepath.Join(dataFolder, excludedFolder)
)
const (
dataFolder, reportsFolder, excludedFolder = "data", "reports", "excluded"
)
// Client is a client for accessing vulndb reports from a git repository.
type Client struct {
byFile map[string]*Report
byIssue map[int]*Report
byAlias map[string][]*Report
}
// NewClient returns a Client for accessing the reports in
// the given repo, which must contain directories "data/reports"
// and "data/excluded".
func NewClient(repo *git.Repository) (*Client, error) {
c := newClient()
if err := c.addReports(repo); err != nil {
return nil, err
}
return c, nil
}
// NewDefaultClient returns a Client that reads reports from
// https://github.com/golang/vulndb.
func NewDefaultClient(ctx context.Context) (*Client, error) {
const url = "https://github.com/golang/vulndb"
vulndb, err := gitrepo.Clone(ctx, url)
if err != nil {
return nil, err
}
return NewClient(vulndb)
}
// NewTestClient returns a Client based on a map from filenames to
// reports.
//
// Intended for testing.
func NewTestClient(filesToReports map[string]*Report) (*Client, error) {
c := newClient()
for fname, r := range filesToReports {
if err := c.addReport(fname, r); err != nil {
return nil, err
}
}
return c, nil
}
// List returns all reports (regular and excluded), in an
// indeterminate order.
func (c *Client) List() []*Report {
return maps.Values(c.byFile)
}
// XRef returns cross-references for a report.
// The output, matches, is a map from filenames to aliases (CVE & GHSA IDs)
// and modules (excluding std and cmd).
func (c *Client) XRef(r *Report) (matches map[string][]string) {
mods := make(map[string]bool)
for _, m := range r.Modules {
if mod := m.Module; mod != "" && mod != "std" && mod != "cmd" {
mods[m.Module] = true
}
}
// matches is a map from filename -> alias/module
matches = make(map[string][]string)
for fname, rr := range c.byFile {
for _, alias := range rr.Aliases() {
if slices.Contains(r.Aliases(), alias) {
matches[fname] = append(matches[fname], alias)
}
}
for _, m := range rr.Modules {
if mods[m.Module] {
k := "Module " + m.Module
matches[fname] = append(matches[fname], k)
}
}
}
return matches
}
// Report returns the report with the given filename in vulndb, or
// (nil, false) if not found.
func (c *Client) Report(filename string) (r *Report, ok bool) {
r, ok = c.byFile[filename]
return
}
// HasReport returns whether the Github issue id has
// a corresponding report in vulndb.
func (c *Client) HasReport(githubID int) (found bool) {
_, found = c.byIssue[githubID]
return
}
// ReportsByAlias returns a list of reports in vulndb with the given
// alias.
func (c *Client) ReportsByAlias(alias string) []*Report {
return c.byAlias[alias]
}
// AliasHasReport returns whether the given alias exists in vulndb.
func (c *Client) AliasHasReport(alias string) bool {
_, ok := c.byAlias[alias]
return ok
}
func newClient() *Client {
return &Client{
byIssue: make(map[int]*Report),
byFile: make(map[string]*Report),
byAlias: make(map[string][]*Report),
}
}
func (c *Client) addReports(repo *git.Repository) error {
root, err := gitrepo.Root(repo)
if err != nil {
return err
}
return root.Files().ForEach(func(f *object.File) error {
if !isYAMLReport(f) {
return nil
}
content, err := f.Contents()
if err != nil {
return err
}
var r Report
if err := yaml.Unmarshal([]byte(content), &r); err != nil {
return err
}
return c.addReport(f.Name, &r)
})
}
func isYAMLReport(f *object.File) bool {
dir, ext := filepath.Dir(f.Name), filepath.Ext(f.Name)
return (dir == YAMLDir || dir == ExcludedDir) && ext == ".yaml"
}
func (c *Client) addReport(filename string, r *Report) error {
_, _, iss, err := ParseFilepath(filename)
if err != nil {
return err
}
c.byFile[filename] = r
c.byIssue[iss] = r
for _, alias := range r.Aliases() {
c.byAlias[alias] = append(c.byAlias[alias], r)
}
return nil
}