| // 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" |
| "reflect" |
| "runtime" |
| "sort" |
| "strings" |
| "testing" |
| |
| "golang.org/x/exp/vulndb/internal/audit" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/go/packages/packagestest" |
| "golang.org/x/vulndb/osv" |
| ) |
| |
| // 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]) |
| } |
| } |
| |
| r, err := run(cfg, []string{hashiVaultOkta}, false, []string{test.source}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| sort.SliceStable(r.Findings, func(i int, j int) bool { return audit.FindingCompare(r.Findings[i], r.Findings[j]) }) |
| if fs := testFindings(r.Findings); !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.") |
| } |
| |
| // make sure the dependencies are present |
| if _, err := exec.LookPath("tar"); err != nil { |
| t.Skip("tar needed for this test.") |
| } |
| if _, err := exec.LookPath("unzip"); err != nil { |
| t.Skip("unzip needed for this test.") |
| } |
| |
| 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]) |
| } |
| } |
| |
| r, err := run(cfg, []string{"./..."}, false, []string{test.source}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| sort.SliceStable(r.Findings, func(i int, j int) bool { return audit.FindingCompare(r.Findings[i], r.Findings[j]) }) |
| if fs := testFindings(r.Findings); !subset(test.want, fs) { |
| t.Errorf("want %v subset of findings; got %v", test.want, fs) |
| } |
| } |
| } |
| |
| func vulnsToString(vulns []*osv.Entry) string { |
| var s string |
| for _, v := range vulns { |
| s += fmt.Sprintf("\t%v\n", v) |
| } |
| return s |
| } |
| |
| func TestFilterVulsn(t *testing.T) { |
| vulns := []*osv.Entry{ |
| {Package: osv.Package{Name: "example.com/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "1.0.0"}}}}, |
| {Package: osv.Package{Name: "example.com/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "2.0.0"}}}}, |
| {Package: osv.Package{Name: "example.com/c"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "3.0.0"}}}}, |
| } |
| pkgs := map[string]string{ |
| "example.com/a": "v0.0.1", |
| "example.com/b": "v1.0.0", |
| "example.com/c": "v9.0.0", |
| } |
| |
| filtered := filterVulns(vulns, pkgs) |
| |
| expected := []*osv.Entry{ |
| {Package: osv.Package{Name: "example.com/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "1.0.0"}}}}, |
| {Package: osv.Package{Name: "example.com/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "2.0.0"}}}}, |
| } |
| if !reflect.DeepEqual(filtered, expected) { |
| t.Errorf("filterVulns returned unexpected results: got\n%swant\n%s", vulnsToString(filtered), vulnsToString(expected)) |
| } |
| } |
| |
| func TestUnreachable(t *testing.T) { |
| r := &results{ |
| Vulns: []*osv.Entry{ |
| {ID: "0", Package: osv.Package{Name: "example.com/a"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "1.0.0"}}}}, |
| {ID: "1", Package: osv.Package{Name: "example.com/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "2.0.0"}}}}, |
| }, |
| Findings: []audit.Finding{ |
| {Vulns: []osv.Entry{{ID: "0"}}}, |
| }, |
| } |
| |
| expected := []*osv.Entry{ |
| {ID: "1", Package: osv.Package{Name: "example.com/b"}, Affects: osv.Affects{Ranges: []osv.AffectsRange{{Type: osv.TypeSemver, Fixed: "2.0.0"}}}}, |
| } |
| unreachable := r.unreachable() |
| if !reflect.DeepEqual(unreachable, expected) { |
| t.Errorf("unreachable returned unexpected results: got\n%swant\n%s", vulnsToString(unreachable), vulnsToString(expected)) |
| } |
| } |