blob: a5f08c4ce158f29c9ca757105ff91a169bec15e5 [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 (
"bytes"
"strings"
"testing"
"golang.org/x/vulndb/internal/osv"
)
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"},
}
noop = func(*Report) {}
)
func validReport(f func(r *Report)) Report {
r := Report{
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{
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{
Excluded: "NOT_GO_CODE",
CVEs: []string{"CVE-2022-1234545"},
}
f(&r)
return r
}
func TestLint(t *testing.T) {
for _, test := range []struct {
desc string
dir string // default: "reports/"
report Report
want []string
}{
{
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",
report: validReport(func(r *Report) {
r.Description = ""
}),
want: []string{"missing description"},
},
{
desc: "missing summary",
report: validReport(func(r *Report) {
r.Summary = ""
}),
want: []string{"missing summary"},
},
{
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: "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: missing package",
report: validStdReport(func(r *Report) {
r.Modules[0].Packages[0].Package = ""
}),
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: "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: "cve and cve metadata both present",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-1111"}
r.CVEMetadata = &CVEMeta{
ID: "CVE-0000-1111",
CWE: "a cwe",
}
}),
want: []string{"only one of cve and cve_metadata.id should be present"},
},
{
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 id",
report: validReport(func(r *Report) {
r.CVEs = nil
r.CVEMetadata = &CVEMeta{
ID: "CVE.0000.1111",
CWE: "a cwe",
}
}),
want: []string{"malformed cve_metadata.id identifier"},
},
{
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: unfixed/missing links",
report: validStdReport(func(r *Report) {
r.References = []*Reference{
{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.
"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: "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 in wrong dir",
report: validExcludedReport(noop),
want: []string{
`report in reports/ must not have excluded set`,
`no modules`,
`missing description`,
`missing summary`,
},
},
{
desc: "report in wrong dir",
dir: "excluded",
report: validReport(noop),
want: []string{
`report in excluded/ must have excluded set`,
},
},
{
desc: "excluded missing CVE/GHSA",
dir: "excluded",
report: validExcludedReport(func(r *Report) {
r.CVEs = nil
r.GHSAs = nil
}),
want: []string{
`excluded report must have at least one associated CVE or GHSA`,
},
},
{
desc: "excluded",
dir: "excluded",
report: validExcludedReport(noop),
// No lints.
},
} {
test := test
t.Run(test.desc, func(t *testing.T) {
dir := test.dir
if dir == "" {
dir = "reports"
}
got := test.report.Lint(dir + "/GO-0000-000.yaml")
var missing []string
for _, w := range test.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 {
var buf bytes.Buffer
if err := test.report.encode(&buf); err != nil {
t.Error(err)
}
t.Errorf("missing expected lint errors in report:\n"+
"%v\n"+
"got: %q\n"+
"want: %q\n", buf.String(), 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 test.want {
if strings.Contains(g, w) {
found = true
continue
}
}
if !found {
unexpected = append(unexpected, g)
}
}
if len(unexpected) > 0 {
var buf bytes.Buffer
if err := test.report.encode(&buf); err != nil {
t.Error(err)
}
t.Errorf("unexpected lint errors in report:\n"+
"%v\n"+
"got: %q\n", buf.String(), unexpected)
}
}
})
}
}