238 lines
7.4 KiB
Go
238 lines
7.4 KiB
Go
// Copyright 2021 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 vulncheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/vuln/internal"
|
|
"golang.org/x/vuln/internal/buildinfo"
|
|
"golang.org/x/vuln/internal/client"
|
|
"golang.org/x/vuln/internal/govulncheck"
|
|
"golang.org/x/vuln/internal/semver"
|
|
)
|
|
|
|
// Bin is an abstraction of Go binary containing
|
|
// minimal information needed by govulncheck.
|
|
type Bin struct {
|
|
// Path of the main package.
|
|
Path string `json:"path,omitempty"`
|
|
// Main module. When present, it never has empty information.
|
|
Main *packages.Module `json:"main,omitempty"`
|
|
Modules []*packages.Module `json:"modules,omitempty"`
|
|
PkgSymbols []buildinfo.Symbol `json:"pkgSymbols,omitempty"`
|
|
GoVersion string `json:"goVersion,omitempty"`
|
|
GOOS string `json:"goos,omitempty"`
|
|
GOARCH string `json:"goarch,omitempty"`
|
|
}
|
|
|
|
// Binary detects presence of vulnerable symbols in bin and
|
|
// emits findings to handler.
|
|
func Binary(ctx context.Context, handler govulncheck.Handler, bin *Bin, cfg *govulncheck.Config, client *client.Client) error {
|
|
vr, err := binary(ctx, handler, bin, cfg, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cfg.ScanLevel.WantSymbols() {
|
|
return emitCallFindings(handler, binaryCallstacks(vr))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// binary detects presence of vulnerable symbols in bin.
|
|
// It does not compute call graphs so the corresponding
|
|
// info in Result will be empty.
|
|
func binary(ctx context.Context, handler govulncheck.Handler, bin *Bin, cfg *govulncheck.Config, client *client.Client) (*Result, error) {
|
|
graph := NewPackageGraph(bin.GoVersion)
|
|
mods := append(bin.Modules, graph.GetModule(internal.GoStdModulePath))
|
|
|
|
if bin.Main != nil {
|
|
mods = append(mods, bin.Main)
|
|
}
|
|
|
|
graph.AddModules(mods...)
|
|
|
|
if err := handler.SBOM(bin.SBOM()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := handler.Progress(&govulncheck.Progress{Message: fetchingVulnsMessage}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mv, err := FetchVulnerabilities(ctx, client, mods)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Emit OSV entries immediately in their raw unfiltered form.
|
|
if err := emitOSVs(handler, mv); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := handler.Progress(&govulncheck.Progress{Message: checkingBinVulnsMessage}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Emit warning message for ancient Go binaries, defined as binaries
|
|
// built with Go version without support for debug.BuildInfo (< go1.18).
|
|
if semver.Valid(bin.GoVersion) && semver.Less(bin.GoVersion, "go1.18") {
|
|
p := &govulncheck.Progress{Message: fmt.Sprintf("warning: binary built with Go version %s, only standard library vulnerabilities will be checked", bin.GoVersion)}
|
|
if err := handler.Progress(p); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if bin.GOOS == "" || bin.GOARCH == "" {
|
|
p := &govulncheck.Progress{Message: fmt.Sprintf("warning: failed to extract build system specification GOOS: %s GOARCH: %s\n", bin.GOOS, bin.GOARCH)}
|
|
if err := handler.Progress(p); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
affVulns := affectingVulnerabilities(mv, bin.GOOS, bin.GOARCH)
|
|
if err := emitModuleFindings(handler, affVulns); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !cfg.ScanLevel.WantPackages() || len(affVulns) == 0 {
|
|
return &Result{}, nil
|
|
}
|
|
|
|
// Group symbols per package to avoid querying affVulns all over again.
|
|
var pkgSymbols map[string][]string
|
|
if len(bin.PkgSymbols) == 0 {
|
|
// The binary exe is stripped. We currently cannot detect inlined
|
|
// symbols for stripped binaries (see #57764), so we report
|
|
// vulnerabilities at the go.mod-level precision.
|
|
pkgSymbols = allKnownVulnerableSymbols(affVulns)
|
|
} else {
|
|
pkgSymbols = packagesAndSymbols(bin)
|
|
}
|
|
|
|
impVulns := binImportedVulnPackages(graph, pkgSymbols, affVulns)
|
|
// Emit information on imported vulnerable packages now to
|
|
// mimic behavior of source.
|
|
if err := emitPackageFindings(handler, impVulns); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return result immediately if not in symbol mode to mimic the
|
|
// behavior of source.
|
|
if !cfg.ScanLevel.WantSymbols() || len(impVulns) == 0 {
|
|
return &Result{Vulns: impVulns}, nil
|
|
}
|
|
|
|
symVulns := binVulnSymbols(graph, pkgSymbols, affVulns)
|
|
return &Result{Vulns: symVulns}, nil
|
|
}
|
|
|
|
func packagesAndSymbols(bin *Bin) map[string][]string {
|
|
pkgSymbols := make(map[string][]string)
|
|
for _, sym := range bin.PkgSymbols {
|
|
// If the name of the package is main, we need to expand
|
|
// it to its full path as that is what vuln db uses.
|
|
if sym.Pkg == "main" && bin.Path != "" {
|
|
pkgSymbols[bin.Path] = append(pkgSymbols[bin.Path], sym.Name)
|
|
} else {
|
|
pkgSymbols[sym.Pkg] = append(pkgSymbols[sym.Pkg], sym.Name)
|
|
}
|
|
}
|
|
return pkgSymbols
|
|
}
|
|
|
|
func binImportedVulnPackages(graph *PackageGraph, pkgSymbols map[string][]string, affVulns affectingVulns) []*Vuln {
|
|
var vulns []*Vuln
|
|
for pkg := range pkgSymbols {
|
|
for _, osv := range affVulns.ForPackage(internal.UnknownModulePath, pkg) {
|
|
vuln := &Vuln{
|
|
OSV: osv,
|
|
Package: graph.GetPackage(pkg),
|
|
}
|
|
vulns = append(vulns, vuln)
|
|
}
|
|
}
|
|
return vulns
|
|
}
|
|
|
|
func binVulnSymbols(graph *PackageGraph, pkgSymbols map[string][]string, affVulns affectingVulns) []*Vuln {
|
|
var vulns []*Vuln
|
|
for pkg, symbols := range pkgSymbols {
|
|
for _, symbol := range symbols {
|
|
for _, osv := range affVulns.ForSymbol(internal.UnknownModulePath, pkg, symbol) {
|
|
vuln := &Vuln{
|
|
OSV: osv,
|
|
Symbol: symbol,
|
|
Package: graph.GetPackage(pkg),
|
|
}
|
|
vulns = append(vulns, vuln)
|
|
}
|
|
}
|
|
}
|
|
return vulns
|
|
}
|
|
|
|
// allKnownVulnerableSymbols returns all known vulnerable symbols for packages in graph.
|
|
// If all symbols of a package are vulnerable, that is modeled as a wild car symbol "<pkg-path>/*".
|
|
func allKnownVulnerableSymbols(affVulns affectingVulns) map[string][]string {
|
|
pkgSymbols := make(map[string][]string)
|
|
for _, mv := range affVulns {
|
|
for _, osv := range mv.Vulns {
|
|
for _, affected := range osv.Affected {
|
|
for _, p := range affected.EcosystemSpecific.Packages {
|
|
syms := p.Symbols
|
|
if len(syms) == 0 {
|
|
// If every symbol of pkg is vulnerable, we would ideally
|
|
// compute every symbol mentioned in the pkg and then add
|
|
// Vuln entry for it, just as we do in Source. However,
|
|
// we don't have code of pkg here and we don't even have
|
|
// pkg symbols used in stripped binary, so we add a placeholder
|
|
// symbol.
|
|
//
|
|
// Note: this should not affect output of govulncheck since
|
|
// in binary mode no symbol/call stack information is
|
|
// communicated back to the user.
|
|
syms = []string{fmt.Sprintf("%s/*", p.Path)}
|
|
}
|
|
|
|
pkgSymbols[p.Path] = append(pkgSymbols[p.Path], syms...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return pkgSymbols
|
|
}
|
|
|
|
func (bin *Bin) SBOM() (sbom *govulncheck.SBOM) {
|
|
sbom = &govulncheck.SBOM{}
|
|
if bin.Main != nil {
|
|
sbom.Roots = []string{bin.Main.Path}
|
|
sbom.Modules = append(sbom.Modules, &govulncheck.Module{
|
|
Path: bin.Main.Path,
|
|
Version: bin.Main.Version,
|
|
})
|
|
}
|
|
|
|
sbom.GoVersion = bin.GoVersion
|
|
for _, mod := range bin.Modules {
|
|
if mod.Replace != nil {
|
|
mod = mod.Replace
|
|
}
|
|
sbom.Modules = append(sbom.Modules, &govulncheck.Module{
|
|
Path: mod.Path,
|
|
Version: mod.Version,
|
|
})
|
|
}
|
|
|
|
// add stdlib to mirror source mode output
|
|
sbom.Modules = append(sbom.Modules, &govulncheck.Module{
|
|
Path: internal.GoStdModulePath,
|
|
Version: bin.GoVersion,
|
|
})
|
|
|
|
return sbom
|
|
}
|