blob: b162e46c391b474854d93779d54c8a749126cf26 [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"
"fmt"
"io"
"path/filepath"
"strings"
"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)
cve5Dir = filepath.Join(dataFolder, "cve", "v5")
)
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][]*File
byModule map[string][]*File
}
// 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
}
func NewLocalClient(ctx context.Context, path string) (*Client, error) {
repo, err := gitrepo.Open(ctx, path)
if err != nil {
return nil, err
}
return NewClient(repo)
}
const VulndbURL = "https://github.com/golang/vulndb"
// NewDefaultClient returns a Client that reads reports from
// https://github.com/golang/vulndb.
func NewDefaultClient(ctx context.Context) (*Client, error) {
vulndb, err := gitrepo.Clone(ctx, VulndbURL)
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)
}
type Xrefs struct {
// map from aliases to files
Aliases map[string][]*File
// map from modules to files
Modules map[string][]*File
}
func fprintMap(out io.Writer, m map[string][]*File) {
sortedKeys := func(m map[string][]*File) []string {
s := slices.Clone(maps.Keys(m))
slices.Sort(s)
return s
}
for _, k := range sortedKeys(m) {
fs := m[k]
fmt.Fprintf(out, "- %s appears in %d other report(s):\n", k, len(fs))
for _, f := range fs {
fmt.Fprintf(out, " - %s (https://github.com/golang/vulndb/issues/%d)", f.Filename, f.IssNum)
if f.Report.IsExcluded() {
fmt.Fprintf(out, " %v", f.Report.Excluded)
}
fmt.Fprintf(out, "\n")
}
}
}
func (xs *Xrefs) ToString(aliasTitle, moduleTitle, noneMessage string) string {
if len(xs.Modules) == 0 && len(xs.Aliases) == 0 {
return noneMessage
}
out := &strings.Builder{}
if len(xs.Aliases) != 0 {
fmt.Fprint(out, aliasTitle+"\n")
fprintMap(out, xs.Aliases)
if len(xs.Modules) != 0 {
fmt.Fprintf(out, "\n")
}
}
if len(xs.Modules) != 0 {
fmt.Fprint(out, moduleTitle+"\n")
fprintMap(out, xs.Modules)
}
return out.String()
}
type File struct {
Filename string
IssNum int
*Report
}
// XRef returns cross-references for a report.
func (c *Client) XRef(r *Report) *Xrefs {
x := &Xrefs{
Aliases: make(map[string][]*File),
Modules: make(map[string][]*File),
}
for _, alias := range r.Aliases() {
for _, f := range c.byAlias[alias] {
if r.ID == f.Report.ID {
continue
}
x.Aliases[alias] = append(x.Aliases[alias], f)
}
}
for _, m := range r.Modules {
if m.IsFirstParty() {
continue
}
for _, f := range c.byModule[m.Module] {
if r.ID == f.Report.ID {
continue
}
x.Modules[m.Module] = append(x.Modules[m.Module], f)
}
}
return x
}
// 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 {
var rs []*Report
for _, f := range c.byAlias[alias] {
rs = append(rs, f.Report)
}
return rs
}
// ReportsByModule returns a list of reports in vulndb with the given
// module.
func (c *Client) ReportsByModule(module string) []*Report {
var rs []*Report
for _, f := range c.byModule[module] {
rs = append(rs, f.Report)
}
return rs
}
// 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][]*File),
byModule: make(map[string][]*File),
}
}
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.Name) {
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(fname string) bool {
dir, ext := filepath.Dir(fname), filepath.Ext(fname)
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
}
f := &File{
Filename: filename,
IssNum: iss,
Report: r,
}
c.byFile[filename] = r
c.byIssue[iss] = r
for _, alias := range r.Aliases() {
c.byAlias[alias] = append(c.byAlias[alias], f)
}
for _, m := range r.Modules {
c.byModule[m.Module] = append(c.byModule[m.Module], f)
}
return nil
}