blob: 306617ba1a6af64dc6c8c784c0bfc3f28e8d3dea [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"
"net/http"
"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/osvutils"
"golang.org/x/vulndb/internal/pkgsite"
"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")
skipChecks = flag.Bool("skip-checks", false, "for fix, skip all checks except lint")
skipAlias = flag.Bool("skip-alias", false, "for fix, skip adding new GHSAs and CVEs")
skipSymbols = flag.Bool("skip-symbols", false, "for fix, don't load package for symbols checks")
skipPackages = flag.Bool("skip-packages", false, "for fix, don't check if packages exist")
skipRefs = flag.Bool("skip-refs", false, "for fix, don't check if references exist")
)
type fix struct {
*fixer
*filenameParser
noSkip
}
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, env environment) error {
f.fixer = new(fixer)
f.filenameParser = new(filenameParser)
return setupAll(ctx, env, f.fixer, f.filenameParser)
}
func (*fix) close() error { return nil }
func (f *fix) run(ctx context.Context, input any) error {
r := input.(*yamlReport)
return f.fixAndWriteAll(ctx, r, false)
}
type fixer struct {
*linter
*aliasFinder
*fileWriter
pkc *pkgsite.Client
}
func (f *fixer) setup(ctx context.Context, env environment) error {
f.pkc = env.PkgsiteClient()
f.linter = new(linter)
f.aliasFinder = new(aliasFinder)
f.fileWriter = new(fileWriter)
return setupAll(ctx, env, f.linter, f.aliasFinder, f.fileWriter)
}
func (f *fixer) fixAndWriteAll(ctx context.Context, r *yamlReport, addNotes bool) error {
fixed := f.fix(ctx, r, addNotes)
// fix may have partially succeeded, so write the report no matter what.
if err := f.write(r); err != nil {
return err
}
if fixed {
return f.writeDerived(r)
}
return fmt.Errorf("%s: could not fix all errors; requires manual review", r.ID)
}
func (f *fixer) fix(ctx context.Context, r *yamlReport, addNotes bool) (fixed bool) {
fixed = true
if lints := r.Lint(f.pxc); *force || len(lints) > 0 {
r.Fix(f.pxc)
}
if !*skipChecks {
if ok := f.allChecks(ctx, r, addNotes); !ok {
fixed = false
}
}
// Check for remaining lint errors.
if addNotes {
if r.LintAsNotes(f.pxc) {
log.Warnf("%s: still has lint errors after fix", r.ID)
fixed = false
}
} else {
if lints := r.Lint(f.pxc); len(lints) > 0 {
log.Warnf("%s: still has lint errors after fix:\n\t- %s", r.ID, strings.Join(lints, "\n\t- "))
fixed = false
}
}
return fixed
}
// allChecks runs additional checks/fixes that are slow and require a network connection.
// Unlike lint checks which cannot be bypassed, these *should* pass before submit,
// but it is possible to bypass them if needed with the "-skip-checks" flag.
func (f *fixer) allChecks(ctx context.Context, r *yamlReport, addNotes bool) (ok bool) {
ok = true
fixErr := func(f string, v ...any) {
log.Errf(r.ID+": "+f, v...)
if addNotes {
r.AddNote(report.NoteTypeFix, f, v...)
}
ok = false
}
if !*skipPackages {
log.Infof("%s: checking that all packages exist", r.ID)
if err := r.CheckPackages(ctx, f.pkc); err != nil {
fixErr("package error: %s", err)
}
}
if !*skipSymbols {
log.Infof("%s: checking symbols (use -skip-symbols to skip this)", r.ID)
if err := r.checkSymbols(); err != nil {
fixErr("symbol error: %s", err)
}
}
if !*skipAlias {
log.Infof("%s: checking for missing GHSAs and CVEs (use -skip-alias to skip this)", r.ID)
if added := r.addMissingAliases(ctx, f.aliasFinder); added > 0 {
log.Infof("%s: added %d missing aliases", r.ID, added)
}
}
if !*skipRefs {
// For now, this is a fix check instead of a lint.
log.Infof("%s: checking that all references are reachable", r.ID)
checkRefs(r.References, fixErr)
}
return ok
}
func checkRefs(refs []*report.Reference, fixErr func(f string, v ...any)) {
for _, r := range refs {
resp, err := http.Head(r.URL)
if err != nil {
fixErr("%q may not exist: %v", r.URL, err)
continue
}
defer resp.Body.Close()
// For now, only error on status 404, which is unambiguously a problem.
// An experiment to error on all non-200 status codes brought up some
// ambiguous cases where the link is still viewable in a browser, e.g.:
// - 429 Too Many Requests (https://vuldb.com/)
// - 503 Service Unavailable (http://blog.recurity-labs.com/2017-08-10/scm-vulns):
// - 403 Forbidden (https://www.sap.com/documents/2022/02/fa865ea4-167e-0010-bca6-c68f7e60039b.html)
if resp.StatusCode == http.StatusNotFound {
fixErr("%q may not exist: HTTP GET returned status %s", r.URL, resp.Status)
}
}
}
func (r *yamlReport) checkSymbols() error {
if r.IsExcluded() {
log.Infof("%s: excluded, skipping symbol checks", r.ID)
return nil
}
if len(r.Modules) == 0 {
log.Infof("%s: no modules, skipping symbol checks", r.ID)
return nil
}
for _, m := range r.Modules {
if len(m.Packages) == 0 {
log.Infof("%s: module %s has no packages, skipping symbol checks", r.ID, m.Module)
return nil
}
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.
ranges, err := m.Versions.ToSemverRanges()
if err != nil {
return err
}
affected, err := osvutils.AffectsSemver(ranges, 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 m.VulnerableAt == nil || ver != m.VulnerableAt.Version {
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 len(p.AllSymbols()) == 0 && p.SkipFixSymbols != "" {
log.Warnf("%s: skip_fix not needed", r.Filename)
continue
}
if len(p.AllSymbols()) == 0 {
log.Infof("%s: skipping symbol checks for package %s (no symbols)", r.ID, p.Package)
continue
}
if p.SkipFixSymbols != "" {
log.Infof("%s: skipping symbol checks for package %s (reason: %q)", r.ID, p.Package, p.SkipFixSymbols)
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(r.ID, 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(id string, syms, excluded []string) []string {
if len(excluded) == 0 {
return syms
}
var newSyms []string
for _, d := range syms {
if slices.Contains(excluded, d) {
log.Infof("%s: removed excluded symbol %s", id, 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
}