blob: 2c6c800dbf3d10faa928e0d6d74eebb63e672580 [file] [log] [blame]
// Copyright 2022 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 scan
import (
"context"
"fmt"
"path/filepath"
"sort"
"golang.org/x/tools/go/packages"
"golang.org/x/vuln/internal"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/vulncheck"
)
// runSource reports vulnerabilities that affect the analyzed packages.
//
// Vulnerabilities can be called (affecting the package, because a vulnerable
// symbol is actually exercised) or just imported by the package
// (likely having a non-affecting outcome).
func runSource(ctx context.Context, handler govulncheck.Handler, cfg *config, client *client.Client, dir string) error {
if len(cfg.patterns) == 0 {
return nil
}
var pkgs []*packages.Package
graph := vulncheck.NewPackageGraph(cfg.GoVersion)
pkgConfig := &packages.Config{
Dir: dir,
Tests: cfg.test,
Env: cfg.env,
}
pkgs, err := graph.LoadPackages(pkgConfig, cfg.tags, cfg.patterns)
if err != nil {
// Try to provide a meaningful and actionable error message.
if !fileExists(filepath.Join(dir, "go.mod")) {
return fmt.Errorf("govulncheck: %v", errNoGoMod)
}
if isGoVersionMismatchError(err) {
return fmt.Errorf("govulncheck: %v\n\n%v", errGoVersionMismatch, err)
}
return fmt.Errorf("govulncheck: loading packages: %w", err)
}
if err := handler.Progress(sourceProgressMessage(pkgs)); err != nil {
return err
}
vr, err := vulncheck.Source(ctx, handler, pkgs, &cfg.Config, client, graph)
if err != nil {
return err
}
callStacks := vulncheck.CallStacks(vr)
return emitCalledVulns(handler, callStacks)
}
func emitCalledVulns(handler govulncheck.Handler, callstacks map[*vulncheck.Vuln]vulncheck.CallStack) error {
var vulns []*vulncheck.Vuln
for v := range callstacks {
vulns = append(vulns, v)
}
sort.SliceStable(vulns, func(i, j int) bool {
return vulns[i].Symbol < vulns[j].Symbol
})
for _, vuln := range vulns {
stack := callstacks[vuln]
if stack == nil {
continue
}
fixed := vulncheck.FixedVersion(vulncheck.ModPath(vuln.ImportSink.Module), vulncheck.ModVersion(vuln.ImportSink.Module), vuln.OSV.Affected)
handler.Finding(&govulncheck.Finding{
OSV: vuln.OSV.ID,
FixedVersion: fixed,
Trace: tracefromEntries(stack),
})
}
return nil
}
// tracefromEntries creates a sequence of
// frames from vcs. Position of a Frame is the
// call position of the corresponding stack entry.
func tracefromEntries(vcs vulncheck.CallStack) []*govulncheck.Frame {
var frames []*govulncheck.Frame
for i := len(vcs) - 1; i >= 0; i-- {
e := vcs[i]
fr := frameFromPackage(e.Function.Package)
fr.Function = e.Function.Name
fr.Receiver = e.Function.Receiver()
if e.Call == nil || e.Call.Pos == nil {
fr.Position = nil
} else {
fr.Position = &govulncheck.Position{
Filename: e.Call.Pos.Filename,
Offset: e.Call.Pos.Offset,
Line: e.Call.Pos.Line,
Column: e.Call.Pos.Column,
}
}
frames = append(frames, fr)
}
return frames
}
func frameFromPackage(pkg *packages.Package) *govulncheck.Frame {
fr := &govulncheck.Frame{}
if pkg != nil {
fr.Module = pkg.Module.Path
fr.Version = pkg.Module.Version
fr.Package = pkg.PkgPath
}
if pkg.Module.Replace != nil {
fr.Module = pkg.Module.Replace.Path
fr.Version = pkg.Module.Replace.Version
}
return fr
}
// sourceProgressMessage returns a string of the form
//
// "Scanning your code and P packages across M dependent modules for known vulnerabilities..."
//
// P is the number of strictly dependent packages of
// topPkgs and Y is the number of their modules.
func sourceProgressMessage(topPkgs []*packages.Package) *govulncheck.Progress {
pkgs, mods := depPkgsAndMods(topPkgs)
pkgsPhrase := fmt.Sprintf("%d package", pkgs)
if pkgs != 1 {
pkgsPhrase += "s"
}
modsPhrase := fmt.Sprintf("%d dependent module", mods)
if mods != 1 {
modsPhrase += "s"
}
msg := fmt.Sprintf("Scanning your code and %s across %s for known vulnerabilities...", pkgsPhrase, modsPhrase)
return &govulncheck.Progress{Message: msg}
}
// depPkgsAndMods returns the number of packages that
// topPkgs depend on and the number of their modules.
func depPkgsAndMods(topPkgs []*packages.Package) (int, int) {
tops := make(map[string]bool)
depPkgs := make(map[string]bool)
depMods := make(map[string]bool)
for _, t := range topPkgs {
tops[t.PkgPath] = true
}
var visit func(*packages.Package, bool)
visit = func(p *packages.Package, top bool) {
path := p.PkgPath
if depPkgs[path] {
return
}
if tops[path] && !top {
// A top package that is a dependency
// will not be in depPkgs, so we skip
// reiterating on it here.
return
}
// We don't count a top-level package as
// a dependency even when they are used
// as a dependent package.
if !tops[path] {
depPkgs[path] = true
if p.Module != nil &&
p.Module.Path != internal.GoStdModulePath && // no module for stdlib
p.Module.Path != internal.UnknownModulePath { // no module for unknown
depMods[p.Module.Path] = true
}
}
for _, d := range p.Imports {
visit(d, false)
}
}
for _, t := range topPkgs {
visit(t, true)
}
return len(depPkgs), len(depMods)
}