blob: ac75359756030d5b696f5bfe02dd6c3c6f5eebf8 [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 main
import (
"context"
"flag"
"fmt"
"regexp"
"runtime"
"strings"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
"golang.org/x/vulndb/cmd/vulnreport/log"
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/osvutils"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/symbols"
)
var (
force = flag.Bool("f", false, "for fix, force Fix to run even if there are no lint errors")
skipAlias = flag.Bool("skip-alias", false, "for fix, skip adding new GHSAs and CVEs")
skipSymbols = flag.Bool("skip-symbols", false, "for lint and fix, don't load package for symbols checks")
)
type fix struct {
pc *proxy.Client
gc *ghsa.Client
filenameParser
}
func (fix) name() string { return "fix" }
func (fix) usage() (string, string) {
const desc = "fix a YAML report"
return filenameArgs, desc
}
func (f *fix) setup(ctx context.Context) error {
f.pc = proxy.NewDefaultClient()
f.gc = ghsa.NewClient(ctx, *githubToken)
return nil
}
func (*fix) close() error { return nil }
func (f *fix) run(ctx context.Context, filename string) (err error) {
r, err := report.Read(filename)
if err != nil {
return err
}
return fixReport(ctx, r, filename, f.pc, f.gc)
}
func fixReport(ctx context.Context, r *report.Report, filename string, pc *proxy.Client, gc *ghsa.Client) error {
if err := r.CheckFilename(filename); err != nil {
return err
}
// We may make partial progress on fixing a report, so write the
// report even if a fatal error occurs somewhere.
defer func() {
if err := r.Write(filename); err != nil {
log.Err(err)
}
}()
if lints := r.Lint(pc); *force || len(lints) > 0 {
r.Fix(pc)
}
if lints := r.Lint(pc); len(lints) > 0 {
log.Warnf("%s still has lint errors after fix:\n\t- %s", filename, strings.Join(lints, "\n\t- "))
}
if !*skipSymbols {
log.Infof("%s: checking packages and symbols (use -skip-symbols to skip this)", r.ID)
if err := checkReportSymbols(r); err != nil {
return err
}
}
if !*skipAlias {
log.Infof("%s: checking for missing GHSAs and CVEs (use -skip-alias to skip this)", r.ID)
if added := addMissingAliases(ctx, r, gc); added > 0 {
log.Infof("%s: added %d missing aliases", r.ID, added)
}
}
if !r.IsExcluded() {
if err := writeOSV(r); err != nil {
return err
}
}
if r.CVEMetadata != nil {
if err := writeCVE(r); err != nil {
return err
}
}
return nil
}
func checkReportSymbols(r *report.Report) error {
if r.IsExcluded() {
log.Infof("%s is excluded, skipping symbol checks", r.ID)
return nil
}
for _, m := range r.Modules {
if m.IsFirstParty() {
gover := runtime.Version()
ver := semverForGoVersion(gover)
// If some symbol is in the std library at a different version,
// we may derive the wrong symbols for this package and other.
// In this case, skip updating DerivedSymbols.
affected, err := osvutils.AffectsSemver(report.AffectedRanges(m.Versions), ver)
if err != nil {
return err
}
if ver == "" || !affected {
log.Warnf("%s: current Go version %q is not in a vulnerable range, skipping symbol checks for module %s", r.ID, gover, m.Module)
continue
}
if ver != m.VulnerableAt {
log.Warnf("%s: current Go version %q does not match vulnerable_at version (%s) for module %s", r.ID, ver, m.VulnerableAt, m.Module)
}
}
for _, p := range m.Packages {
if p.SkipFix != "" {
log.Infof("%s: skipping symbol checks for package %s (reason: %q)", r.ID, p.Package, p.SkipFix)
continue
}
syms, err := symbols.Exported(m, p)
if err != nil {
return fmt.Errorf("package %s: %w", p.Package, err)
}
// Remove any derived symbols that were marked as excluded by a human.
syms = removeExcluded(syms, p.ExcludedSymbols)
if !cmp.Equal(syms, p.DerivedSymbols) {
p.DerivedSymbols = syms
log.Infof("%s: updated derived symbols for package %s", r.ID, p.Package)
}
}
}
return nil
}
func removeExcluded(syms, excluded []string) []string {
if len(excluded) == 0 {
return syms
}
var newSyms []string
for _, d := range syms {
if slices.Contains(excluded, d) {
log.Infof("removed excluded symbol %s", d)
continue
}
newSyms = append(newSyms, d)
}
return newSyms
}
// Regexp for matching go tags. The groups are:
// 1 the major.minor version
// 2 the patch version, or empty if none
// 3 the entire prerelease, if present
// 4 the prerelease type ("beta" or "rc")
// 5 the prerelease number
var tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc)(\d+))?$`)
// versionForTag returns the semantic version for a Go version string,
// or "" if the version string doesn't correspond to a Go release or beta.
func semverForGoVersion(v string) string {
m := tagRegexp.FindStringSubmatch(v)
if m == nil {
return ""
}
version := m[1]
if m[2] != "" {
version += m[2]
} else {
version += ".0"
}
if m[3] != "" {
version += "-" + m[4] + "." + m[5]
}
return version
}