blob: 81bf77e9d3f4599211b36e71e90f799802e28ea0 [file] [log] [blame]
// Copyright 2022 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 report
import (
"errors"
"flag"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/vulndb/internal/osv"
"golang.org/x/vulndb/internal/proxy"
)
var realProxy = flag.Bool("proxy", false, "if true, contact the real module proxy and update expected responses")
var (
validStdLibReferences = []*Reference{
{Type: osv.ReferenceTypeFix, URL: "https://go.dev/cl/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://groups.google.com/g/golang-announce/c/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://go.dev/issue/12345"},
}
validCVEMetadata = &CVEMeta{
ID: "CVE-0000-1111",
CWE: "CWE XXX: A CWE description",
}
noop = func(*Report) {}
)
func validReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Modules: []*Module{{
Module: "golang.org/x/net",
VulnerableAt: "1.2.3",
Packages: []*Package{{
Package: "golang.org/x/net/http2",
}},
}},
Description: "description",
Summary: "A summary",
CVEs: []string{"CVE-1234-0000"},
}
f(&r)
return r
}
func validStdReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Modules: []*Module{{
Module: "std",
VulnerableAt: "1.2.3",
Packages: []*Package{{
Package: "net/http",
}},
}},
Description: "description",
Summary: "A summary",
References: validStdLibReferences,
}
f(&r)
return r
}
func validExcludedReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Excluded: "NOT_GO_CODE",
CVEs: []string{"CVE-2022-1234545"},
}
f(&r)
return r
}
func TestLint(t *testing.T) {
pc, err := proxy.NewTestClient(t, *realProxy)
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
desc string
report Report
pc *proxy.Client
want []string
}{
{
desc: "ok module-version pair",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.0",
},
}})
}),
pc: pc,
// No lints.
},
{
desc: "invalid module-version pair",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.5", // does not exist
},
}})
}),
pc: pc,
want: []string{`version 0.2.5 does not exist`},
},
{
desc: "non-canonical module",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "github.com/golang/vuln",
Versions: []VersionRange{
{
Introduced: "0.1.0",
},
}})
}),
pc: pc,
want: []string{`module is not canonical`},
},
{
desc: "multiple problems",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "github.com/golang/vuln",
Versions: []VersionRange{
{
Introduced: "0.1.0",
Fixed: "0.2.5", // does not exist
},
{
Introduced: "0.2.6", // does not exist
},
}})
}),
pc: pc,
want: []string{"2 versions do not exist: 0.2.5, 0.2.6 and module is not canonical"},
},
{
desc: "nil proxy client",
report: validReport(noop),
pc: nil,
want: []string{"proxy client is nil"},
},
} {
test := test
t.Run(test.desc, func(t *testing.T) {
got := test.report.Lint(test.pc)
checkLints(t, got, test.want)
})
}
}
func TestLintOffline(t *testing.T) {
for _, test := range []struct {
desc string
report Report
want []string
}{
{
desc: "no ID",
report: validReport(func(r *Report) {
r.ID = ""
}),
want: []string{"missing ID"},
},
{
desc: "no modules",
report: validReport(func(r *Report) {
r.Modules = nil
}),
want: []string{"no modules"},
},
{
desc: "missing module path",
report: validReport(func(r *Report) {
r.Modules[0].Module = ""
}),
want: []string{"missing module"},
},
{
desc: "missing description & advisory",
report: validReport(func(r *Report) {
r.Description = ""
r.References = nil
}),
want: []string{"missing advisory"},
},
{
desc: "missing description with advisory ok",
report: validReport(func(r *Report) {
r.Description = ""
r.References = []*Reference{
{Type: osv.ReferenceTypeAdvisory, URL: "https://example.com"},
}
}),
want: nil,
},
{
desc: "missing description (Go CVE)",
report: validReport(func(r *Report) {
r.Description = ""
r.CVEs = nil
r.CVEMetadata = validCVEMetadata
}),
want: []string{"missing description"},
},
{
desc: "description line length too long",
report: validReport(func(r *Report) {
r.Description = "This line is too long; it needs to be shortened to less than 80 characters to pass the lint check"
}),
want: []string{"description contains line > 80 characters long"},
},
{
desc: "description: long word OK",
report: validReport(func(r *Report) {
r.Description = "http://1234567890.abcdefghijklmnopqrstuvwxyz.1234567890.abcdefghijklmnopqrstuvwxyz" // 82 chars ok if single word
}),
want: []string{},
},
{
desc: "missing summary",
report: validReport(func(r *Report) {
r.Summary = ""
}),
want: []string{"missing summary"},
},
{
desc: "summary has TODO",
report: validReport(func(r *Report) {
r.Summary = "TODO: fill this out"
}),
want: []string{"summary contains a TODO"},
},
{
desc: "summary too long",
report: validReport(func(r *Report) {
r.Summary = "This summary is too long; it needs to be shortened to less than 101 characters to pass the lint check"
}),
want: []string{"too long"},
},
{
desc: "summary ending in period",
report: validReport(func(r *Report) {
r.Summary = "This summary is a sentence, not a phrase."
}),
want: []string{"should not end in a period"},
},
{
desc: "missing package path",
report: validReport(func(r *Report) {
r.Modules[0].Packages[0].Package = ""
}),
want: []string{"missing package"},
},
{
desc: "missing vulnerable at and skip fix",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Packages[0].SkipFix = ""
}),
want: []string{"missing skip_fix and vulnerable_at"},
},
{
desc: "skip fix given",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Packages[0].SkipFix = "a reason"
}),
want: []string{},
},
{
desc: "vulnerable at and skip fix given",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = "1.2.3"
r.Modules[0].Packages[0].SkipFix = "a reason"
}),
want: []string{},
},
{
desc: "vulnerable_at outside vulnerable range",
report: validStdReport(func(r *Report) {
r.Modules[0].VulnerableAt = "2.0.0"
r.Modules[0].Versions = []VersionRange{
{Fixed: "1.2.1"},
}
}),
want: []string{"vulnerable_at version 2.0.0 is not inside vulnerable range"},
},
{
desc: "unsupported versions",
report: validStdReport(func(r *Report) {
r.Modules[0].UnsupportedVersions = []UnsupportedVersion{
{Version: "1.2.1", Type: "unknown"},
}
}),
want: []string{"version issue: 1 unsupported version(s)"},
},
{
desc: "third party: module is not a prefix of package",
report: validReport(func(r *Report) {
r.Modules[0].Module = "example.com/module"
r.Modules[0].Packages[0].Package = "example.com/package"
}),
want: []string{"module must be a prefix of package"},
},
{
desc: "third party: invalid import path",
report: validReport(func(r *Report) {
r.Modules[0].Module = "invalid."
r.Modules[0].Packages[0].Package = "invalid."
}),
want: []string{"malformed import path"},
},
{
desc: "standard library: empty package",
report: validStdReport(func(r *Report) {
r.Modules[0].Packages[0].Package = ""
}),
want: []string{"missing package"},
},
{
desc: "standard library: missing packages",
report: validStdReport(func(r *Report) {
r.Modules[0].Packages = nil
}),
want: []string{"missing package"},
},
{
desc: "toolchain: wrong module",
report: validStdReport(func(r *Report) {
r.Modules[0].Module = "std"
r.Modules[0].Packages[0].Package = "cmd/go"
}),
want: []string{`should be in module "cmd", not "std"`},
},
{
desc: "overlapping version ranges",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
// Two fixed versions in a row with no introduced.
{Fixed: "1.2.1"}, {Fixed: "1.3.2"},
}
}),
want: []string{"introduced and fixed versions must alternate"},
},
{
desc: "fixed before introduced",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
{
Introduced: "1.3.0",
Fixed: "1.2.1",
},
}
}),
want: []string{`range events must be in strictly ascending order (found 1.3.0>=1.2.1)`},
},
{
desc: "versions still checked if no vulnerable_at",
report: validStdReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Versions = []VersionRange{
// Two fixed versions in a row with no introduced.
{Fixed: "1.2.1"}, {Fixed: "1.3.2"},
}
}),
want: []string{
"introduced and fixed versions must alternate",
"missing skip_fix and vulnerable_at",
},
},
{
desc: "invalid semantic version",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
{
Introduced: "1.3.X",
},
}
}),
want: []string{`invalid or non-canonical semver version (found 1.3.X)`},
},
{
desc: "bad cve identifier",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE.1234.5678"}
}),
want: []string{"malformed cve identifier"},
},
{
desc: "bad ghsa identifier",
report: validReport(func(r *Report) {
r.GHSAs = []string{"GHSA-123"}
}),
want: []string{"GHSA-123 is not a valid GHSA"},
},
{
desc: "cve and cve metadata both present",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-1111"}
r.CVEMetadata = validCVEMetadata
}),
want: nil,
},
{
desc: "missing cve metadata required fields",
report: validReport(func(r *Report) {
r.CVEs = nil
r.CVEMetadata = &CVEMeta{
// missing fields
}
}),
want: []string{"cve_metadata.id is required", "cve_metadata.cwe is required"},
},
{
desc: "bad cve metadata",
report: validReport(func(r *Report) {
r.CVEs = nil
r.CVEMetadata = &CVEMeta{
ID: "CVE.0000.1111",
CWE: "TODO",
}
}),
want: []string{"malformed cve_metadata.id identifier", "cve_metadata.cwe contains a TODO"},
},
{
desc: "invalid reference type",
report: validReport(func(r *Report) {
r.References = append(r.References, &Reference{
Type: "INVALID",
URL: "http://go.dev/",
})
}),
want: []string{"not a valid reference type"},
},
{
desc: "multiple advisory links",
report: validReport(func(r *Report) {
r.References = append(r.References, &Reference{
Type: "ADVISORY",
URL: "http://go.dev/a",
}, &Reference{
Type: "ADVISORY",
URL: "http://go.dev/b",
})
}),
want: []string{"at most one advisory link"},
},
{
desc: "redundant advisory links",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-0000", "CVE-0000-0001"}
r.GHSAs = []string{"GHSA-0000-0000-0000"}
r.References = append(r.References, &Reference{
Type: "WEB",
URL: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-0000-0000",
}, &Reference{
Type: "WEB",
URL: "https://nvd.nist.gov/vuln/detail/CVE-0000-0001",
}, &Reference{
Type: "WEB",
URL: "https://nvd.nist.gov/vuln/detail/CVE-0000-0002", // ok
}, &Reference{
Type: "WEB",
URL: "https://github.com/advisories/GHSA-0000-0000-0000",
}, &Reference{
Type: "WEB",
URL: "https://github.com/advisories/GHSA-0000-0000-0001", // ok
})
}),
want: []string{
"redundant non-advisory reference to CVE-0000-0000",
"redundant non-advisory reference to CVE-0000-0001",
"redundant non-advisory reference to GHSA-0000-0000-0000",
},
},
{
desc: "unfixed links",
report: validReport(func(r *Report) {
r.References = []*Reference{
{Type: osv.ReferenceTypeFix, URL: "https://github.com/golang/go/commit/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://github.com/golang/go/issues/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://golang.org/xxx"},
{Type: osv.ReferenceTypeWeb, URL: "https://groups.google.com/forum/#!/golang-announce/12345/1/"},
}
}),
want: []string{
`"https://github.com/golang/go/issues/12345" should be "https://go.dev/issue/12345"`,
`"https://golang.org/xxx" should be "https://go.dev/xxx"`,
`"https://github.com/golang/go/commit/12345" should be "https://go.googlesource.com/+/12345"`,
`"https://groups.google.com/forum/#!/golang-announce/12345/1/" should be "https://groups.google.com/g/golang-announce/c/12345/m/1/"`},
},
{
desc: "standard library: incorrect links",
report: validStdReport(func(r *Report) {
r.References = []*Reference{
{Type: osv.ReferenceTypeAdvisory, URL: "http://www.example.com"},
{Type: osv.ReferenceTypeFix, URL: "https://go-review.googlesource.com/c/go/+/12345"},
{Type: osv.ReferenceTypeFix, URL: "https://github.com/golang/go/commit/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://github.com/golang/go/issues/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://go.dev/"},
// no announce link
}
}),
want: []string{
// Standard library specific errors.
"advisory reference should not be set",
"fix reference should match",
"report reference should match",
"references should contain an announcement link",
"web references should only contain announcement links",
// Unfixed link errors.
`"https://github.com/golang/go/commit/12345" should be "https://go.googlesource.com/+/12345"`,
`"https://github.com/golang/go/issues/12345" should be "https://go.dev/issue/12345"`,
},
},
{
desc: "standard library: missing links",
report: validStdReport(func(r *Report) {
r.References = []*Reference{
// no links
}
}),
want: []string{
"references should contain at least one report",
"references should contain at least one fix",
"references should contain an announcement link",
},
},
{
desc: "invalid URL",
report: validReport(func(r *Report) {
r.References = []*Reference{
{
Type: osv.ReferenceTypeFix,
URL: "go.dev/cl/12345", // needs "https://" prefix
},
}
}),
want: []string{
`"go.dev/cl/12345" is not a valid URL`,
},
},
{
desc: "excluded missing/incorrect fields",
report: validExcludedReport(func(r *Report) {
r.Excluded = "not a real reason"
r.Modules = nil
r.CVEs = nil
r.GHSAs = nil
}),
want: []string{
"not a valid excluded reason",
"no modules",
"excluded report must have at least one associated CVE or GHSA",
},
},
{
desc: "related field",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-1111"}
r.Related = []string{
"not-an-id", // bad
"CVE-0000-1111", // bad (duplicate)
"CVE-0000-1112", // ok
"GHSA-0000-0000-0000", // ok
"GO-1990-0001", // ok
}
}),
want: []string{
"not-an-id is not a recognized identifier",
"CVE-0000-1111 is also listed among aliases",
},
},
{
desc: "invalid module-version pair ignored",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.5", // does not exist
},
}})
}),
// No lints: in offline mode, versions aren't checked.
},
{
desc: "valid excluded",
report: validExcludedReport(noop),
// No lints.
},
} {
test := test
t.Run(test.desc, func(t *testing.T) {
got := test.report.LintOffline()
checkLints(t, got, test.want)
})
}
}
func checkLints(t *testing.T, got, want []string) {
var missing []string
for _, w := range want {
found := false
for _, g := range got {
if strings.Contains(g, w) {
found = true
continue
}
}
if !found {
missing = append(missing, w)
}
}
if len(missing) > 0 {
t.Errorf("missing expected lint errors in report:\n"+
"got: %q\n"+
"want: %q\n", got, missing)
}
// Check for unexpected lint errors if there are no missing ones.
if len(missing) == 0 {
var unexpected []string
for _, g := range got {
found := false
for _, w := range want {
if strings.Contains(g, w) {
found = true
continue
}
}
if !found {
unexpected = append(unexpected, g)
}
}
if len(unexpected) > 0 {
t.Errorf("unexpected lint errors in report:\n"+
"got: %q\n", unexpected)
}
}
}
func TestCheckFilename(t *testing.T) {
for _, test := range []struct {
desc string
filename string
report Report
wantErr error
}{
{
desc: "wrong ID",
filename: "data/reports/GO-0000-0000.yaml",
report: validReport(
func(r *Report) {
r.ID = "GO-0000-1111"
}),
wantErr: errWrongID,
},
{
desc: "excluded in correct directory",
filename: "data/excluded/GO-0000-0000.yaml",
report: validExcludedReport(noop),
wantErr: nil,
},
{
desc: "excluded in wrong directory",
filename: "data/wrong/GO-0000-0000.yaml",
report: validExcludedReport(noop),
wantErr: errWrongDir,
},
{
desc: "non-excluded in correct directory",
filename: "data/reports/GO-0000-0000.yaml",
report: validReport(noop),
wantErr: nil,
},
{
desc: "non-excluded in wrong directory",
filename: "data/wrong/GO-0000-0000.yaml",
report: validReport(noop),
wantErr: errWrongDir,
},
} {
test := test
t.Run(test.desc, func(t *testing.T) {
if err := test.report.CheckFilename(test.filename); !errors.Is(err, test.wantErr) {
t.Errorf("CheckFilename(%s) = %v, want error %v", test.filename, err, test.wantErr)
}
})
}
}
func TestLintAsNotes(t *testing.T) {
// A report with lints.
report := validReport(
func(r *Report) {
r.Summary = ""
r.Notes = []*Note{
{Body: "an existing lint that will be deleted", Type: NoteTypeLint},
{Body: "a note added by a human", Type: NoteTypeNone}}
},
)
found := report.LintAsNotes(nil)
if !found {
t.Error("LintAsNotes() = false, want true")
}
want, got := []*Note{
{Body: "a note added by a human", Type: NoteTypeNone}, // preserved
{Body: "missing summary", Type: NoteTypeLint},
{Body: "proxy client is nil; cannot perform all lint checks", Type: NoteTypeLint}}, report.Notes
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
}