| // 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" |
| "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() |
| func testContext(t *testing.T) ([]*ssa.Package, ModuleVulnerabilities) { |
| 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, _, 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)) |
| } |
| |
| modVulns := ModuleVulnerabilities{ |
| { |
| mod: &packages.Module{Path: "thirdparty.org/vulnerabilities", Version: "v1.0.1"}, |
| vulns: []*osv.Entry{ |
| { |
| ID: "V1", |
| Affected: []osv.Affected{{ |
| Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}, |
| Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, |
| EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"VulnData.Vuln", "VulnData.VulnOnPtr"}}, |
| }}, |
| }, |
| { |
| ID: "V2", |
| Affected: []osv.Affected{{ |
| Package: osv.Package{Name: "thirdparty.org/vulnerabilities/vuln"}, |
| Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.1"}, {Fixed: "1.0.2"}}}}, |
| EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"VG"}}, |
| }}, |
| }, |
| }, |
| }, |
| } |
| |
| return ssaPkgs, modVulns |
| } |
| |
| 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 |
| } |
| |
| // 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), |
| weight: f.weight, |
| } |
| nfs = append(nfs, nf) |
| } |
| return nfs |
| } |
| |
| 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", "") |
| } |