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
+}