vulndb: add govulncheck command
Co-authored-by: Roland Shoemaker <roland@golang.org>
Co-authored-by: Filippo Valsorda <filippo@golang.org>
Change-Id: I1394b1665595466259a0f5572086d90cab079ed6
Reviewed-on: https://go-review.googlesource.com/c/exp/+/329809
Trust: Filippo Valsorda <filippo@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
diff --git a/vulndb/go.mod b/vulndb/go.mod
new file mode 100644
index 0000000..4eed685
--- /dev/null
+++ b/vulndb/go.mod
@@ -0,0 +1,14 @@
+module golang.org/x/exp/vulndb
+
+go 1.17
+
+require (
+ golang.org/x/tools v0.1.4-0.20210618183400-d25f90668280
+ golang.org/x/vulndb v0.0.0-20210616170126-cf0f9f1f871d
+)
+
+require (
+ golang.org/x/mod v0.4.2 // indirect
+ golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+)
diff --git a/vulndb/go.sum b/vulndb/go.sum
new file mode 100644
index 0000000..8889276
--- /dev/null
+++ b/vulndb/go.sum
@@ -0,0 +1,35 @@
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.4-0.20210618183400-d25f90668280 h1:bvZzMlhjbBrvAqAeuknuBOeOUnTHzR5zqg+y2mrOGKY=
+golang.org/x/tools v0.1.4-0.20210618183400-d25f90668280/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/vulndb v0.0.0-20210616170126-cf0f9f1f871d h1:enPb1wKlBGD6XfVrYlyH6teMbuprkh5Q3rvIlSRTZBc=
+golang.org/x/vulndb v0.0.0-20210616170126-cf0f9f1f871d/go.mod h1:zjTClCE7c55KnXf1MqfecbAQyuVrc24QEcrThAnl3A0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/vulndb/govulncheck/main.go b/vulndb/govulncheck/main.go
new file mode 100644
index 0000000..ec38f44
--- /dev/null
+++ b/vulndb/govulncheck/main.go
@@ -0,0 +1,224 @@
+// 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.
+
+// Command govulncheck reports known vulnerabilities filed in a vulnerability database
+// (see https://golang.org/design/draft-vulndb) that affect a given package or binary.
+//
+// It uses static analysis or the binary's symbol table to narrow down reports to only
+// those that potentially affect the application.
+//
+// WARNING WARNING WARNING
+//
+// govulncheck is still experimental and neither its output or the vulnerability
+// database should be relied on to be stable or comprehensive. It also performs no
+// caching of vulnerability database entries.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "sort"
+ "strings"
+
+ "golang.org/x/exp/vulndb/internal/audit"
+ "golang.org/x/exp/vulndb/internal/binscan"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/go/ssa/ssautil"
+)
+
+var (
+ jsonFlag = flag.Bool("json", false, "")
+ verboseFlag = flag.Bool("verbose", false, "")
+ importsFlag = flag.Bool("imports", false, "")
+)
+
+const usage = `govulncheck: identify known vulnerabilities by call graph traversal.
+
+Usage:
+
+ govulncheck [-imports] {package pattern...}
+
+ govulncheck {binary path}
+
+Flags:
+
+ -imports Perform a broad scan with more false positives, which reports all
+ vulnerabilities found in any transitively imported package, regardless
+ of whether they are reachable.
+
+ -json Print vulnerability findings in JSON format.
+
+ -verbose Print progress information.
+
+govulncheck can be used with either one or more package patterns (i.e. golang.org/x/crypto/...
+or ./...) or with a single path to a Go binary. In the latter case module and symbol
+information will be extracted from the binary in order to detect vulnerable symbols
+and the -imports flag is disregarded.
+
+The environment variable GOVULNDB can be set to a comma-separate list of vulnerability
+database URLs, with http://, https://, or file:// protocols. Entries from multiple
+databases are merged.
+`
+
+func main() {
+ flag.Usage = func() { fmt.Fprintln(os.Stderr, usage) }
+ flag.Parse()
+
+ if len(flag.Args()) == 0 {
+ fmt.Fprintln(os.Stderr, usage)
+ os.Exit(1)
+ }
+
+ dbs := []string{"https://storage.googleapis.com/go-vulndb"}
+ if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
+ dbs = strings.Split(GOVULNDB, ",")
+ }
+
+ cfg := &packages.Config{
+ Mode: packages.LoadAllSyntax | packages.NeedModule,
+ }
+
+ findings, err := run(cfg, flag.Args(), *importsFlag, dbs)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "govulncheck: %s\n", err)
+ os.Exit(1)
+ }
+
+ sort.SliceStable(findings, func(i int, j int) bool { return audit.FindingCompare(findings[i], findings[j]) })
+ presentTo(os.Stdout, findings)
+}
+
+// presentTo pretty-prints findings to out.
+func presentTo(out io.Writer, findings []audit.Finding) {
+ if !*jsonFlag {
+ for _, finding := range findings {
+ finding.Write(out)
+ out.Write([]byte{'\n'})
+ }
+ return
+ }
+ b, err := json.MarshalIndent(findings, "", "\t")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "govulncheck: %s\n", err)
+ os.Exit(1)
+ }
+ out.Write(b)
+ out.Write([]byte{'\n'})
+}
+
+// allPkgPaths computes a list of all packages, in
+// the form of their paths, reachable from pkgs.
+func allPkgPaths(pkgs []*packages.Package) []string {
+ paths := make(map[string]bool)
+ for _, pkg := range pkgs {
+ pkgPaths(pkg, paths)
+ }
+
+ var ps []string
+ for p := range paths {
+ ps = append(ps, p)
+ }
+ return ps
+}
+
+func pkgPaths(pkg *packages.Package, paths map[string]bool) {
+ if _, ok := paths[pkg.PkgPath]; ok {
+ return
+ }
+ paths[pkg.PkgPath] = true
+ for _, imp := range pkg.Imports {
+ pkgPaths(imp, paths)
+ }
+}
+
+func isFile(path string) bool {
+ s, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return !s.IsDir()
+}
+
+func run(cfg *packages.Config, patterns []string, importsOnly bool, dbs []string) ([]audit.Finding, error) {
+ if len(patterns) == 1 && isFile(patterns[0]) {
+ packages, symbols, err := binscan.ExtractPackagesAndSymbols(patterns[0])
+ if err != nil {
+ return nil, err
+ }
+
+ paths := make([]string, 0, len(packages))
+ for pkg := range packages {
+ paths = append(paths, pkg)
+ }
+
+ vulns, err := audit.LoadVulnerabilities(dbs, paths)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load vulnerability dbs: %v", err)
+ }
+ env := audit.Env{OS: runtime.GOOS, Arch: runtime.GOARCH, PkgVersions: packages, Vulns: vulns}
+
+ return audit.VulnerablePackageSymbols(symbols, env), nil
+ }
+
+ // Load packages.
+ if *verboseFlag {
+ fmt.Println("loading packages...")
+ }
+ pkgs, err := packages.Load(cfg, patterns...)
+ if err != nil {
+ return nil, err
+ }
+ if packages.PrintErrors(pkgs) > 0 {
+ return nil, fmt.Errorf("packages contain errors")
+ }
+ if *verboseFlag {
+ fmt.Printf("\t%d loaded packages\n", len(pkgs))
+ }
+
+ // Load database.
+ if *verboseFlag {
+ fmt.Println("loading database...")
+ }
+ vulns, err := audit.LoadVulnerabilities(dbs, allPkgPaths(pkgs))
+ if err != nil {
+ return nil, fmt.Errorf("failed to load vulnerability dbs: %v", err)
+ }
+
+ if *verboseFlag {
+ fmt.Printf("\t%d known vulnerabilities.\n", len(vulns))
+ }
+
+ // Load package versions.
+ pkgVersions := audit.PackageVersions(pkgs)
+
+ // Load SSA.
+ if *verboseFlag {
+ fmt.Println("building ssa...")
+ }
+ prog, ssaPkgs := ssautil.AllPackages(pkgs, 0)
+ prog.Build()
+ if *verboseFlag {
+ fmt.Println("\tbuilt ssa.")
+ }
+
+ // Compute the findings.
+ if *verboseFlag {
+ fmt.Println("detecting vulnerabilities...")
+ }
+ var findings []audit.Finding
+ env := audit.Env{OS: runtime.GOOS, Arch: runtime.GOARCH, PkgVersions: pkgVersions, Vulns: vulns}
+ if importsOnly {
+ findings = audit.VulnerableImports(ssaPkgs, env)
+ } else {
+ findings = audit.VulnerableSymbols(ssaPkgs, env)
+ }
+ if *verboseFlag {
+ fmt.Printf("\t%d detected findings.\n", len(findings))
+ }
+ return findings, nil
+}
diff --git a/vulndb/govulncheck/main_test.go b/vulndb/govulncheck/main_test.go
new file mode 100644
index 0000000..abe99e9
--- /dev/null
+++ b/vulndb/govulncheck/main_test.go
@@ -0,0 +1,365 @@
+// 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 main
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/http"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "testing"
+
+ "golang.org/x/exp/vulndb/internal/audit"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/go/packages/packagestest"
+)
+
+// TODO(zpavlinovic): improve integration tests.
+
+// goYamlVuln contains vulnerability info for github.com/go-yaml/yaml package.
+var goYamlVuln string = `[{"ID":"GO-2020-0036","Published":"2021-04-14T12:00:00Z","Modified":"2021-04-14T12:00:00Z","Withdrawn":null,"Aliases":["CVE-2019-11254"],"Package":{"Name":"github.com/go-yaml/yaml","Ecosystem":"go"},"Details":"An attacker can craft malicious YAML which will consume significant\nsystem resources when Unmarshalled.\n","Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v2.2.8+incompatible"}]},"References":[{"Type":"code review","URL":"https://github.com/go-yaml/yaml/pull/555"},{"Type":"fix","URL":"https://github.com/go-yaml/yaml/commit/53403b58ad1b561927d19068c655246f2db79d48"},{"Type":"misc","URL":"https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=18496"}],"ecosystem_specific":{"Symbols":["yaml_parser_fetch_more_tokens"],"URL":"https://go.googlesource.com/vulndb/+/refs/heads/main/reports/GO-2020-0036.toml"}},{"ID":"GO-2021-0061","Published":"2021-04-14T12:00:00Z","Modified":"2021-04-14T12:00:00Z","Withdrawn":null,"Package":{"Name":"github.com/go-yaml/yaml","Ecosystem":"go"},"Details":"A maliciously crafted input can cause resource exhaustion due to\nalias chasing.\n","Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v2.2.3+incompatible"}]},"References":[{"Type":"code review","URL":"https://github.com/go-yaml/yaml/pull/375"},{"Type":"fix","URL":"https://github.com/go-yaml/yaml/commit/bb4e33bf68bf89cad44d386192cbed201f35b241"}],"ecosystem_specific":{"Symbols":["decoder.unmarshal"],"URL":"https://go.googlesource.com/vulndb/+/refs/heads/main/reports/GO-2021-0061.toml"}}]`
+
+// cryptoSSHVuln contains vulnerability info for golang.org/x/crypto/ssh.
+var cryptoSSHVuln string = `[{"ID":"GO-2020-0012","Published":"2021-04-14T12:00:00Z","Modified":"2021-04-14T12:00:00Z","Withdrawn":null,"Aliases":["CVE-2020-9283"],"Package":{"Name":"golang.org/x/crypto/ssh","Ecosystem":"go"},"Details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it.\n","Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v0.0.0-20200220183623-bac4c82f6975"}]},"References":[{"Type":"code review","URL":"https://go-review.googlesource.com/c/crypto/+/220357"},{"Type":"fix","URL":"https://github.com/golang/crypto/commit/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"Type":"misc","URL":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"}],"ecosystem_specific":{"Symbols":["parseED25519","ed25519PublicKey.Verify","parseSKEd25519","skEd25519PublicKey.Verify","NewPublicKey"],"URL":"https://go.googlesource.com/vulndb/+/refs/heads/main/reports/GO-2020-0012.toml"}},{"ID":"GO-2020-0013","Published":"2021-04-14T12:00:00Z","Modified":"2021-04-14T12:00:00Z","Withdrawn":null,"Aliases":["CVE-2017-3204"],"Package":{"Name":"golang.org/x/crypto/ssh","Ecosystem":"go"},"Details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\n[ClientConfig.HostKeyCallback] is not set.\n","Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v0.0.0-20170330155735-e4e2799dd7aa"}]},"References":[{"Type":"code review","URL":"https://go-review.googlesource.com/38701"},{"Type":"fix","URL":"https://github.com/golang/crypto/commit/e4e2799dd7aab89f583e1d898300d96367750991"},{"Type":"misc","URL":"https://github.com/golang/go/issues/19767"},{"Type":"misc","URL":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"}],"ecosystem_specific":{"Symbols":["NewClientConn"],"URL":"https://go.googlesource.com/vulndb/+/refs/heads/main/reports/GO-2020-0013.toml"}}]`
+
+// k8sAPIServerVuln contains vulnerability info for k8s.io/apiextensions-apiserver/pkg/apiserver.
+var k8sAPIServerVuln string = `[{"ID":"GO-2021-0062","Published":"2021-04-14T12:00:00Z","Modified":"2021-04-14T12:00:00Z","Withdrawn":null,"Aliases":["CVE-2019-11253"],"Package":{"Name":"k8s.io/apiextensions-apiserver/pkg/apiserver","Ecosystem":"go"},"Details":"A maliciously crafted YAML or JSON message can cause resource\nexhaustion.\n","Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v0.17.0"}]},"References":[{"Type":"code review","URL":"https://github.com/kubernetes/kubernetes/pull/83261"},{"Type":"fix","URL":"https://github.com/kubernetes/apiextensions-apiserver/commit/9cfd100448d12f999fbf913ae5d4fef2fcd66871"},{"Type":"misc","URL":"https://github.com/kubernetes/kubernetes/issues/83253"},{"Type":"misc","URL":"https://gist.github.com/bgeesaman/0e0349e94cd22c48bf14d8a9b7d6b8f2"}],"ecosystem_specific":{"Symbols":["NewCustomResourceDefinitionHandler"],"URL":"https://go.googlesource.com/vulndb/+/refs/heads/main/reports/GO-2021-0062.toml"}}]`
+
+// index for dbs containing some entries for each vuln package.
+// The timestamp for package is set to random moment in the past.
+var index string = `{
+ "k8s.io/apiextensions-apiserver/pkg/apiserver": "2021-01-01T12:00:00.000000000-08:00",
+ "golang.org/x/crypto/ssh": "2021-01-01T12:00:00.000000000-08:00",
+ "github.com/go-yaml/yaml": "2021-01-01T12:00:00.000000000-08:00"
+}`
+
+var vulns = map[string]string{
+ "github.com/go-yaml/yaml.json": goYamlVuln,
+ "golang.org/x/crypto/ssh.json": cryptoSSHVuln,
+ "k8s.io/apiextensions-apiserver/pkg/apiserver.json": k8sAPIServerVuln,
+}
+
+// addToLocalDb adds vuln for package p to local db at path db.
+func addToLocalDb(db, p, vuln string) error {
+ if err := os.MkdirAll(path.Join(db, filepath.Dir(p)), fs.ModePerm); err != nil {
+ return err
+ }
+
+ f, err := os.Create(path.Join(db, p))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ f.Write([]byte(vuln))
+ return nil
+}
+
+// addToServerDb adds vuln for package p to localhost server identified by its handler.
+func addToServerDb(handler *http.ServeMux, p, vuln string) {
+ handler.HandleFunc("/"+p, func(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, vuln) })
+}
+
+// envUpdate updates an environment e by setting the key to value.
+func envUpdate(e []string, key, value string) []string {
+ var nenv []string
+ for _, kv := range e {
+ if strings.HasPrefix(kv, key+"=") {
+ nenv = append(nenv, key+"="+value)
+ } else {
+ nenv = append(nenv, kv)
+ }
+ }
+ return nenv
+}
+
+// cmd type encapsulating a shell command and its context.
+type cmd struct {
+ dir string
+ env []string
+ name string
+ args []string
+}
+
+// execAll executes a sequence of commands cmd. Exits on a first
+// encountered error returning the error and the accumulated output.
+func execAll(cmds []cmd) ([]byte, error) {
+ var out []byte
+ for _, c := range cmds {
+ o, err := execCmd(c.dir, c.env, c.name, c.args...)
+ out = append(out, o...)
+ if err != nil {
+ return o, err
+ }
+ }
+ return out, nil
+}
+
+// execCmd runs the command name with arg in dir location with the env environment.
+func execCmd(dir string, env []string, name string, arg ...string) ([]byte, error) {
+ cmd := exec.Command(name, arg...)
+ cmd.Dir = dir
+ cmd.Env = env
+ return cmd.CombinedOutput()
+}
+
+// finding abstraction of Finding, for test purposes.
+type finding struct {
+ symbol string
+ traceLen int
+}
+
+func testFindings(finds []audit.Finding) []finding {
+ var fs []finding
+ for _, f := range finds {
+ fs = append(fs, finding{symbol: f.Symbol, traceLen: len(f.Trace)})
+ }
+ return fs
+}
+
+func subset(finds1, finds2 []finding) bool {
+ fs2 := make(map[finding]bool)
+ for _, f := range finds2 {
+ fs2[f] = true
+ }
+
+ for _, f := range finds1 {
+ if !fs2[f] {
+ return false
+ }
+ }
+ return true
+}
+
+func TestHashicorpVault(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
+ e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+ {
+ Name: "foo",
+ },
+ })
+ defer e.Cleanup()
+
+ hashiVaultOkta := "github.com/hashicorp/vault/builtin/credential/okta"
+
+ // Go get hashicorp-vault okta package v1.6.3.
+ env := envUpdate(e.Config.Env, "GOPROXY", "https://proxy.golang.org,direct")
+ if out, err := execCmd(e.Config.Dir, env, "go", "get", hashiVaultOkta+"@v1.6.3"); err != nil {
+ t.Logf("failed to get %s: %s", hashiVaultOkta+"@v1.6.3", out)
+ t.Fatal(err)
+ }
+
+ // run goaudit.
+ cfg := &packages.Config{
+ Mode: packages.LoadAllSyntax | packages.NeedModule,
+ Tests: false,
+ Dir: e.Config.Dir,
+ }
+
+ // Create a local filesystem db.
+ dbPath := path.Join(e.Config.Dir, "db")
+ addToLocalDb(dbPath, "index.json", index)
+ // Create a local server db.
+ sMux := http.NewServeMux()
+ s := http.Server{Addr: ":8080", Handler: sMux}
+ go func() { s.ListenAndServe() }()
+ defer func() { s.Shutdown(context.Background()) }()
+ addToServerDb(sMux, "index.json", index)
+
+ for _, test := range []struct {
+ source string
+ // list of packages whose vulns should be addded to source
+ toAdd []string
+ want []finding
+ }{
+ // test local db without yaml, which should result in no findings.
+ {source: "file://" + dbPath, want: nil,
+ toAdd: []string{"golang.org/x/crypto/ssh.json", "k8s.io/apiextensions-apiserver/pkg/apiserver.json"}},
+ // add yaml to the local db, which should produce 2 findings.
+ {source: "file://" + dbPath, toAdd: []string{"github.com/go-yaml/yaml.json"},
+ want: []finding{
+ {"github.com/go-yaml/yaml.decoder.unmarshal", 6},
+ {"github.com/go-yaml/yaml.yaml_parser_fetch_more_tokens", 12}},
+ },
+ // repeat the similar experiment with a server db.
+ {source: "http://localhost:8080", toAdd: []string{"k8s.io/apiextensions-apiserver/pkg/apiserver.json"}, want: nil},
+ {source: "http://localhost:8080", toAdd: []string{"golang.org/x/crypto/ssh.json", "github.com/go-yaml/yaml.json"},
+ want: []finding{
+ {"github.com/go-yaml/yaml.decoder.unmarshal", 6},
+ {"github.com/go-yaml/yaml.yaml_parser_fetch_more_tokens", 12}},
+ },
+ } {
+ for _, add := range test.toAdd {
+ if strings.HasPrefix(test.source, "file://") {
+ addToLocalDb(dbPath, add, vulns[add])
+ } else {
+ addToServerDb(sMux, add, vulns[add])
+ }
+ }
+
+ finds, err := run(cfg, []string{hashiVaultOkta}, false, []string{test.source})
+ if err != nil {
+ t.Fatal(err)
+ }
+ sort.SliceStable(finds, func(i int, j int) bool { return audit.FindingCompare(finds[i], finds[j]) })
+ if fs := testFindings(finds); !subset(test.want, fs) {
+ t.Errorf("want %v subset of findings; got %v", test.want, fs)
+ }
+ }
+}
+
+// isSecure checks if http resp was made over a secure connection.
+func isSecure(resp *http.Response) bool {
+ if resp.TLS == nil {
+ return false
+ }
+
+ // Check the final URL scheme too for good measure.
+ if resp.Request.URL.Scheme != "https" {
+ return false
+ }
+
+ return true
+}
+
+// download fetches the content at url and stores it at destination location.
+func download(url, destination string) error {
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+ }
+ client := &http.Client{Transport: tr}
+ resp, err := client.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if !isSecure(resp) {
+ return fmt.Errorf("insecure connection to %s", url)
+ }
+
+ out, err := os.Create(destination)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ _, err = io.Copy(out, resp.Body)
+ return err
+}
+
+// TestKubernetes requires the following system dependencies:
+// - make, tar, unzip, and gcc.
+// More information on installing kubernetes: https://github.com/kubernetes/kubernetes.
+// Note that the whole installation will require roughly 5GB of disk.
+func TestKubernetes(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode.")
+ }
+ e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+ {
+ Name: "foo",
+ },
+ })
+ defer e.Cleanup()
+
+ // Environments and directories to build and download both k8s and go.
+ env := envUpdate(e.Config.Env, "GOPROXY", "https://proxy.golang.org,direct")
+ dir := e.Config.Dir
+ k8sDir := path.Join(e.Config.Dir, "kubernetes-1.15.11")
+ k8sEnv := envUpdate(env, "PATH", path.Join(e.Config.Dir, "go/bin")+":"+os.Getenv("PATH"))
+
+ // Download kubernetes v1.15.11 and the go version 1.12 needed to build it.
+ if err := download("https://github.com/kubernetes/kubernetes/archive/v1.15.11.zip", path.Join(dir, "v1.15.11")); err != nil {
+ t.Fatal(err)
+ }
+ goZip := "go1.12.17." + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz"
+ if err := download("https://golang.org/dl/"+goZip, path.Join(dir, goZip)); err != nil {
+ t.Fatal(err)
+ }
+
+ // Unzip k8s and go, and then build the k8s.
+ if out, err := execAll([]cmd{
+ {dir, env, "unzip", []string{"v1.15.11"}},
+ {dir, env, "tar", []string{"-xf", goZip}},
+ {k8sDir, k8sEnv, "make", nil},
+ }); err != nil {
+ t.Logf("failed to build k8s: %s", out)
+ t.Fatal(err)
+ }
+
+ // Create a local filesystem db.
+ dbPath := path.Join(e.Config.Dir, "db")
+ addToLocalDb(dbPath, "index.json", index)
+ // Create a local server db.
+ sMux := http.NewServeMux()
+ s := http.Server{Addr: ":8080", Handler: sMux}
+ go func() { s.ListenAndServe() }()
+ defer func() { s.Shutdown(context.Background()) }()
+ addToServerDb(sMux, "index.json", index)
+
+ // run goaudit.
+ cfg := &packages.Config{
+ Mode: packages.LoadAllSyntax | packages.NeedModule,
+ Tests: false,
+ Dir: path.Join(e.Config.Dir, "kubernetes-1.15.11"),
+ }
+
+ for _, test := range []struct {
+ source string
+ // list of packages whose vulns should be addded to source
+ toAdd []string
+ want []finding
+ }{
+ // test local db with only apiserver vuln, which should result in a single finding.
+ {source: "file://" + dbPath, toAdd: []string{"github.com/go-yaml/yaml.json", "k8s.io/apiextensions-apiserver/pkg/apiserver.json"},
+ want: []finding{{"k8s.io/apiextensions-apiserver/pkg/apiserver.NewCustomResourceDefinitionHandler", 3}}},
+ // add the rest of the vulnerabilites, resulting in more findings.
+ {source: "file://" + dbPath, toAdd: []string{"golang.org/x/crypto/ssh.json"},
+ want: []finding{
+ {"golang.org/x/crypto/ssh.NewPublicKey", 1},
+ {"k8s.io/apiextensions-apiserver/pkg/apiserver.NewCustomResourceDefinitionHandler", 3},
+ {"golang.org/x/crypto/ssh.NewPublicKey", 4},
+ {"golang.org/x/crypto/ssh.parseED25519", 9},
+ }},
+ // repeat similar experiment with a server db.
+ {source: "http://localhost:8080", toAdd: []string{"github.com/go-yaml/yaml.json"}, want: nil},
+ {source: "http://localhost:8080", toAdd: []string{"golang.org/x/crypto/ssh.json", "k8s.io/apiextensions-apiserver/pkg/apiserver.json"},
+ want: []finding{
+ {"golang.org/x/crypto/ssh.NewPublicKey", 1},
+ {"k8s.io/apiextensions-apiserver/pkg/apiserver.NewCustomResourceDefinitionHandler", 3},
+ {"golang.org/x/crypto/ssh.NewPublicKey", 4},
+ {"golang.org/x/crypto/ssh.parseED25519", 9},
+ }},
+ } {
+ for _, add := range test.toAdd {
+ if strings.HasPrefix(test.source, "file://") {
+ addToLocalDb(dbPath, add, vulns[add])
+ } else {
+ addToServerDb(sMux, add, vulns[add])
+ }
+ }
+
+ finds, err := run(cfg, []string{"./..."}, false, []string{test.source})
+ if err != nil {
+ t.Fatal(err)
+ }
+ sort.SliceStable(finds, func(i int, j int) bool { return audit.FindingCompare(finds[i], finds[j]) })
+ if fs := testFindings(finds); !subset(test.want, fs) {
+ t.Errorf("want %v subset of findings; got %v", test.want, fs)
+ }
+ }
+}
diff --git a/vulndb/internal/audit/detect.go b/vulndb/internal/audit/detect.go
new file mode 100644
index 0000000..1e93412
--- /dev/null
+++ b/vulndb/internal/audit/detect.go
@@ -0,0 +1,231 @@
+// 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 audit finds vulnerabilities affecting Go packages.
+package audit
+
+import (
+ "fmt"
+ "go/token"
+ "io"
+
+ "golang.org/x/vulndb/osv"
+)
+
+// Preamble with types and common functionality used by vulnerability detection mechanisms in detect_*.go files.
+
+// Finding represents a finding for the use of a vulnerable symbol or an imported vulnerable package.
+// Provides info on symbol location, trace leading up to the symbol use, and associated vulnerabilities.
+type Finding struct {
+ Symbol string
+ Position *token.Position `json:",omitempty"`
+ Type SymbolType
+ Vulns []osv.Entry
+ Trace []TraceElem
+
+ // Approximate measure for indicating how useful the finding might be to the audit client.
+ // The smaller the weight, the more useful is the finding.
+ weight int
+}
+
+// SymbolType represents a type of a symbol use: function, global, or an import statement.
+type SymbolType int
+
+// enum values for SymbolType.
+const (
+ FunctionType SymbolType = iota
+ ImportType
+ GlobalType
+)
+
+// TraceElem represents an entry in the finding trace. Represents a function call or an import statement.
+type TraceElem struct {
+ Description string
+ Position *token.Position `json:",omitempty"`
+}
+
+// Env encapsulates information for querying if an imported symbol/package is vulnerable:
+// - platform info
+// - package versions
+// - vulnerability db
+type Env struct {
+ OS string
+ Arch string
+ PkgVersions map[string]string
+ Vulns []*osv.Entry
+}
+
+// Write method for findings showing the trace and the associated vulnerabilities.
+func (f Finding) Write(w io.Writer) {
+ var pos string
+ if f.Position != nil {
+ pos = fmt.Sprintf(" (%s)", f.Position)
+ }
+ fmt.Fprintf(w, "Trace:\n%s%s\n", f.Symbol, pos)
+ writeTrace(w, f.Trace)
+ io.WriteString(w, "\n")
+ writeVulns(w, f.Vulns)
+ io.WriteString(w, "\n")
+}
+
+// writeTrace in reverse order, e.g., entry point is written last.
+func writeTrace(w io.Writer, trace []TraceElem) {
+ for i := len(trace) - 1; i >= 0; i-- {
+ trace[i].Write(w)
+ io.WriteString(w, "\n")
+ }
+}
+
+func writeVulns(w io.Writer, vulns []osv.Entry) {
+ fmt.Fprintf(w, "Vulnerabilities:\n")
+ for _, v := range vulns {
+ fmt.Fprintf(w, "%s (%s)\n", v.Package.Name, v.EcosystemSpecific.URL)
+ }
+}
+
+func (e TraceElem) Write(w io.Writer) {
+ var pos string
+ if e.Position != nil {
+ pos = fmt.Sprintf(" (%s)", e.Position)
+ }
+ fmt.Fprintf(w, "%s%s", e.Description, pos)
+}
+
+// MarshalText implements the encoding.TextMarshaler interface.
+func (s SymbolType) MarshalText() ([]byte, error) {
+ var name string
+ switch s {
+ default:
+ name = "unrecognized"
+ case FunctionType:
+ name = "function"
+ case ImportType:
+ name = "import"
+ case GlobalType:
+ name = "global"
+ }
+ return []byte(name), nil
+}
+
+func matchingVulns(os, arch, version string, vulns []*osv.Entry) []*osv.Entry {
+ var matches []*osv.Entry
+ for _, vuln := range vulns {
+ if matchesPlatformAndVersion(os, arch, version, vuln) {
+ matches = append(matches, vuln)
+ }
+ }
+ return matches
+}
+
+// matchesPlatformAndVersion checks if `os`, `arch`, and `version` match the vulnerability `vuln`.
+func matchesPlatformAndVersion(os, arch, version string, vuln *osv.Entry) bool {
+ return matchesPlatform(os, vuln.EcosystemSpecific.GOOS) && matchesPlatform(arch, vuln.EcosystemSpecific.GOARCH) && vuln.Affects.AffectsSemver(version)
+}
+
+// matchesPlatform checks if `platform`, typically os or system architecture,
+// matches `platforms`. Empty `platforms` is also a match.
+func matchesPlatform(platform string, platforms []string) bool {
+ if len(platforms) == 0 {
+ return true
+ }
+
+ for _, p := range platforms {
+ if platform == p {
+ return true
+ }
+ }
+ return false
+}
+
+// pkgVulnerabilities map for fast lookup on vulnerable packages.
+// Maps package paths to their vulnerabilities.
+type pkgVulnerabilities map[string][]*osv.Entry
+
+// createPkgVulns creates a fast package-vulnerability look-up map for `vulns`.
+func createPkgVulns(vulns []*osv.Entry) pkgVulnerabilities {
+ pkgVulns := make(pkgVulnerabilities)
+ for _, vuln := range vulns {
+ pkgVulns[vuln.Package.Name] = append(pkgVulns[vuln.Package.Name], vuln)
+ }
+ return pkgVulns
+}
+
+// vulnerabilities returns a list of vulnerabilities that deem `pkgPath` vulnerable at `version` as well
+// as `arch` architecture and `os` operating system. Assumes version strings in `pkgVulns` are well-formed;
+// otherwise, the correctness of the results is not guaranteed.
+func (pkgVulns pkgVulnerabilities) vulnerabilities(pkgPath, version, arch, os string) []*osv.Entry {
+ vulns, ok := pkgVulns[pkgPath]
+ if !ok {
+ return nil
+ }
+ return matchingVulns(os, arch, version, vulns)
+}
+
+func queryPkgVulns(pkgPath string, env Env, pkgVulns pkgVulnerabilities) []*osv.Entry {
+ version, ok := env.PkgVersions[pkgPath]
+ if !ok {
+ return nil
+ }
+ return pkgVulns.vulnerabilities(pkgPath, version, env.Arch, env.OS)
+}
+
+// symVulnerabilities map for fast lookup on vulnerable symbols.
+// Maps package paths to symbols to their vulnerabilities.
+type symVulnerabilities map[string]map[string][]*osv.Entry
+
+// Represents any symbol. Used to model vulnerabilities in
+// symVulnerabilties that define every symbol as vulnerable.
+const symWildCard = "*"
+
+// createSymVulns creates a fast symbol-vulnerability look-up map for `vulns`.
+func createSymVulns(vulns []*osv.Entry) symVulnerabilities {
+ symVulns := make(symVulnerabilities)
+ for _, vuln := range vulns {
+ if len(vuln.EcosystemSpecific.Symbols) == 0 {
+ // If vuln.Symbols is empty, every symbol is vulnerable.
+ symVulns.add(symWildCard, vuln)
+ } else {
+ for _, sym := range vuln.EcosystemSpecific.Symbols {
+ symVulns.add(sym, vuln)
+ }
+ }
+ }
+ return symVulns
+}
+
+func (symVulns symVulnerabilities) add(symbol string, v *osv.Entry) {
+ syms := symVulns[v.Package.Name]
+ if syms == nil {
+ syms = make(map[string][]*osv.Entry)
+ symVulns[v.Package.Name] = syms
+ }
+ syms[symbol] = append(syms[symbol], v)
+}
+
+// vulnerabilities returns a list of vulnerabilities that deem `symbol` from package `pkgPath` vulnerable at
+// `version`, architecture `arch`, and operating system `os`. Assumes version strings in `symVulns` are well-formed;
+// otherwise, the correctness of the results is not guaranteed.
+func (symVulns symVulnerabilities) vulnerabilities(symbol, pkgPath, version, arch, os string) []*osv.Entry {
+ pkgVulns, ok := symVulns[pkgPath]
+ if !ok {
+ return nil
+ }
+
+ var vulns []*osv.Entry
+ vulns = append(vulns, pkgVulns[symbol]...)
+ vulns = append(vulns, pkgVulns[symWildCard]...)
+ if len(vulns) == 0 {
+ return nil
+ }
+
+ return matchingVulns(os, arch, version, vulns)
+}
+
+func querySymbolVulns(symbol, pkgPath string, symVulns symVulnerabilities, env Env) []*osv.Entry {
+ version, ok := env.PkgVersions[pkgPath]
+ if !ok {
+ return nil
+ }
+ return symVulns.vulnerabilities(symbol, pkgPath, version, env.Arch, env.OS)
+}
diff --git a/vulndb/internal/audit/detect_binary.go b/vulndb/internal/audit/detect_binary.go
new file mode 100644
index 0000000..b5dd324
--- /dev/null
+++ b/vulndb/internal/audit/detect_binary.go
@@ -0,0 +1,33 @@
+// 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 audit
+
+import (
+ "fmt"
+)
+
+// VulnerablePackageSymbols returns a list of vulnerability findings for per-package symbols
+// in packageSymbols, given the vulnerability and platform info captured in env.
+//
+// Returned Findings only have Symbol, Type, and Vulns fields set.
+func VulnerablePackageSymbols(packageSymbols map[string][]string, env Env) []Finding {
+ symVulns := createSymVulns(env.Vulns)
+
+ var findings []Finding
+ for pkg, symbols := range packageSymbols {
+ for _, symbol := range symbols {
+ if vulns := querySymbolVulns(symbol, pkg, symVulns, env); len(vulns) > 0 {
+ findings = append(findings,
+ Finding{
+ Symbol: fmt.Sprintf("%s.%s", pkg, symbol),
+ Type: GlobalType,
+ Vulns: serialize(vulns),
+ })
+ }
+ }
+ }
+
+ return findings
+}
diff --git a/vulndb/internal/audit/detect_callgraph.go b/vulndb/internal/audit/detect_callgraph.go
new file mode 100644
index 0000000..5adb8f5
--- /dev/null
+++ b/vulndb/internal/audit/detect_callgraph.go
@@ -0,0 +1,296 @@
+// 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 audit
+
+import (
+ "container/list"
+ "fmt"
+ "go/token"
+ "strings"
+
+ "golang.org/x/tools/go/callgraph"
+ "golang.org/x/tools/go/ssa"
+ "golang.org/x/tools/go/ssa/ssautil"
+
+ "golang.org/x/tools/go/callgraph/cha"
+ "golang.org/x/tools/go/callgraph/vta"
+)
+
+// VulnerableSymbols returns a list of vulnerability findings for symbols transitively reachable
+// through the callgraph built using VTA analysis from the entry points of pkgs, given the
+// vulnerability and platform info captured in env.
+//
+// Returns all findings reachable from pkgs while analyzing each package only once, prefering findings
+// of shorter import traces. For instance, given call chains
+// A() -> B() -> V
+// A() -> D() -> B() -> V
+// D() -> B() -> V
+// where A and D are top level packages and V is a vulnerable symbol, VulnerableSymbols can return either
+// A() -> B() -> V
+// or
+// D() -> B() -> V
+// as traces of transitively using a vulnerable symbol V.
+//
+// Panics if packages in pkgs do not belong to the same program.
+func VulnerableSymbols(pkgs []*ssa.Package, env Env) []Finding {
+ prog := pkgsProgram(pkgs)
+ if prog == nil {
+ panic("packages in pkgs must belong to a single common program")
+ }
+ entries := entryPoints(pkgs)
+ callGraph := callGraph(prog, entries)
+
+ queue := list.New()
+ for _, entry := range entries {
+ queue.PushBack(&callChain{f: entry})
+ }
+
+ symVulns := createSymVulns(env.Vulns)
+ var findings []Finding
+ seen := make(map[*ssa.Function]bool)
+ for queue.Len() > 0 {
+ front := queue.Front()
+ v := front.Value.(*callChain)
+ queue.Remove(front)
+
+ if seen[v.f] {
+ continue
+ }
+ seen[v.f] = true
+
+ finds, calls := funcVulnsAndCalls(v, symVulns, env, callGraph)
+ findings = append(findings, finds...)
+ for _, call := range calls {
+ queue.PushBack(call)
+ }
+ }
+
+ return findings
+}
+
+// callGraph builds a call graph of prog based on VTA analysis.
+func callGraph(prog *ssa.Program, entries []*ssa.Function) *callgraph.Graph {
+ entrySlice := make(map[*ssa.Function]bool)
+ for _, e := range entries {
+ entrySlice[e] = true
+ }
+ initial := cha.CallGraph(prog)
+ allFuncs := ssautil.AllFunctions(prog)
+
+ fslice := forwardReachableFrom(entrySlice, initial)
+ // Keep only actually linked functions.
+ pruneSlice(fslice, allFuncs)
+ vtaCg := vta.CallGraph(fslice, initial)
+
+ // Repeat the process once more, this time using
+ // the produced VTA call graph as the base graph.
+ fslice = forwardReachableFrom(entrySlice, vtaCg)
+ pruneSlice(fslice, allFuncs)
+
+ return vta.CallGraph(fslice, vtaCg)
+}
+
+func entryPoints(topPackages []*ssa.Package) []*ssa.Function {
+ var entries []*ssa.Function
+ for _, pkg := range topPackages {
+ if pkg.Pkg.Name() == "main" {
+ // for "main" packages the only valid entry points are the "main"
+ // function and any "init#" functions, even if there are other
+ // exported functions or types. similarly to isEntry it should be
+ // safe to ignore the validity of the main or init# signatures,
+ // since the compiler will reject malformed definitions,
+ // and the init function is synthetic
+ entries = append(entries, memberFuncs(pkg.Members["main"], pkg.Prog)...)
+ for name, member := range pkg.Members {
+ if strings.HasPrefix(name, "init#") || name == "init" {
+ entries = append(entries, memberFuncs(member, pkg.Prog)...)
+ }
+ }
+ continue
+ }
+ for _, member := range pkg.Members {
+ for _, f := range memberFuncs(member, pkg.Prog) {
+ if isEntry(f) {
+ entries = append(entries, f)
+ }
+ }
+ }
+ }
+ return entries
+}
+
+func isEntry(f *ssa.Function) bool {
+ // it should be safe to ignore checking that the signature of the "init" function
+ // is valid, since it is synthetic
+ if f.Name() == "init" && f.Synthetic == "package initializer" {
+ return true
+ }
+
+ return f.Synthetic == "" && f.Object() != nil && f.Object().Exported()
+}
+
+// callChain helps doing BFS over package call graph while remembering the call stack.
+type callChain struct {
+ // nil for entry points of the chain.
+ call ssa.CallInstruction
+ f *ssa.Function
+ parent *callChain
+}
+
+func (chain *callChain) trace() []TraceElem {
+ if chain == nil {
+ return nil
+ }
+
+ var pos *token.Position
+ desc := fmt.Sprintf("%s.%s(...)", pkgPath(chain.f), chain.f.Name())
+ if chain.call != nil {
+ pos = instrPosition(chain.call)
+ if unresolved(chain.call) {
+ // In case of a statically unresolved call site, communicate to the client
+ // that this was approximatelly resolved to chain.f.
+ desc = fmt.Sprintf("%s(...) [approx. resolved to %s]", callName(chain.call), chain.f)
+ }
+ } else {
+ // No call information means the function is an entry point.
+ pos = funcPosition(chain.f)
+ }
+
+ return append(chain.parent.trace(), TraceElem{Description: desc, Position: pos})
+}
+
+// weight computes an approximate measure of how useful the call chain would
+// be to the client as a trace. The smaller the value, the more useful the chain.
+// Currently defined as the number of unresolved call sites in the chain.
+func (chain *callChain) weight() int {
+ if chain == nil {
+ return 0
+ }
+
+ callWeight := 0
+ if unresolved(chain.call) {
+ callWeight = 1
+ }
+ return callWeight + chain.parent.weight()
+}
+
+// funcVulnsAndCalls returns a list of symbol findings for function at the top
+// of chain and next calls to analyze.
+func funcVulnsAndCalls(chain *callChain, symVulns symVulnerabilities, env Env, callGraph *callgraph.Graph) ([]Finding, []*callChain) {
+ var findings []Finding
+ var calls []*callChain
+ for _, b := range chain.f.Blocks {
+ for _, instr := range b.Instrs {
+ // First collect all findings for globals except callees in function call statements.
+ findings = append(findings, globalFindings(globalUses(instr), chain, symVulns, env)...)
+
+ // Callees are handled separately to produce call findings rather than global findings.
+ site, ok := instr.(ssa.CallInstruction)
+ if !ok {
+ continue
+ }
+
+ callees := siteCallees(site, callGraph)
+ for _, callee := range callees {
+ c := &callChain{call: site, f: callee, parent: chain}
+ calls = append(calls, c)
+
+ if f := callFinding(c, symVulns, env); f != nil {
+ findings = append(findings, *f)
+ }
+ }
+ }
+ }
+ return findings, calls
+}
+
+// globalFindings returns findings for vulnerable globals among globalUses.
+// Assumes each use in globalUses is a use of a global variable. Can generate
+// duplicates when globalUses contains duplicates.
+func globalFindings(globalUses []*ssa.Value, chain *callChain, symVulns symVulnerabilities, env Env) []Finding {
+ if underRelatedVuln(chain, symVulns, env) {
+ return nil
+ }
+
+ var findings []Finding
+ for _, o := range globalUses {
+ g := (*o).(*ssa.Global)
+ vulns := querySymbolVulns(g.Name(), g.Package().Pkg.Path(), symVulns, env)
+ if len(vulns) > 0 {
+ findings = append(findings,
+ Finding{
+ Symbol: fmt.Sprintf("%s.%s", g.Package().Pkg.Path(), g.Name()),
+ Trace: chain.trace(),
+ Position: valPosition(*o, chain.f),
+ Type: GlobalType,
+ Vulns: serialize(vulns),
+ weight: chain.weight()})
+ }
+ }
+ return findings
+}
+
+// callFinding returns vulnerability finding for the call made at the top of the chain.
+// If there is no vulnerability or no call information, then nil is returned.
+// TODO(zpavlinovic): remove ssa info from higher-order calls.
+func callFinding(chain *callChain, symVulns symVulnerabilities, env Env) *Finding {
+ if underRelatedVuln(chain, symVulns, env) {
+ return nil
+ }
+
+ callee := chain.f
+ call := chain.call
+ if callee == nil || call == nil {
+ return nil
+ }
+
+ vulns := querySymbolVulns(dbFuncName(callee), callee.Package().Pkg.Path(), symVulns, env)
+ if len(vulns) > 0 {
+ c := chain
+ if !unresolved(call) {
+ // If the last call is a resolved callsite, remove the edge from the trace as that
+ // information is provided in the symbol field.
+ c = c.parent
+ }
+ return &Finding{
+ Symbol: fmt.Sprintf("%s.%s", callee.Package().Pkg.Path(), dbFuncName(callee)),
+ Trace: c.trace(),
+ Position: instrPosition(call),
+ Type: FunctionType,
+ Vulns: serialize(vulns),
+ weight: c.weight()}
+ }
+
+ return nil
+}
+
+// Checks if a potential vulnerability in chain.f is analyzed only because
+// a previous vulnerability in the same package as chain.f has been seen.
+// For instance, for the chain P1:A -> P2:B -> P2:C where both B and C are
+// vulnerable, the function returns true since B is already vulnerable and
+// has hence been reported. Clients are likely not interested in vulnerabilties
+// inside of a function that is already deemed vulnerable. This is an optimization
+// step to stop flooding of findings when a package has a lot of known vulnerable
+// symbols (e.g., all of them).
+//
+// Note that for P1:A -> P2:B -> P3:D -> P2:C the function returns false. This
+// is because C is called from D that comes from a different package.
+func underRelatedVuln(chain *callChain, symVulns symVulnerabilities, env Env) bool {
+ pkg := pkgPath(chain.f)
+
+ c := chain
+ for {
+ c = c.parent
+ // Analyze the immediate substack related to pkg.
+ if c == nil || pkgPath(c.f) != pkg {
+ break
+ }
+ // TODO: can we optimize using the information on findings already reported?
+ if len(querySymbolVulns(dbFuncName(c.f), c.f.Pkg.Pkg.Path(), symVulns, env)) > 0 {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vulndb/internal/audit/detect_callgraph_test.go b/vulndb/internal/audit/detect_callgraph_test.go
new file mode 100644
index 0000000..a2723f7
--- /dev/null
+++ b/vulndb/internal/audit/detect_callgraph_test.go
@@ -0,0 +1,80 @@
+// 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 audit
+
+import (
+ "go/token"
+ "reflect"
+ "sort"
+ "testing"
+
+ "golang.org/x/vulndb/osv"
+)
+
+func TestSymbolVulnDetectionVTA(t *testing.T) {
+ pkgs, env := testProgAndEnv(t)
+ got := projectFindings(VulnerableSymbols(pkgs, env))
+
+ // There should be four call chains reported with VTA-VTA version, in the following order:
+ // T:T1() -> vuln.VG [use of global at line 4]
+ // T:T1() -> A:A1() -> vuln.VulnData.Vuln() [call at A.go:14]
+ // T:T2() -> vuln.Vuln() [approx.resolved] -> vuln.VG [use of global at vuln.go:4]
+ // T:T1() -> vuln.VulnData.Vuln() [approx. resolved] [call at testdata.go:13]
+ // Without VTA-VTA, we would alse have the following false positive:
+ // T:T2() -> vuln.VulnData.Vuln() [approx. resolved] [call at testdata.go:26]
+ want := []Finding{
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln.VG",
+ Trace: []TraceElem{
+ {Description: "command-line-arguments.T1(...)", Position: &token.Position{Line: 11, Filename: "T.go"}},
+ },
+ Type: GlobalType,
+ Position: &token.Position{Line: 5, Filename: "vuln.go"},
+ Vulns: []osv.Entry{{Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 0,
+ },
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln.VulnData.Vuln",
+ Trace: []TraceElem{
+ {Description: "command-line-arguments.T1(...)", Position: &token.Position{Line: 11, Filename: "T.go"}},
+ {Description: "a.org/A.A1(...)", Position: &token.Position{Line: 14, Filename: "T.go"}}},
+ Type: FunctionType,
+ Position: &token.Position{Line: 15, Filename: "A.go"},
+ Vulns: []osv.Entry{{Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 0,
+ },
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln.VG",
+ Trace: []TraceElem{
+ {Description: "command-line-arguments.T2(...)", Position: &token.Position{Line: 20, Filename: "T.go"}},
+ {Description: "command-line-arguments.t0(...) [approx. resolved to thirdparty.org/vulnerabilities/vuln.Vuln]", Position: &token.Position{Line: 22, Filename: "T.go"}},
+ },
+ Type: GlobalType,
+ Position: &token.Position{Line: 5, Filename: "vuln.go"},
+ Vulns: []osv.Entry{{Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 1,
+ },
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln.VulnData.Vuln",
+ Trace: []TraceElem{
+ {Description: "command-line-arguments.T1(...)", Position: &token.Position{Line: 11, Filename: "T.go"}},
+ {Description: "a.org/A.I.Vuln(...) [approx. resolved to (thirdparty.org/vulnerabilities/vuln.VulnData).Vuln]", Position: &token.Position{Line: 14, Filename: "T.go"}}},
+ Type: FunctionType,
+ Position: &token.Position{Line: 14, Filename: "T.go"},
+ Vulns: []osv.Entry{{Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 1,
+ },
+ }
+
+ if len(want) != len(got) {
+ t.Errorf("want %d findings; got %d", len(want), len(got))
+ return
+ }
+
+ sort.SliceStable(got, func(i int, j int) bool { return FindingCompare(got[i], got[j]) })
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("want %v findings (projected); got %v", want, got)
+ }
+}
diff --git a/vulndb/internal/audit/detect_imports.go b/vulndb/internal/audit/detect_imports.go
new file mode 100644
index 0000000..40a1b1f
--- /dev/null
+++ b/vulndb/internal/audit/detect_imports.go
@@ -0,0 +1,81 @@
+// 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 audit
+
+import (
+ "container/list"
+ "go/types"
+
+ "golang.org/x/tools/go/ssa"
+)
+
+// VulnerableImports returns a list of vulnerability findings for packages imported by `pkgs`
+// given the vulnerability and platform info captured in `env`.
+//
+// Returns all findings reachable from `pkgs` while analyzing each package only once, prefering
+// findings of shorter import traces. For instance, given import chains
+// A -> B -> V
+// A -> D -> B -> V
+// D -> B -> V
+// where A and D are top level packages and V is a vulnerable package, VulnerableImports can return either
+// A -> B -> V
+// or
+// D -> B -> V
+// as traces of importing a vulnerable package V.
+func VulnerableImports(pkgs []*ssa.Package, env Env) []Finding {
+ pkgVulns := createPkgVulns(env.Vulns)
+
+ var findings []Finding
+ seen := make(map[string]bool)
+ queue := list.New()
+ for _, pkg := range pkgs {
+ queue.PushBack(&importChain{pkg: pkg.Pkg})
+ }
+
+ for queue.Len() > 0 {
+ front := queue.Front()
+ v := front.Value.(*importChain)
+ queue.Remove(front)
+
+ pkg := v.pkg
+ if pkg == nil {
+ continue
+ }
+
+ if seen[pkg.Path()] {
+ continue
+ }
+ seen[pkg.Path()] = true
+
+ for _, imp := range pkg.Imports() {
+ vulns := queryPkgVulns(imp.Path(), env, pkgVulns)
+ if len(vulns) > 0 {
+ findings = append(findings,
+ Finding{
+ Symbol: imp.Path(),
+ Type: ImportType,
+ Trace: v.trace(),
+ Vulns: serialize(vulns),
+ weight: len(v.trace())})
+ }
+ queue.PushBack(&importChain{pkg: imp, parent: v})
+ }
+ }
+
+ return findings
+}
+
+// importChain helps doing BFS over package imports while remembering import chains.
+type importChain struct {
+ pkg *types.Package
+ parent *importChain
+}
+
+func (chain *importChain) trace() []TraceElem {
+ if chain == nil {
+ return nil
+ }
+ return append(chain.parent.trace(), TraceElem{Description: chain.pkg.Path()})
+}
diff --git a/vulndb/internal/audit/detect_imports_test.go b/vulndb/internal/audit/detect_imports_test.go
new file mode 100644
index 0000000..0e36097
--- /dev/null
+++ b/vulndb/internal/audit/detect_imports_test.go
@@ -0,0 +1,52 @@
+// 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 audit
+
+import (
+ "reflect"
+ "sort"
+ "testing"
+
+ "golang.org/x/vulndb/osv"
+)
+
+func TestImportedPackageVulnDetection(t *testing.T) {
+ pkgs, env := testProgAndEnv(t)
+ got := projectFindings(VulnerableImports(pkgs, env))
+
+ // There should be two chains reported in the following order:
+ // T -> vuln
+ // T -> A -> vuln
+ want := []Finding{
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln",
+ Trace: []TraceElem{{Description: "command-line-arguments"}},
+ Type: ImportType,
+ Vulns: []osv.Entry{
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}},
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 1,
+ },
+ {
+ Symbol: "thirdparty.org/vulnerabilities/vuln",
+ Trace: []TraceElem{{Description: "command-line-arguments"}, {Description: "a.org/A"}},
+ Type: ImportType,
+ Vulns: []osv.Entry{
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}},
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}}},
+ weight: 2,
+ },
+ }
+
+ if len(want) != len(got) {
+ t.Errorf("want %d findings; got %d", len(want), len(got))
+ return
+ }
+
+ sort.SliceStable(got, func(i int, j int) bool { return FindingCompare(got[i], got[j]) })
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("want %v findings (projected); got %v", want, got)
+ }
+}
diff --git a/vulndb/internal/audit/detect_test.go b/vulndb/internal/audit/detect_test.go
new file mode 100644
index 0000000..a90c7e3
--- /dev/null
+++ b/vulndb/internal/audit/detect_test.go
@@ -0,0 +1,122 @@
+// 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 audit
+
+import (
+ "testing"
+
+ "golang.org/x/vulndb/osv"
+)
+
+var testVulnerabilities = []*osv.Entry{
+ {
+ Package: osv.Package{
+ Name: "xyz.org/vuln",
+ },
+ Affects: osv.Affects{
+ Ranges: []osv.AffectsRange{
+ {
+ Type: osv.TypeSemver,
+ Introduced: "v1.0.1",
+ Fixed: "v3.2.6",
+ },
+ },
+ },
+ EcosystemSpecific: osv.GoSpecific{
+ Symbols: []string{"foo", "bar"},
+ GOOS: []string{"amd64"},
+ GOARCH: []string{"linux"},
+ },
+ },
+ {
+ Package: osv.Package{
+ Name: "xyz.org/vuln",
+ },
+ Affects: osv.Affects{
+ Ranges: []osv.AffectsRange{
+ {
+ Type: osv.TypeSemver,
+ Fixed: "v4.0.0",
+ },
+ },
+ },
+ EcosystemSpecific: osv.GoSpecific{
+ Symbols: []string{"foo"},
+ },
+ },
+ {
+ Package: osv.Package{
+ Name: "abc.org/morevuln",
+ },
+ },
+}
+
+func TestPackageVulnCreationAndChecking(t *testing.T) {
+ pkgVulns := createPkgVulns(testVulnerabilities)
+ if len(pkgVulns) != 2 {
+ t.Errorf("want 2 package paths; got %d", len(pkgVulns))
+ }
+
+ for _, test := range []struct {
+ path string
+ version string
+ os string
+ arch string
+ noVulns int
+ }{
+ // xyz.org/vuln has foo and bar vulns for linux, and just foo for windows.
+ {"xyz.org/vuln", "v1.0.1", "amd64", "linux", 2},
+ {"xyz.org/vuln", "v1.0.1", "amd64", "windows", 1},
+ {"xyz.org/vuln", "v2.4.5", "amd64", "linux", 2},
+ {"xyz.org/vuln", "v3.2.7", "amd64", "linux", 1},
+ // foo for linux must be at version before v4.0.0.
+ {"xyz.org/vuln", "v5.4.5", "amd64", "linux", 0},
+ // abc.org/morevuln has vulnerabilities for any symbol, platform, and version
+ {"abc.org/morevuln", "v11.0.1", "amd64", "linux", 1},
+ {"abc.org/morevuln", "v300.0.1", "i386", "windows", 1},
+ } {
+ if vulns := pkgVulns.vulnerabilities(test.path, test.version, test.arch, test.os); len(vulns) != test.noVulns {
+ t.Errorf("want %d vulnerabilities for %s (v:%s, o:%s, a:%s); got %d",
+ test.noVulns, test.path, test.version, test.os, test.path, len(vulns))
+ }
+ }
+}
+
+func TestSymbolVulnCreationAndChecking(t *testing.T) {
+ symVulns := createSymVulns(testVulnerabilities)
+ if len(symVulns) != 2 {
+ t.Errorf("want 2 package paths; got %d", len(symVulns))
+ }
+
+ for _, test := range []struct {
+ symbol string
+ path string
+ version string
+ os string
+ arch string
+ numVulns int
+ }{
+ // foo appears twice as a vulnerable symbol for "xyz.org/vuln" and bar once.
+ {"foo", "xyz.org/vuln", "v1.0.1", "amd64", "linux", 2},
+ {"bar", "xyz.org/vuln", "v1.0.1", "amd64", "linux", 1},
+ // foo and bar detected vulns should go down by one for windows platform as well as i386 architecture.
+ {"foo", "xyz.org/vuln", "v1.0.1", "amd64", "windows", 1},
+ {"bar", "xyz.org/vuln", "v1.0.1", "i386", "linux", 0},
+ // There should be no findings for foo and bar at module version v5.0.0.
+ {"foo", "xyz.org/vuln", "v5.0.0", "amd64", "linux", 0},
+ {"bar", "xyz.org/vuln", "v5.0.0", "amd64", "linux", 0},
+ // symbol is not a vulnerable symbol for xyz.org/vuln and bogus package is not in the database.
+ {"symbol", "xyz.org/vuln", "v1.0.1", "amd64", "linux", 0},
+ {"foo", "bogus", "v1.0.1", "amd64", "linux", 0},
+ // abc.org/morevuln has vulnerabilities for any symbol, platform, and version
+ {"symbol", "abc.org/morevuln", "v2.0.1", "amd64", "linux", 1},
+ {"lobmys", "abc.org/morevuln", "v300.0.1", "i386", "windows", 1},
+ } {
+ if vulns := symVulns.vulnerabilities(test.symbol, test.path, test.version, test.arch, test.os); len(vulns) != test.numVulns {
+ t.Errorf("want %d vulnerabilities for %s (p:%s v:%s, o:%s, a:%s); got %d",
+ test.numVulns, test.symbol, test.path, test.version, test.os, test.arch, len(vulns))
+ }
+ }
+}
diff --git a/vulndb/internal/audit/helpers_test.go b/vulndb/internal/audit/helpers_test.go
new file mode 100644
index 0000000..f00cd7e
--- /dev/null
+++ b/vulndb/internal/audit/helpers_test.go
@@ -0,0 +1,165 @@
+// 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 audit
+
+import (
+ "go/token"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/go/packages/packagestest"
+ "golang.org/x/tools/go/ssa"
+ "golang.org/x/tools/go/ssa/ssautil"
+ "golang.org/x/vulndb/osv"
+)
+
+// Loads test program and environment with the following import structure
+// T
+// / | \
+// A | B
+// \ | /
+// \ | A
+// \ | /
+// vuln
+// where `vuln` is a package containing some vulnerabilities. The definition
+// of T can be found in testdata/top_package.go, A is in testdata/a_dep.go,
+// B is in testdata/b_dep.go, and vuln is in testdata/vuln.go.
+//
+// The program has the following vulnerabilities that should be reported
+// T:T1() -> vuln.VG
+// T:T1() -> A:A1() -> vuln.VulnData.Vuln()
+// T:T2() -> vuln.Vuln() [approx.resolved] -> vuln.VG
+// T:T1() -> vuln.VulnData.Vuln() [approx. resolved]
+//
+// The following vulnerability should not be reported as it is redundant:
+// T:T1() -> A:A1() -> B:B1() -> vuln.VulnData.Vuln()
+//
+// The produced environment is based on testdata/dbs vulnerability databases.
+func testProgAndEnv(t *testing.T) ([]*ssa.Package, Env) {
+ e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+ {
+ Name: "golang.org/vulntest",
+ Files: map[string]interface{}{"T/T.go": readFile(t, "testdata/top_package.go")},
+ },
+ {
+ Name: "a.org@v1.1.1",
+ Files: map[string]interface{}{"A/A.go": readFile(t, "testdata/a_dep.go")},
+ },
+ {
+ Name: "b.org@v1.2.2",
+ Files: map[string]interface{}{"B/B.go": readFile(t, "testdata/b_dep.go")},
+ },
+ {
+ Name: "thirdparty.org/vulnerabilities@v1.0.1",
+ Files: map[string]interface{}{"vuln/vuln.go": readFile(t, "testdata/vuln.go")},
+ },
+ })
+ defer e.Cleanup()
+
+ _, ssaPkgs, pkgs, err := loadAndBuildPackages(e, "/vulntest/T/T.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ssaPkgs) != 1 {
+ t.Errorf("want 1 top level SSA package; got %d", len(ssaPkgs))
+ }
+
+ vulnsToLoad := []string{"thirdparty.org/vulnerabilities", "bogus.org/module"}
+ dbSources := []string{fileSource(t, "testdata/dbs/bogus.db.org"), fileSource(t, "testdata/dbs/golang.deepgo.org")}
+ vulns, err := LoadVulnerabilities(dbSources, vulnsToLoad)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return ssaPkgs, Env{OS: "linux", Arch: "amd64", Vulns: vulns, PkgVersions: PackageVersions(pkgs)}
+}
+
+func loadAndBuildPackages(e *packagestest.Exported, file string) (*ssa.Program, []*ssa.Package, []*packages.Package, error) {
+ e.Config.Mode |= packages.NeedModule | packages.LoadAllSyntax
+ // Get the path to the test file.
+ filepath := path.Join(e.Temp(), file)
+ pkgs, err := packages.Load(e.Config, filepath)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ prog, ssaPkgs := ssautil.AllPackages(pkgs, 0)
+ prog.Build()
+ return prog, ssaPkgs, pkgs, nil
+}
+
+// projectPosition simplifies position to only filename and location info.
+func projectPosition(pos *token.Position) *token.Position {
+ if pos == nil {
+ return nil
+ }
+ fname := pos.Filename
+ if fname != "" {
+ fname = filepath.Base(fname)
+ }
+ return &token.Position{Line: pos.Line, Filename: fname}
+}
+
+// projectTrace simplifies traces for testing comparison purposes
+// by simplifying position info.
+func projectTrace(trace []TraceElem) []TraceElem {
+ var nt []TraceElem
+ for _, e := range trace {
+ nt = append(nt, TraceElem{Description: e.Description, Position: projectPosition(e.Position)})
+ }
+ return nt
+}
+
+// projectVulns simplifies vulnerabilities for testing comparison purposes
+// to only package path.
+func projectVulns(vulns []osv.Entry) []osv.Entry {
+ var nv []osv.Entry
+ for _, v := range vulns {
+ nv = append(nv, osv.Entry{Package: osv.Package{Name: v.Package.Name}})
+ }
+ return nv
+}
+
+// projectFindings simplifies findings for testing comparison purposes. Traces
+// are removed their position info, finding's position only contains file and
+// line info, and vulnerabilities only have package path.
+func projectFindings(findings []Finding) []Finding {
+ var nfs []Finding
+ for _, f := range findings {
+ nf := Finding{
+ Type: f.Type,
+ Symbol: f.Symbol,
+ Position: projectPosition(f.Position),
+ Trace: projectTrace(f.Trace),
+ Vulns: projectVulns(f.Vulns),
+ weight: f.weight,
+ }
+ nfs = append(nfs, nf)
+ }
+ return nfs
+}
+
+// fileSource creates a file URI for a database path `db`. If `db` is
+// relative, the source is made absolute w.r.t. the current directory.
+func fileSource(t *testing.T, db string) string {
+ cd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return "file://" + path.Join(cd, db)
+}
+
+func readFile(t *testing.T, path string) string {
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Fatalf("failed to load code from `%v`: %v", path, err)
+ }
+ return strings.ReplaceAll(string(content), "// go:build ignore", "")
+}
diff --git a/vulndb/internal/audit/order.go b/vulndb/internal/audit/order.go
new file mode 100644
index 0000000..0f595f7
--- /dev/null
+++ b/vulndb/internal/audit/order.go
@@ -0,0 +1,66 @@
+// 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 audit
+
+import (
+ "fmt"
+ "strings"
+)
+
+// FindingCompare compares two findings in terms of their approximate usefulness to the user.
+// A finding that either has 1) shorter trace, or 2) less unresolved call sites in the trace
+// is considered smaller, i.e., better.
+func FindingCompare(finding1, finding2 Finding) bool {
+ if len(finding1.Trace) < len(finding2.Trace) {
+ return true
+ } else if len(finding2.Trace) < len(finding1.Trace) {
+ return false
+ }
+ if finding1.weight < finding2.weight {
+ return true
+ } else if finding2.weight < finding1.weight {
+ return false
+ }
+ // At this point we just need to make sure the ordering is deterministic.
+ // TODO(zpavlinovic): is there a more meaningful ordering?
+ return findingStrCompare(finding1, finding2)
+}
+
+// findingStrCompare compares string representation of findings pointwise by fields.
+func findingStrCompare(finding1, finding2 Finding) bool {
+ symCmp := strings.Compare(finding1.Symbol, finding2.Symbol)
+ if symCmp == -1 {
+ return true
+ } else if symCmp == 1 {
+ return false
+ }
+
+ typeStr1 := fmt.Sprintf("%v", finding1.Type)
+ typeStr2 := fmt.Sprintf("%v", finding2.Type)
+ typeCmp := strings.Compare(typeStr1, typeStr2)
+ if typeCmp == -1 {
+ return true
+ } else if typeCmp == 1 {
+ return false
+ }
+
+ posStr1 := fmt.Sprintf("%v", finding1.Position)
+ posStr2 := fmt.Sprintf("%v", finding2.Position)
+ posCmp := strings.Compare(posStr1, posStr2)
+ if posCmp == -1 {
+ return true
+ } else if posCmp == 1 {
+ return false
+ }
+
+ traceStr1 := fmt.Sprintf("%v", finding1.Trace)
+ traceStr2 := fmt.Sprintf("%v", finding2.Trace)
+ traceCmp := strings.Compare(traceStr1, traceStr2)
+ if traceCmp == 1 {
+ return false
+ }
+
+ return true
+}
diff --git a/vulndb/internal/audit/slicing.go b/vulndb/internal/audit/slicing.go
new file mode 100644
index 0000000..b976f16
--- /dev/null
+++ b/vulndb/internal/audit/slicing.go
@@ -0,0 +1,54 @@
+// 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 audit
+
+import (
+ "golang.org/x/tools/go/callgraph"
+ "golang.org/x/tools/go/ssa"
+)
+
+// forwardReachableFrom computes the set of functions forward reachable from `sources`.
+// A function f is reachable from a function g if f is an anonymous function defined
+// in g or a function called in g as given by the callgraph `cg`.
+func forwardReachableFrom(sources map[*ssa.Function]bool, cg *callgraph.Graph) map[*ssa.Function]bool {
+ m := make(map[*ssa.Function]bool)
+ for s := range sources {
+ forward(s, cg, m)
+ }
+ return m
+}
+
+func forward(f *ssa.Function, cg *callgraph.Graph, seen map[*ssa.Function]bool) {
+ if _, ok := seen[f]; ok {
+ return
+ }
+ seen[f] = true
+ var buf [10]*ssa.Value // avoid alloc in common case
+ for _, b := range f.Blocks {
+ for _, instr := range b.Instrs {
+ switch i := instr.(type) {
+ case ssa.CallInstruction:
+ for _, c := range siteCallees(i, cg) {
+ forward(c, cg, seen)
+ }
+ default:
+ for _, op := range i.Operands(buf[:0]) {
+ if fn, ok := (*op).(*ssa.Function); ok {
+ forward(fn, cg, seen)
+ }
+ }
+ }
+ }
+ }
+}
+
+// pruneSlice removes functions in `slice` that are in `toPrune`.
+func pruneSlice(slice map[*ssa.Function]bool, toPrune map[*ssa.Function]bool) {
+ for f := range slice {
+ if _, ok := toPrune[f]; !ok {
+ delete(slice, f)
+ }
+ }
+}
diff --git a/vulndb/internal/audit/slicing_test.go b/vulndb/internal/audit/slicing_test.go
new file mode 100644
index 0000000..782b6c3
--- /dev/null
+++ b/vulndb/internal/audit/slicing_test.go
@@ -0,0 +1,55 @@
+// 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 audit
+
+import (
+ "reflect"
+ "testing"
+
+ "golang.org/x/tools/go/callgraph/cha"
+ "golang.org/x/tools/go/packages/packagestest"
+ "golang.org/x/tools/go/ssa"
+)
+
+// funcsToString returns a set of function names for `funcs`.
+func funcsToString(funcs map[*ssa.Function]bool) map[string]bool {
+ fs := make(map[string]bool)
+ for f := range funcs {
+ fs[dbFuncName(f)] = true
+ }
+ return fs
+}
+
+func TestSlicing(t *testing.T) {
+ e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+ {
+ Name: "some/module",
+ Files: map[string]interface{}{"slice/slice.go": readFile(t, "testdata/slice.go")},
+ },
+ })
+ prog, pkgs, _, err := loadAndBuildPackages(e, "/module/slice/slice.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pkg := pkgs[0]
+ sources := map[*ssa.Function]bool{pkg.Func("Apply"): true, pkg.Func("Do"): true}
+ fs := funcsToString(forwardReachableFrom(sources, cha.CallGraph(prog)))
+ want := map[string]bool{
+ "Apply": true,
+ "Apply$1": true,
+ "X": true,
+ "Y": true,
+ "Do": true,
+ "Do$1": true,
+ "Do$1$1": true,
+ "debug": true,
+ "A.Foo": true,
+ "B.Foo": true,
+ }
+ if !reflect.DeepEqual(want, fs) {
+ t.Errorf("want %v; got %v", want, fs)
+ }
+}
diff --git a/vulndb/internal/audit/testdata/a_dep.go b/vulndb/internal/audit/testdata/a_dep.go
new file mode 100644
index 0000000..56e3664
--- /dev/null
+++ b/vulndb/internal/audit/testdata/a_dep.go
@@ -0,0 +1,43 @@
+// go:build ignore
+
+package A
+
+import (
+ "thirdparty.org/vulnerabilities/vuln"
+)
+
+type I interface {
+ Vuln()
+}
+
+func A1() I {
+ v := vuln.VulnData{}
+ v.Vuln() // vuln use
+ return v
+}
+
+func A2() func() {
+ return vuln.Vuln
+}
+
+func A3() func() {
+ return func() {}
+}
+
+type vulnWrap struct {
+ V I
+}
+
+func A4(f func(i I)) vulnWrap {
+ f(vuln.VulnData{})
+ return vulnWrap{}
+}
+
+func doWrap(i I) {
+ w := vulnWrap{}
+ w.V = i
+}
+
+// Part of a test program consisting of packages found
+// in top_package.go, b_dep.go, and vuln.go. For more
+// details, see testProgAndEnv function in helpers_test.go.
diff --git a/vulndb/internal/audit/testdata/b_dep.go b/vulndb/internal/audit/testdata/b_dep.go
new file mode 100644
index 0000000..2090059
--- /dev/null
+++ b/vulndb/internal/audit/testdata/b_dep.go
@@ -0,0 +1,22 @@
+// go:build ignore
+
+package B
+
+import (
+ "a.org/A"
+)
+
+type internal struct{}
+
+func (i internal) Vuln() {}
+
+func B1() {
+ A.A1() // transitive vuln use but should not be reported
+ var i A.I
+ i = internal{}
+ i.Vuln() // no vuln use
+}
+
+// Part of a test program consisting of packages found in
+// vuln.go, a_dep.go, and b_dep.go. For more details, see
+// testProgAndEnv function in helpers_test.go.
diff --git a/vulndb/internal/audit/testdata/dbs/bogus.db.org/bogus.org/module.json b/vulndb/internal/audit/testdata/dbs/bogus.db.org/bogus.org/module.json
new file mode 100644
index 0000000..222d03e
--- /dev/null
+++ b/vulndb/internal/audit/testdata/dbs/bogus.db.org/bogus.org/module.json
@@ -0,0 +1,21 @@
+[
+ {
+ "package": {
+ "name": "bogus.org/module/vuln"
+ },
+ "affects": {
+ "ranges": [
+ {
+ "type": "SEMVER",
+ "fixed": "v2.0.0"
+ }
+ ]
+ },
+ "ecosystem_specific": {
+ "symbols": [
+ "Bogus"
+ ],
+ "url": "bogus.org/bogus/README.doc"
+ }
+ }
+]
diff --git a/vulndb/internal/audit/testdata/dbs/bogus.db.org/index.json b/vulndb/internal/audit/testdata/dbs/bogus.db.org/index.json
new file mode 100644
index 0000000..b7fa1b0
--- /dev/null
+++ b/vulndb/internal/audit/testdata/dbs/bogus.db.org/index.json
@@ -0,0 +1,3 @@
+{
+ "bogus.org/module/vuln": "2020-03-06T09:21:06.31369157-07:00"
+}
diff --git a/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/index.json b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/index.json
new file mode 100644
index 0000000..9129812
--- /dev/null
+++ b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/index.json
@@ -0,0 +1,3 @@
+{
+ "thirdparty.org/vulnerabilities/vuln": "2020-04-05T10:21:50.21362171-07:00"
+}
diff --git a/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/README b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/README
new file mode 100644
index 0000000..37db9a9
--- /dev/null
+++ b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/README
@@ -0,0 +1,4 @@
+Contains json files modeling vulnerability info for the module
+`thirdparty.org/vulnerabilities`.
+
+Also used for testing the robustness of loading a local vulnerability database.
diff --git a/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/vulnerabilities.json b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/vulnerabilities.json
new file mode 100644
index 0000000..cfc4d70
--- /dev/null
+++ b/vulndb/internal/audit/testdata/dbs/golang.deepgo.org/thirdparty.org/vulnerabilities.json
@@ -0,0 +1,70 @@
+[
+ {
+ "package": {
+ "name": "thirdparty.org/vulnerabilities/vuln"
+ },
+ "affects": {
+ "ranges": [
+ {
+ "type": "SEMVER",
+ "introduced": "v1.0.0",
+ "fixed": "v1.0.4"
+ },
+ {
+ "type": "SEMVER",
+ "introduced": "v1.1.2"
+ }
+ ]
+ },
+ "ecosystem_specific": {
+ "symbols": [
+ "VulnData.Vuln",
+ "VulnData.VulnOnPtr"
+ ],
+ "url": "thirdparty.org/vulnerabilities/README.doc"
+ }
+ },
+ {
+ "package": {
+ "name": "thirdparty.org/vulnerabilities/vuln"
+ },
+ "affects": {
+ "ranges": [
+ {
+ "type": "SEMVER",
+ "introduced": "v1.2.0",
+ "fixed": "v1.3.2"
+ }
+ ]
+ },
+ "ecosystem_specific": {
+ "goarch": [
+ "amd64"
+ ],
+ "goos": [
+ "linux"
+ ],
+ "url": "thirdparty.org/vulnerabilities/README_amd64.doc"
+ }
+ },
+ {
+ "package": {
+ "name": "thirdparty.org/vulnerabilities/vuln"
+ },
+ "affects": {
+ "ranges": [
+ {
+ "type": "SEMVER",
+ "introduced": "v1.0.1",
+ "fixed": "v1.0.2"
+ }
+ ]
+ },
+ "ecosystem_specific": {
+ "symbols": [
+ "VG"
+ ],
+ "url": "thirdparty.org/vulnerabilities/README_global.doc"
+ }
+ }
+]
diff --git a/vulndb/internal/audit/testdata/slice.go b/vulndb/internal/audit/testdata/slice.go
new file mode 100644
index 0000000..5fbbe68
--- /dev/null
+++ b/vulndb/internal/audit/testdata/slice.go
@@ -0,0 +1,57 @@
+// go:build ignore
+
+package testdata
+
+func X() {}
+func Y() {}
+
+// not reachable
+func id(i int) int {
+ return i
+}
+
+// not reachable
+func inc(i int) int {
+ return i + 1
+}
+
+func Apply(b bool, h func()) {
+ if b {
+ func() {
+ print("applied")
+ }()
+ return
+ }
+ h()
+}
+
+type I interface {
+ Foo()
+}
+
+type A struct{}
+
+func (a A) Foo() {}
+
+// not reachable
+func (a A) Bar() {}
+
+type B struct{}
+
+func (b B) Foo() {}
+
+func debug(s string) {
+ print(s)
+}
+
+func Do(i I, input string) {
+ debug(input)
+
+ i.Foo()
+
+ func(x string) {
+ func(l int) {
+ print(l)
+ }(len(x))
+ }(input)
+}
diff --git a/vulndb/internal/audit/testdata/top_package.go b/vulndb/internal/audit/testdata/top_package.go
new file mode 100644
index 0000000..8b045b8
--- /dev/null
+++ b/vulndb/internal/audit/testdata/top_package.go
@@ -0,0 +1,34 @@
+// go:build ignore
+
+package T
+
+import (
+ "a.org/A"
+ "b.org/B"
+ "thirdparty.org/vulnerabilities/vuln"
+)
+
+func T1(x bool) {
+ print(vuln.VG) // vuln use
+ if x {
+ A.A1().Vuln() // vuln use
+ } else {
+ B.B1() // no vuln use
+ }
+}
+
+func T2(x bool) {
+ if x {
+ A.A2()() // vuln use. The return value of A.A2() is stored in register t0
+ } else {
+ A.A3()()
+ w := A.A4(benign)
+ w.V.Vuln() // no vuln use with vta-vta
+ }
+}
+
+func benign(i A.I) {}
+
+// Part of a test program consisting of packages found in
+// vuln.go, a_dep.go, and b_dep.go. For more details,
+// see testProgAndEnv function in helpers_test.go
diff --git a/vulndb/internal/audit/testdata/vuln.go b/vulndb/internal/audit/testdata/vuln.go
new file mode 100644
index 0000000..c981eff
--- /dev/null
+++ b/vulndb/internal/audit/testdata/vuln.go
@@ -0,0 +1,17 @@
+// go:build ignore
+
+package vuln
+
+var VG int
+
+type VulnData struct{}
+
+func (v VulnData) Vuln() {}
+
+func Vuln() {
+ print(VG)
+}
+
+// Part of a test program consisting of packages found in
+// top_package.go, a_dep.go, and b_dep.go. For more details,
+// see testProgAndEnv function in helpers_test.go.
diff --git a/vulndb/internal/audit/utils.go b/vulndb/internal/audit/utils.go
new file mode 100644
index 0000000..c7395a7
--- /dev/null
+++ b/vulndb/internal/audit/utils.go
@@ -0,0 +1,212 @@
+// 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 audit
+
+import (
+ "bytes"
+ "fmt"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/go/callgraph"
+ "golang.org/x/tools/go/types/typeutil"
+
+ "golang.org/x/tools/go/ssa"
+
+ "golang.org/x/vulndb/osv"
+)
+
+// instrPosition gives the position of `instr`. Returns empty token.Position
+// if no file information on `instr` is available.
+func instrPosition(instr ssa.Instruction) *token.Position {
+ pos := instr.Parent().Prog.Fset.Position(instr.Pos())
+ return &pos
+}
+
+// valPosition gives the position of `v` inside of `f`. Assumes `v` is used in
+// `f`. Returns empty token.Position if no file information on `f` is available.
+func valPosition(v ssa.Value, f *ssa.Function) *token.Position {
+ pos := f.Prog.Fset.Position(v.Pos())
+ return &pos
+}
+
+// funcPosition gives the position of `f`. Returns empty token.Position
+// if no file information on `f` is available.
+func funcPosition(f *ssa.Function) *token.Position {
+ pos := f.Prog.Fset.Position(f.Pos())
+ return &pos
+}
+
+// siteCallees computes a set of callees for call site `call` given program `callgraph`.
+func siteCallees(call ssa.CallInstruction, callgraph *callgraph.Graph) []*ssa.Function {
+ var matches []*ssa.Function
+
+ node := callgraph.Nodes[call.Parent()]
+ if node == nil {
+ return nil
+ }
+
+ for _, edge := range node.Out {
+ callee := edge.Callee.Func
+ // Skip synthetic functions wrapped around source functions.
+ if edge.Site == call && callee.Synthetic == "" {
+ matches = append(matches, callee)
+ }
+ }
+ return matches
+}
+
+func callName(call ssa.CallInstruction) string {
+ if !call.Common().IsInvoke() {
+ return fmt.Sprintf("%s.%s", call.Parent().Pkg.Pkg.Path(), call.Common().Value.Name())
+ }
+ buf := new(bytes.Buffer)
+ types.WriteType(buf, call.Common().Value.Type(), nil)
+ return fmt.Sprintf("%s.%s", buf, call.Common().Method.Name())
+}
+
+func unresolved(call ssa.CallInstruction) bool {
+ if call == nil {
+ return false
+ }
+ return call.Common().StaticCallee() == nil
+}
+
+// pkgsProgram returns the single common program to which all pkgs belong, if such.
+// Otherwise, returns nil.
+func pkgsProgram(pkgs []*ssa.Package) *ssa.Program {
+ var prog *ssa.Program
+ for _, pkg := range pkgs {
+ if prog == nil {
+ prog = pkg.Prog
+ } else if prog != pkg.Prog {
+ return nil
+ }
+ }
+ return prog
+}
+
+// globalUses returns a list of global uses by an instruction.
+// Global function callees are disregarded as they are preferred as call uses.
+func globalUses(instr ssa.Instruction) []*ssa.Value {
+ ops := instr.Operands(nil)
+ if _, ok := instr.(ssa.CallInstruction); ok {
+ ops = ops[1:]
+ }
+
+ var glbs []*ssa.Value
+ for _, o := range ops {
+ if _, ok := (*o).(*ssa.Global); ok {
+ glbs = append(glbs, o)
+ }
+ }
+ return glbs
+}
+
+// Computes function name consistent with the function namings used in vulnerability
+// databases. Effectively, a qualified name of a function local to its enclosing package.
+// If a receiver is a pointer, this information is not encoded in the resulting name. The
+// name of anonymous functions is simply "". The function names are unique subject to the
+// enclosing package, but not globally.
+//
+// Examples:
+// func (a A) foo (...) {...} -> A.foo
+// func foo(...) {...} -> foo
+// func (b *B) bar (...) {...} -> B.bar
+func dbFuncName(f *ssa.Function) string {
+ var typeFormat func(t types.Type) string
+ typeFormat = func(t types.Type) string {
+ switch tt := t.(type) {
+ case *types.Pointer:
+ return typeFormat(tt.Elem())
+ case *types.Named:
+ return tt.Obj().Name()
+ default:
+ return types.TypeString(t, func(p *types.Package) string { return "" })
+ }
+ }
+ selectBound := func(f *ssa.Function) types.Type {
+ // If f is a "bound" function introduced by ssa for a given type, return the type.
+ // When "f" is a "bound" function, it will have 1 free variable of that type within
+ // the function. This is subject to change when ssa changes.
+ if len(f.FreeVars) == 1 && strings.HasPrefix(f.Synthetic, "bound ") {
+ return f.FreeVars[0].Type()
+ }
+ return nil
+ }
+ selectThunk := func(f *ssa.Function) types.Type {
+ // If f is a "thunk" function introduced by ssa for a given type, return the type.
+ // When "f" is a "thunk" function, the first parameter will have that type within
+ // the function. This is subject to change when ssa changes.
+ params := f.Signature.Params() // params.Len() == 1 then params != nil.
+ if strings.HasPrefix(f.Synthetic, "thunk ") && params.Len() >= 1 {
+ if first := params.At(0); first != nil {
+ return first.Type()
+ }
+ }
+ return nil
+ }
+ var qprefix string
+ if recv := f.Signature.Recv(); recv != nil {
+ qprefix = typeFormat(recv.Type())
+ } else if btype := selectBound(f); btype != nil {
+ qprefix = typeFormat(btype)
+ } else if ttype := selectThunk(f); ttype != nil {
+ qprefix = typeFormat(ttype)
+ }
+
+ if qprefix == "" {
+ return f.Name()
+ }
+ return qprefix + "." + f.Name()
+}
+
+// memberFuncs returns functions associated with the `member`:
+// 1) `member` itself if `member` is a function
+// 2) `member` methods if `member` is a type
+// 3) empty list otherwise
+func memberFuncs(member ssa.Member, prog *ssa.Program) []*ssa.Function {
+ switch t := member.(type) {
+ case *ssa.Type:
+ methods := typeutil.IntuitiveMethodSet(t.Type(), &prog.MethodSets)
+ var funcs []*ssa.Function
+ for _, m := range methods {
+ if f := prog.MethodValue(m); f != nil {
+ funcs = append(funcs, f)
+ }
+ }
+ return funcs
+ case *ssa.Function:
+ return []*ssa.Function{t}
+ default:
+ return nil
+ }
+}
+
+// Returns the path of a package `f` belongs to. Covers both
+// the case when `f` is an anonymous and a synthetic function.
+func pkgPath(f *ssa.Function) string {
+ // Handle all user defined functions.
+ if p := f.Package(); p != nil && p.Pkg != nil {
+ return p.Pkg.Path()
+ }
+ // Cover synthetic functions as well.
+ if o := f.Object(); o != nil && o.Pkg() != nil {
+ return o.Pkg().Path()
+ }
+ // Not reachable in principle.
+ return ""
+}
+
+// serialize transforms []*osv.Entry into []osv.Entry as to
+// allow serialization of Finding.
+func serialize(vulns []*osv.Entry) []osv.Entry {
+ var vs []osv.Entry
+ for _, v := range vulns {
+ vs = append(vs, *v)
+ }
+ return vs
+}
diff --git a/vulndb/internal/audit/version.go b/vulndb/internal/audit/version.go
new file mode 100644
index 0000000..47a38ab
--- /dev/null
+++ b/vulndb/internal/audit/version.go
@@ -0,0 +1,62 @@
+// 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 audit
+
+import (
+ "golang.org/x/tools/go/packages"
+)
+
+// Returns module version of a package pkg. If the version is "" and the module is
+// replaced by another module with the same path, replaced module version is returned.
+// TODO(zpavlinovic): check if this is complete/correct.
+func version(pkg *packages.Package) string {
+ module := pkg.Module
+ if module == nil {
+ return ""
+ }
+
+ if module.Version != "" {
+ return module.Version
+ }
+
+ if module.Replace == nil || module.Replace.Path != module.Path {
+ return ""
+ }
+ return module.Replace.Version
+}
+
+// populateVersionInfo recursively populates pkgVersions for the input package pkg and its transitive dependencies.
+func populatePkgVersions(pkg *packages.Package, pkgVersions map[string]string, seen map[string]bool) {
+ if _, ok := seen[pkg.PkgPath]; ok {
+ return
+ }
+ seen[pkg.PkgPath] = true
+
+ version := version(pkg)
+ if version != "" {
+ pkgVersions[pkg.PkgPath] = version
+ }
+
+ for _, imp := range pkg.Imports {
+ populatePkgVersions(imp, pkgVersions, seen)
+ }
+}
+
+// PackageVersions computes a map from a path of every package in pkgs and
+// its transitive dependencies to their module version. If module or its
+// version are not present, the corresponding package is not in the map.
+//
+// Does not check for well-formedness of version strings. If such strings
+// exist, the produced map can lead to confusing results down the line.
+// (Well-formedness of version strings should be checked by external tools,
+// such as using golang.org/x/tools/go/packages.Load to construct pkgs.)
+func PackageVersions(pkgs []*packages.Package) map[string]string {
+ pkgVersions := make(map[string]string)
+ seen := make(map[string]bool)
+ for _, pkg := range pkgs {
+ populatePkgVersions(pkg, pkgVersions, seen)
+ }
+ return pkgVersions
+}
diff --git a/vulndb/internal/audit/version_test.go b/vulndb/internal/audit/version_test.go
new file mode 100644
index 0000000..fb55cc5
--- /dev/null
+++ b/vulndb/internal/audit/version_test.go
@@ -0,0 +1,79 @@
+// 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 audit
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/packages/packagestest"
+)
+
+func TestPackageVersionInfo(t *testing.T) {
+ // Export package testdata with a program depending on a vulnerability package
+ // vuln with version "v1.0.1".
+ e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+ {
+ Name: "golang.org/vulntest",
+ Files: map[string]interface{}{
+ "testdata/testdata.go": `
+ package testdata
+
+ import (
+ "thirdparty.org/vulnerabilities/vuln"
+ )
+
+ func Lib1() {
+ vuln.Vuln()
+ }
+ `,
+ },
+ },
+ {
+ Name: "thirdparty.org/vulnerabilities@v1.0.1",
+ Files: map[string]interface{}{
+ "vuln/vuln.go": `
+ package vuln
+
+ import (
+ "abc.org/xyz/foo"
+ )
+
+ func Vuln() { foo.Foo() }
+ `,
+ },
+ },
+ {
+ Name: "abc.org/xyz@v0.0.0-20201002170205-7f63de1d35b0",
+ Files: map[string]interface{}{
+ "foo/foo.go": `
+ package foo
+
+ func Foo() { }
+ `,
+ },
+ },
+ })
+ defer e.Cleanup()
+
+ _, _, pkgs, err := loadAndBuildPackages(e, "/vulntest/testdata/testdata.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v := PackageVersions(pkgs)
+ for _, test := range []struct {
+ path string
+ version string
+ in bool
+ }{
+ {"command-line-arguments", "", false},
+ {"thirdparty.org/vulnerabilities/vuln", "v1.0.1", true},
+ {"abc.org/xyz/foo", "v0.0.0-20201002170205-7f63de1d35b0", true},
+ } {
+ if version, ok := v[test.path]; ok != test.in || version != test.version {
+ t.Errorf("want package %s at version %s in=%t package-version map; got %s and %t", test.path, test.version, test.in, version, ok)
+ }
+ }
+}
diff --git a/vulndb/internal/audit/vulnerability.go b/vulndb/internal/audit/vulnerability.go
new file mode 100644
index 0000000..4ba5937
--- /dev/null
+++ b/vulndb/internal/audit/vulnerability.go
@@ -0,0 +1,22 @@
+// 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 audit
+
+import (
+ "golang.org/x/vulndb/osv"
+
+ "golang.org/x/vulndb/client"
+)
+
+// LoadVulnerabilities fetches vulnerabilities for pkgs in dbs. Currently,
+// no caching is enabled.
+// TODO: add cache support once it is amenable to side-effect free testing.
+func LoadVulnerabilities(dbs []string, pkgs []string) ([]*osv.Entry, error) {
+ dbClient, err := client.NewClient(dbs, client.Options{})
+ if err != nil {
+ return nil, err
+ }
+ return dbClient.Get(pkgs)
+}
diff --git a/vulndb/internal/audit/vulnerability_test.go b/vulndb/internal/audit/vulnerability_test.go
new file mode 100644
index 0000000..9b7f2de
--- /dev/null
+++ b/vulndb/internal/audit/vulnerability_test.go
@@ -0,0 +1,59 @@
+// 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 audit
+
+import (
+ "os"
+ "path"
+ "reflect"
+ "testing"
+
+ "golang.org/x/vulndb/osv"
+)
+
+// Testing utility function that simplifies vulns by projecting each vulnerability
+// to Path, and Symbol fields only.
+func vulnProject(vulns []*osv.Entry) map[string][]osv.Entry {
+ projVulns := make(map[string][]osv.Entry)
+ for _, vuln := range vulns {
+ projVulns[vuln.Package.Name] = append(projVulns[vuln.Package.Name],
+ osv.Entry{Package: osv.Package{Name: vuln.Package.Name}, EcosystemSpecific: osv.GoSpecific{Symbols: vuln.EcosystemSpecific.Symbols}})
+ }
+ return projVulns
+}
+
+func TestLoadVulnerabilities(t *testing.T) {
+ cd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ vulns, err := LoadVulnerabilities([]string{"file://" + path.Join(cd, "testdata/dbs/bogus.db.org"), "file://" + path.Join(cd, "testdata/dbs/golang.deepgo.org")},
+ []string{"thirdparty.org/vulnerabilities", "bogus.org/module"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testVulnDb := make(map[string][]osv.Entry)
+ testVulnDb["thirdparty.org/vulnerabilities/vuln"] = []osv.Entry{
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"},
+ EcosystemSpecific: osv.GoSpecific{Symbols: []string{"VulnData.Vuln", "VulnData.VulnOnPtr"}},
+ },
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}},
+ {Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"},
+ EcosystemSpecific: osv.GoSpecific{Symbols: []string{"VG"}},
+ },
+ }
+ testVulnDb["bogus.org/module/vuln"] = []osv.Entry{
+ {Package: osv.Package{Name: "bogus.org/module/vuln"},
+ EcosystemSpecific: osv.GoSpecific{Symbols: []string{"Bogus"}},
+ },
+ }
+
+ projVulnDb := vulnProject(vulns)
+ if !reflect.DeepEqual(testVulnDb, projVulnDb) {
+ t.Errorf("want %v vulnerability database; got (simplified) %v", testVulnDb, projVulnDb)
+ }
+}
diff --git a/vulndb/internal/binscan/exe.go b/vulndb/internal/binscan/exe.go
new file mode 100644
index 0000000..a52c0a1
--- /dev/null
+++ b/vulndb/internal/binscan/exe.go
@@ -0,0 +1,344 @@
+// 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 binscan
+
+// This file is a somewhat modified version of cmd/go/internal/version/exe.go
+// that adds functionality for extracting the PCLN table.
+
+import (
+ "bytes"
+ "debug/elf"
+ "debug/macho"
+ "debug/pe"
+ "fmt"
+
+ // "internal/xcoff"
+ "io"
+ "os"
+)
+
+// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
+type exe interface {
+ // Close closes the underlying file.
+ Close() error
+
+ // ReadData reads and returns up to size byte starting at virtual address addr.
+ ReadData(addr, size uint64) ([]byte, error)
+
+ // DataStart returns the writable data segment start address.
+ DataStart() uint64
+
+ PCLNTab() ([]byte, uint64)
+}
+
+// openExe opens file and returns it as an exe.
+func openExe(file string) (exe, error) {
+ f, err := os.Open(file)
+ if err != nil {
+ return nil, err
+ }
+ data := make([]byte, 16)
+ if _, err := io.ReadFull(f, data); err != nil {
+ return nil, err
+ }
+ f.Seek(0, 0)
+ if bytes.HasPrefix(data, []byte("\x7FELF")) {
+ e, err := elf.NewFile(f)
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ return &elfExe{f, e}, nil
+ }
+ if bytes.HasPrefix(data, []byte("MZ")) {
+ e, err := pe.NewFile(f)
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ return &peExe{f, e}, nil
+ }
+ if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
+ e, err := macho.NewFile(f)
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ return &machoExe{f, e}, nil
+ }
+ // TODO(rolandshoemaker): we cannot support XCOFF files due to the usage of internal/xcoff.
+ // Once this code is moved into the stdlib, this support can be re-enabled.
+ // if bytes.HasPrefix(data, []byte{0x01, 0xDF}) || bytes.HasPrefix(data, []byte{0x01, 0xF7}) {
+ // e, err := xcoff.NewFile(f)
+ // if err != nil {
+ // f.Close()
+ // return nil, err
+ // }
+ // return &xcoffExe{f, e}, nil
+
+ // }
+ return nil, fmt.Errorf("unrecognized executable format")
+}
+
+// elfExe is the ELF implementation of the exe interface.
+type elfExe struct {
+ os *os.File
+ f *elf.File
+}
+
+func (x *elfExe) Close() error {
+ return x.os.Close()
+}
+
+func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
+ for _, prog := range x.f.Progs {
+ if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
+ n := prog.Vaddr + prog.Filesz - addr
+ if n > size {
+ n = size
+ }
+ data := make([]byte, n)
+ _, err := prog.ReadAt(data, int64(addr-prog.Vaddr))
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+ }
+ }
+ return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *elfExe) DataStart() uint64 {
+ for _, s := range x.f.Sections {
+ if s.Name == ".go.buildinfo" {
+ return s.Addr
+ }
+ }
+ for _, p := range x.f.Progs {
+ if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {
+ return p.Vaddr
+ }
+ }
+ return 0
+}
+
+func (x *elfExe) PCLNTab() ([]byte, uint64) {
+ var offset uint64
+ text := x.f.Section(".text")
+ if text != nil {
+ offset = text.Offset
+ }
+ pclntab := x.f.Section(".gopclntab")
+ if pclntab == nil {
+ pclntab = x.f.Section(".data.rel.ro.gopclntab")
+ if pclntab == nil {
+ panic("no pclntab")
+ }
+ }
+ b, err := pclntab.Data()
+ if err != nil {
+ panic(err)
+ }
+ return b, offset
+}
+
+// peExe is the PE (Windows Portable Executable) implementation of the exe interface.
+type peExe struct {
+ os *os.File
+ f *pe.File
+}
+
+func (x *peExe) Close() error {
+ return x.os.Close()
+}
+
+func (x *peExe) imageBase() uint64 {
+ switch oh := x.f.OptionalHeader.(type) {
+ case *pe.OptionalHeader32:
+ return uint64(oh.ImageBase)
+ case *pe.OptionalHeader64:
+ return oh.ImageBase
+ }
+ return 0
+}
+
+func (x *peExe) ReadData(addr, size uint64) ([]byte, error) {
+ addr -= x.imageBase()
+ for _, sect := range x.f.Sections {
+ if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
+ n := uint64(sect.VirtualAddress+sect.Size) - addr
+ if n > size {
+ n = size
+ }
+ data := make([]byte, n)
+ _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+ }
+ }
+ return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *peExe) DataStart() uint64 {
+ // Assume data is first writable section.
+ const (
+ IMAGE_SCN_CNT_CODE = 0x00000020
+ IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
+ IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
+ IMAGE_SCN_MEM_EXECUTE = 0x20000000
+ IMAGE_SCN_MEM_READ = 0x40000000
+ IMAGE_SCN_MEM_WRITE = 0x80000000
+ IMAGE_SCN_MEM_DISCARDABLE = 0x2000000
+ IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000
+ IMAGE_SCN_ALIGN_32BYTES = 0x600000
+ )
+ for _, sect := range x.f.Sections {
+ if sect.VirtualAddress != 0 && sect.Size != 0 &&
+ sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE {
+ return uint64(sect.VirtualAddress) + x.imageBase()
+ }
+ }
+ return 0
+}
+
+func (x *peExe) PCLNTab() ([]byte, uint64) {
+ var textOffset uint64
+ for _, section := range x.f.Sections {
+ if section.Name == ".text" {
+ textOffset = uint64(section.Offset)
+ break
+ }
+ }
+ var start, end int64
+ var section int
+ for _, symbol := range x.f.Symbols {
+ if symbol.Name == "runtime.pclntab" {
+ start = int64(symbol.Value)
+ section = int(symbol.SectionNumber - 1)
+ } else if symbol.Name == "runtime.epclntab" {
+ end = int64(symbol.Value)
+ break
+ }
+ }
+ if start == 0 || end == 0 {
+ panic("didn't find both start and enc")
+ }
+ offset := int64(x.f.Sections[section].Offset) + start
+ size := end - start
+
+ pclntab := make([]byte, size)
+ if _, err := x.os.ReadAt(pclntab, offset); err != nil {
+ panic(err)
+ }
+
+ return pclntab, textOffset
+}
+
+// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.
+type machoExe struct {
+ os *os.File
+ f *macho.File
+}
+
+func (x *machoExe) Close() error {
+ return x.os.Close()
+}
+
+func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
+ for _, load := range x.f.Loads {
+ seg, ok := load.(*macho.Segment)
+ if !ok {
+ continue
+ }
+ if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 {
+ if seg.Name == "__PAGEZERO" {
+ continue
+ }
+ n := seg.Addr + seg.Filesz - addr
+ if n > size {
+ n = size
+ }
+ data := make([]byte, n)
+ _, err := seg.ReadAt(data, int64(addr-seg.Addr))
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+ }
+ }
+ return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *machoExe) DataStart() uint64 {
+ // Look for section named "__go_buildinfo".
+ for _, sec := range x.f.Sections {
+ if sec.Name == "__go_buildinfo" {
+ return sec.Addr
+ }
+ }
+ // Try the first non-empty writable segment.
+ const RW = 3
+ for _, load := range x.f.Loads {
+ seg, ok := load.(*macho.Segment)
+ if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW {
+ return seg.Addr
+ }
+ }
+ return 0
+}
+
+func (x *machoExe) PCLNTab() ([]byte, uint64) {
+ var textOffset uint64
+ text := x.f.Section("__text")
+ if text != nil {
+ textOffset = uint64(text.Offset)
+ }
+ pclntab := x.f.Section("__gopclntab")
+ if pclntab == nil {
+ panic("no pclntab")
+ }
+ b, err := pclntab.Data()
+ if err != nil {
+ panic("err")
+ }
+ return b, textOffset
+}
+
+// TODO(rolandshoemaker): we cannot support XCOFF files due to the usage of internal/xcoff.
+// Once this code is moved into the stdlib, this support can be re-enabled.
+
+// // xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface.
+// type xcoffExe struct {
+// os *os.File
+// f *xcoff.File
+// }
+//
+// func (x *xcoffExe) Close() error {
+// return x.os.Close()
+// }
+//
+// func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) {
+// for _, sect := range x.f.Sections {
+// if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
+// n := uint64(sect.VirtualAddress+sect.Size) - addr
+// if n > size {
+// n = size
+// }
+// data := make([]byte, n)
+// _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
+// if err != nil {
+// return nil, err
+// }
+// return data, nil
+// }
+// }
+// return nil, fmt.Errorf("address not mapped")
+// }
+//
+// func (x *xcoffExe) DataStart() uint64 {
+// return x.f.SectionByType(xcoff.STYP_DATA).VirtualAddress
+// }
diff --git a/vulndb/internal/binscan/scan.go b/vulndb/internal/binscan/scan.go
new file mode 100644
index 0000000..eb57c23
--- /dev/null
+++ b/vulndb/internal/binscan/scan.go
@@ -0,0 +1,242 @@
+// 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 binscan contains methods for parsing Go binary files for the purpose
+// of extracting module dependency and symbol table information.
+package binscan
+
+// Code in this package is dervied from src/cmd/go/internal/version/version.go
+// and cmd/go/internal/version/exe.go.
+
+import (
+ "bytes"
+ "debug/gosym"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "net/url"
+ "runtime/debug"
+ "strings"
+)
+
+// buildInfoMagic, findVers, and readString are copied from
+// cmd/go/internal/version
+
+// The build info blob left by the linker is identified by
+// a 16-byte header, consisting of buildInfoMagic (14 bytes),
+// the binary's pointer size (1 byte),
+// and whether the binary is big endian (1 byte).
+var buildInfoMagic = []byte("\xff Go buildinf:")
+
+// findVers finds and returns the Go version and module version information
+// in the executable x.
+func findVers(x exe) string {
+ // Read the first 64kB of text to find the build info blob.
+ text := x.DataStart()
+ data, err := x.ReadData(text, 64*1024)
+ if err != nil {
+ return ""
+ }
+ for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
+ if len(data) < 32 {
+ return ""
+ }
+ }
+
+ // Decode the blob.
+ ptrSize := int(data[14])
+ bigEndian := data[15] != 0
+ var bo binary.ByteOrder
+ if bigEndian {
+ bo = binary.BigEndian
+ } else {
+ bo = binary.LittleEndian
+ }
+ var readPtr func([]byte) uint64
+ if ptrSize == 4 {
+ readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
+ } else {
+ readPtr = bo.Uint64
+ }
+ vers := readString(x, ptrSize, readPtr, readPtr(data[16:]))
+ if vers == "" {
+ return ""
+ }
+ mod := readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
+ if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
+ // Strip module framing.
+ mod = mod[16 : len(mod)-16]
+ } else {
+ mod = ""
+ }
+ return mod
+}
+
+// readString returns the string at address addr in the executable x.
+func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
+ hdr, err := x.ReadData(addr, uint64(2*ptrSize))
+ if err != nil || len(hdr) < 2*ptrSize {
+ return ""
+ }
+ dataAddr := readPtr(hdr)
+ dataLen := readPtr(hdr[ptrSize:])
+ data, err := x.ReadData(dataAddr, dataLen)
+ if err != nil || uint64(len(data)) < dataLen {
+ return ""
+ }
+ return string(data)
+}
+
+// readBuildInfo is copied from runtime/debug
+func readBuildInfo(data string) (*debug.BuildInfo, bool) {
+ if len(data) == 0 {
+ return nil, false
+ }
+
+ const (
+ pathLine = "path\t"
+ modLine = "mod\t"
+ depLine = "dep\t"
+ repLine = "=>\t"
+ )
+
+ readEntryFirstLine := func(elem []string) (debug.Module, bool) {
+ if len(elem) != 2 && len(elem) != 3 {
+ return debug.Module{}, false
+ }
+ sum := ""
+ if len(elem) == 3 {
+ sum = elem[2]
+ }
+ return debug.Module{
+ Path: elem[0],
+ Version: elem[1],
+ Sum: sum,
+ }, true
+ }
+
+ var (
+ info = &debug.BuildInfo{}
+ last *debug.Module
+ line string
+ ok bool
+ )
+ // Reverse of cmd/go/internal/modload.PackageBuildInfo
+ for len(data) > 0 {
+ i := strings.IndexByte(data, '\n')
+ if i < 0 {
+ break
+ }
+ line, data = data[:i], data[i+1:]
+ switch {
+ case strings.HasPrefix(line, pathLine):
+ elem := line[len(pathLine):]
+ info.Path = elem
+ case strings.HasPrefix(line, modLine):
+ elem := strings.Split(line[len(modLine):], "\t")
+ last = &info.Main
+ *last, ok = readEntryFirstLine(elem)
+ if !ok {
+ return nil, false
+ }
+ case strings.HasPrefix(line, depLine):
+ elem := strings.Split(line[len(depLine):], "\t")
+ last = new(debug.Module)
+ info.Deps = append(info.Deps, last)
+ *last, ok = readEntryFirstLine(elem)
+ if !ok {
+ return nil, false
+ }
+ case strings.HasPrefix(line, repLine):
+ elem := strings.Split(line[len(repLine):], "\t")
+ if len(elem) != 3 {
+ return nil, false
+ }
+ if last == nil {
+ return nil, false
+ }
+ last.Replace = &debug.Module{
+ Path: elem[0],
+ Version: elem[1],
+ Sum: elem[2],
+ }
+ last = nil
+ }
+ }
+ return info, true
+}
+
+// ExtractPackagesAndSymbols extracts the symbols, packages, and their associated module versions
+// from a Go binary. Stripped binaries are not supported.
+func ExtractPackagesAndSymbols(binPath string) (map[string]string, map[string][]string, error) {
+ x, err := openExe(binPath)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ mod := findVers(x)
+
+ bi, ok := readBuildInfo(mod)
+ if !ok {
+ return nil, nil, err
+ }
+
+ deps := map[string]string{}
+ for _, dep := range bi.Deps {
+ if dep == nil {
+ continue
+ }
+ if dep.Replace != nil {
+ deps[dep.Replace.Path] = dep.Replace.Version
+ continue
+ }
+ deps[dep.Path] = dep.Version
+ }
+
+ pclntab, textOffset := x.PCLNTab()
+ lineTab := gosym.NewLineTable(pclntab, textOffset)
+ if lineTab == nil {
+ return nil, nil, errors.New("invalid line table")
+ }
+ tab, err := gosym.NewTable(nil, lineTab)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ packageSymbols := map[string][]string{}
+ for _, f := range tab.Funcs {
+ if f.Func == nil {
+ continue
+ }
+ symName := f.Func.BaseName()
+ if r := f.Func.ReceiverName(); r != "" {
+ if strings.HasPrefix(r, "(*") {
+ r = strings.Trim(r, "(*)")
+ }
+ symName = fmt.Sprintf("%s.%s", r, symName)
+ }
+
+ pkgName := f.Func.PackageName()
+ if pkgName == "" {
+ continue
+ }
+ pkgName, err := url.PathUnescape(pkgName)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ packageSymbols[pkgName] = append(packageSymbols[pkgName], symName)
+ }
+
+ versionedPackages := map[string]string{}
+ // TODO: this is rather inefficient, but probably fine for most programs
+ for pkg := range packageSymbols {
+ for mod, version := range deps {
+ if strings.HasPrefix(pkg, mod) {
+ versionedPackages[pkg] = version
+ }
+ }
+ }
+ return versionedPackages, packageSymbols, nil
+}